From 00b8aec59c6fc879b37cc17fce78a710daee3321 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Wed, 9 Aug 2023 20:44:31 -0400 Subject: [PATCH 001/332] first try at adreport flag --- R/simulators.R | 4 +++- R/tmb_model.R | 13 ++++++++----- src/macpan2.cpp | 7 ++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/R/simulators.R b/R/simulators.R index 18b33d57..c7ebc47d 100644 --- a/R/simulators.R +++ b/R/simulators.R @@ -53,6 +53,7 @@ Simulators = function(model) { , ... , .mats_to_save = .mats_to_return , .mats_to_return = "state" + , .do_pred_sdreport = TRUE , .dimnames = list() , .tmb_cpp = "macpan2" , .initialize_ad_fun = TRUE @@ -66,7 +67,8 @@ Simulators = function(model) { , .structure_labels = self$model$labels ), expr_list = self$model$expr_list(), - time_steps = Time(time_steps) + time_steps = Time(time_steps), + do_pred_sdreport = .do_pred_sdreport )$simulator(tmb_cpp = .tmb_cpp, initialize_ad_fun = .initialize_ad_fun) } return_object(self, "Simulators") diff --git a/R/tmb_model.R b/R/tmb_model.R index 07b1856a..ea8f0d14 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -677,6 +677,7 @@ Daily = function(start_date, end_date) { #' @param random An object of class \code{\link{OptParamsList}}. #' @param obj_fn An object of class \code{\link{ObjectiveFunction}}. #' @param time_steps An object of class \code{\link{Time}}. +#' @param do_pred_sdreport A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()} #' #' @return Object of class \code{TMBModel} with the following methods. #' @@ -738,7 +739,8 @@ TMBModel = function( params = OptParamsList(0), random = OptParamsList(), obj_fn = ObjectiveFunction(~0), - time_steps = Time(0L) + time_steps = Time(0L), + do_pred_sdreport = TRUE ) { ## Inheritance self = Base() @@ -750,11 +752,12 @@ TMBModel = function( self$random = random self$obj_fn = obj_fn self$time_steps = time_steps + self$do_pred_sdreport = do_pred_sdreport ## Standard Methods self$data_arg = function() { - existing_literals = self$expr_list$.literals(self$init_mats$.names()) - expr_list = self$expr_list$data_arg(self$init_mats$.names()) + existing_literals = self$expr_list$.literals(self$init_mats$.names()) + expr_list = self$expr_list$data_arg(self$init_mats$.names()) c( self$init_mats$data_arg(), expr_list, @@ -763,8 +766,8 @@ TMBModel = function( self$obj_fn$data_arg(self$init_mats$.names() , .existing_literals = existing_literals ), - self$time_steps$data_arg() - + self$time_steps$data_arg(), + list(values_adreport = as.integer(self$do_pred_sdreport)) ) } self$param_arg = function() { diff --git a/src/macpan2.cpp b/src/macpan2.cpp index d56e5b80..859c7b68 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -1991,6 +1991,9 @@ Type objective_function::operator() () DATA_IVECTOR(o_table_x); DATA_IVECTOR(o_table_i); + // Flags + DATA_INTEGER(values_adreport); + // DATA_STRUCT(settings, settings_struct); #ifdef MP_VERBOSE @@ -2310,7 +2313,9 @@ Type objective_function::operator() () } REPORT(values) - ADREPORT(values) + if (values_adreport == 1) { + ADREPORT(values) + } // 7 Calc the return of the objective function From 9272882731bb00492e0f5b555d256ad384fe6dd4 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Wed, 9 Aug 2023 20:46:30 -0400 Subject: [PATCH 002/332] update docs for adreport flag --- man/TMBModel.Rd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index e04d3f92..35b7eaaa 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -10,7 +10,8 @@ TMBModel( params = OptParamsList(0), random = OptParamsList(), obj_fn = ObjectiveFunction(~0), - time_steps = Time(0L) + time_steps = Time(0L), + do_pred_sdreport = TRUE ) } \arguments{ @@ -25,6 +26,8 @@ TMBModel( \item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} \item{time_steps}{An object of class \code{\link{Time}}.} + +\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} } \value{ Object of class \code{TMBModel} with the following methods. From 1ea1d03b0858e558213f87e32969c51fca8e5822 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 10 Aug 2023 15:03:53 -0400 Subject: [PATCH 003/332] testing code for adreport flag --- misc/experiments/adreport.R | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 misc/experiments/adreport.R diff --git a/misc/experiments/adreport.R b/misc/experiments/adreport.R new file mode 100644 index 00000000..b3e761d5 --- /dev/null +++ b/misc/experiments/adreport.R @@ -0,0 +1,51 @@ +library(macpan2) +library(macpan2helpers) +library(tidyverse) + +setup_everything <- function(v = 1.0) { + ## can't call the arg do_pred_sdreport or we get a recursive + ## default arg reference error ... + ## can't specify as an integer here ??? + mk_sim <- function(do_pred_sdreport = v, + init_state = c(S = 99, I = 1, R = 0)) { + sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) + sim <- sir$simulators$tmb( + do_pred_sdreport = do_pred_sdreport, + time_steps = 100, + state = init_state, + flow = c(foi = NA, gamma = 0.1), + beta = 0.2, + N = empty_matrix + ) + return(sim) +} +sir_simulator <- mk_sim() +sir_results = sir_simulator$report(.phases = "during") +set.seed(101) +sir_prevalence = (sir_results + |> dplyr::select(-c(matrix, col)) + |> filter(row == "I") + |> mutate(obs_val = value + rnorm(n(), sd = 1)) +) +mk_calibrate(sir_simulator, + data = data.frame(I_obs = sir_prevalence$obs_val), + params = list(beta = 1, I_sd = 1), + transforms = list(beta = "log", I_sd = "log"), + exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), + ) + fit <- sir_simulator$optimize$nlminb() + return(sir_simulator) +} + +S1 <- setup_everything() +ss1 <- TMB::sdreport(S1$ad_fun()) +length(ss1$sd) + +S2 <- setup_everything(1L) +S3 <- setup_everything(TRUE) +## Error in self$check(x) : +## Error in valid$check(x) : mats component is not of type numeric + +S4 <- setup_everything(0) +ss4 <- TMB::sdreport(S4$ad_fun()) +length(ss4$sd) ## ugh, same length ... From ab63dc5200a9080bdbb8a74da937dbb2d1df15af Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 10 Aug 2023 15:59:24 -0400 Subject: [PATCH 004/332] adreport flag seems to be working --- R/simulators.R | 4 ++-- R/tmb_model.R | 2 +- misc/experiments/adreport.R | 18 +++++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/R/simulators.R b/R/simulators.R index c7ebc47d..df4f4f3c 100644 --- a/R/simulators.R +++ b/R/simulators.R @@ -53,11 +53,11 @@ Simulators = function(model) { , ... , .mats_to_save = .mats_to_return , .mats_to_return = "state" - , .do_pred_sdreport = TRUE + , .do_pred_sdreport = 1L , .dimnames = list() , .tmb_cpp = "macpan2" , .initialize_ad_fun = TRUE - ) { + ) { TMBModel( init_mats = CompartmentalMatsList(self$model, state, flow , ... diff --git a/R/tmb_model.R b/R/tmb_model.R index ea8f0d14..6f42bf78 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -740,7 +740,7 @@ TMBModel = function( random = OptParamsList(), obj_fn = ObjectiveFunction(~0), time_steps = Time(0L), - do_pred_sdreport = TRUE + do_pred_sdreport = 1L ) { ## Inheritance self = Base() diff --git a/misc/experiments/adreport.R b/misc/experiments/adreport.R index b3e761d5..0ab26154 100644 --- a/misc/experiments/adreport.R +++ b/misc/experiments/adreport.R @@ -2,6 +2,8 @@ library(macpan2) library(macpan2helpers) library(tidyverse) +## note: typos in $tmb don't give warnings (assumes it's another matrix +## you want to add ...) setup_everything <- function(v = 1.0) { ## can't call the arg do_pred_sdreport or we get a recursive ## default arg reference error ... @@ -10,7 +12,7 @@ setup_everything <- function(v = 1.0) { init_state = c(S = 99, I = 1, R = 0)) { sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) sim <- sir$simulators$tmb( - do_pred_sdreport = do_pred_sdreport, + .do_pred_sdreport = do_pred_sdreport, time_steps = 100, state = init_state, flow = c(foi = NA, gamma = 0.1), @@ -34,18 +36,20 @@ mk_calibrate(sir_simulator, exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), ) fit <- sir_simulator$optimize$nlminb() - return(sir_simulator) + ss <- TMB::sdreport(sir_simulator$ad_fun()) + return(ss) } S1 <- setup_everything() -ss1 <- TMB::sdreport(S1$ad_fun()) length(ss1$sd) S2 <- setup_everything(1L) +all.equal(S1, S2) S3 <- setup_everything(TRUE) -## Error in self$check(x) : -## Error in valid$check(x) : mats component is not of type numeric +all.equal(S1, S3) S4 <- setup_everything(0) -ss4 <- TMB::sdreport(S4$ad_fun()) -length(ss4$sd) ## ugh, same length ... +length(ss4$sd) + +S5 <- setup_everything(FALSE) +all.equal(S4, S5) From 9bde9a1c1ca8bea30fba71c1006874fe81e97172 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 10 Aug 2023 16:12:55 -0400 Subject: [PATCH 005/332] working on adreport flag stuff --- misc/experiments/adreport.R | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/misc/experiments/adreport.R b/misc/experiments/adreport.R index 0ab26154..adc69d0e 100644 --- a/misc/experiments/adreport.R +++ b/misc/experiments/adreport.R @@ -1,14 +1,15 @@ library(macpan2) library(macpan2helpers) library(tidyverse) +library(peakRAM) ## note: typos in $tmb don't give warnings (assumes it's another matrix ## you want to add ...) -setup_everything <- function(v = 1.0) { +setup_everything <- function(adreport = 1.0, jointCov = FALSE) { ## can't call the arg do_pred_sdreport or we get a recursive ## default arg reference error ... ## can't specify as an integer here ??? - mk_sim <- function(do_pred_sdreport = v, + mk_sim <- function(do_pred_sdreport = adreport, init_state = c(S = 99, I = 1, R = 0)) { sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) sim <- sir$simulators$tmb( @@ -36,10 +37,11 @@ mk_calibrate(sir_simulator, exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), ) fit <- sir_simulator$optimize$nlminb() - ss <- TMB::sdreport(sir_simulator$ad_fun()) + ss <- TMB::sdreport(sir_simulator$ad_fun(), getJointPrecision = jointCov) return(ss) } +## various configurations S1 <- setup_everything() length(ss1$sd) @@ -53,3 +55,11 @@ length(ss4$sd) S5 <- setup_everything(FALSE) all.equal(S4, S5) + +## jointCov makes no difference in this case because we don't have random effects ... +p1 <- peakRAM(s1 <- setup_everything(TRUE)) +p2 <- peakRAM(s2 <- setup_everything(FALSE)) +p3 <- peakRAM(s3 <- setup_everything(TRUE, jointCov = TRUE)) +p4 <- peakRAM(s4 <- setup_everything(FALSE, jointCov = TRUE)) + +rbind(p1, p2, p3, p4) From ba4dbe947b017425affac0ff7056de47f6be37d8 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 10 Aug 2023 19:30:56 -0400 Subject: [PATCH 006/332] progress on adreport example --- misc/experiments/adreport.R | 65 --------- misc/experiments/adreport/adreport.R | 184 +++++++++++++++++++++++++ misc/experiments/adreport/adreport.rda | Bin 0 -> 569 bytes 3 files changed, 184 insertions(+), 65 deletions(-) delete mode 100644 misc/experiments/adreport.R create mode 100644 misc/experiments/adreport/adreport.R create mode 100644 misc/experiments/adreport/adreport.rda diff --git a/misc/experiments/adreport.R b/misc/experiments/adreport.R deleted file mode 100644 index adc69d0e..00000000 --- a/misc/experiments/adreport.R +++ /dev/null @@ -1,65 +0,0 @@ -library(macpan2) -library(macpan2helpers) -library(tidyverse) -library(peakRAM) - -## note: typos in $tmb don't give warnings (assumes it's another matrix -## you want to add ...) -setup_everything <- function(adreport = 1.0, jointCov = FALSE) { - ## can't call the arg do_pred_sdreport or we get a recursive - ## default arg reference error ... - ## can't specify as an integer here ??? - mk_sim <- function(do_pred_sdreport = adreport, - init_state = c(S = 99, I = 1, R = 0)) { - sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) - sim <- sir$simulators$tmb( - .do_pred_sdreport = do_pred_sdreport, - time_steps = 100, - state = init_state, - flow = c(foi = NA, gamma = 0.1), - beta = 0.2, - N = empty_matrix - ) - return(sim) -} -sir_simulator <- mk_sim() -sir_results = sir_simulator$report(.phases = "during") -set.seed(101) -sir_prevalence = (sir_results - |> dplyr::select(-c(matrix, col)) - |> filter(row == "I") - |> mutate(obs_val = value + rnorm(n(), sd = 1)) -) -mk_calibrate(sir_simulator, - data = data.frame(I_obs = sir_prevalence$obs_val), - params = list(beta = 1, I_sd = 1), - transforms = list(beta = "log", I_sd = "log"), - exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), - ) - fit <- sir_simulator$optimize$nlminb() - ss <- TMB::sdreport(sir_simulator$ad_fun(), getJointPrecision = jointCov) - return(ss) -} - -## various configurations -S1 <- setup_everything() -length(ss1$sd) - -S2 <- setup_everything(1L) -all.equal(S1, S2) -S3 <- setup_everything(TRUE) -all.equal(S1, S3) - -S4 <- setup_everything(0) -length(ss4$sd) - -S5 <- setup_everything(FALSE) -all.equal(S4, S5) - -## jointCov makes no difference in this case because we don't have random effects ... -p1 <- peakRAM(s1 <- setup_everything(TRUE)) -p2 <- peakRAM(s2 <- setup_everything(FALSE)) -p3 <- peakRAM(s3 <- setup_everything(TRUE, jointCov = TRUE)) -p4 <- peakRAM(s4 <- setup_everything(FALSE, jointCov = TRUE)) - -rbind(p1, p2, p3, p4) diff --git a/misc/experiments/adreport/adreport.R b/misc/experiments/adreport/adreport.R new file mode 100644 index 00000000..7f1ab861 --- /dev/null +++ b/misc/experiments/adreport/adreport.R @@ -0,0 +1,184 @@ +library(macpan2) +library(macpan2helpers) +library(tidyverse) +library(peakRAM) + +## note: typos in $tmb don't give warnings (assumes it's another matrix +## you want to add ...) +setup_everything <- function(adreport = 1.0, jointCov = FALSE) { + ## can't call the arg do_pred_sdreport or we get a recursive + ## default arg reference error ... + ## can't specify as an integer here ??? + mk_sim <- function(do_pred_sdreport = adreport, + init_state = c(S = 99, I = 1, R = 0)) { + sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) + sim <- sir$simulators$tmb( + .do_pred_sdreport = do_pred_sdreport, + time_steps = 100, + state = init_state, + flow = c(foi = NA, gamma = 0.1), + beta = 0.2, + N = empty_matrix + ) + return(sim) +} +sir_simulator <- mk_sim() +sir_results = sir_simulator$report(.phases = "during") +set.seed(101) +sir_prevalence = (sir_results + |> dplyr::select(-c(matrix, col)) + |> filter(row == "I") + |> mutate(obs_val = value + rnorm(n(), sd = 1)) +) +mk_calibrate(sir_simulator, + data = data.frame(I_obs = sir_prevalence$obs_val), + params = list(beta = 1, I_sd = 1), + transforms = list(beta = "log", I_sd = "log"), + exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), + ) + fit <- sir_simulator$optimize$nlminb() + ss <- TMB::sdreport(sir_simulator$ad_fun(), getJointPrecision = jointCov) + return(ss) +} + +## various configurations +S1 <- setup_everything() +length(S1$sd) + +S2 <- setup_everything(1L) +stopifnot(all.equal(S1, S2)) +S3 <- setup_everything(TRUE) +stopifnot(all.equal(S1, S3)) + +S4 <- setup_everything(0) +stopifnot(length(S4$sd) == 0) + +S5 <- setup_everything(FALSE) +stopifnot(all.equal(S4, S5)) + +## jointCov makes no difference in this case because we don't have random effects ... +p1 <- peakRAM(s1 <- setup_everything(TRUE)) +p2 <- peakRAM(s2 <- setup_everything(FALSE)) +p3 <- peakRAM(s3 <- setup_everything(TRUE, jointCov = TRUE)) +p4 <- peakRAM(s4 <- setup_everything(FALSE, jointCov = TRUE)) + +results_simple <- rbind(p1, p2, p3, p4) + + +### RBF example from time-varying vignette + +## change the structure slightly, because we want to count the time/memory +## only of the sdreport() part (the setup and optimization was pretty trivial +## in the previous case), and because re-running the setup just to try out +## getJointPrecision TRUE/FALSE cases is a waste of time + +setup2 <- function(adreport = TRUE, d = 20, n = 500) { + sir = Compartmental(system.file("starter_models", "sir_waning", package = "macpan2")) + ## ----sim_rbf------------------------------------------------------------------ + set.seed(1L) + simulator = sir$simulators$tmb( + time_steps = n + , state = c(S = 100000 - 500, I = 500, R = 0) + , flow = c(foi = NA, gamma = 0.2, wane = 0.01) + , beta = 1 + , N = 100000 + , X = rbf(n, d) + , b = rnorm(d, sd = 0.01) + , incidence = empty_matrix + , eta = empty_matrix + , .do_pred_sdreport = adreport + , .mats_to_save = c("state", "incidence", "beta") + , .mats_to_return = c("state", "incidence", "beta") + ) + simulator$insert$expressions( + eta ~ gamma * exp(X %*% b) + , .phase = "before" + , .at = Inf + ) + simulator$insert$expressions( + beta ~ eta[time_step(1)] / clamp(S/N, 1/100) + , .phase = "during" + , .at = 1) + simulator$insert$expressions( + incidence ~ I + , .vec_by_states = "total_inflow" + , .phase = "during" + , .at = Inf + ) + simulator$replace$params( + default = rnorm(d, sd = 0.01) + , mat = rep("b", d) + , row = seq_len(d) - 1L + ) + obs_I <- ( + simulator$report(.phases = "during") + |> filter(row == "I", matrix == "state") + |> mutate(across(value, ~ rnorm(n(), ., sd = 50))) + |> pull(value) + ) + simulator$add$matrices( + I_obs = obs_I, + I_sim = empty_matrix, + log_lik = empty_matrix, + .mats_to_save = c("I_sim"), + .mats_to_return = c("I_sim") + ) + simulator$insert$expressions( + I_sim ~ I, + .phase = "during", + .at = Inf + ) + simulator$add$matrices(I_sd = 1, rbf_sd = 1) + simulator$insert$expressions( + log_lik ~ -sum(dnorm(I_obs, rbind_time(I_sim), I_sd)) + + -1*sum(dnorm(b, 0.0, rbf_sd)), + .phase = "after" + ) + simulator$replace$obj_fn(~ log_lik) + simulator$add$transformations(Log("I_sd")) + simulator$add$transformations(Log("rbf_sd")) + params <- read.delim(sep = "|", header = TRUE, + text = " +mat | row | col | default +log_I_sd | 0 | 0 | 0 +log_rbf_sd | 0 | 0 | 1 +") + simulator$replace$params_frame(params) + rparams <- data.frame( + mat = "b", + row = 0:19, + col = 0, + default = 0) + simulator$replace$random_frame(rparams) + fit <- simulator$optimize$nlminb() + return(simulator) +} + + +sdr <- function(simulator, jointCov = FALSE) { + ss <- TMB::sdreport(simulator$ad_fun(), getJointPrecision = jointCov) + return(ss) +} + +## now see how time and memory use scale with +## * setup2 config: +## * number of time steps (n) +## * number of RBF elements (d) +## * adreport TRUE/FALSE +## * sdr config: +## * jointCov + +p5A <- peakRAM(S5A <- setup2()) +p5B1 <- peakRAM(S5B1 <- sdr(S5A)) +p5B2 <- peakRAM(S5B2 <- sdr(S5A, jointCov = TRUE)) + +p6A <- peakRAM(S6A <- setup2(adreport = FALSE)) +p6B1 <- peakRAM(S6B1 <- sdr(S6A)) +p6B2 <- peakRAM(S6B2 <- sdr(S6A, jointCov = TRUE)) + +results_rbf <- do.call(rbind, mget(ls(pattern="^p[56]"))) + +## now (1) check that cov matrices in jointCov case are the right size, +## (2) try simulating from ensemble ... + +save("results_rbf", "results_simple", file = "adreport.rda") diff --git a/misc/experiments/adreport/adreport.rda b/misc/experiments/adreport/adreport.rda new file mode 100644 index 0000000000000000000000000000000000000000..a15dcafa4fa3300be6273d72680ed4d7f7155f29 GIT binary patch literal 569 zcmV-90>=FxiwFP!000000}FDAFy@NjVqjokW?*3flB_@`18ZoAo2~@|0}B(7!^ptG zzzL+ei&BeAb4rTii;~iS3Wb3}Kn)B6K+FbJ!W(SrXro)4T2fkIq@f8FysdPB0jAIXSsDm5ILv082?VnoW`jbBfx-T=F%UQ~z<~WukaPoFpMwfS z++m9cMBf%qu%v?=RQ&@e{TO790Rvj_v*sn{rWQj3oCOx(Zl!t2C7Jno@y>}kIZ#0X z*PO(H;?$J*kj&iF_~O)LsJKu_eo10Ze2}AWd?-lTH`56!BN&jHn2jodYB^_7ez_j9 z17I=ETwv-5WwI0i;~9x<1Xp5)P-2Fp1Qh~BJxQ|Ie*~qBnRD!)$*kcpRQq86?xFiSk*Wvw&vg01SR^LE)HxV}(_jWHsX_TE z3 Date: Thu, 7 Sep 2023 11:55:08 -0400 Subject: [PATCH 007/332] tests passing allowing refactoring to continue --- Makefile | 13 ++- NAMESPACE | 2 + R/binary_operator.R | 6 +- R/dev_tools.R | 2 +- R/engine_methods.R | 105 ++++++++++++++++++++++ R/parameter_files.R | 0 R/tmb_model.R | 69 ++++++++++++--- man/ConstIntVecs.Rd | 14 +++ man/MethList.Rd | 14 +++ man/Method.Rd | 22 +++++ man/TMBModel.Rd | 9 +- misc/build/run_vignette_code.R | 33 +++++++ misc/dev/dev.cpp | 152 +++++++++++++++++++++++++++++++- vignettes/calibration.Rmd | 8 +- vignettes/hello_products.Rmd | 8 +- vignettes/model_definitions.Rmd | 10 ++- vignettes/quickstart2.Rmd | 2 +- 17 files changed, 440 insertions(+), 29 deletions(-) create mode 100644 R/engine_methods.R create mode 100644 R/parameter_files.R create mode 100644 man/ConstIntVecs.Rd create mode 100644 man/MethList.Rd create mode 100644 man/Method.Rd create mode 100644 misc/build/run_vignette_code.R diff --git a/Makefile b/Makefile index a777cb98..d4ebbaa0 100644 --- a/Makefile +++ b/Makefile @@ -41,9 +41,15 @@ quick-doc-install: R/*.R misc/dev/dev.cpp make quick-install -quick-test: +quick-test-all: make quick-doc-install + make run-vignette-code + make run-tests make run-examples + + +quick-test: + make quick-doc-install make run-tests @@ -55,6 +61,11 @@ run-tests: Rscript -e "library(macpan2); testthat::test_package(\"macpan2\")" +run-vignette-code: + cd vignettes + Rscript misc/build/run_vignette_code.R + + coverage-report:: coverage.html coverage.html: R/*.R src/macpan2.cpp tests/testthat/*.R Rscript -e "covr::report(file = \"coverage.html\")" diff --git a/NAMESPACE b/NAMESPACE index 310456fe..94fb9d93 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,6 +28,7 @@ export(CSVReader) export(Collection) export(Compartmental) export(CompartmentalSimulator) +export(ConstIntVecs) export(DerivationExtractor) export(Derivations2ExprList) export(Euler) @@ -40,6 +41,7 @@ export(Logit) export(MathExpressionFromFunc) export(MathExpressionFromStrings) export(MatsList) +export(MethList) export(Model) export(ModelCollection) export(ModelFiles) diff --git a/R/binary_operator.R b/R/binary_operator.R index 149d9614..ccaade2e 100644 --- a/R/binary_operator.R +++ b/R/binary_operator.R @@ -25,7 +25,11 @@ valid_binop = oor::ValidityMessager( #' #' Convert a function that represents an elementwise binary #' operator into one that is consistent with the \code{C++} -#' engine. +#' engine. This function is intended to clarify how \pkg{macpan2} +#' treats binary operators, which is a little different from +#' base \proglang{R}. The difference is clarified in +#' `vignette("binary_operator")`, and \code{BinaryOperator} is +#' primarily used as a resource for that vignette. #' #' @param operator A binary operator. r binop_validity_message #' @return A binary operator consistent with the \code{C++} engine. diff --git a/R/dev_tools.R b/R/dev_tools.R index 305c9bc7..bba41ef9 100644 --- a/R/dev_tools.R +++ b/R/dev_tools.R @@ -1,7 +1,7 @@ dev_in_root = function() "inst" %in% list.dirs(full.names = FALSE) dev_in_dev = function() "dev.cpp" %in% list.files(full.names = FALSE) dev_in_test = function() "testthat" %in% list.dirs("..", full.names = FALSE) -## TODO: dev_in_test or something like that +dev_in_vig = function() "vignette_status.Rmd" %in% list.files(full.names = FALSE) dev_file = function(suffix = "", ext = "cpp") { cpp = function(path) { diff --git a/R/engine_methods.R b/R/engine_methods.R new file mode 100644 index 00000000..15bd768a --- /dev/null +++ b/R/engine_methods.R @@ -0,0 +1,105 @@ +#' Method List +#' +#' @param ... List of \code{\link{Method}} objects. +#' @export +MethList = function(...) { + self = Base() + + # Args + self$methods = list(...) + + # DATA_IVECTOR(meth_type_id); + # // DATA_IVECTOR(meth_id); + # DATA_IVECTOR(meth_n_mats); + # DATA_IVECTOR(meth_n_int_vecs); + # DATA_IVECTOR(meth_mat_id); + # DATA_IVECTOR(meth_int_vec_id); + # Static + self$.null_data_arg = setNames( + rep(list(integer()), 5), + sprintf("meth_%s", c( + "type_id" + #, "id" + , "n_mats" + , "n_int_vecs" + , "mat_id" + , "int_vec_id" + ) + ) + ) + + self$data_arg = function(method_names, mat_names, const_names) { + l = self$.null_data_arg + for (i in seq_along(self$methods)) { + new_arg = self$methods[[i]]$data_arg(method_names, mat_names, const_names) + for (e in names(l)) { + l[[e]] = c(l[[e]], as.integer(new_arg[[e]])) + } + } + l + } + + return_object(self, "MethList") +} + +#' Engine Method Class +#' +#' Engine methods allow users to interact with the engine +#' by specifying a unique \code{name}. This name is used to reference recipes +#' for computing matrices from other matrices and constants. +#' +#' @param name Method name. +#' @param mat_args List of names of matrices that will be used by the method +#' to produce an output matrix. +#' @param const_args List of names of constants that will be used by the method +#' to produce an output matrix. +Method = function(name, mat_args, const_args) { + self = Base() + + # Args + self$name = name + self$mat_args = mat_args + self$const_args = const_args + + # Static + ## abstract -- instantiate with implementation classes (e.g. MethodRowIndexer) + self$meth_type_id = NA_integer_ + + # Private + self$.method_id = function(method_names) match(self$name, method_names) - 1L + self$.mat_ids = function(mat_names) match(self$mat_args, mat_names) - 1L + self$.const_ids = function(const_names) match(self$const_args, const_names) - 1L + + # Standard Methods + self$data_arg = function(method_names, mat_names, const_names) { + list( + ## these must be length-1 integer vectors + meth_type_id = self$meth_type_id + , meth_id = self$meth_type_id + , meth_n_mats = length(self$mat_args) + , meth_n_const = length(self$const_args) + + ## these must be length-meth_n_mats and length-meth_n_const respectively + , meth_mat_id = self$.mat_ids(mat_names) + , meth_const_id = self$.const_ids(const_names) + ) + } + + return_object(self, "Method") +} + +meth_cls_types = c("MethodRowIndexer") + +mk_meth_cls = function(cls_nm, meth_type_id) { + pf = parent.frame() + force(pf) + force(cls_nm) + force(meth_type_id) + f = function(name, mat_args, const_args) { + self = Method(name, mat_args, const_args) + self$meth_type_id = meth_type_id + return_object(self, cls_nm) + } + assign(cls_nm, f, envir = pf) +} +for (i in seq_along(meth_cls_types)) mk_meth_cls(meth_cls_types[i], i) diff --git a/R/parameter_files.R b/R/parameter_files.R new file mode 100644 index 00000000..e69de29b diff --git a/R/tmb_model.R b/R/tmb_model.R index d7e5a6a4..9e88dbca 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -275,14 +275,24 @@ MatsList = function(... # ) self = Base() - ## Args -- TODO: these shouldn't be private - self$.initial_mats = lapply(list(...), as.matrix) + ## Args + ## TODO: these shouldn't be private but we have a problem. + ## the original sin was putting dots in front of arguments that + ## correspond to public Arg fields. this will require a breaking + ## change to fix where we remove dots from all arguments that should + ## actually be stored publicly. i initially was implicitly thinking + ## of arguments starting with dots as arguments that aren't really + ## part of the method signature being depended on, but this is silly + ## in hindsight ... what does that even mean? self$.mats_to_save = .mats_to_save self$.mats_to_return = .mats_to_return self$.dimnames = .dimnames self$.structure_labels = .structure_labels #self$.init_saved_dims = .init_saved_dims + # Static + self$.initial_mats = lapply(list(...), as.matrix) + self$mats_save_hist = function() names(self$.initial_mats) %in% self$.mats_to_save self$mats_return = function() names(self$.initial_mats) %in% self$.mats_to_return # self$mats_save_dims = function() { @@ -666,6 +676,34 @@ Daily = function(start_date, end_date) { } +#' Constant Integer Vectors +#' +#' Make a list of integer vectors available for engine methods. +#' +#' @param ... Named arguments, each of which can be coerced to an integer vector. +#' @export +ConstIntVecs = function(...) { + self = Base() + + # Args + self$list = lapply(list(...), as.integer) + if (length(self$list) == 0L) self$list = list(integer()) + + # Standard Methods + self$data_arg = function() { + # DATA_IVECTOR(const_int_vec); + # DATA_IVECTOR(const_n_int_vecs); + list( + const_int_vec = unlist(self$list, use.names = FALSE), + const_n_int_vecs = unlist(lapply(self$list, length), use.names = FALSE) + ) + } + self$const_names = function() names(self$list) + + return_object(self, "ConstIntVecs") +} + + #' TMB Model #' #' Define a compartmental model in TMB. This model uses the spec @@ -677,6 +715,8 @@ Daily = function(start_date, end_date) { #' @param random An object of class \code{\link{OptParamsList}}. #' @param obj_fn An object of class \code{\link{ObjectiveFunction}}. #' @param time_steps An object of class \code{\link{Time}}. +#' @param meth_list An object of class \code{\link{MethList}}. +#' @param const_int_vecs An object of class \code{\link{ConstIntVecs}}. #' #' @return Object of class \code{TMBModel} with the following methods. #' @@ -728,17 +768,20 @@ Daily = function(start_date, end_date) { #' ) #' sir$data_arg() #' sir$param_arg() +#' sir$simulator()$report() #' #' @useDynLib macpan2 #' @importFrom TMB MakeADFun #' @export TMBModel = function( - init_mats = MatsList(), - expr_list = ExprList(), - params = OptParamsList(0), - random = OptParamsList(), - obj_fn = ObjectiveFunction(~0), - time_steps = Time(0L) + init_mats = MatsList() + , expr_list = ExprList() + , params = OptParamsList(0) + , random = OptParamsList() + , obj_fn = ObjectiveFunction(~0) + , time_steps = Time(0L) + , meth_list = MethList() + , const_int_vecs = ConstIntVecs() ) { ## Inheritance self = Base() @@ -750,21 +793,23 @@ TMBModel = function( self$random = random self$obj_fn = obj_fn self$time_steps = time_steps + self$meth_list = meth_list + self$const_int_vecs = const_int_vecs ## Standard Methods self$data_arg = function() { existing_literals = self$expr_list$.literals(self$init_mats$.names()) - expr_list = self$expr_list$data_arg(self$init_mats$.names()) c( self$init_mats$data_arg(), - expr_list, + self$expr_list$data_arg(self$init_mats$.names()), self$params$data_arg(self$init_mats$.names()), self$random$data_arg(self$init_mats$.names(), .type_string = "r"), self$obj_fn$data_arg(self$init_mats$.names() , .existing_literals = existing_literals ), - self$time_steps$data_arg() - + self$time_steps$data_arg(), + self$meth_list$data_arg(), + self$const_int_vecs$data_arg() ) } self$param_arg = function() { diff --git a/man/ConstIntVecs.Rd b/man/ConstIntVecs.Rd new file mode 100644 index 00000000..7ec716b7 --- /dev/null +++ b/man/ConstIntVecs.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tmb_model.R +\name{ConstIntVecs} +\alias{ConstIntVecs} +\title{Constant Integer Vectors} +\usage{ +ConstIntVecs(...) +} +\arguments{ +\item{...}{Named arguments, each of which can be coerced to an integer vector.} +} +\description{ +Make a list of integer vectors available for engine methods. +} diff --git a/man/MethList.Rd b/man/MethList.Rd new file mode 100644 index 00000000..8fcd2f05 --- /dev/null +++ b/man/MethList.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/engine_methods.R +\name{MethList} +\alias{MethList} +\title{Method List} +\usage{ +MethList(...) +} +\arguments{ +\item{...}{List of \code{\link{Method}} objects.} +} +\description{ +Method List +} diff --git a/man/Method.Rd b/man/Method.Rd new file mode 100644 index 00000000..a2e39210 --- /dev/null +++ b/man/Method.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/engine_methods.R +\name{Method} +\alias{Method} +\title{Engine Method Class} +\usage{ +Method(name, mat_args, const_args) +} +\arguments{ +\item{name}{Method name.} + +\item{mat_args}{List of names of matrices that will be used by the method +to produce an output matrix.} + +\item{const_args}{List of names of constants that will be used by the method +to produce an output matrix.} +} +\description{ +Engine methods allow users to interact with the engine +by specifying a unique \code{name}. This name is used to reference recipes +for computing matrices from other matrices and constants. +} diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index e04d3f92..d813fb8d 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -10,7 +10,9 @@ TMBModel( params = OptParamsList(0), random = OptParamsList(), obj_fn = ObjectiveFunction(~0), - time_steps = Time(0L) + time_steps = Time(0L), + meth_list = MethList(), + const_int_vecs = ConstIntVecs() ) } \arguments{ @@ -25,6 +27,10 @@ TMBModel( \item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} \item{time_steps}{An object of class \code{\link{Time}}.} + +\item{meth_list}{An object of class \code{\link{MethList}}.} + +\item{const_int_vecs}{An object of class \code{\link{ConstIntVecs}}.} } \value{ Object of class \code{TMBModel} with the following methods. @@ -82,5 +88,6 @@ sir = TMBModel( ) sir$data_arg() sir$param_arg() +sir$simulator()$report() } diff --git a/misc/build/run_vignette_code.R b/misc/build/run_vignette_code.R new file mode 100644 index 00000000..160cda9e --- /dev/null +++ b/misc/build/run_vignette_code.R @@ -0,0 +1,33 @@ +# Function to extract and execute R code from Rmd file +extract_and_source_r_code <- function(vignette_file) { + # Create a temporary R script file (with .R extension) + temp_script_file <- sub(".Rmd$", ".R", vignette_file) + + # Use knitr's purl to extract R code from the Rmd file + knitr::purl(vignette_file, output = temp_script_file) + + # Run the extracted R script using source() + message(sprintf("Now running code from %s", vignette_file)) + result = try(source(temp_script_file)) + + # Clean up the temporary script file + # file.remove(temp_script_file) + + if (inherits(result, "try-error")) { + stop(sprintf("Failed to run code from %s", vignette_file)) + } +} + +# Function to process all Rmd files in a directory +source_r_code_from_vignettes <- function(vignette_directory) { + rmd_files <- list.files( + vignette_directory, + pattern = "\\.Rmd$", + full.names = TRUE + ) + for (vignette_file in rmd_files) { + extract_and_source_r_code(vignette_file) + } +} + +source_r_code_from_vignettes("vignettes") diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 1f7440c5..e365da7b 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -108,6 +108,10 @@ enum macpan2_func { //, MP2_LOGIT = 46 // fwrap,null: logit(x) }; +enum macpan2_meth { + METH_ROW_EXTRACT = 1 +}; + // Helper function template int CheckIndices( @@ -206,6 +210,98 @@ struct ListOfMatrices { } }; + +// struct ListOfIntVecs { +// +// vector > v_vectors; +// +// ListOfIntVecs(SEXP ii) { +// int n = length(ii); +// vector > vs(n); +// v_vectors = vs; +// +// for (int i = 0; i < n; i++) { +// v_vectors[i] = asVector(VECTOR_ELT(ii, i)); +// } +// } +// ListOfIntVecs() { +// } +// +// ListOfIntVecs(const ListOfIntVecs& another) { +// v_vectors = another.v_vectors; +// } +// +// ListOfIntVecs & operator=(const ListOfIntVecs& another) { +// v_vectors = another.v_vectors; +// return *this; +// } +// }; + + +class ListOfIntVecs { +public: + std::vector> nestedVector; + + // Constructor that takes all_ints and vec_lens vectors + ListOfIntVecs(const std::vector& all_ints, const std::vector& vec_lens) { + size_t totalElements = 0; + for (int size : vec_lens) { + totalElements += size; + } + + if (all_ints.size() != totalElements) { + // Handle mismatched sizes as needed + throw std::invalid_argument("all_ints and vec_lens sizes do not match."); + } + + size_t xIndex = 0; + for (int size : vec_lens) { + std::vector innerVector; + for (int i = 0; i < size; ++i) { + innerVector.push_back(all_ints[xIndex++]); + } + nestedVector.push_back(innerVector); + } + } + + // Overload [] operator to access the inner vectors by index + std::vector& operator[](size_t index) { + if (index < nestedVector.size()) { + return nestedVector[index]; + } else { + // Handle out-of-range access here (you can throw an exception or handle it as needed) + throw std::out_of_range("Index out of range"); + } + } + + // Method to return the number of vector objects in the struct + size_t size() const { + return nestedVector.size(); + } + + // Member function to filter the ListOfIntVecs by indices in a vector object + ListOfIntVecs filterByIndices(const std::vector& i) const { + std::vector filtered_all_ints; + std::vector filtered_vec_lens; + + for (int index : i) { + if (index >= 0 && static_cast(index) < nestedVector.size()) { + const std::vector& innerVector = nestedVector[static_cast(index)]; + filtered_all_ints.insert(filtered_all_ints.end(), innerVector.begin(), innerVector.end()); + filtered_vec_lens.push_back(innerVector.size()); + } else { + // Handle out-of-range index or negative index as needed + throw std::out_of_range("Index out of range"); + } + } + + return ListOfIntVecs(filtered_all_ints, filtered_vec_lens); + } +}; + + + + template class ExprEvaluator { public: @@ -238,6 +334,10 @@ class ExprEvaluator { const vector& table_x, const vector& table_n, const vector& table_i, + const vector& meth_type_id, // vector over user defined methods, identifying a type of method + ListOfIntVecs& meth_mats, + ListOfIntVecs& meth_int_vecs, + ListOfIntVecs& valid_int_vecs, ListOfMatrices& valid_vars, const vector& valid_literals, int row = 0 @@ -245,27 +345,49 @@ class ExprEvaluator { { // Variables to use locally in function bodies matrix m, m1, m2; // return values + vector v; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars - int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2; + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2, curr_meth_id; if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. switch (table_n[row]) { + case -2: // methods (pre-processed matrices) + std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; + curr_meth_id = table_x[row]+1; + v = meth_mats.size(); + + // vector > int_vec_args(meth_int_vecs.v_vectors.size()); + // for (int i=0; i::Zero(v.size(), m.cols()); + // for (int i=0; i::Zero(1,1); m.coeffRef(0,0) = valid_literals[table_x[row]]; return m; - case 0: + case 0: // matrices m = valid_vars.m_matrices[table_x[row]]; return m; - default: + default: // expressions int n = table_n[row]; vector > args(n); vector index2mats(n); for (int i=0; i::operator() () // Literals DATA_VECTOR(literals); + // Methods + DATA_IVECTOR(meth_type_id); + // DATA_IVECTOR(meth_id); + DATA_IVECTOR(meth_n_mats); + DATA_IVECTOR(meth_n_int_vecs); + DATA_IVECTOR(meth_mat_id); + DATA_IVECTOR(meth_int_vec_id); + + // Constant Integer Vectors + DATA_IVECTOR(const_int_vec); + DATA_IVECTOR(const_n_int_vecs); + // Objective function parse table DATA_IVECTOR(o_table_n); DATA_IVECTOR(o_table_x); @@ -2054,6 +2188,9 @@ Type objective_function::operator() () ); + ListOfIntVecs const_int_vecs(const_int_vec, const_n_int_vecs); + ListOfIntVecs meth_mats(meth_mat_id, meth_n_mats); + ListOfIntVecs meth_int_vecs(meth_int_vec_id, meth_n_int_vecs); ////////////////////////////////// // Define an expression evaluator @@ -2079,6 +2216,7 @@ Type objective_function::operator() () p_table_x, p_table_n, p_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, p_table_row @@ -2093,6 +2231,7 @@ Type objective_function::operator() () p_table_x, p_table_n, p_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, p_table_row @@ -2141,6 +2280,7 @@ Type objective_function::operator() () p_table_x, p_table_n, p_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, p_table_row2 @@ -2155,6 +2295,7 @@ Type objective_function::operator() () p_table_x, p_table_n, p_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, p_table_row2 @@ -2203,6 +2344,7 @@ Type objective_function::operator() () p_table_x, p_table_n, p_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, p_table_row @@ -2217,6 +2359,7 @@ Type objective_function::operator() () p_table_x, p_table_n, p_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, p_table_row @@ -2321,6 +2464,7 @@ Type objective_function::operator() () o_table_x, o_table_n, o_table_i, + meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, mats, literals, 0 diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index 97466c61..6dd8e80a 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -702,7 +702,7 @@ plot(measles$date, simulated_incidence, type = "l", xlab = "time") It looks nothing like the observed measles series, but illustrates the ability to generate complex incidence patterns not present in the simple SIR model without radial basis functions and waning immunity. -```{r rbf_model, echo=FALSE} +```{r rbf_model, eval=FALSE, echo=TRUE} simulator = ("https://github.com" |> file.path("canmod/macpan2") |> file.path("raw/main") @@ -714,7 +714,7 @@ simulator = ("https://github.com" We modify the simulation object to be able to fit to the measles data. -```{r rbf_modify, eval=FALSE} +```{r rbf_modify, eval=TRUE} simulator$add$matrices( reports = measles$cases , log_lik = empty_matrix @@ -752,12 +752,12 @@ simulator$optimize$nlminb(control = list(eval.max = 10000, iter.max = 10000, tra -```{r rbf_opt_get} +```{r rbf_opt_get, eval = FALSE} simulator$optimization_history$get()[[3]] ## the 3 is there because we tried two other times ``` Here the red data are fitted and black observed. -```{r plot_rbf_res, fig.width=6} +```{r plot_rbf_res, fig.width=6, eval = FALSE} simulated_incidence = filter(simulator$report(.phases = "during"), matrix == "incidence")$value plot(measles$date, measles$cases, xlab = "time", type = "l") lines(measles$date, simulated_incidence, col = 2) diff --git a/vignettes/hello_products.Rmd b/vignettes/hello_products.Rmd index eb3ba836..1c39f23b 100644 --- a/vignettes/hello_products.Rmd +++ b/vignettes/hello_products.Rmd @@ -24,9 +24,9 @@ knitr::opts_chunk$set( cat_file = function(...) { cat(readLines(file.path(...)), sep = "\n") } -prod_dir = file.path("..", "inst", "starter_models", "SI_products", "hello_products") -si_dir = file.path("..", "inst", "starter_models", "SI_products", "hello_si") -age_dir = file.path("..", "inst", "starter_models", "SI_products", "hello_age") +prod_dir = system.file("starter_models", "SI_products", "hello_products", package = "macpan2") +si_dir = system.file("starter_models", "SI_products", "hello_si", package = "macpan2") +age_dir = system.file("starter_models", "SI_products", "hello_age", package = "macpan2") ``` ## Read Models @@ -164,6 +164,8 @@ prod$flows() ## Combining the Derivations + + ## Simulating the Resulting Product Model diff --git a/vignettes/model_definitions.Rmd b/vignettes/model_definitions.Rmd index fe6f06b5..c3cc57e8 100644 --- a/vignettes/model_definitions.Rmd +++ b/vignettes/model_definitions.Rmd @@ -27,7 +27,15 @@ knitr::opts_chunk$set( ``` ```{r, echo = FALSE} -model_repo = "../inst/starter_models" +print("GETWD!!") +print(getwd()) +if (macpan2:::dev_in_root()) { + model_repo = "inst/starter_models" +} else if (macpan2:::dev_in_vig()) { + model_repo = "../inst/starter_models" +} else { + stop("dunno") +} make_model_files = function(path){ try(ModelFiles(path), silent = TRUE) } diff --git a/vignettes/quickstart2.Rmd b/vignettes/quickstart2.Rmd index 83914c21..9beecdd0 100644 --- a/vignettes/quickstart2.Rmd +++ b/vignettes/quickstart2.Rmd @@ -7,7 +7,7 @@ vignette: > %\VignetteEncoding{UTF-8} --- -[![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) +[![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) ```{r, include = FALSE} knitr::opts_chunk$set( From b77a7ccf3978ec56782d4d8903e8c83fe729b243 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 7 Sep 2023 13:43:07 -0400 Subject: [PATCH 008/332] feels like it is time to add methods to parse table now --- man/BinaryOperator.Rd | 6 +- misc/dev/dev.cpp | 225 +++++++++++--------------- src/macpan2.cpp | 279 ++++++++++++++++++++++---------- vignettes/model_definitions.Rmd | 2 - 4 files changed, 294 insertions(+), 218 deletions(-) diff --git a/man/BinaryOperator.Rd b/man/BinaryOperator.Rd index ede79ff8..dd428059 100644 --- a/man/BinaryOperator.Rd +++ b/man/BinaryOperator.Rd @@ -15,7 +15,11 @@ A binary operator consistent with the \code{C++} engine. \description{ Convert a function that represents an elementwise binary operator into one that is consistent with the \code{C++} -engine. +engine. This function is intended to clarify how \pkg{macpan2} +treats binary operators, which is a little different from +base \proglang{R}. The difference is clarified in +\code{vignette("binary_operator")}, and \code{BinaryOperator} is +primarily used as a resource for that vignette. } \examples{ set.seed(1L) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index e365da7b..1dfa1cbc 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -195,8 +195,7 @@ struct ListOfMatrices { } } - ListOfMatrices() { // Default Constructor - } + ListOfMatrices() {} // Default constructor // Copy constructor ListOfMatrices(const ListOfMatrices& another) { @@ -208,40 +207,32 @@ struct ListOfMatrices { m_matrices = another.m_matrices; return *this; } -}; + // Square bracket operator to get more than one matrix back + ListOfMatrices operator[](const std::vector& indices) const { + ListOfMatrices result; -// struct ListOfIntVecs { -// -// vector > v_vectors; -// -// ListOfIntVecs(SEXP ii) { -// int n = length(ii); -// vector > vs(n); -// v_vectors = vs; -// -// for (int i = 0; i < n; i++) { -// v_vectors[i] = asVector(VECTOR_ELT(ii, i)); -// } -// } -// ListOfIntVecs() { -// } -// -// ListOfIntVecs(const ListOfIntVecs& another) { -// v_vectors = another.v_vectors; -// } -// -// ListOfIntVecs & operator=(const ListOfIntVecs& another) { -// v_vectors = another.v_vectors; -// return *this; -// } -// }; + for (int index : indices) { + if (index >= 0 && index < m_matrices.size()) { + result.m_matrices.push_back(m_matrices[index]); + } else { + // Handle out-of-range index or negative index as needed + throw std::out_of_range("Index out of range"); + } + } + + return result; + } +}; class ListOfIntVecs { public: std::vector> nestedVector; + // Default constructor + ListOfIntVecs() {} + // Constructor that takes all_ints and vec_lens vectors ListOfIntVecs(const std::vector& all_ints, const std::vector& vec_lens) { size_t totalElements = 0; @@ -279,23 +270,20 @@ class ListOfIntVecs { return nestedVector.size(); } - // Member function to filter the ListOfIntVecs by indices in a vector object - ListOfIntVecs filterByIndices(const std::vector& i) const { - std::vector filtered_all_ints; - std::vector filtered_vec_lens; + // Square bracket operator with a vector argument + ListOfIntVecs operator[](const std::vector& indices) const { + ListOfIntVecs result; - for (int index : i) { + for (int index : indices) { if (index >= 0 && static_cast(index) < nestedVector.size()) { - const std::vector& innerVector = nestedVector[static_cast(index)]; - filtered_all_ints.insert(filtered_all_ints.end(), innerVector.begin(), innerVector.end()); - filtered_vec_lens.push_back(innerVector.size()); + result.nestedVector.push_back(nestedVector[static_cast(index)]); } else { // Handle out-of-range index or negative index as needed throw std::out_of_range("Index out of range"); } } - return ListOfIntVecs(filtered_all_ints, filtered_vec_lens); + return result; } }; @@ -304,11 +292,42 @@ class ListOfIntVecs { template class ExprEvaluator { +private: + vector mats_save_hist; + vector table_x; + vector table_n; + vector table_i; + vector meth_type_id; // vector over user defined methods, identifying a type of method + ListOfIntVecs meth_mats; + ListOfIntVecs meth_int_vecs; + ListOfIntVecs valid_int_vecs; + vector valid_literals; public: // constructor - ExprEvaluator() { + ExprEvaluator( + vector& mats_save_hist_, + vector& table_x_, + vector& table_n_, + vector& table_i_, + vector& meth_type_id_, + ListOfIntVecs& meth_mats_, + ListOfIntVecs& meth_int_vecs_, + ListOfIntVecs& valid_int_vecs_, + vector& valid_literals_ + + ) { error_code = 0; // non-zero means error has occurred; otherwise, no error expr_row = 0; + mats_save_hist = mats_save_hist_; + table_x = table_x_; + table_n = table_n_; + table_i = table_i_; + meth_type_id = meth_type_id_; + meth_mats = meth_mats_; + meth_int_vecs = meth_int_vecs_; + valid_int_vecs = valid_int_vecs_; + valid_literals = valid_literals_; + strcpy(error_message, "OK"); }; @@ -328,21 +347,13 @@ class ExprEvaluator { // evaluators matrix EvalExpr( - const vector >& hist, - int t, - const vector& mats_save_hist, - const vector& table_x, - const vector& table_n, - const vector& table_i, - const vector& meth_type_id, // vector over user defined methods, identifying a type of method - ListOfIntVecs& meth_mats, - ListOfIntVecs& meth_int_vecs, - ListOfIntVecs& valid_int_vecs, - ListOfMatrices& valid_vars, - const vector& valid_literals, - int row = 0 + const vector >& hist, // current simulation history + int t, // current time step + ListOfMatrices& valid_vars, // current list of values of each matrix + int row = 0 // current expression parse table row being evaluated ) { + // Variables to use locally in function bodies matrix m, m1, m2; // return values vector v; @@ -356,7 +367,7 @@ class ExprEvaluator { case -2: // methods (pre-processed matrices) std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; curr_meth_id = table_x[row]+1; - v = meth_mats.size(); + // v = meth_mats.size(); // vector > int_vec_args(meth_int_vecs.v_vectors.size()); // for (int i=0; i > args(n); vector index2mats(n); for (int i=0; i::operator() () ////////////////////////////////// // Define an expression evaluator - ExprEvaluator exprEvaluator; + ExprEvaluator exprEvaluator( + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + meth_type_id, + meth_mats, + meth_int_vecs, + const_int_vecs, + literals + ); + ExprEvaluator objFunEvaluator( + mats_save_hist, // this seems odd given that objective functions can't access history + o_table_x, + o_table_n, + o_table_i, + meth_type_id, + meth_mats, + meth_int_vecs, + const_int_vecs, + literals + ); ////////////////////////////////// // 3 Pre-simulation @@ -2209,32 +2239,14 @@ Type objective_function::operator() () matrix result; if (expr_sim_block[i]==1) { SIMULATE { - result = exprEvaluator.EvalExpr( - simulation_history, - 0, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - p_table_row + result = exprEvaluator.EvalExpr( + simulation_history, 0, mats, p_table_row ); } } else - result = exprEvaluator.EvalExpr( - simulation_history, - 0, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - p_table_row + result = exprEvaluator.EvalExpr( + simulation_history, 0, mats, p_table_row ); if (exprEvaluator.GetErrorCode()) { @@ -2274,31 +2286,13 @@ Type objective_function::operator() () if (expr_sim_block[i]==1) { SIMULATE { result = exprEvaluator.EvalExpr( - simulation_history, - k+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - p_table_row2 + simulation_history, k+1, mats, p_table_row2 ); } } else result = exprEvaluator.EvalExpr( - simulation_history, - k+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - p_table_row2 + simulation_history, k+1, mats, p_table_row2 ); if (exprEvaluator.GetErrorCode()) { @@ -2338,31 +2332,13 @@ Type objective_function::operator() () if (expr_sim_block[i]==1) { SIMULATE { result = exprEvaluator.EvalExpr( - simulation_history, - time_steps+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - p_table_row + simulation_history, time_steps+1, mats, p_table_row ); } } else result = exprEvaluator.EvalExpr( - simulation_history, - time_steps+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - p_table_row + simulation_history, time_steps+1, mats, p_table_row ); if (exprEvaluator.GetErrorCode()) { @@ -2457,18 +2433,7 @@ Type objective_function::operator() () // 7 Calc the return of the objective function matrix ret; - ret = exprEvaluator.EvalExpr( - simulation_history, - time_steps+2, - mats_save_hist, - o_table_x, - o_table_n, - o_table_i, - meth_type_id, meth_mats, meth_int_vecs, const_int_vecs, - mats, - literals, - 0 - ); + ret = objFunEvaluator.EvalExpr(simulation_history, time_steps+2, mats, 0); if (exprEvaluator.GetErrorCode()) { REPORT_ERROR; diff --git a/src/macpan2.cpp b/src/macpan2.cpp index d56e5b80..e85032de 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -109,6 +109,10 @@ enum macpan2_func { //, MP2_LOGIT = 46 // fwrap,null: logit(x) }; +enum macpan2_meth { + METH_ROW_EXTRACT = 1 +}; + // Helper function template int CheckIndices( @@ -192,8 +196,7 @@ struct ListOfMatrices { } } - ListOfMatrices() { // Default Constructor - } + ListOfMatrices() {} // Default constructor // Copy constructor ListOfMatrices(const ListOfMatrices& another) { @@ -205,15 +208,127 @@ struct ListOfMatrices { m_matrices = another.m_matrices; return *this; } + + // Square bracket operator to get more than one matrix back + ListOfMatrices operator[](const std::vector& indices) const { + ListOfMatrices result; + + for (int index : indices) { + if (index >= 0 && index < m_matrices.size()) { + result.m_matrices.push_back(m_matrices[index]); + } else { + // Handle out-of-range index or negative index as needed + throw std::out_of_range("Index out of range"); + } + } + + return result; + } }; + +class ListOfIntVecs { +public: + std::vector> nestedVector; + + // Default constructor + ListOfIntVecs() {} + + // Constructor that takes all_ints and vec_lens vectors + ListOfIntVecs(const std::vector& all_ints, const std::vector& vec_lens) { + size_t totalElements = 0; + for (int size : vec_lens) { + totalElements += size; + } + + if (all_ints.size() != totalElements) { + // Handle mismatched sizes as needed + throw std::invalid_argument("all_ints and vec_lens sizes do not match."); + } + + size_t xIndex = 0; + for (int size : vec_lens) { + std::vector innerVector; + for (int i = 0; i < size; ++i) { + innerVector.push_back(all_ints[xIndex++]); + } + nestedVector.push_back(innerVector); + } + } + + // Overload [] operator to access the inner vectors by index + std::vector& operator[](size_t index) { + if (index < nestedVector.size()) { + return nestedVector[index]; + } else { + // Handle out-of-range access here (you can throw an exception or handle it as needed) + throw std::out_of_range("Index out of range"); + } + } + + // Method to return the number of vector objects in the struct + size_t size() const { + return nestedVector.size(); + } + + // Square bracket operator with a vector argument + ListOfIntVecs operator[](const std::vector& indices) const { + ListOfIntVecs result; + + for (int index : indices) { + if (index >= 0 && static_cast(index) < nestedVector.size()) { + result.nestedVector.push_back(nestedVector[static_cast(index)]); + } else { + // Handle out-of-range index or negative index as needed + throw std::out_of_range("Index out of range"); + } + } + + return result; + } +}; + + + + template class ExprEvaluator { +private: + vector mats_save_hist; + vector table_x; + vector table_n; + vector table_i; + vector meth_type_id; // vector over user defined methods, identifying a type of method + ListOfIntVecs meth_mats; + ListOfIntVecs meth_int_vecs; + ListOfIntVecs valid_int_vecs; + vector valid_literals; public: // constructor - ExprEvaluator() { + ExprEvaluator( + vector& mats_save_hist_, + vector& table_x_, + vector& table_n_, + vector& table_i_, + vector& meth_type_id_, + ListOfIntVecs& meth_mats_, + ListOfIntVecs& meth_int_vecs_, + ListOfIntVecs& valid_int_vecs_, + vector& valid_literals_ + + ) { error_code = 0; // non-zero means error has occurred; otherwise, no error expr_row = 0; + mats_save_hist = mats_save_hist_; + table_x = table_x_; + table_n = table_n_; + table_i = table_i_; + meth_type_id = meth_type_id_; + meth_mats = meth_mats_; + meth_int_vecs = meth_int_vecs_; + valid_int_vecs = valid_int_vecs_; + valid_literals = valid_literals_; + strcpy(error_message, "OK"); }; @@ -233,40 +348,56 @@ class ExprEvaluator { // evaluators matrix EvalExpr( - const vector >& hist, - int t, - const vector& mats_save_hist, - const vector& table_x, - const vector& table_n, - const vector& table_i, - ListOfMatrices& valid_vars, - const vector& valid_literals, - int row = 0 + const vector >& hist, // current simulation history + int t, // current time step + ListOfMatrices& valid_vars, // current list of values of each matrix + int row = 0 // current expression parse table row being evaluated ) { + // Variables to use locally in function bodies matrix m, m1, m2; // return values + vector v; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars - int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2; + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2, curr_meth_id; if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. switch (table_n[row]) { + case -2: // methods (pre-processed matrices) + std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; + curr_meth_id = table_x[row]+1; + v = meth_mats.size(); + + // vector > int_vec_args(meth_int_vecs.v_vectors.size()); + // for (int i=0; i::Zero(v.size(), m.cols()); + // for (int i=0; i::Zero(1,1); m.coeffRef(0,0) = valid_literals[table_x[row]]; return m; - case 0: + case 0: // matrices m = valid_vars.m_matrices[table_x[row]]; return m; - default: + default: // expressions int n = table_n[row]; vector > args(n); vector index2mats(n); for (int i=0; i::operator() () // Literals DATA_VECTOR(literals); + // Methods + DATA_IVECTOR(meth_type_id); + // DATA_IVECTOR(meth_id); + DATA_IVECTOR(meth_n_mats); + DATA_IVECTOR(meth_n_int_vecs); + DATA_IVECTOR(meth_mat_id); + DATA_IVECTOR(meth_int_vec_id); + + // Constant Integer Vectors + DATA_IVECTOR(const_int_vec); + DATA_IVECTOR(const_n_int_vecs); + // Objective function parse table DATA_IVECTOR(o_table_n); DATA_IVECTOR(o_table_x); @@ -2055,10 +2198,34 @@ Type objective_function::operator() () ); + ListOfIntVecs const_int_vecs(const_int_vec, const_n_int_vecs); + ListOfIntVecs meth_mats(meth_mat_id, meth_n_mats); + ListOfIntVecs meth_int_vecs(meth_int_vec_id, meth_n_int_vecs); ////////////////////////////////// // Define an expression evaluator - ExprEvaluator exprEvaluator; + ExprEvaluator exprEvaluator( + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + meth_type_id, + meth_mats, + meth_int_vecs, + const_int_vecs, + literals + ); + ExprEvaluator objFunEvaluator( + mats_save_hist, // this seems odd given that objective functions can't access history + o_table_x, + o_table_n, + o_table_i, + meth_type_id, + meth_mats, + meth_int_vecs, + const_int_vecs, + literals + ); ////////////////////////////////// // 3 Pre-simulation @@ -2073,30 +2240,14 @@ Type objective_function::operator() () matrix result; if (expr_sim_block[i]==1) { SIMULATE { - result = exprEvaluator.EvalExpr( - simulation_history, - 0, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - mats, - literals, - p_table_row + result = exprEvaluator.EvalExpr( + simulation_history, 0, mats, p_table_row ); } } else - result = exprEvaluator.EvalExpr( - simulation_history, - 0, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - mats, - literals, - p_table_row + result = exprEvaluator.EvalExpr( + simulation_history, 0, mats, p_table_row ); if (exprEvaluator.GetErrorCode()) { @@ -2136,29 +2287,13 @@ Type objective_function::operator() () if (expr_sim_block[i]==1) { SIMULATE { result = exprEvaluator.EvalExpr( - simulation_history, - k+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - mats, - literals, - p_table_row2 + simulation_history, k+1, mats, p_table_row2 ); } } else result = exprEvaluator.EvalExpr( - simulation_history, - k+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - mats, - literals, - p_table_row2 + simulation_history, k+1, mats, p_table_row2 ); if (exprEvaluator.GetErrorCode()) { @@ -2198,29 +2333,13 @@ Type objective_function::operator() () if (expr_sim_block[i]==1) { SIMULATE { result = exprEvaluator.EvalExpr( - simulation_history, - time_steps+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - mats, - literals, - p_table_row + simulation_history, time_steps+1, mats, p_table_row ); } } else result = exprEvaluator.EvalExpr( - simulation_history, - time_steps+1, - mats_save_hist, - p_table_x, - p_table_n, - p_table_i, - mats, - literals, - p_table_row + simulation_history, time_steps+1, mats, p_table_row ); if (exprEvaluator.GetErrorCode()) { @@ -2315,17 +2434,7 @@ Type objective_function::operator() () // 7 Calc the return of the objective function matrix ret; - ret = exprEvaluator.EvalExpr( - simulation_history, - time_steps+2, - mats_save_hist, - o_table_x, - o_table_n, - o_table_i, - mats, - literals, - 0 - ); + ret = objFunEvaluator.EvalExpr(simulation_history, time_steps+2, mats, 0); if (exprEvaluator.GetErrorCode()) { REPORT_ERROR; diff --git a/vignettes/model_definitions.Rmd b/vignettes/model_definitions.Rmd index c3cc57e8..2081258f 100644 --- a/vignettes/model_definitions.Rmd +++ b/vignettes/model_definitions.Rmd @@ -27,8 +27,6 @@ knitr::opts_chunk$set( ``` ```{r, echo = FALSE} -print("GETWD!!") -print(getwd()) if (macpan2:::dev_in_root()) { model_repo = "inst/starter_models" } else if (macpan2:::dev_in_vig()) { From ab7b114fbc02515cf7c8c8060d94a4ab6f20fdb4 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 7 Sep 2023 15:06:13 -0400 Subject: [PATCH 009/332] prep for adding methods and constants to expression parsing --- NAMESPACE | 1 + R/basis_functions.R | 5 +++++ R/engine_methods.R | 9 +++++---- R/parse_expr.R | 36 ++++++++++++++++++++++++++++++++---- man/Method.Rd | 8 ++++---- man/parse_expr_list.Rd | 19 ++++++++++++++++--- man/rbf.Rd | 6 +++++- src/macpan2.cpp | 2 +- 8 files changed, 69 insertions(+), 17 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 94fb9d93..7c922640 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -42,6 +42,7 @@ export(MathExpressionFromFunc) export(MathExpressionFromStrings) export(MatsList) export(MethList) +export(Method) export(Model) export(ModelCollection) export(ModelFiles) diff --git a/R/basis_functions.R b/R/basis_functions.R index c7ed0a37..605b9e3f 100644 --- a/R/basis_functions.R +++ b/R/basis_functions.R @@ -1,9 +1,14 @@ #' Radial Basis Functions #' +#' Compute a set of radial basis functions (`dimension` of them). +#' #' @param time_steps number of time steps in the model #' @param dimension number of gaussians in the basis #' @param scale width of the gaussians #' +#' @examples +#' matplot(rbf(100, 5), type = "l") +#' #' @export rbf = function(time_steps, dimension, scale = time_steps / dimension) { s = scale diff --git a/R/engine_methods.R b/R/engine_methods.R index 15bd768a..66854bba 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -49,10 +49,11 @@ MethList = function(...) { #' for computing matrices from other matrices and constants. #' #' @param name Method name. -#' @param mat_args List of names of matrices that will be used by the method -#' to produce an output matrix. -#' @param const_args List of names of constants that will be used by the method -#' to produce an output matrix. +#' @param mat_args Character vector of names of matrices that will be used by +#' the method to produce an output matrix. +#' @param const_args Character vector of names of constants that will be used +#' by the method to produce an output matrix. +#' @export Method = function(name, mat_args, const_args) { self = Base() diff --git a/R/parse_expr.R b/R/parse_expr.R index 94b72be9..f2a50b66 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -58,6 +58,7 @@ make_expr_parser = function( # convert a formula to the initial state of a list that could be # recursively parsed using parse_expr formula_to_parsing_list = function(x) { + if (interactive()) browser() stopifnot( "formulas are the only calls that can be parsed" = as.character(x[[1]]) == '~' @@ -275,13 +276,22 @@ empty_matrix = matrix(numeric(0L), 0L, 0L) #' Parse Expression List #' #' Parse a list of one-sided formulas representing expressions -#' in a compartmental model. +#' in a compartmental model. All parsed expressions in the +#' output list will share an environment. +#' this environment contains the same set of +#' functions, variables (aka matrices), +#' literals, methods, and offset. these components are key +#' to the definition of the model and therefore should be common for each +#' expression in the model. #' #' @param expr_list List of one-sided formulas. #' @param valid_vars Named list of numerical matrices that can #' be referred to in the formulas. -#' @param valid_literals An optional existing numeric vector of valid literals -#' from a related expression list. +#' @param valid_literals An optional numeric vector of initial valid literals +#' from a related expression list. Additional literals in the expressions +#' themselves will be discovered and added to this list. +#' @param valid_methods \code{\link{MethList}} object. +#' @param valid_int_vecs \code{\link{ConstIntVecs}} object. #' @param offset The zero-based row index for the first row of the table. #' This is useful when combining tables. #' @@ -289,14 +299,32 @@ empty_matrix = matrix(numeric(0L), 0L, 0L) parse_expr_list = function(expr_list , valid_vars , valid_literals = numeric(0L) + , valid_methods = MethList() + , valid_int_vecs = ConstIntVecs() , offset = 0L ) { - eval_env = list2env(nlist(valid_funcs, valid_vars, valid_literals, offset)) + eval_env = nlist( + valid_funcs, valid_vars, valid_literals, + valid_methods, valid_int_vecs, offset + ) |> list2env() pe_list = list() for (i in seq_along(expr_list)) { + + ## all expressions should share an environment. + ## this environment contains the same set of + ## functions, variables (aka matrices), + ## literals, methods, and offset. these components + ## define the model environment(expr_list[[i]]) = eval_env + + ## do the parsing pe_list[[i]] = parse_expr(expr_list[[i]]) + + ## grow valid literals as they are discovered in the expressions eval_env$valid_literals = pe_list[[i]]$valid_literals + + ## bump the row-index offset to prepare for the next expression + ## to be parsed eval_env$offset = eval_env$offset + nrow(pe_list[[i]]$parse_table) } p_tables = lapply(pe_list, getElement, "parse_table") diff --git a/man/Method.Rd b/man/Method.Rd index a2e39210..c02d36e4 100644 --- a/man/Method.Rd +++ b/man/Method.Rd @@ -9,11 +9,11 @@ Method(name, mat_args, const_args) \arguments{ \item{name}{Method name.} -\item{mat_args}{List of names of matrices that will be used by the method -to produce an output matrix.} +\item{mat_args}{Character vector of names of matrices that will be used by +the method to produce an output matrix.} -\item{const_args}{List of names of constants that will be used by the method -to produce an output matrix.} +\item{const_args}{Character vector of names of constants that will be used +by the method to produce an output matrix.} } \description{ Engine methods allow users to interact with the engine diff --git a/man/parse_expr_list.Rd b/man/parse_expr_list.Rd index d7091c4e..c1272378 100644 --- a/man/parse_expr_list.Rd +++ b/man/parse_expr_list.Rd @@ -8,6 +8,8 @@ parse_expr_list( expr_list, valid_vars, valid_literals = numeric(0L), + valid_methods = MethList(), + valid_int_vecs = ConstIntVecs(), offset = 0L ) } @@ -17,13 +19,24 @@ parse_expr_list( \item{valid_vars}{Named list of numerical matrices that can be referred to in the formulas.} -\item{valid_literals}{An optional existing numeric vector of valid literals -from a related expression list.} +\item{valid_literals}{An optional numeric vector of initial valid literals +from a related expression list. Additional literals in the expressions +themselves will be discovered and added to this list.} + +\item{valid_methods}{\code{\link{MethList}} object.} + +\item{valid_int_vecs}{\code{\link{ConstIntVecs}} object.} \item{offset}{The zero-based row index for the first row of the table. This is useful when combining tables.} } \description{ Parse a list of one-sided formulas representing expressions -in a compartmental model. +in a compartmental model. All parsed expressions in the +output list will share an environment. +this environment contains the same set of +functions, variables (aka matrices), +literals, methods, and offset. these components are key +to the definition of the model and therefore should be common for each +expression in the model. } diff --git a/man/rbf.Rd b/man/rbf.Rd index 806737ad..5b7d5a02 100644 --- a/man/rbf.Rd +++ b/man/rbf.Rd @@ -14,5 +14,9 @@ rbf(time_steps, dimension, scale = time_steps/dimension) \item{scale}{width of the gaussians} } \description{ -Radial Basis Functions +Compute a set of radial basis functions (\code{dimension} of them). +} +\examples{ +matplot(rbf(100, 5), type = "l") + } diff --git a/src/macpan2.cpp b/src/macpan2.cpp index e85032de..ab73ee19 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -368,7 +368,7 @@ class ExprEvaluator { case -2: // methods (pre-processed matrices) std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; curr_meth_id = table_x[row]+1; - v = meth_mats.size(); + // v = meth_mats.size(); // vector > int_vec_args(meth_int_vecs.v_vectors.size()); // for (int i=0; i Date: Fri, 8 Sep 2023 20:19:36 -0400 Subject: [PATCH 010/332] dev docs and simplifying data_arg methods --- R/expr_list.R | 226 ++++++++++++++++++++++ R/parse_expr.R | 4 +- R/tmb_model.R | 296 ++++++----------------------- man/ExprList.Rd | 2 +- misc/dev/dev.cpp | 3 +- src/macpan2.cpp | 3 +- tests/testthat/test-tmb-model.R | 3 + vignettes/development_patterns.Rmd | 289 +++++++++++++++++++++++----- 8 files changed, 537 insertions(+), 289 deletions(-) create mode 100644 R/expr_list.R diff --git a/R/expr_list.R b/R/expr_list.R new file mode 100644 index 00000000..82657022 --- /dev/null +++ b/R/expr_list.R @@ -0,0 +1,226 @@ +ExprListUtils = function() { + self = Base() + self$.mat_names = function(...) { + as.character(unlist(list(...))) + } + self$.init_valid_vars = function() { + initial_valid_vars(names(self$init_mats)) + } + self$.parsed_expr_list = function(... + , .existing_literals = numeric(0L) + , .offset = 0L + ) { + parse_expr_list(self$.all_rhs(self$expr_list()) + , valid_vars = self$.init_valid_vars() + , valid_literals = .existing_literals + , offset = .offset + ) + } + self$.set_name_prefix = function(x, prefix) { + setNames(x, paste(prefix, names(x), sep = "")) + } + self$.does_assign = function(x) { + raw_lhs = self$.lhs(x) + } + self$.lhs = function(x) { + as.character(x[[2L]]) + } + self$.rhs = function(x) { + if (length(x) == 3L) e = x[c(1L, 3L)] else e = x + e + } + self$.all_lhs = function(x) vapply(x, self$.lhs, character(1L)) + self$.all_rhs = function(x) lapply(x, self$.rhs) + return_object(self, "ExprListUtils") +} + +#' Expression List +#' +#' Create a list of expressions for defining a compartmental model in TMB. +#' +#' @param before List of formulas to be evaluated in the order provided before +#' the simulation loop begins. Each \code{\link{formula}} must have a left hand +#' side that gives the name of the matrix being updated, and a right hand side +#' giving an expression containing only the names of matrices in the model, +#' functions defined in \code{macpan2.cpp}, and numerical literals (e.g. +#' \code{3.14}). The available functions are described in +#' \code{\link{engine_functions}}. Names can be provided for the components of +#' \code{before}, and these names do not have to be unique. These names are +#' used by the \code{.simulate_exprs} argument. +#' @param during List of formulas to be evaluated at every iteration of the +#' simulation loop, with the same rules as \code{before}. +#' @param after List of formulas to be evaluated after the simulation loop, +#' with the same rules as \code{before}. +#' @param .simulate_exprs Character vector of names of expressions to be +#' evaluated within TMB simulate blocks. This is useful when an expression +#' cannot be evaluated during the computation of the objective function and +#' its gradients (e.g. if the expression contains randomness or other +#' discontinuities that will break the automatic differentiation machinery +#' of TMB). +#' +#' @return Object of class \code{ExprList} with the following methods. +#' +#' ## Methods +#' +#' * `$data_arg(...)`: Return the following components of the data structure +#' to pass to C++. +#' * `expr_output_id` -- Indices into the list of matrices identifying the +#' matrix being produced. +#' * `expr_sim_block` -- Identified whether or not the expression should be +#' evaluated inside a simulate macro within TMB. +#' * `expr_num_p_table_rows` -- Number of rows associated with each +#' expression in the parse table (`p_table_*`) +#' * `eval_schedule` -- Vector giving the number of expressions to evaluate +#' in each phase (before, during, or after) of the simulation. +#' * `p_table_x` -- Parse table column giving an index for looking up either +#' function, matrix, or literal. +#' * `p_table_n` -- Parse table column giving the number of arguments in +#' functions. +#' * `p_table_i` -- Parse table column giving indices for looking up the +#' rows in the parse table corresponding with the first argument of the +#' function. +#' +#' ## Method Arguments +#' +#' * `...`: Character vector containing the names of the matrices in the model. +#' +#' +#' @importFrom oor method_apply +#' @export +ExprList = function( + before = list() + , during = list() + , after = list() + , .simulate_exprs = character(0L) + ) { + self = ExprListUtils() + lhs = function(x) x[[2L]] + valid_expr_list = ValidityMessager( + All( + is.list, ## list of ... + MappedAllTest(Is("formula")), ## ... formulas that are ... + TestPipeline(MappedSummarizer(length), MappedAllTest(TestRange(3L, 3L))), ## ... two-sided formula + TestPipeline(MappedSummarizer(lhs, is.symbol), MappedAllTest(TestTrue())) ## ... only one symbol on the lhs + ), + "Model expressions must be two-sided assignment formulas,", + "without subsetting on the left-hand-side", + "(i.e. x ~ 1 is fine, but x[0] ~ 1 is not)." + ) + + ## Args + self$before = valid_expr_list$assert(before) + self$during = valid_expr_list$assert(during) + self$after = valid_expr_list$assert(after) + self$.simulate_exprs = valid$char$assert(.simulate_exprs) + + self$expr_list = function() unname(c(self$before, self$during, self$after)) + self$expr_nms = function() { + nms = names(c(self$before, self$during, self$after)) + if (is.null(nms)) nms = rep("", length(self$expr_list())) + nms + } + + self$.eval_schedule = function() { + c(length(self$before), length(self$during), length(self$after)) + } + + self$.expr_sim_block = function() { + as.integer(self$expr_nms() %in% self$.simulate_exprs) + } + + self$.expr_output_id = function() { + all_names = names(self$init_mats) + output_names = valid$engine_outputs(all_names)$assert( + self$.all_lhs(self$expr_list()) + ) + m = match(output_names, all_names) + if (any(is.na(m))) { + stop( + "\nThe following updated variables are not " + ) + } + as.integer(m - 1L) + } + self$.expr_num_p_table_rows = function(...) { + self$.parsed_expr_list(...)$num_p_table_rows + } + + ## list of three equal length integer vectors + ## p_table_x, p_table_n, p_table_i + self$.parse_table = function(...) { + l = as.list(self$.parsed_expr_list(...)$parse_table[c("x", "n", "i")]) + self$.set_name_prefix(l, "p_table_") + } + self$.literals = function(...) { + self$.parsed_expr_list(...)$valid_literals + } + + self$data_arg = function() { + expr_output_id = self$.expr_output_id() + r = c( + list( + expr_output_id = as.integer(expr_output_id), + expr_sim_block = as.integer(self$.expr_sim_block()), + expr_num_p_table_rows = as.integer(self$.expr_num_p_table_rows()), + eval_schedule = as.integer(self$.eval_schedule()) + ), + self$.parse_table() + ) + valid$expr_arg$assert(r) + } + self$insert = function(... + , .at = 1L + , .phase = c("before", "during", "after") + , .simulate_exprs = character(0L) + ) { + .phase = match.arg(.phase) + input = list(before = self$before, during = self$during, after = self$after) + input[[.phase]] = append(input[[.phase]], list(...), after = .at - 1L) + input$.simulate_exprs = unique(c(self$.simulate_exprs, .simulate_exprs)) + do.call(ExprList, input) + } + self$print_exprs = function(file = "") { + to = cumsum(self$.eval_schedule()) + from = c(0L, to[1:2]) + 1L + msgs = c( + "Before the simulation loop (t = 0):", + "At every iteration of the simulation loop (t = 1 to T):", + "After the simulation loop (t = T):" + ) + for (i in 1:3) { + if (self$.eval_schedule()[i] > 0L) { + expr_strings = lapply(self$expr_list()[from[i]:to[i]], deparse) + tab_size = nchar(self$.eval_schedule()[i]) + fmt = sprintf("%%%ii: %%s", tab_size) + tab = paste0(rep(" ", tab_size), collapse = "") + expr_n_lines = vapply(expr_strings, length, integer(1L)) + make_expr_numbers = function(s, i) { + s[1L] = sprintf(fmt, i, s[1L]) + if (length(s) > 1L) { + s[-1L] = paste(tab, s[-1L], sep = "") + } + s + } + expr_char = unlist(mapply(make_expr_numbers + , expr_strings + , seq_len(self$.eval_schedule()[i]) + , SIMPLIFY = FALSE + , USE.NAMES = FALSE + )) + lines = c( + "---------------------", + msgs[i], + "---------------------", + expr_char, + "" + ) + cat(lines, file = file, sep = "\n", append = i != 1L) + } + } + } + + # Composition + self$init_mats = MatsList() + + return_object(self, "ExprList") +} diff --git a/R/parse_expr.R b/R/parse_expr.R index f2a50b66..c3e21364 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -58,7 +58,7 @@ make_expr_parser = function( # convert a formula to the initial state of a list that could be # recursively parsed using parse_expr formula_to_parsing_list = function(x) { - if (interactive()) browser() + #if (interactive()) browser() stopifnot( "formulas are the only calls that can be parsed" = as.character(x[[1]]) == '~' @@ -226,7 +226,7 @@ get_indices = function(x, vec, vec_type, expr_as_string, zero_based = FALSE) { "\nthe expression given by:\n", expr_as_string, "\n\n", "contained the following ", vec_type, ":\n", - paste0(missing_items, collapse = " "), "\n\n", + paste0(unique(missing_items), collapse = " "), "\n\n", " that were not found in the list of available ", vec_type, ":\n", paste0(vec, collapse = " "), # TODO: smarter pasting when this list gets big pointers diff --git a/R/tmb_model.R b/R/tmb_model.R index 9e88dbca..a4c269a7 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -1,225 +1,3 @@ -ExprListUtils = function() { - self = Base() - self$.mat_names = function(...) { - as.character(unlist(list(...))) - } - self$.init_valid_vars = function(...) { - initial_valid_vars(self$.mat_names(...)) - } - self$.parsed_expr_list = function(... - , .existing_literals = numeric(0L) - , .offset = 0L - ) { - parse_expr_list(self$.all_rhs(self$expr_list()) - , valid_vars = self$.init_valid_vars(...) - , valid_literals = .existing_literals - , offset = .offset - ) - } - self$.set_name_prefix = function(x, prefix) { - setNames(x, paste(prefix, names(x), sep = "")) - } - self$.does_assign = function(x) { - raw_lhs = self$.lhs(x) - } - self$.lhs = function(x) { - as.character(x[[2L]]) - } - self$.rhs = function(x) { - if (length(x) == 3L) e = x[c(1L, 3L)] else e = x - e - } - self$.all_lhs = function(x) vapply(x, self$.lhs, character(1L)) - self$.all_rhs = function(x) lapply(x, self$.rhs) - return_object(self, "ExprListUtils") -} - -#' Expression List -#' -#' Create a list of expressions for defining a compartmental model in TMB. -#' -#' @param before List of formulas to be evaluated in the order provided before -#' the simulation loop begins. Each \code{\link{formula}} must have a left hand -#' side that gives the name of the matrix being updated, and a right hand side -#' giving an expression containing only the names of matrices in the model, -#' functions defined in \code{macpan2.cpp}, and numerical literals (e.g. -#' \code{3.14}). The available functions are described in -#' \code{\link{engine_functions}}. Names can be provided for the components of -#' \code{before}, and these names do not have to be unique. These names are -#' used by the \code{.simulate_exprs} argument. -#' @param during List of formulas to be evaluated at every iteration of the -#' simulation loop, with the same rules as \code{before}. -#' @param after List of formulas to be evaluated after the simulation loop, -#' with the same rules as \code{before}. -#' @param .simulate_exprs Character vector of names of expressions to be -#' evaluated within TMB simulate blocks. This is useful when an expression -#' cannot be evaluated during the computation of the objective function and -#' its gradients (e.g. if the expression contains randomness or other -#' discontinuities that will break the automatic differentiation machinery -#' of TMB). -#' -#' @return Object of class \code{ExprList} with the following methods. -#' -#' ## Methods -#' -#' * `$data_arg(...)`: Return the following components of the data structure -#' to pass to C++. -#' * `expr_output_id` -- Indices into the list of matrices identifying the -#' matrix being produced. -#' * `expr_sim_block` -- Identified whether or not the expression should be -#' evaluated inside a simulate macro within TMB. -#' * `expr_num_p_table_rows` -- Number of rows associated with each -#' expression in the parse table (`p_table_*`) -#' * `eval_schedule` -- Vector giving the number of expressions to evaluate -#' in each phase (before, during, or after) of the simulation. -#' * `p_table_x` -- Parse table column giving an index for looking up either -#' function, matrix, or literal. -#' * `p_table_n` -- Parse table column giving the number of arguments in -#' functions. -#' * `p_table_i` -- Parse table column giving indices for looking up the -#' rows in the parse table corresponding with the first argument of the -#' function. -#' -#' ## Method Arguments -#' -#' * `...`: Character vector containing the names of the matrices in the model. -#' -#' -#' @importFrom oor method_apply -#' @export -ExprList = function( - before = list() - , during = list() - , after = list() - , .simulate_exprs = character(0L) - ) { - self = ExprListUtils() - lhs = function(x) x[[2L]] - valid_expr_list = ValidityMessager( - All( - is.list, ## list of ... - MappedAllTest(Is("formula")), ## ... formulas that are ... - TestPipeline(MappedSummarizer(length), MappedAllTest(TestRange(3L, 3L))), ## ... two-sided formula - TestPipeline(MappedSummarizer(lhs, is.symbol), MappedAllTest(TestTrue())) ## ... only one symbol on the lhs - ), - "Model expressions must be two-sided assignment formulas,", - "without subsetting on the left-hand-side", - "(i.e. x ~ 1 is fine, but x[0] ~ 1 is not)." - ) - - ## Args - self$before = valid_expr_list$assert(before) - self$during = valid_expr_list$assert(during) - self$after = valid_expr_list$assert(after) - self$.simulate_exprs = valid$char$assert(.simulate_exprs) - - self$expr_list = function() unname(c(self$before, self$during, self$after)) - self$expr_nms = function() { - nms = names(c(self$before, self$during, self$after)) - if (is.null(nms)) nms = rep("", length(self$expr_list())) - nms - } - - self$.eval_schedule = function() { - c(length(self$before), length(self$during), length(self$after)) - } - - self$.expr_sim_block = function() { - as.integer(self$expr_nms() %in% self$.simulate_exprs) - } - - self$.expr_output_id = function(...) { - all_names = self$.mat_names(...) - output_names = valid$engine_outputs(all_names)$assert( - self$.all_lhs(self$expr_list()) - ) - m = match(output_names, all_names) - if (any(is.na(m))) { - stop( - "\nThe following updated variables are not " - ) - } - as.integer(m - 1L) - } - self$.expr_num_p_table_rows = function(...) { - self$.parsed_expr_list(...)$num_p_table_rows - } - - ## list of three equal length integer vectors - ## p_table_x, p_table_n, p_table_i - self$.parse_table = function(...) { - l = as.list(self$.parsed_expr_list(...)$parse_table[c("x", "n", "i")]) - self$.set_name_prefix(l, "p_table_") - } - self$.literals = function(...) { - self$.parsed_expr_list(...)$valid_literals - } - - self$data_arg = function(...) { - expr_output_id = self$.expr_output_id(...) - r = c( - list( - expr_output_id = as.integer(expr_output_id), - expr_sim_block = as.integer(self$.expr_sim_block()), - expr_num_p_table_rows = as.integer(self$.expr_num_p_table_rows(...)), - eval_schedule = as.integer(self$.eval_schedule()) - ), - self$.parse_table(...) - ) - valid$expr_arg$assert(r) - } - self$insert = function(... - , .at = 1L - , .phase = c("before", "during", "after") - , .simulate_exprs = character(0L) - ) { - .phase = match.arg(.phase) - input = list(before = self$before, during = self$during, after = self$after) - input[[.phase]] = append(input[[.phase]], list(...), after = .at - 1L) - input$.simulate_exprs = unique(c(self$.simulate_exprs, .simulate_exprs)) - do.call(ExprList, input) - } - self$print_exprs = function(file = "") { - to = cumsum(self$.eval_schedule()) - from = c(0L, to[1:2]) + 1L - msgs = c( - "Before the simulation loop (t = 0):", - "At every iteration of the simulation loop (t = 1 to T):", - "After the simulation loop (t = T):" - ) - for (i in 1:3) { - if (self$.eval_schedule()[i] > 0L) { - expr_strings = lapply(self$expr_list()[from[i]:to[i]], deparse) - tab_size = nchar(self$.eval_schedule()[i]) - fmt = sprintf("%%%ii: %%s", tab_size) - tab = paste0(rep(" ", tab_size), collapse = "") - expr_n_lines = vapply(expr_strings, length, integer(1L)) - make_expr_numbers = function(s, i) { - s[1L] = sprintf(fmt, i, s[1L]) - if (length(s) > 1L) { - s[-1L] = paste(tab, s[-1L], sep = "") - } - s - } - expr_char = unlist(mapply(make_expr_numbers - , expr_strings - , seq_len(self$.eval_schedule()[i]) - , SIMPLIFY = FALSE - , USE.NAMES = FALSE - )) - lines = c( - "---------------------", - msgs[i], - "---------------------", - expr_char, - "" - ) - cat(lines, file = file, sep = "\n", append = i != 1L) - } - } - } - return_object(self, "ExprList") -} #' Matrix List #' @@ -292,6 +70,7 @@ MatsList = function(... # Static self$.initial_mats = lapply(list(...), as.matrix) + if (length(self$.initial_mats) == 0L) names(self$.initial_mats) = character() self$mats_save_hist = function() names(self$.initial_mats) %in% self$.mats_to_save self$mats_return = function() names(self$.initial_mats) %in% self$.mats_to_return @@ -486,11 +265,11 @@ OptParamsList = function(... } d } - self$data_arg = function(..., .type_string = c("p", "r")) { + self$data_arg = function(.type_string = c("p", "r")) { .type_string = match.arg(.type_string) r = setNames( list(self$.par_id - , self$.mat_id(...) + , self$.mat_id(names(self$init_mats)) , self$.row_id , self$.col_id ), @@ -498,6 +277,7 @@ OptParamsList = function(... ) valid$opt_params_list_arg$assert(r) } + self$init_mats = MatsList() return_object(self, "OptParamsList") } @@ -617,18 +397,28 @@ ObjectiveFunction = function(obj_fn_expr) { ## Standard Methods self$expr_list = function() list(self$obj_fn_expr) - self$.literals = function(..., .existing_literals) { - self$.parsed_expr_list(..., .existing_literals = .existing_literals)$valid_literals - } - self$.parse_table = function(..., .existing_literals) { - l = as.list(self$.parsed_expr_list(..., .existing_literals = .existing_literals)$parse_table) + self$.literals = function(.existing_literals) { + self$.parsed_expr_list( + names(self$init_mats), + .existing_literals = .existing_literals + )$valid_literals + } + self$.parse_table = function(.existing_literals) { + l = as.list(self$.parsed_expr_list( + names(self$init_mats), + .existing_literals = .existing_literals + )$parse_table) self$.set_name_prefix(l[c("x", "n", "i")], "o_table_") } - self$data_arg = function(..., .existing_literals) { - p = self$.parse_table(..., .existing_literals = .existing_literals) - p$literals = self$.literals(..., .existing_literals = .existing_literals) + self$data_arg = function(.existing_literals) { + p = self$.parse_table(.existing_literals = .existing_literals) + p$literals = self$.literals(.existing_literals = .existing_literals) p } + + ## Composition + self$init_mats = MatsList() + return_object(self, "ObjectiveFunction") } @@ -798,15 +588,13 @@ TMBModel = function( ## Standard Methods self$data_arg = function() { - existing_literals = self$expr_list$.literals(self$init_mats$.names()) + existing_literals = self$expr_list$.literals() c( self$init_mats$data_arg(), - self$expr_list$data_arg(self$init_mats$.names()), - self$params$data_arg(self$init_mats$.names()), - self$random$data_arg(self$init_mats$.names(), .type_string = "r"), - self$obj_fn$data_arg(self$init_mats$.names() - , .existing_literals = existing_literals - ), + self$expr_list$data_arg(), + self$params$data_arg(), + self$random$data_arg("r"), + self$obj_fn$data_arg(existing_literals), self$time_steps$data_arg(), self$meth_list$data_arg(), self$const_int_vecs$data_arg() @@ -840,17 +628,47 @@ TMBModel = function( TMBSimulator(self, tmb_cpp = tmb_cpp, initialize_ad_fun = initialize_ad_fun) } + + ## Composition self$add = TMBAdder(self) self$insert = TMBInserter(self) self$print = TMBPrinter(self) self$replace = TMBReplacer(self) + + ## Refreshments + self$refresh_init_mats = function(init_mats) { + self$init_mats = init_mats + self$expr_list$init_mats = init_mats + self$obj_fn$init_mats = init_mats + self$params$init_mats = init_mats + self$random$init_mats = init_mats + } + self$refresh_expr_list = function(expr_list) { + self$expr_list = expr_list + self$refresh_init_mats(self$init_mats) + } + self$refresh_obj_fn = function(obj_fn) { + self$obj_fn = obj_fn + self$refresh_init_mats(self$init_mats) + } + self$refresh_params = function(params) { + self$params = params + self$refresh_init_mats(self$init_mats) + } + self$refresh_random = function(random) { + self$random = random + self$refresh_init_mats(self$init_mats) + } + self$refresh_init_mats(self$init_mats) + return_object( valid$tmb_model$assert(self), "TMBModel" ) } + TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { self = tmb_simulator self$compartmental_model = compartmental_model diff --git a/man/ExprList.Rd b/man/ExprList.Rd index f8bb2ec5..5ee1d645 100644 --- a/man/ExprList.Rd +++ b/man/ExprList.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R +% Please edit documentation in R/expr_list.R \name{ExprList} \alias{ExprList} \title{Expression List} diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 1dfa1cbc..58908f5d 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -2011,12 +2011,13 @@ class ExprEvaluator { #define REPORT_ERROR { \ int error = exprEvaluator.GetErrorCode(); \ int expr_row = exprEvaluator.GetExprRow(); \ + const char* err_msg = exprEvaluator.GetErrorMessage(); \ REPORT(error); \ REPORT(expr_row); \ \ logfile.open (LOG_FILE_NAME, std::ios_base::app); \ logfile << "Error code = " << error << std::endl; \ - logfile << "Error message = " << exprEvaluator.GetErrorMessage() << std::endl; \ + logfile << "Error message = " << err_msg << std::endl; \ logfile << "Expression row = " << expr_row << std::endl; \ logfile.close(); \ } diff --git a/src/macpan2.cpp b/src/macpan2.cpp index ab73ee19..b5b6190e 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -2012,12 +2012,13 @@ class ExprEvaluator { #define REPORT_ERROR { \ int error = exprEvaluator.GetErrorCode(); \ int expr_row = exprEvaluator.GetExprRow(); \ + const char* err_msg = exprEvaluator.GetErrorMessage(); \ REPORT(error); \ REPORT(expr_row); \ \ logfile.open (LOG_FILE_NAME, std::ios_base::app); \ logfile << "Error code = " << error << std::endl; \ - logfile << "Error message = " << exprEvaluator.GetErrorMessage() << std::endl; \ + logfile << "Error message = " << err_msg << std::endl; \ logfile << "Expression row = " << expr_row << std::endl; \ logfile.close(); \ } diff --git a/tests/testthat/test-tmb-model.R b/tests/testthat/test-tmb-model.R index fa154a7c..71d1530a 100644 --- a/tests/testthat/test-tmb-model.R +++ b/tests/testthat/test-tmb-model.R @@ -15,4 +15,7 @@ test_that("simulator method works", { row.names = integer(0), class = "data.frame" ) ) + + null_model = TMBModel() + null_model$init_mats$.names() }) diff --git a/vignettes/development_patterns.Rmd b/vignettes/development_patterns.Rmd index f2d4b442..7a4c1760 100644 --- a/vignettes/development_patterns.Rmd +++ b/vignettes/development_patterns.Rmd @@ -14,10 +14,190 @@ knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) +library(macpan2) +library(oor) ``` [![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) +## Introduction + +The `macpan2` package uses a custom object oriented framework. Wait ... don't leave ... it really isn't that complicated as you will see in the Basics section. + +Many object oriented programming frameworks exist for R, so why another? We wanted a more conventional object oriented system (so the standard use of S3 and S4 was not ideal) and we wanted to avoid dependencies on third-party packages (so the `R6` package was also not ideal). + +We tried to keep our system as close to basic R concepts as possible. It can be described as S3 methods plus R `environment`s. + +## Basics of the Object Oriented Framework + +### Constructing and Using Objects + +To understand `macpan2`, users and developers need to understand how to construct objects. Objects in `macpan2` have `S3` class attributes and are standard `R` `environment`s with some additional restrictions. Before looking at those restrictions, we will illustrate the basic idea with an example. + +The `macpan2` package comes with a set of `vignette("example_models", package = "macpan2")`. One simple example is an SIR model stored here. +```{r sir_path} +(sir_path = system.file("starter_models", "sir", package = "macpan2")) +``` + +This path points to a directory with the following files. +```{r sir_ls} +list.files(sir_path) +``` + +We could read in one of these files using standard R methods, but here we illustrate objects by using the `CSVReader` function. This function returns an object of class `CSVReader`. +```{r} +sir_flows_reader = CSVReader(sir_path, "flows.csv") +sir_flows_reader +``` + +We get a print out of the `S3` class of the object, which is a vector with three items. +```{r} +class(sir_flows_reader) +``` + +Why are there three items? This is the standard `S3` method of inheritance. If this doesn't make sense, it doesn't matter now. + +We also saw that this item is just an `R` `environment`. More about this later. + +The more important thing is that there is a `read` function listed. This function is a method, but importantly this is not a standard `S3` method. Why is it not an `S3` method? You guessed it, we will cover that below. + +We call methods, like the `read()` method, in the following way. + +```{r} +sir_flows_reader$read() +``` + +This reader has read in the CSV file that it is configured to read. + +This syntax might look strange to many R users, who will be used to something more like this. + +```{r} +read.csv(file.path(sir_path, "flows.csv")) +``` + +The reason why `sir_reader$read()` works without any arguments is that the path to read is stored in the `sir_reader` object. We can see (almost) everything stored in an object using the `ls` function. + +```{r} +ls(sir_flows_reader) +``` + +Here we see that, along with the `read` method, there is something else called `file`, which is just the file path that the reader is configured to read. + +```{r} +sir_flows_reader$file +``` + +Object components that are not methods are called fields. + +That's the basic idea of how to use objects in `macpan2`. Here's a summary. + +* Objects are standard `R` `environment`s +* Objects have standard `R` S3 `class`es +* Objects have fields and methods +* Object methods are functions that can make use of other object components + +### Defining Classes + +To define a class we write a function called a constructor. We have aleady seen a constructor -- the `CSVReader` function above. Let's make our own. Let's make a class that can generate sequences of numbers. + +To warm up we will create a class that does nothing and contains nothing, but which illustrates the basic boilerplate code for creating a class. + +```{r} +DoesNothing = function() { + self = Base() + return_object(self, "DoesNothing") +} +does_nothing = DoesNothing() +does_nothing +``` + +The first line in this constructor uses the `Base` function to create an `environment` called `self`. The second line sets `self`s S3 `class` to `DoesNothing` and returns this newly created S3 object. + +To make this class more interesting we store an integer as a field. + +```{r} +DoesNothing = function(n) { + self = Base() + self$n = n ## save value of the argument in the object + return_object(self, "DoesNothing") +} +does_nothing = DoesNothing(10) +does_nothing +``` + +This looks identical to the first version, but now we have stored a value for `n`. +```{r} +does_nothing$n == 10 +``` + +Finally we add a method so that we can do something, and change the name do describe what it can do. + +```{r} +SimpleSequence = function(n) { + self = Base() + self$n = n + self$generate = function() seq_len(self$n) + return_object(self, "SimpleSequence") +} +simple_sequence = SimpleSequence(10) +simple_sequence$generate() +``` + +Notice that all fields and methods stored in `self` (e.g. `self$n`) can be used in methods by using the `$` operator to extract the value of the field or method from `self`. The technical reason why this works is that the `self` `environment` is in the `environment` of every method in the `self` `environment`. In fact, the `self` `environment` is the only thing in the `environment` of each method. If this seems mind-binding, don't worry about it. + + +Those are the basics of class definitions. Here's a summary. + +* Class definitions are functions for constructing objects of that class +* The first thing to do in a class definition is create the `self` `environment` +* The last thing to do in a class definition is return the `self` `environment` as an S3 object +* In the middle of a class definition one adds methods and fields to the `self` `environment` +* The `self` `environment` is the only thing in the `environment`s of the methods in `self` (don't worry about it) + + +## Details + +### Objects + +In `macpan2`, objects are standard `R` `environment`s with the following characteristics + +### Class Definitions + +Developers can define a class by defining a standard R function that returns an instance of that class. + +We talked a bit about technical details that you shouldn't worry about in the basics of defining classes. But there is one technicality that you should worry about. Objects created in a constructor can only be used in methods if they are accessible through the `self` `environment`. So for example, the following code fails. + +```{r} +BadClass = function() { + self = Base() + x = 10 + self$f = function() x^2 + return_object(self, "BadClass") +} +try(BadClass()$f()) +``` + +This is good because it forces you to be specific about where method dependencies are coming from. What would have been worse is if the above code succeeded in the following way. + +```{r} +x = 10 +BadClass()$f() +``` + +Why did this 'work' now? It doesn't matter because you will never have this problem if you just always refer to `self` explicitly in methods. In particular, the proper approach would be the following. + +```{r} +GoodClass = function() { + self = Base() + self$x = 10 + self$f = function() self$x^2 + return_object(self, "GoodClass") +} +GoodClass()$f() +``` + +### Inheritance + ## Principles There will be trade-offs among these principles, but they are good guidelines. @@ -50,22 +230,9 @@ Here are some design patterns for complying with these principles. Alternative versions of a class have the same set of methods as the initial version. It needs change then it becomes easy to swap out one alternative for another. For example, the `Reader()` classes all have a single method -- `$read()` -- without arguments. Therefore, any bit of functionality that requires data to be read in can be modified simply by writing a new reader and swapping it in for the old one, without needing to modify any of the code that calls the `$read()` method. The methods in alternative classes should return the same type of object, but obviously the return value itself can and should vary. -### Types of Object Components - -There should only be the following types of object components. - -1. Args -- Fields storing arguments to the constructor -2. Static -- Fields storing values derived from arguments to the constructor -3. Standard methods -- Methods computing and returning values derived from arguments to the constructor -4. Composition -- Objects with further fields and methods -5. A `$refesh()` method that can be called to update #3 components in-place -6. Private methods -- Methods that should only be called by other methods in the class - -How to decide what kind of component to use for each piece of information will depend on the following considerations. +### Argument Fields -#### Argument Fields - -These are the simplest kinds of object components, and essentially behave as lists. For example here is an object with two argument fields. +These are the simplest kinds of object components, and essentially behave as lists. Argument fields store arguments to the constructor. For example here is an object with two argument fields. ```{r, eval = FALSE} A = function(x, y) { ... @@ -84,9 +251,9 @@ a$y == 20 ## TRUE Note that although it is possible to set such fields, it is not recommended. Rather one should use `$refresh()` methods as described below. -#### Static Fields +### Static Fields -Static fields are similar to argument fields, but the contain derived quantities that depend on the arguments rather than the arguments themselves. A simple example is to store the sum of two arguments in a static field. +Static fields store values derived from arguments to the constructor. Static fields are similar to argument fields, but they contain derived quantities that depend on the arguments rather than the arguments themselves. A simple example is to store the sum of two arguments in a static field. ```{r, eval = FALSE} A = function(x, y) { ... @@ -97,13 +264,13 @@ A = function(x, y) { Note that static fields may need to be updated by `$refresh()` methods. -#### Standard Methods +### Standard Methods -Standard methods should only be used if they are cheap to run, so that regeneration and consistency are balanced. But this pattern is generally the preferred option, because it is simplest to reason about and maintain because it more directly ensures consistency. +Standard methods compute and return values derived from arguments to the constructor. These methods should only be used if they are cheap to run, so that regeneration and consistency are balanced. But this pattern is generally the preferred option, because it is simplest to reason about and maintain because it more directly ensures consistency. -#### Composition +### Composition -Composition of objects and classes looks like this. +Objects can be composed of other objects. Composition of objects and classes looks like this. ```{r, eval = FALSE} A = function(...) { @@ -128,48 +295,80 @@ a$b$method(...) This keeps classes small because `B` can have methods instead of `A`, and small classes are easier to test and stabilize. Testing of `A` can focus on the methods directly in `A`, and then `A` can be extended by composing new classes like `B`. -#### Refresh Methods +### Refresh Methods + +Methods refreshing fields when shallow copies of those fields are in several composed objects ... When a field gets edited, the simplest thing to do is -Static fields should be used if they are expensive to compute. Consistency can be managed by producing static fields in a `$refresh()` method -- this is a good way to balance regeneration with consistency. This way, developers of other classes that are composed components can make use of the `$refresh()` method to reliably keep everything up-to-date and consistent. In this way static fields and `$refresh()` methods work together to balance the cost of regeneration with the necessity of consistency. -In the constructor, this `$refresh()` pattern looks like this. +#### Private Methods + +Private methods should only be used by other methods in the class. There is nothing stoping a developer or a user from calling a private method, but there is no guarantee that the private method with have consistent behaviour or even exist. To communicate privacy, private methods should start with a dot as the following example shows. ```{r, eval = FALSE} A = function(...) { ... - self$refresh = function() { - self$expensive_field_1 = f_1(...) + self$.private = function(...) {...} + ... + self$public = function(...) { + ... + self.private(...) ... - self$expensive_field_n = f_n(...) } - self$refresh() - ... - self$b = B(a) -} -... -B = function(a) { - ... - self$a = a ## an instance of `A` - ... - self$method_to_modify_a = function(...) {...} - self$a$refresh() - ... } ``` -#### Private Methods -Private methods should only be used by other methods in the class. There is nothing stoping a developer or a user from calling a private method, but there is no guarantee that the private method with have consistent behaviour or even exist. To communicate privacy, private methods should start with a dot as the following example shows. +## Object Editing + + + +## Method Caching + +Developers can manage the performance costs of computationally expensive methods through method caching. When a developer calls a cached method for the first time, it computes the result, stores it in a cache, and returns the result. Subsequent method evaluations simply retrieve the cached value, improving efficiency. Developers can ensure consistency by invalidating the cache whenever objects change, allowing them to balance the cost of regeneration with the need for consistency. ```{r, eval = FALSE} -A = function(...) { +A = function(..., method_dependency, ...) { ... - self$.private = function(...) {...} + self$method_dependency = method_dependency ... - self$public = function(...) { + self$expensive_method_1 = function() { ... - self.private(...) + } + ... + self$expensive_method_2 = function() { + ... + } + ... + self$cheap_method = function() { + ... + } + ... + self$modify_dependency = function(...) { + ... + self$cache$expensive_method_1$invalidate() + self$cache$expensive_method_2$invalidate() ... } + ... + initialize_cache(self, "expensive_method_1", "expensive_method_2") + ... } + +a = A() + +# takes time to return +a$expensive_method() + +# return immediately by returning the same value computed previously +# and stored in the cache +a$expensive_method() + +# change object and invalidate the cache to enforce consistency +a$modify_dependency(...) + +# again takes time to return, but the value is different because the +# object was modified +a$expensive_method() ``` + + From 23737e78d187d31191ecc1ca9b8b6cd8732d69bb Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Sep 2023 08:36:30 -0400 Subject: [PATCH 011/332] working towards engine updates --- R/const_int_vec.R | 26 ++ R/engine_methods.R | 2 +- R/mats_list.R | 191 +++++++++++ R/objective_function.R | 66 ++++ R/opt_params.R | 163 ++++++++++ R/time.R | 41 +++ R/tmb_model.R | 496 ----------------------------- R/tmb_model_editors.R | 28 +- vignettes/development_patterns.Rmd | 29 +- 9 files changed, 523 insertions(+), 519 deletions(-) create mode 100644 R/const_int_vec.R create mode 100644 R/mats_list.R create mode 100644 R/objective_function.R create mode 100644 R/opt_params.R create mode 100644 R/time.R diff --git a/R/const_int_vec.R b/R/const_int_vec.R new file mode 100644 index 00000000..62f8834a --- /dev/null +++ b/R/const_int_vec.R @@ -0,0 +1,26 @@ +#' Constant Integer Vectors +#' +#' Make a list of integer vectors available for engine methods. +#' +#' @param ... Named arguments, each of which can be coerced to an integer vector. +#' @export +ConstIntVecs = function(...) { + self = Base() + + # Args + self$list = lapply(list(...), as.integer) + if (length(self$list) == 0L) self$list = list(integer()) + + # Standard Methods + self$data_arg = function() { + # DATA_IVECTOR(const_int_vec); + # DATA_IVECTOR(const_n_int_vecs); + list( + const_int_vec = unlist(self$list, use.names = FALSE), + const_n_int_vecs = unlist(lapply(self$list, length), use.names = FALSE) + ) + } + self$const_names = function() names(self$list) + + return_object(self, "ConstIntVecs") +} diff --git a/R/engine_methods.R b/R/engine_methods.R index 66854bba..670a31aa 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -72,7 +72,7 @@ Method = function(name, mat_args, const_args) { self$.const_ids = function(const_names) match(self$const_args, const_names) - 1L # Standard Methods - self$data_arg = function(method_names, mat_names, const_names) { + self$data_arg = function(mat_names, const_names) { list( ## these must be length-1 integer vectors meth_type_id = self$meth_type_id diff --git a/R/mats_list.R b/R/mats_list.R new file mode 100644 index 00000000..27bbf882 --- /dev/null +++ b/R/mats_list.R @@ -0,0 +1,191 @@ +#' Matrix List +#' +#' Create a list of initial values for matrices used to define a compartmental +#' model in TMB. +#' +#' @param ... Named objects that can be coerced to numerical matrices. +#' @param .mats_to_save Character vector naming matrices to be saved at each +#' set in the simulation so that some calculations can make use of past value +#' (e.g. delayed effects) and/or to be able to retrieved the simulation +#' history after the simulation is complete. +#' @param .mats_to_return Character vector naming matrices to be returned +#' after the simulate is complete. +#' @param .dimnames Named list of \code{\link{dimnames}} for matrices that change +#' their dimensions over the simulation steps. These names correspond to the +#' names of the matrices. The output of the simulations will try their best +#' to honor these names, but if the shape of the matrix is too inconsistent +#' with the \code{\link{dimnames}} then numerical indices will be used instead. +#' For matrices that do not change their dimensions, set \code{\link{dimnames}} +#' by adding \code{\link{dimnames}} to the matrices passed to \code{...}. +#' @param .structure_labels An optional object for obtaining labels of +#' elements of special vectors and matrices. Such an object can be found in +#' the `$labels` field of a \code{\link{Compartmental}} model. Note that this +#' is an advanced technique. +#' +#' @return Object of class \code{MatsList} with the following methods. +#' +#' ## Methods +#' +#' * `$data_arg()`: Return the following components of the data structure +#' to pass to C++. +#' * `mats` -- Unnamed list of numeric matrices. +#' * `mats_save_hist` -- Boolean vector identifying which matrices should +#' have their history saved. +#' * `mats_return` -- Boolean vector identifying which matrices should be +#' returned after a simulation. +#' * `$mat_dims()`: Return a data frame giving the numbers of rows and columns +#' of each matrix in the list. +#' * `$add_mats(...)`: Add matrices to the list and return a new +#' regenerated \code{MatsList} object. +#' +#' @export +MatsList = function(... + , .mats_to_save = character(0L) + , .mats_to_return = character(0L) + , .dimnames = list() + , .structure_labels = NullLabels() + #, .init_saved_dims = list() + ) { + # self = EditableArgs(MatsList + # , lapply(list(...), as.matrix) + # , list() + # ) + self = Base() + + ## Args + ## TODO: these shouldn't be private but we have a problem. + ## the original sin was putting dots in front of arguments that + ## correspond to public Arg fields. this will require a breaking + ## change to fix where we remove dots from all arguments that should + ## actually be stored publicly. i initially was implicitly thinking + ## of arguments starting with dots as arguments that aren't really + ## part of the method signature being depended on, but this is silly + ## in hindsight ... what does that even mean? + self$.mats_to_save = .mats_to_save + self$.mats_to_return = .mats_to_return + self$.dimnames = .dimnames + self$.structure_labels = .structure_labels + #self$.init_saved_dims = .init_saved_dims + + # Static + self$.initial_mats = lapply(list(...), as.matrix) + if (length(self$.initial_mats) == 0L) names(self$.initial_mats) = character() + + self$mats_save_hist = function() names(self$.initial_mats) %in% self$.mats_to_save + self$mats_return = function() names(self$.initial_mats) %in% self$.mats_to_return + # self$mats_save_dims = function() { + # msh = self$mats_save_hist() + # msd = setNames( + # lapply(self$.initial_mats[msh], dim), + # self$.names()[msh] + # ) + # for (m in names(self$.init_saved_dims)) { + # + # } + # } + + ## Standard methods + self$get = function(variable_name) { + i = which(self$.names() == valid$char1$assert(variable_name)) + if (length(i) == 1L) { + return(self$.mats()[[i]]) + } else { + i = which(variable_name == self$.structure_labels$state()) + if (length(i) == 1L) { + return(self$get("state")[i]) + } + i = which(variable_name == self$.structure_labels$flow()) + if (length(i) == 1L) { + return(self$get("flow")[i]) + } + } + stop( + "\nNo variable called ", variable_name, " in the list:\n", + paste0( + c( + self$.names(), + self$.structure_labels$state(), + self$.structure_labels$flow() + ), + collapse = "; " + ) + ) + } + self$.names = function() names(self$.initial_mats) + self$.mats = function() unname(self$.initial_mats) + dimnames_handle_nulls = function(x) { + if (is.null(dimnames(x))) return(NULL) + if (is.null(rownames(x))) rownames(x) = "" + if (is.null(colnames(x))) colnames(x) = "" + dimnames(x) + } + not_null = function(x) !is.null(x) + dn = lapply(self$.initial_mats, dimnames_handle_nulls) + for (mat_nm in names(.dimnames)) dn[[mat_nm]] = .dimnames[[mat_nm]] + self$.dimnames = Filter(not_null, dn) + + self$.dim = setNames( + lapply(self$.mats(), dim), + self$.names() + ) + self$.nrow = vapply(self$.mats(), nrow, integer(1L), USE.NAMES = FALSE) + self$.ncol = vapply(self$.mats(), ncol, integer(1L), USE.NAMES = FALSE) + self$mat_dims = function() { + data.frame(mat = self$.names(), nrow = self$.nrow, ncol = self$.ncol) + } + self$data_arg = function() { + r = list( + mats = self$.mats(), + mats_save_hist = self$mats_save_hist(), + mats_return = self$mats_return() + ) + valid$mats_arg$assert(r) + } + + ## add _new_ matrices -- error if a matrix with the + ## same name already exists + self$add_mats = function(... + , .mats_to_save = character(0L) + , .mats_to_return = character(0L) + , .dimnames = list() + ) { + args = c(self$.initial_mats, list(...)) + dups = duplicated(names(args)) + if (any(dups)) { + stop( + "\nThe following matrices were added, but already existed:\n", + paste0(names(args)[dups], collapse = ", ") + ## TODO: fill in what to do about it + ) + } + args$.mats_to_save = union(self$.mats_to_save, .mats_to_save) + args$.mats_to_return = union(self$.mats_to_return, .mats_to_return) + args$.dimnames = c(self$.dimnames, .dimnames) + args$.structure_labels = self$.structure_labels + do.call(MatsList, args) + } + + ## add new matrices or update existing matrices as the + ## case may be -- no error or warning if you happen to + ## overwrite an existing matrix + self$update_mats = function(... + , .mats_to_save = character(0L) + , .mats_to_return = character(0L) + , .dimnames = list() + ) { + args = self$.initial_mats + new_args = list(...) + args[names(new_args)] = new_args + args$.mats_to_save = union(self$.mats_to_save, .mats_to_save) + args$.mats_to_return = union(self$.mats_to_return, .mats_to_return) + args$.dimnames = self$.dimnames + args$.dimnames[names(.dimnames)] = .dimnames + args$.structure_labels = self$.structure_labels + do.call(MatsList, args) + } + return_object(self, "MatsList") +} + +#' @export +names.MatsList = function(x) x$.names() + diff --git a/R/objective_function.R b/R/objective_function.R new file mode 100644 index 00000000..21d25e51 --- /dev/null +++ b/R/objective_function.R @@ -0,0 +1,66 @@ +#' Objective Function +#' +#' Define the objective function of a compartmental model in TMB. +#' +#' @param obj_fn_expr One sided \code{\link{formula}} giving the objective +#' function of a TMB model. The right hand side expression must contain only +#' the names of matrices in the model, functions defined in \code{macpan2.cpp}, +#' and numerical literals (e.g. \code{3.14}). +#' +#' @return Object of class \code{ObjectiveFunction} with the following methods. +#' +#' ## Methods +#' +#' * `data_arg(..., .existing_literals)` -- Return the following components of the data structure +#' to pass to C++. +#' * `o_table_x` -- Objective function parse table column giving an index for looking up either +#' function, matrix, or literal. +#' * `o_table_n` -- Objective function parse table column giving the number of arguments in +#' functions. +#' * `o_table_i` -- Objective function parse table column giving indices for looking up the +#' rows in the parse table corresponding with the first argument of the +#' function. +#' * `literals` -- Numeric vector of literals that can were used in the +#' expressions of the model. +#' +#' ## Method Arguments +#' +#' * `...`: Character vector containing the names of the matrices in the model. +#' * `.existing_literals`: Numeric vector giving the literals used in the +#' model expressions produced before the objective function. +#' +#' @export +ObjectiveFunction = function(obj_fn_expr) { + + ## Inherit Private Methods + self = ExprListUtils() + + ## Args + self$obj_fn_expr = obj_fn_expr + + ## Standard Methods + self$expr_list = function() list(self$obj_fn_expr) + self$.literals = function(.existing_literals) { + self$.parsed_expr_list( + names(self$init_mats), + .existing_literals = .existing_literals + )$valid_literals + } + self$.parse_table = function(.existing_literals) { + l = as.list(self$.parsed_expr_list( + names(self$init_mats), + .existing_literals = .existing_literals + )$parse_table) + self$.set_name_prefix(l[c("x", "n", "i")], "o_table_") + } + self$data_arg = function(.existing_literals) { + p = self$.parse_table(.existing_literals = .existing_literals) + p$literals = self$.literals(.existing_literals = .existing_literals) + p + } + + ## Composition + self$init_mats = MatsList() + + return_object(self, "ObjectiveFunction") +} diff --git a/R/opt_params.R b/R/opt_params.R new file mode 100644 index 00000000..77118e1e --- /dev/null +++ b/R/opt_params.R @@ -0,0 +1,163 @@ +#' Optimization Parameters List +#' +#' Create an object for specifying matrix elements to be optimized or integrated +#' out of the objective function using a Laplace transform. +#' +#' @param ... Objects that can be coerced to numeric vectors, which will be +#' concatenated to produce the default value of the parameter vector. +#' @param par_id Integer vector identifying elements of the parameter vector +#' to be used to replace elements of the model matrices. +#' @param mat Character vector the same length as `par_id` giving the names of +#' the matrices containing the elements to replace. +#' @param row_id Integer vector the same length as `par_id` giving the row +#' indices of the matrix elements to replace with parameter values. +#' @param col_id Integer vector the same length as `par_id` giving the column +#' indices of the matrix elements to replace with parameter values. +#' +#' @return Object of class \code{OptParamsList} with the following methods. +#' +#' ## Methods +#' +#' * `$data_arg(..., .type_string = c("p", "r"))`: Return the following components of the data structure +#' to pass to C++. +#' * `{.type_string}_par_id` -- Integers identifying the replacing parameter. +#' * `{.type_string}_mat_id` -- Integers identifying the matrix within which +#' an element is to be replaced. +#' * `{.type_string}_row_id` -- Integers identifying the rows within matrices +#' to replace. +#' * `{.type_string}_col_id` -- Integers identifying the columns within +#' matrices to replace. +#' * `$vector()`: Return the initial value of the numerical parameter vector. +#' * `$data_frame()`: Return a data frame with each row describing a parameter. +#' +#' ## Method Arguments +#' +#' * `...`: Character vector containing the names of the matrices in the model. +#' * `.type_string`: Either `"p"` or `"r"` indicating whether the object +#' is to be used to represent fixed parameters to be optimized or random +#' parameters to be integrated out using the Laplace transform. +#' +#' @export +OptParamsList = function(... + , par_id = integer(0L) + , mat = character(0L) + , row_id = integer(0L) + , col_id = integer(0L) + ) { + self = Base() + self$.vector = as.numeric(unlist(list(...))) + self$vector = function() self$.vector + + # TMB needs at least one parameter (unless this is for a random effect), + # so setting to zero without setting anything else so that it doesn't + # actually get used + #if (length(self$.vector) == 0L) self$.vector = 0 + self$.mat = mat + self$.par_id = par_id + self$.row_id = row_id + self$.col_id = col_id + self$.mat_id = function(...) { + match(self$.mat, as.character(unlist(list(...)))) - 1L + } + self$data_frame = function(...) { + d = data.frame(par_id = self$.par_id + , mat = self$.mat + , row = self$.row_id + , col = self$.col_id + , default = self$.vector[self$.par_id + 1L] + ) + alternative_vectors = list(...) + for (v in names(alternative_vectors)) { + d[[v]] = alternative_vectors[[v]][self$.par_id + 1L] + } + d + } + self$data_arg = function(.type_string = c("p", "r")) { + .type_string = match.arg(.type_string) + r = setNames( + list(self$.par_id + , self$.mat_id(names(self$init_mats)) + , self$.row_id + , self$.col_id + ), + paste(.type_string, c("par", "mat", "row", "col"), "id", sep = "_") + ) + valid$opt_params_list_arg$assert(r) + } + self$init_mats = MatsList() + return_object(self, "OptParamsList") +} + +OptParamsFrameStruc = function(..., frame) { + OptParamsList(... + , par_id = frame$par_id + , mat = frame$mat + , row_id = frame$row_id + , col_id = frame$col_id + ) +} + + +## alternative constructor of OptParamsList +OptParamsFrame = function(frame, .dimnames = list()) { + for (c in names(frame)) { + if (is.character(frame[[c]])) frame[[c]] = trimws(frame[[c]]) + } + row_col_ids = make_row_col_ids(frame$mat, frame$row, frame$col, .dimnames) + args = c( + as.list(as.numeric(frame$default)), + list( + par_id = seq_len(nrow(frame)) - 1L, ## zero-based c++ indices + mat = frame$mat, + row_id = row_col_ids$row_id, + col_id = row_col_ids$col_id + ) + ) + do.call(OptParamsList, args) +} + +OptParamsFile = function(file_path + , csv_reader = CSVReader + , json_reader = JSONReader + , txt_reader = TXTReader + ) { + self = Files(dirname(file_path) + , reader_spec(basename(file_path), csv_reader) + ) + self$.col_map = c( + Matrix = "mat" + , mat = "mat" + , Mat = "mat" + , Row = "row" + , row = "row" + , Column = "col" + , Col = "col" + , col = "col" + , type = "type" + , Type = "type" + , value = "default" + , Value = "default" + , Val = "default" + , val = "default" + , Default = "default" + , default = "default" + ) + self$.param = c("param", "par", "fixed", "fixef") + self$.random = c("random", "ran", "rand", "ranef") + self$frame = function() { + x = self$get("parameters") + names(x) = self$.col_map[names(x)] + x + } + self$params_frame = function(.dimnames = list()) { + f = self$frame() + f[tolower(f[["type"]]) %in% self$.param, , drop = FALSE] + #if (nrow(f) == 0L) return(OptParamsList()) + } + self$random_frame = function(.dimnames = list()) { + f = self$frame() + f[tolower(f[["type"]]) %in% self$.random, , drop = FALSE] + #if (nrow(f) == 0L) return(OptParamsList()) + } + self +} diff --git a/R/time.R b/R/time.R new file mode 100644 index 00000000..f4c8915c --- /dev/null +++ b/R/time.R @@ -0,0 +1,41 @@ +#' Time +#' +#' Define the number of time steps in a compartmental model in TMB. +#' +#' @param time_steps Number of time steps in the simulation loop. +#' +#' @return Object of class \code{Time} with the following methods. +#' +#' ## Methods +#' +#' * `$data_arg()` -- Return the following components of the data structure +#' to pass to C++. +#' * `time_steps` -- Number of time steps in the simulation loop. +#' +#' @export +Time = function(time_steps) { + self = Base() + self$time_steps = time_steps + self$data_arg = function() list(time_steps = self$time_steps) + return_object(self, "Time") +} + +DiffTime = function(start_time, end_time) { + self = Base() + self$start_time = valid$scalar$assert(start_time) + self$end_time = valid$scalar$assert(end_time) + self$start = function() {} +} + +Daily = function(start_date, end_date) { + self = Base() + self$start_date = as.Date(start_date) + self$end_date = as.Date(end_date) + self$time_steps = function() { + (self$end_date + |> difftime(self$start_date, units = "days") + |> as.integer() + ) + } + self$data_arg = function() list(list_steps = self$time_steps()) +} diff --git a/R/tmb_model.R b/R/tmb_model.R index a4c269a7..81e5fe4b 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -1,499 +1,3 @@ - -#' Matrix List -#' -#' Create a list of initial values for matrices used to define a compartmental -#' model in TMB. -#' -#' @param ... Named objects that can be coerced to numerical matrices. -#' @param .mats_to_save Character vector naming matrices to be saved at each -#' set in the simulation so that some calculations can make use of past value -#' (e.g. delayed effects) and/or to be able to retrieved the simulation -#' history after the simulation is complete. -#' @param .mats_to_return Character vector naming matrices to be returned -#' after the simulate is complete. -#' @param .dimnames Named list of \code{\link{dimnames}} for matrices that change -#' their dimensions over the simulation steps. These names correspond to the -#' names of the matrices. The output of the simulations will try their best -#' to honor these names, but if the shape of the matrix is too inconsistent -#' with the \code{\link{dimnames}} then numerical indices will be used instead. -#' For matrices that do not change their dimensions, set \code{\link{dimnames}} -#' by adding \code{\link{dimnames}} to the matrices passed to \code{...}. -#' @param .structure_labels An optional object for obtaining labels of -#' elements of special vectors and matrices. Such an object can be found in -#' the `$labels` field of a \code{\link{Compartmental}} model. Note that this -#' is an advanced technique. -#' -#' @return Object of class \code{MatsList} with the following methods. -#' -#' ## Methods -#' -#' * `$data_arg()`: Return the following components of the data structure -#' to pass to C++. -#' * `mats` -- Unnamed list of numeric matrices. -#' * `mats_save_hist` -- Boolean vector identifying which matrices should -#' have their history saved. -#' * `mats_return` -- Boolean vector identifying which matrices should be -#' returned after a simulation. -#' * `$mat_dims()`: Return a data frame giving the numbers of rows and columns -#' of each matrix in the list. -#' * `$add_mats(...)`: Add matrices to the list and return a new -#' regenerated \code{MatsList} object. -#' -#' @export -MatsList = function(... - , .mats_to_save = character(0L) - , .mats_to_return = character(0L) - , .dimnames = list() - , .structure_labels = NullLabels() - #, .init_saved_dims = list() - ) { - # self = EditableArgs(MatsList - # , lapply(list(...), as.matrix) - # , list() - # ) - self = Base() - - ## Args - ## TODO: these shouldn't be private but we have a problem. - ## the original sin was putting dots in front of arguments that - ## correspond to public Arg fields. this will require a breaking - ## change to fix where we remove dots from all arguments that should - ## actually be stored publicly. i initially was implicitly thinking - ## of arguments starting with dots as arguments that aren't really - ## part of the method signature being depended on, but this is silly - ## in hindsight ... what does that even mean? - self$.mats_to_save = .mats_to_save - self$.mats_to_return = .mats_to_return - self$.dimnames = .dimnames - self$.structure_labels = .structure_labels - #self$.init_saved_dims = .init_saved_dims - - # Static - self$.initial_mats = lapply(list(...), as.matrix) - if (length(self$.initial_mats) == 0L) names(self$.initial_mats) = character() - - self$mats_save_hist = function() names(self$.initial_mats) %in% self$.mats_to_save - self$mats_return = function() names(self$.initial_mats) %in% self$.mats_to_return - # self$mats_save_dims = function() { - # msh = self$mats_save_hist() - # msd = setNames( - # lapply(self$.initial_mats[msh], dim), - # self$.names()[msh] - # ) - # for (m in names(self$.init_saved_dims)) { - # - # } - # } - - ## Standard methods - self$get = function(variable_name) { - i = which(self$.names() == valid$char1$assert(variable_name)) - if (length(i) == 1L) { - return(self$.mats()[[i]]) - } else { - i = which(variable_name == self$.structure_labels$state()) - if (length(i) == 1L) { - return(self$get("state")[i]) - } - i = which(variable_name == self$.structure_labels$flow()) - if (length(i) == 1L) { - return(self$get("flow")[i]) - } - } - stop( - "\nNo variable called ", variable_name, " in the list:\n", - paste0( - c( - self$.names(), - self$.structure_labels$state(), - self$.structure_labels$flow() - ), - collapse = "; " - ) - ) - } - self$.names = function() names(self$.initial_mats) - self$.mats = function() unname(self$.initial_mats) - dimnames_handle_nulls = function(x) { - if (is.null(dimnames(x))) return(NULL) - if (is.null(rownames(x))) rownames(x) = "" - if (is.null(colnames(x))) colnames(x) = "" - dimnames(x) - } - not_null = function(x) !is.null(x) - dn = lapply(self$.initial_mats, dimnames_handle_nulls) - for (mat_nm in names(.dimnames)) dn[[mat_nm]] = .dimnames[[mat_nm]] - self$.dimnames = Filter(not_null, dn) - - self$.dim = setNames( - lapply(self$.mats(), dim), - self$.names() - ) - self$.nrow = vapply(self$.mats(), nrow, integer(1L), USE.NAMES = FALSE) - self$.ncol = vapply(self$.mats(), ncol, integer(1L), USE.NAMES = FALSE) - self$mat_dims = function() { - data.frame(mat = self$.names(), nrow = self$.nrow, ncol = self$.ncol) - } - self$data_arg = function() { - r = list( - mats = self$.mats(), - mats_save_hist = self$mats_save_hist(), - mats_return = self$mats_return() - ) - valid$mats_arg$assert(r) - } - - ## add _new_ matrices -- error if a matrix with the - ## same name already exists - self$add_mats = function(... - , .mats_to_save = character(0L) - , .mats_to_return = character(0L) - , .dimnames = list() - ) { - args = c(self$.initial_mats, list(...)) - dups = duplicated(names(args)) - if (any(dups)) { - stop( - "\nThe following matrices were added, but already existed:\n", - paste0(names(args)[dups], collapse = ", ") - ## TODO: fill in what to do about it - ) - } - args$.mats_to_save = union(self$.mats_to_save, .mats_to_save) - args$.mats_to_return = union(self$.mats_to_return, .mats_to_return) - args$.dimnames = c(self$.dimnames, .dimnames) - args$.structure_labels = self$.structure_labels - do.call(MatsList, args) - } - - ## add new matrices or update existing matrices as the - ## case may be -- no error or warning if you happen to - ## overwrite an existing matrix - self$update_mats = function(... - , .mats_to_save = character(0L) - , .mats_to_return = character(0L) - , .dimnames = list() - ) { - args = self$.initial_mats - new_args = list(...) - args[names(new_args)] = new_args - args$.mats_to_save = union(self$.mats_to_save, .mats_to_save) - args$.mats_to_return = union(self$.mats_to_return, .mats_to_return) - args$.dimnames = self$.dimnames - args$.dimnames[names(.dimnames)] = .dimnames - args$.structure_labels = self$.structure_labels - do.call(MatsList, args) - } - return_object(self, "MatsList") -} - -#' @export -names.MatsList = function(x) x$.names() - - -#' Optimization Parameters List -#' -#' Create an object for specifying matrix elements to be optimized or integrated -#' out of the objective function using a Laplace transform. -#' -#' @param ... Objects that can be coerced to numeric vectors, which will be -#' concatenated to produce the default value of the parameter vector. -#' @param par_id Integer vector identifying elements of the parameter vector -#' to be used to replace elements of the model matrices. -#' @param mat Character vector the same length as `par_id` giving the names of -#' the matrices containing the elements to replace. -#' @param row_id Integer vector the same length as `par_id` giving the row -#' indices of the matrix elements to replace with parameter values. -#' @param col_id Integer vector the same length as `par_id` giving the column -#' indices of the matrix elements to replace with parameter values. -#' -#' @return Object of class \code{OptParamsList} with the following methods. -#' -#' ## Methods -#' -#' * `$data_arg(..., .type_string = c("p", "r"))`: Return the following components of the data structure -#' to pass to C++. -#' * `{.type_string}_par_id` -- Integers identifying the replacing parameter. -#' * `{.type_string}_mat_id` -- Integers identifying the matrix within which -#' an element is to be replaced. -#' * `{.type_string}_row_id` -- Integers identifying the rows within matrices -#' to replace. -#' * `{.type_string}_col_id` -- Integers identifying the columns within -#' matrices to replace. -#' * `$vector()`: Return the initial value of the numerical parameter vector. -#' * `$data_frame()`: Return a data frame with each row describing a parameter. -#' -#' ## Method Arguments -#' -#' * `...`: Character vector containing the names of the matrices in the model. -#' * `.type_string`: Either `"p"` or `"r"` indicating whether the object -#' is to be used to represent fixed parameters to be optimized or random -#' parameters to be integrated out using the Laplace transform. -#' -#' @export -OptParamsList = function(... - , par_id = integer(0L) - , mat = character(0L) - , row_id = integer(0L) - , col_id = integer(0L) - ) { - self = Base() - self$.vector = as.numeric(unlist(list(...))) - self$vector = function() self$.vector - - # TMB needs at least one parameter (unless this is for a random effect), - # so setting to zero without setting anything else so that it doesn't - # actually get used - #if (length(self$.vector) == 0L) self$.vector = 0 - self$.mat = mat - self$.par_id = par_id - self$.row_id = row_id - self$.col_id = col_id - self$.mat_id = function(...) { - match(self$.mat, as.character(unlist(list(...)))) - 1L - } - self$data_frame = function(...) { - d = data.frame(par_id = self$.par_id - , mat = self$.mat - , row = self$.row_id - , col = self$.col_id - , default = self$.vector[self$.par_id + 1L] - ) - alternative_vectors = list(...) - for (v in names(alternative_vectors)) { - d[[v]] = alternative_vectors[[v]][self$.par_id + 1L] - } - d - } - self$data_arg = function(.type_string = c("p", "r")) { - .type_string = match.arg(.type_string) - r = setNames( - list(self$.par_id - , self$.mat_id(names(self$init_mats)) - , self$.row_id - , self$.col_id - ), - paste(.type_string, c("par", "mat", "row", "col"), "id", sep = "_") - ) - valid$opt_params_list_arg$assert(r) - } - self$init_mats = MatsList() - return_object(self, "OptParamsList") -} - -OptParamsFrameStruc = function(..., frame) { - OptParamsList(... - , par_id = frame$par_id - , mat = frame$mat - , row_id = frame$row_id - , col_id = frame$col_id - ) -} - - -## alternative constructor of OptParamsList -OptParamsFrame = function(frame, .dimnames = list()) { - for (c in names(frame)) { - if (is.character(frame[[c]])) frame[[c]] = trimws(frame[[c]]) - } - row_col_ids = make_row_col_ids(frame$mat, frame$row, frame$col, .dimnames) - args = c( - as.list(as.numeric(frame$default)), - list( - par_id = seq_len(nrow(frame)) - 1L, ## zero-based c++ indices - mat = frame$mat, - row_id = row_col_ids$row_id, - col_id = row_col_ids$col_id - ) - ) - do.call(OptParamsList, args) -} - -OptParamsFile = function(file_path - , csv_reader = CSVReader - , json_reader = JSONReader - , txt_reader = TXTReader - ) { - self = Files(dirname(file_path) - , reader_spec(basename(file_path), csv_reader) - ) - self$.col_map = c( - Matrix = "mat" - , mat = "mat" - , Mat = "mat" - , Row = "row" - , row = "row" - , Column = "col" - , Col = "col" - , col = "col" - , type = "type" - , Type = "type" - , value = "default" - , Value = "default" - , Val = "default" - , val = "default" - , Default = "default" - , default = "default" - ) - self$.param = c("param", "par", "fixed", "fixef") - self$.random = c("random", "ran", "rand", "ranef") - self$frame = function() { - x = self$get("parameters") - names(x) = self$.col_map[names(x)] - x - } - self$params_frame = function(.dimnames = list()) { - f = self$frame() - f[tolower(f[["type"]]) %in% self$.param, , drop = FALSE] - #if (nrow(f) == 0L) return(OptParamsList()) - } - self$random_frame = function(.dimnames = list()) { - f = self$frame() - f[tolower(f[["type"]]) %in% self$.random, , drop = FALSE] - #if (nrow(f) == 0L) return(OptParamsList()) - } - self -} - -#' Objective Function -#' -#' Define the objective function of a compartmental model in TMB. -#' -#' @param obj_fn_expr One sided \code{\link{formula}} giving the objective -#' function of a TMB model. The right hand side expression must contain only -#' the names of matrices in the model, functions defined in \code{macpan2.cpp}, -#' and numerical literals (e.g. \code{3.14}). -#' -#' @return Object of class \code{ObjectiveFunction} with the following methods. -#' -#' ## Methods -#' -#' * `data_arg(..., .existing_literals)` -- Return the following components of the data structure -#' to pass to C++. -#' * `o_table_x` -- Objective function parse table column giving an index for looking up either -#' function, matrix, or literal. -#' * `o_table_n` -- Objective function parse table column giving the number of arguments in -#' functions. -#' * `o_table_i` -- Objective function parse table column giving indices for looking up the -#' rows in the parse table corresponding with the first argument of the -#' function. -#' * `literals` -- Numeric vector of literals that can were used in the -#' expressions of the model. -#' -#' ## Method Arguments -#' -#' * `...`: Character vector containing the names of the matrices in the model. -#' * `.existing_literals`: Numeric vector giving the literals used in the -#' model expressions produced before the objective function. -#' -#' @export -ObjectiveFunction = function(obj_fn_expr) { - - ## Inherit Private Methods - self = ExprListUtils() - - ## Args - self$obj_fn_expr = obj_fn_expr - - ## Standard Methods - self$expr_list = function() list(self$obj_fn_expr) - self$.literals = function(.existing_literals) { - self$.parsed_expr_list( - names(self$init_mats), - .existing_literals = .existing_literals - )$valid_literals - } - self$.parse_table = function(.existing_literals) { - l = as.list(self$.parsed_expr_list( - names(self$init_mats), - .existing_literals = .existing_literals - )$parse_table) - self$.set_name_prefix(l[c("x", "n", "i")], "o_table_") - } - self$data_arg = function(.existing_literals) { - p = self$.parse_table(.existing_literals = .existing_literals) - p$literals = self$.literals(.existing_literals = .existing_literals) - p - } - - ## Composition - self$init_mats = MatsList() - - return_object(self, "ObjectiveFunction") -} - - -#' Time -#' -#' Define the number of time steps in a compartmental model in TMB. -#' -#' @param time_steps Number of time steps in the simulation loop. -#' -#' @return Object of class \code{Time} with the following methods. -#' -#' ## Methods -#' -#' * `$data_arg()` -- Return the following components of the data structure -#' to pass to C++. -#' * `time_steps` -- Number of time steps in the simulation loop. -#' -#' @export -Time = function(time_steps) { - self = Base() - self$time_steps = time_steps - self$data_arg = function() list(time_steps = self$time_steps) - return_object(self, "Time") -} - -DiffTime = function(start_time, end_time) { - self = Base() - self$start_time = valid$scalar$assert(start_time) - self$end_time = valid$scalar$assert(end_time) - self$start = function() {} -} - -Daily = function(start_date, end_date) { - self = Base() - self$start_date = as.Date(start_date) - self$end_date = as.Date(end_date) - self$time_steps = function() { - (self$end_date - |> difftime(self$start_date, units = "days") - |> as.integer() - ) - } - self$data_arg = function() list(list_steps = self$time_steps()) -} - - -#' Constant Integer Vectors -#' -#' Make a list of integer vectors available for engine methods. -#' -#' @param ... Named arguments, each of which can be coerced to an integer vector. -#' @export -ConstIntVecs = function(...) { - self = Base() - - # Args - self$list = lapply(list(...), as.integer) - if (length(self$list) == 0L) self$list = list(integer()) - - # Standard Methods - self$data_arg = function() { - # DATA_IVECTOR(const_int_vec); - # DATA_IVECTOR(const_n_int_vecs); - list( - const_int_vec = unlist(self$list, use.names = FALSE), - const_n_int_vecs = unlist(lapply(self$list, length), use.names = FALSE) - ) - } - self$const_names = function() names(self$list) - - return_object(self, "ConstIntVecs") -} - - #' TMB Model #' #' Define a compartmental model in TMB. This model uses the spec diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index d4de0141..7863ece5 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -40,6 +40,8 @@ TMBInserter = function(model) { , .phase = .phase , .simulate_exprs = .simulate_exprs ) + self$model$expr_list$init_mats = self$model$init_mats + self$model$obj_fn$init_mats = self$model$init_mats invisible(self$model) } return_object(self, "TMBInserter") @@ -74,9 +76,9 @@ TMBSimulatorInserter = function(simulator) { args$.at = .at args$.phase = .phase args$.simulate_exprs = .simulate_exprs - self$model$expr_list = do.call( - self$model$expr_list$insert, - args + (self$model$expr_list$insert + |> do.call(args) + |> self$model$refresh_expr_list() ) self$simulator$cache$invalidate() invisible(self$simulator) @@ -112,11 +114,11 @@ TMBSimulatorAdder = function(simulator) { , .mats_to_return = character(0L) , .dimnames = list() ) { - self$model$init_mats = self$model$init_mats$add_mats(... + self$model$init_mats$add_mats(... , .mats_to_save = .mats_to_save , .mats_to_return = .mats_to_return , .dimnames = .dimnames - ) + ) |> self$model$refresh_init_mats() self$simulator$cache$invalidate() invisible(self$simulator) } @@ -157,7 +159,10 @@ TMBSimulatorAdder = function(simulator) { TMBReplacer = function(model) { self = TMBEditor(model) self$obj_fn = function(obj_fn_expr) { - self$model$obj_fn = ObjectiveFunction(obj_fn_expr) + (obj_fn_expr + |> ObjectiveFunction() + |> self$model$refresh_obj_fn() + ) self$model } return_object(self, "TMBReplacer") @@ -167,12 +172,16 @@ TMBSimulatorReplacer = function(simulator) { self = TMBReplacer(simulator$tmb_model) self$simulator = simulator self$obj_fn = function(obj_fn_expr) { - self$model$obj_fn = ObjectiveFunction(obj_fn_expr) + (obj_fn_expr + |> ObjectiveFunction() + |> self$model$refresh_obj_fn() + ) self$simulator$cache$invalidate() invisible(self$simulator) } self$params_frame = function(frame) { - self$model$params = OptParamsFrame(frame, self$model$init_mats$.dimnames) + new_params = OptParamsFrame(frame, self$model$init_mats$.dimnames) + self$model$refresh_params(new_params) self$simulator$cache$invalidate() valid$consistency_params_mats$check(self$model) invisible(self$simulator) @@ -181,7 +190,8 @@ TMBSimulatorReplacer = function(simulator) { self$params_frame(data.frame(default, mat, row, col)) } self$random_frame = function(frame) { - self$model$random = OptParamsFrame(frame, self$model$init_mats$.dimnames) + new_random = OptParamsFrame(frame, self$model$init_mats$.dimnames) + self$model$refresh_random(new_random) self$simulator$cache$invalidate() valid$consistency_random_mats$check(self$model) invisible(self$simulator) diff --git a/vignettes/development_patterns.Rmd b/vignettes/development_patterns.Rmd index 7a4c1760..b8a53007 100644 --- a/vignettes/development_patterns.Rmd +++ b/vignettes/development_patterns.Rmd @@ -20,15 +20,13 @@ library(oor) [![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) -## Introduction - -The `macpan2` package uses a custom object oriented framework. Wait ... don't leave ... it really isn't that complicated as you will see in the Basics section. +## Prerequisits -Many object oriented programming frameworks exist for R, so why another? We wanted a more conventional object oriented system (so the standard use of S3 and S4 was not ideal) and we wanted to avoid dependencies on third-party packages (so the `R6` package was also not ideal). +## Introduction -We tried to keep our system as close to basic R concepts as possible. It can be described as S3 methods plus R `environment`s. +The `macpan2` package uses the standard S3 object-oriented framework for `R`. All objects in `macpan2` are standard `R` `environments`, with standard S3 `class` `attributes`. This approach allows us to integrate with standard generic S3 methods (e.g. `print`, `predict`), while retaining the benefits of programming styles that are common outside of `R` such as data-code bundling and passing by reference. We can get these benefits without dependencies on third-party packages such as `R6` and instead use standard `R` tools, but in an unorthodox yet interesting (to us) way. -## Basics of the Object Oriented Framework +## Basics of the `macpan2` Object Oriented Framework ### Constructing and Using Objects @@ -67,7 +65,7 @@ We call methods, like the `read()` method, in the following way. sir_flows_reader$read() ``` -This reader has read in the CSV file that it is configured to read. +This `sir_flows_reader` has read in the CSV file that it is configured to read. This syntax might look strange to many R users, who will be used to something more like this. @@ -87,7 +85,7 @@ Here we see that, along with the `read` method, there is something else called ` sir_flows_reader$file ``` -Object components that are not methods are called fields. +Object components like this `file` component, which are not methods, are called fields. That's the basic idea of how to use objects in `macpan2`. Here's a summary. @@ -143,7 +141,7 @@ simple_sequence = SimpleSequence(10) simple_sequence$generate() ``` -Notice that all fields and methods stored in `self` (e.g. `self$n`) can be used in methods by using the `$` operator to extract the value of the field or method from `self`. The technical reason why this works is that the `self` `environment` is in the `environment` of every method in the `self` `environment`. In fact, the `self` `environment` is the only thing in the `environment` of each method. If this seems mind-binding, don't worry about it. +Notice that all fields and methods stored in `self` (e.g. `self$n`) can be used in methods by using the `$` operator to extract the value of the field or method from `self`. The technical reason why this works is that the `self` `environment` is in the `environment` of every method in the `self` `environment`. In fact, the `self` `environment` is the only thing in the `environment` of each method. If this seems mind-bending, don't worry about it. Those are the basics of class definitions. Here's a summary. @@ -159,7 +157,12 @@ Those are the basics of class definitions. Here's a summary. ### Objects -In `macpan2`, objects are standard `R` `environment`s with the following characteristics +In `macpan2`, objects are standard `R` `environment`s with an S3 `class` attribute. Therefore, our object oriented style involves only basic foundational `R` concepts: `environment`s and S3 classes. + +There are two types of `environment`s in this setup. The first kind of `environment` is the + +* An S3 `class` `attribute` +* The `environment` of every function in this `environment` is an ### Class Definitions @@ -300,7 +303,7 @@ This keeps classes small because `B` can have methods instead of `A`, and small Methods refreshing fields when shallow copies of those fields are in several composed objects ... When a field gets edited, the simplest thing to do is -#### Private Methods +### Private Methods Private methods should only be used by other methods in the class. There is nothing stoping a developer or a user from calling a private method, but there is no guarantee that the private method with have consistent behaviour or even exist. To communicate privacy, private methods should start with a dot as the following example shows. @@ -318,11 +321,11 @@ A = function(...) { ``` -## Object Editing +### Object Editing -## Method Caching +### Method Caching Developers can manage the performance costs of computationally expensive methods through method caching. When a developer calls a cached method for the first time, it computes the result, stores it in a cache, and returns the result. Subsequent method evaluations simply retrieve the cached value, improving efficiency. Developers can ensure consistency by invalidating the cache whenever objects change, allowing them to balance the cost of regeneration with the need for consistency. From f1b62b71bd88c07c485c34a4718a7d17dec8b7c7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 12 Sep 2023 08:41:03 -0400 Subject: [PATCH 012/332] stop throwing repeated error messages from c++ --- NAMESPACE | 1 + R/log_files.R | 13 +++++++++++++ R/tmb_model.R | 20 +++++++++++++++++--- man/ConstIntVecs.Rd | 2 +- man/LogFile.Rd | 15 +++++++++++++++ man/MatsList.Rd | 2 +- man/ObjectiveFunction.Rd | 2 +- man/OptParamsList.Rd | 2 +- man/TMBModel.Rd | 5 ++++- man/Time.Rd | 2 +- tests/testthat/test-assign.R | 2 +- tests/testthat/test-density.R | 6 +++--- tests/testthat/test-integer-sequences.R | 4 ++-- tests/testthat/test-mat-mult.R | 2 +- tests/testthat/test-rand.R | 4 ++-- tests/testthat/test-recycle.R | 10 +++++----- tests/testthat/test-subsetting.R | 4 ++-- 17 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 R/log_files.R create mode 100644 man/LogFile.Rd diff --git a/NAMESPACE b/NAMESPACE index 7c922640..0d70532b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,6 +37,7 @@ export(Files) export(FlowExpander) export(JSONReader) export(Log) +export(LogFile) export(Logit) export(MathExpressionFromFunc) export(MathExpressionFromStrings) diff --git a/R/log_files.R b/R/log_files.R new file mode 100644 index 00000000..c6a8bd1a --- /dev/null +++ b/R/log_files.R @@ -0,0 +1,13 @@ +#' Log File +#' +#' @param directory Directory within which to store a log file for a +#' model object. +#' +#' @export +LogFile = function(directory = tempdir()) { + file.create(file.path(directory, "log.txt")) + self = Files(directory, reader_spec("log.txt", TXTReader)) + self$log = function() self$get("log") + self$data_arg = function() list(log_file = self$.file_path("log")) + return_object(self, "LogFile") +} diff --git a/R/tmb_model.R b/R/tmb_model.R index 81e5fe4b..764a2933 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -11,6 +11,7 @@ #' @param time_steps An object of class \code{\link{Time}}. #' @param meth_list An object of class \code{\link{MethList}}. #' @param const_int_vecs An object of class \code{\link{ConstIntVecs}}. +#' @param log_file An object of class \code{\link{LogFile}}. #' #' @return Object of class \code{TMBModel} with the following methods. #' @@ -76,6 +77,7 @@ TMBModel = function( , time_steps = Time(0L) , meth_list = MethList() , const_int_vecs = ConstIntVecs() + , log_file = LogFile() ) { ## Inheritance self = Base() @@ -89,6 +91,7 @@ TMBModel = function( self$time_steps = time_steps self$meth_list = meth_list self$const_int_vecs = const_int_vecs + self$log_file = log_file ## Standard Methods self$data_arg = function() { @@ -101,7 +104,8 @@ TMBModel = function( self$obj_fn$data_arg(existing_literals), self$time_steps$data_arg(), self$meth_list$data_arg(), - self$const_int_vecs$data_arg() + self$const_int_vecs$data_arg(), + self$log_file$data_arg() ) } self$param_arg = function() { @@ -261,7 +265,15 @@ TMBSimulationUtils = function() { expr_num = min(which(row < cumsum(expr_num_p_table_rows))) deparse1(self$tmb_model$expr_list$expr_list()[[expr_num]]) } - self$.runner = function(..., .phases = c("before", "during", "after"), .method = c("report", "simulate")) { + self$.err_msg = function() { + re = "^Error message = " + m = grep(re, self$tmb_model$log_file$log(), value = TRUE) + sub(re, "", m) + } + self$.runner = function(... + , .phases = c("before", "during", "after") + , .method = c("report", "simulate") + ) { .method = match.arg(.method) fixed_params = as.numeric(unlist(list(...))) if (length(fixed_params) == 0L) { @@ -271,7 +283,9 @@ TMBSimulationUtils = function() { } if (r$error != 0L) { stop( - "\nError thrown by the TMB engine at the following expression:\n", + "\nThe following error was thrown by the TMB engine:\n ", + self$.err_msg(), + "\nThis error occurred at the following expression:\n ", self$.find_problematic_expression(r$expr_row) ) } diff --git a/man/ConstIntVecs.Rd b/man/ConstIntVecs.Rd index 7ec716b7..33404912 100644 --- a/man/ConstIntVecs.Rd +++ b/man/ConstIntVecs.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R +% Please edit documentation in R/const_int_vec.R \name{ConstIntVecs} \alias{ConstIntVecs} \title{Constant Integer Vectors} diff --git a/man/LogFile.Rd b/man/LogFile.Rd new file mode 100644 index 00000000..3e2abb30 --- /dev/null +++ b/man/LogFile.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/log_files.R +\name{LogFile} +\alias{LogFile} +\title{Log File} +\usage{ +LogFile(directory = tempdir()) +} +\arguments{ +\item{directory}{Directory within which to store a log file for a +model object.} +} +\description{ +Log File +} diff --git a/man/MatsList.Rd b/man/MatsList.Rd index 803a234d..36567062 100644 --- a/man/MatsList.Rd +++ b/man/MatsList.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R +% Please edit documentation in R/mats_list.R \name{MatsList} \alias{MatsList} \title{Matrix List} diff --git a/man/ObjectiveFunction.Rd b/man/ObjectiveFunction.Rd index 7efeca97..a1085108 100644 --- a/man/ObjectiveFunction.Rd +++ b/man/ObjectiveFunction.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R +% Please edit documentation in R/objective_function.R \name{ObjectiveFunction} \alias{ObjectiveFunction} \title{Objective Function} diff --git a/man/OptParamsList.Rd b/man/OptParamsList.Rd index aac7ea84..771fbe84 100644 --- a/man/OptParamsList.Rd +++ b/man/OptParamsList.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R +% Please edit documentation in R/opt_params.R \name{OptParamsList} \alias{OptParamsList} \title{Optimization Parameters List} diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index d813fb8d..c0b57724 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -12,7 +12,8 @@ TMBModel( obj_fn = ObjectiveFunction(~0), time_steps = Time(0L), meth_list = MethList(), - const_int_vecs = ConstIntVecs() + const_int_vecs = ConstIntVecs(), + log_file = LogFile() ) } \arguments{ @@ -31,6 +32,8 @@ TMBModel( \item{meth_list}{An object of class \code{\link{MethList}}.} \item{const_int_vecs}{An object of class \code{\link{ConstIntVecs}}.} + +\item{log_file}{An object of class \code{\link{LogFile}}.} } \value{ Object of class \code{TMBModel} with the following methods. diff --git a/man/Time.Rd b/man/Time.Rd index ca8dd142..09b3fd4a 100644 --- a/man/Time.Rd +++ b/man/Time.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R +% Please edit documentation in R/time.R \name{Time} \alias{Time} \title{Time} diff --git a/tests/testthat/test-assign.R b/tests/testthat/test-assign.R index b993cd8a..0d5b3dee 100644 --- a/tests/testthat/test-assign.R +++ b/tests/testthat/test-assign.R @@ -13,6 +13,6 @@ test_that("assignment indices are checked", { , x = x , .matrix_to_return = "x" ), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) }) diff --git a/tests/testthat/test-density.R b/tests/testthat/test-density.R index 8f82feb7..fa8d5cbb 100644 --- a/tests/testthat/test-density.R +++ b/tests/testthat/test-density.R @@ -1,7 +1,7 @@ test_that("density functions handle not-enough parameter errors", { - expect_error(engine_eval(~dpois(0)), "Error thrown by the TMB engine") - expect_error(engine_eval(~dnorm(0)), "Error thrown by the TMB engine") - expect_error(engine_eval(~dnbinom(0)), "Error thrown by the TMB engine") + expect_error(engine_eval(~dpois(0)), "The following error was thrown by the TMB engine") + expect_error(engine_eval(~dnorm(0)), "The following error was thrown by the TMB engine") + expect_error(engine_eval(~dnbinom(0)), "The following error was thrown by the TMB engine") }) test_that("normal densities respect math", { diff --git a/tests/testthat/test-integer-sequences.R b/tests/testthat/test-integer-sequences.R index 9eab7593..45d7c083 100644 --- a/tests/testthat/test-integer-sequences.R +++ b/tests/testthat/test-integer-sequences.R @@ -5,6 +5,6 @@ test_that("integer sequence contruction is correct", { engine_eval(~seq(from = 5, length = 10, by = 3)) ) expect_equal(matrix(rep(1, 5)), engine_eval(~rep(1, 5))) - expect_error(engine_eval(~10:1), "Error thrown by the TMB engine") - expect_error(engine_eval(~seq(0, 0, 1)), "Error thrown by the TMB engine") + expect_error(engine_eval(~10:1), "The following error was thrown by the TMB engine") + expect_error(engine_eval(~seq(0, 0, 1)), "The following error was thrown by the TMB engine") }) diff --git a/tests/testthat/test-mat-mult.R b/tests/testthat/test-mat-mult.R index cccdb05d..0011b090 100644 --- a/tests/testthat/test-mat-mult.R +++ b/tests/testthat/test-mat-mult.R @@ -8,6 +8,6 @@ test_that("matrix multiplication works", { ) expect_error( engine_eval(~ t(A) %*% x, A = A, x = x), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) }) diff --git a/tests/testthat/test-rand.R b/tests/testthat/test-rand.R index eabcc8c9..36628418 100644 --- a/tests/testthat/test-rand.R +++ b/tests/testthat/test-rand.R @@ -1,6 +1,6 @@ test_that("random number generators handle not-enough parameter errors", { - expect_error(engine_eval(~rnorm(0)), "Error thrown by the TMB engine") - expect_error(engine_eval(~rnbinom(0)), "Error thrown by the TMB engine") + expect_error(engine_eval(~rnorm(0)), "The following error was thrown by the TMB engine") + expect_error(engine_eval(~rnbinom(0)), "The following error was thrown by the TMB engine") }) test_that("negative binomial simulation respects seeds and math", { diff --git a/tests/testthat/test-recycle.R b/tests/testthat/test-recycle.R index 5c8bddec..bb387ceb 100644 --- a/tests/testthat/test-recycle.R +++ b/tests/testthat/test-recycle.R @@ -23,7 +23,7 @@ test_that("recycling is up to spec", { ## fail to recycle a row vector to add matrix columns expect_error( engine_eval(~ recycle(t(x), 1, 2), x = x), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) ## recycle a row vector to add matrix rows @@ -35,23 +35,23 @@ test_that("recycling is up to spec", { ## fail to recycle a column vector to add matrix rows expect_error( engine_eval(~ recycle(x, 2, 1), x = x), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) ## fail to 'multiplicatively recycle' expect_error( engine_eval(~ recycle(x, 6, 6), x = x), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) ## fail if one of the requested dimensions is just totally off, ## either multiplicatively or otherwise expect_error( engine_eval(~ recycle(A, 5, 3), A = A), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) expect_error( engine_eval(~ recycle(A, 4, 4), A = A), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) }) diff --git a/tests/testthat/test-subsetting.R b/tests/testthat/test-subsetting.R index d366a3c1..e6988ff3 100644 --- a/tests/testthat/test-subsetting.R +++ b/tests/testthat/test-subsetting.R @@ -24,11 +24,11 @@ test_that("index bounds are checked", { x = 0.1 * (1:5) expect_error( engine_eval(~x[-1], x = x), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) expect_error( engine_eval(~x[-5], x = x), - "Error thrown by the TMB engine" + "The following error was thrown by the TMB engine" ) expect_equal( engine_eval(~x[0], x = x), From dae5e2f43c517283617d1c1b39fe281a1d770971 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 13 Sep 2023 11:28:42 -0400 Subject: [PATCH 013/332] passing tests with engine methods o also infrastructure cleanup --- CONTRIBUTORS.md | 48 ++++++++++++++++++++++++ Makefile | 4 +- NAMESPACE | 2 + R/const_int_vec.R | 3 ++ R/engine_methods.R | 54 ++++++++++++++------------- R/expr_list.R | 2 + R/log_files.R | 37 ++++++++++++++++-- R/objective_function.R | 1 + R/parse_expr.R | 49 +++++++++++++++++++++--- R/tmb_model.R | 20 +++++++--- README.md | 41 -------------------- _pkgdown.yml | 1 + inst/starter_models/sir/settings.json | 3 +- man/LogFile.Rd | 16 +++++++- man/Method.Rd | 4 +- misc/build/run_vignette_code.R | 1 + misc/dev/dev.cpp | 13 +++++-- src/macpan2.cpp | 13 +++++-- tests/testthat/test-tmb-simulator.R | 2 +- 19 files changed, 217 insertions(+), 97 deletions(-) create mode 100644 CONTRIBUTORS.md diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..20c3adcc --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,48 @@ +# Contributing to `macpan2` + +Thank you for contributing to `macpan2`. Pull requests and issues are welcome! + +Developers can see [here](https://canmod.github.io/macpan2/articles/index.html#developer) for documentation useful those who will contribute code. + +## Developer Installation + +Developers and contributors should clone this repository and call `make` at the command-line in the top level directory. The following `make` rules are available for getting more control over the build process. + +``` +make quick-install # for changes that only modify R source +make quick-doc-install # for changes that modify R source and roxygen comments +make quick-test # quick-doc-install + run-examples + run-tests +make run-examples # help file checks only (without package rebuild) +make run-tests # run scripts in tests (without package rebuild) +make full-install # for all changes, including changes to C++ source +make src-update # push changes to dev.cpp to macpan2.cpp (see below) +make enum-update # register new C++ engine functions on the R-side +make engine-doc-update # generate roxygen comments from comments in dev.cpp +make doc-update # roxygenize +make pkg-build # build the R package +make pkg-install # install the R package from the build +make pkg-check # R package checks +``` + +## `C++` Development + +In most R packages with compiled code, developers edit the source files to be compiled in the `src` directory. In `macpan2` there is a single file in that directory called `macpan2`, which is generated automatically from the file `misc/dev/dev.cpp`. This setup allows for quicker C++ development cycles, because developers can edit `misc/dev/dev.cpp` and then use this file in tests without needing to re-install the package with the new source. In particular, the above hello-world example could use `dev.cpp` as follows. + +``` +library(macpan2) +macpan2:::dev_compile() ## compile dev.cpp +sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) +N = 100 +simulator = sir$simulators$tmb(time_steps = 100 + , state = c(S = N - 1, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.1) + , N = N + , beta = 0.2 + , .tmb_cpp = "dev" ## use dev.cpp +) +sir_sims = simulator$report() +``` + +To update `src/macpan2` to the state of `misc/dev/dev.cpp` one may run `make src-update`. + +Running with `misc/dev/dev.cpp` will print out debugging information in a verbose manner, whereas `src/macpan2.cpp` will not. The `src-update` make rule removes the `#define MP_VERBOSE` flag at the top of the file. diff --git a/Makefile b/Makefile index d4ebbaa0..26bed5fd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SED_RE = \(\,\)*[ ]*\/\/[ ]*\(.*\) ALIAS_RE = [ ]*MP2_\(.*\)\: \(.*\)(\(.*\)) ROXY_RE = ^.*\(\#'.*\)$ VERSION := $(shell sed -n '/^Version: /s///p' DESCRIPTION) - +TEST := "testthat::test_package(\"macpan2\", reporter = \"progress\")" all: make src-update @@ -58,7 +58,7 @@ run-examples: run-tests: - Rscript -e "library(macpan2); testthat::test_package(\"macpan2\")" + Rscript -e "library(macpan2); $(TEST)" run-vignette-code: diff --git a/NAMESPACE b/NAMESPACE index 0d70532b..380f4069 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,9 @@ S3method(c,String) S3method(c,StringData) +S3method(names,ConstIntVecs) S3method(names,MatsList) +S3method(names,MethList) S3method(print,MathExpression) S3method(print,Partition) S3method(print,String) diff --git a/R/const_int_vec.R b/R/const_int_vec.R index 62f8834a..0c7b8bfd 100644 --- a/R/const_int_vec.R +++ b/R/const_int_vec.R @@ -24,3 +24,6 @@ ConstIntVecs = function(...) { return_object(self, "ConstIntVecs") } + +#' @export +names.ConstIntVecs = function(x) names(x$list) diff --git a/R/engine_methods.R b/R/engine_methods.R index 670a31aa..f2e894a3 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -8,18 +8,11 @@ MethList = function(...) { # Args self$methods = list(...) - # DATA_IVECTOR(meth_type_id); - # // DATA_IVECTOR(meth_id); - # DATA_IVECTOR(meth_n_mats); - # DATA_IVECTOR(meth_n_int_vecs); - # DATA_IVECTOR(meth_mat_id); - # DATA_IVECTOR(meth_int_vec_id); # Static self$.null_data_arg = setNames( rep(list(integer()), 5), sprintf("meth_%s", c( "type_id" - #, "id" , "n_mats" , "n_int_vecs" , "mat_id" @@ -28,13 +21,11 @@ MethList = function(...) { ) ) - self$data_arg = function(method_names, mat_names, const_names) { + self$data_arg = function() { l = self$.null_data_arg - for (i in seq_along(self$methods)) { - new_arg = self$methods[[i]]$data_arg(method_names, mat_names, const_names) - for (e in names(l)) { - l[[e]] = c(l[[e]], as.integer(new_arg[[e]])) - } + new_args = method_apply(self$methods, "data_arg") + for (a in names(l)) { + l[[a]] = c(l[[a]], unlist(lapply(new_args, `[[`, a), recursive = FALSE, use.names = FALSE)) } l } @@ -42,6 +33,11 @@ MethList = function(...) { return_object(self, "MethList") } +#' @export +names.MethList = function(x) { + vapply(x$methods, getElement, character(1L), "name") +} + #' Engine Method Class #' #' Engine methods allow users to interact with the engine @@ -51,41 +47,41 @@ MethList = function(...) { #' @param name Method name. #' @param mat_args Character vector of names of matrices that will be used by #' the method to produce an output matrix. -#' @param const_args Character vector of names of constants that will be used +#' @param int_vec_args Character vector of names of constants that will be used #' by the method to produce an output matrix. #' @export -Method = function(name, mat_args, const_args) { +Method = function(name, mat_args, int_vec_args) { self = Base() # Args self$name = name self$mat_args = mat_args - self$const_args = const_args + self$int_vec_args = int_vec_args # Static ## abstract -- instantiate with implementation classes (e.g. MethodRowIndexer) self$meth_type_id = NA_integer_ # Private - self$.method_id = function(method_names) match(self$name, method_names) - 1L - self$.mat_ids = function(mat_names) match(self$mat_args, mat_names) - 1L - self$.const_ids = function(const_names) match(self$const_args, const_names) - 1L + self$.mat_ids = function() match(self$mat_args, names(self$init_mats)) - 1L + self$.int_vec_ids = function() match(self$int_vec_args, names(self$const_int_vecs)) - 1L # Standard Methods - self$data_arg = function(mat_names, const_names) { + self$data_arg = function() { list( ## these must be length-1 integer vectors meth_type_id = self$meth_type_id - , meth_id = self$meth_type_id , meth_n_mats = length(self$mat_args) - , meth_n_const = length(self$const_args) - + , meth_n_int_vecs = length(self$int_vec_args) ## these must be length-meth_n_mats and length-meth_n_const respectively - , meth_mat_id = self$.mat_ids(mat_names) - , meth_const_id = self$.const_ids(const_names) + , meth_mat_id = self$.mat_ids() + , meth_int_vec_id = self$.int_vec_ids() ) } + self$init_mats = MatsList() + self$const_int_vecs = ConstIntVecs() + return_object(self, "Method") } @@ -104,3 +100,11 @@ mk_meth_cls = function(cls_nm, meth_type_id) { assign(cls_nm, f, envir = pf) } for (i in seq_along(meth_cls_types)) mk_meth_cls(meth_cls_types[i], i) + + +#' +EngineMethods = function(...) { + meth_exprs = list(...) + + +} diff --git a/R/expr_list.R b/R/expr_list.R index 82657022..369710bf 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -14,6 +14,7 @@ ExprListUtils = function() { , valid_vars = self$.init_valid_vars() , valid_literals = .existing_literals , offset = .offset + , valid_methods = self$meth_list ) } self$.set_name_prefix = function(x, prefix) { @@ -221,6 +222,7 @@ ExprList = function( # Composition self$init_mats = MatsList() + self$meth_list = MethList() return_object(self, "ExprList") } diff --git a/R/log_files.R b/R/log_files.R index c6a8bd1a..5c56f214 100644 --- a/R/log_files.R +++ b/R/log_files.R @@ -1,13 +1,42 @@ #' Log File #' -#' @param directory Directory within which to store a log file for a -#' model object. +#' @param directory Directory within which to try to store a log file for a +#' model object. If appropriate file access is not available, a temporary +#' file will be used instead. +#' +#' @returns Object of class `LogFile` with the following methods. +#' +#' * `$log()` -- Character vector containing the lines in the log file. +#' * `$data_arg()` -- List containing the components of the `TMB` data +#' structure related to log files. +#' * `$copy(...)` -- Make a copy of the log file at `file.path(...)`. +#' * `$err_msg()` -- Return the current error message in the log file, if any. +#' * Other methods inherited from \code{\link{Files}} #' #' @export LogFile = function(directory = tempdir()) { - file.create(file.path(directory, "log.txt")) - self = Files(directory, reader_spec("log.txt", TXTReader)) + self = Files(fix_dir(directory), reader_spec("log.txt", TXTReader)) self$log = function() self$get("log") self$data_arg = function() list(log_file = self$.file_path("log")) + self$copy = function(...) file.copy(self$.file_path("log"), file.path(...)) + self$err_msg = function() { + re = "^Error message = " + m = grep(re, self$log(), value = TRUE) + sub(re, "", m) + } return_object(self, "LogFile") } + +make_file = function(directory) { + file_path = file.path(directory, "log.txt") + file.create(file_path) + file_path +} +fix_dir = function(directory) { + file_path = make_file(directory) + if (file.access(file_path, c(0, 2, 4)) == -1L) { + directory = tempdir() + file_path = make_file(directory) + } + directory +} diff --git a/R/objective_function.R b/R/objective_function.R index 21d25e51..2592a35d 100644 --- a/R/objective_function.R +++ b/R/objective_function.R @@ -61,6 +61,7 @@ ObjectiveFunction = function(obj_fn_expr) { ## Composition self$init_mats = MatsList() + self$meth_list = MethList() return_object(self, "ObjectiveFunction") } diff --git a/R/parse_expr.R b/R/parse_expr.R index c3e21364..6e31aa49 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -75,6 +75,7 @@ make_expr_parser = function( valid_funcs = environment(x)$valid_funcs, valid_vars = environment(x)$valid_vars, valid_literals = environment(x)$valid_literals, + valid_methods = environment(x)$valid_methods, offset = offset, input_expr_as_string = as.character(x)[2L] ) @@ -151,8 +152,9 @@ finalizer_char = function(x) { finalizer_index = function(x) { valid_funcs = x$valid_funcs valid_vars = x$valid_vars + valid_methods = x$valid_methods valid_literals = as.numeric(x$valid_literals) - x$valid_funcs = x$valid_vars = x$valid_literals = NULL + x$valid_funcs = x$valid_vars = x$valid_literals = x$valid_methods = NULL # remove the tilde function, which is in the first position, # and adjust the indices in i accordingly @@ -164,17 +166,36 @@ finalizer_index = function(x) { i[i == -1L] = 0L }) + x_char = unlist(lapply(x$x, as.character)) + # classify the different types of objects is_literal = unlist(lapply(x$x, is.numeric)) is_func = x$n != 0L - is_var = (!is_func) & (!is_literal) + is_meth = x_char %in% names(valid_methods) + is_var = x_char %in% names(valid_vars) + is_not_found = !(is_literal | is_func | is_meth | is_var) + + if (any(is_not_found)) { + missing_items = x_char[is_not_found] + stop( + "\nthe expression given by:\n", + x$expr_as_string, "\n\n", + "contained the following symbols:\n", + paste0(unique(missing_items), collapse = " "), "\n\n", + " that were not found in the list of available symbols:\n", + paste0(x_char[!is_literal], collapse = " "), # TODO: smarter pasting when this list gets big + "\n\nConsider adding these missing symbols somewhere in your model." + ) + } # identify literals with -1 in the 'number of arguments' new_valid_literals = as.numeric(x$x[is_literal]) x$n[is_literal] = -1L + # identify methods with -2 in the 'number of arguments' + x$n[is_meth] = -2L + # convert character identifiers to integers - x_char = unlist(lapply(x$x, as.character)) x_int = integer(length(x$x)) if (any(is_func)) { x_int[is_func] = get_indices(x_char[is_func] @@ -193,14 +214,25 @@ finalizer_index = function(x) { ) } if (any(is_literal)) { - x_int[is_literal] = length(valid_literals) + seq_along(new_valid_literals) - 1L + x_int[is_literal] = (length(valid_literals) + + seq_along(new_valid_literals) + - 1L + ) valid_literals = c(valid_literals, new_valid_literals) } + if (any(is_meth)) { + get_indices(x_char[is_meth] + , vec = valid_methods + , vec_type = "methods" + , expr_as_string = x$input_expr_as_string + , zero_based = FALSE + ) + } x$x = as.integer(x_int) x$i = as.integer(x$i) nlist( parse_table = as.data.frame(x), - valid_funcs, valid_vars, valid_literals + valid_funcs, valid_vars, valid_methods, valid_literals ) } @@ -221,6 +253,13 @@ get_indices = function(x, vec, vec_type, expr_as_string, zero_based = FALSE) { "\n", sep = "" ) + } else if (vec_type == "methods") { + pointers = "\nHelp for ?engine_methods is under construction." + } else if (vec_type == "integer") { + pointers = paste( + "\nPlease ensure that engine methods refer to the right integer vectors", + sep = "" + ) } stop( "\nthe expression given by:\n", diff --git a/R/tmb_model.R b/R/tmb_model.R index 764a2933..0d279364 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -151,6 +151,7 @@ TMBModel = function( self$obj_fn$init_mats = init_mats self$params$init_mats = init_mats self$random$init_mats = init_mats + for (m in self$meth_list$methods) m$init_mats = init_mats } self$refresh_expr_list = function(expr_list) { self$expr_list = expr_list @@ -168,7 +169,19 @@ TMBModel = function( self$random = random self$refresh_init_mats(self$init_mats) } + self$refresh_const_int_vecs = function(const_int_vecs) { + self$const_int_vecs = const_int_vecs + for (m in self$meth_list$methods) m$const_int_vecs = const_int_vecs + } + self$refresh_meth_list = function(meth_list) { + self$meth_list = meth_list + self$expr_list$meth_list = meth_list + self$obj_fn$meth_list = meth_list + self$refresh_const_int_vecs(self$const_int_vecs) + self$refresh_init_mats(self$init_mats) + } self$refresh_init_mats(self$init_mats) + self$refresh_meth_list(self$meth_list) return_object( valid$tmb_model$assert(self), @@ -265,11 +278,6 @@ TMBSimulationUtils = function() { expr_num = min(which(row < cumsum(expr_num_p_table_rows))) deparse1(self$tmb_model$expr_list$expr_list()[[expr_num]]) } - self$.err_msg = function() { - re = "^Error message = " - m = grep(re, self$tmb_model$log_file$log(), value = TRUE) - sub(re, "", m) - } self$.runner = function(... , .phases = c("before", "during", "after") , .method = c("report", "simulate") @@ -284,7 +292,7 @@ TMBSimulationUtils = function() { if (r$error != 0L) { stop( "\nThe following error was thrown by the TMB engine:\n ", - self$.err_msg(), + self$tmb_model$log_file$err_msg(), "\nThis error occurred at the following expression:\n ", self$.find_problematic_expression(r$expr_row) ) diff --git a/README.md b/README.md index dd1fcc03..6f51b989 100644 --- a/README.md +++ b/README.md @@ -50,47 +50,6 @@ simulator = sir$simulators$tmb(time_steps = 100 sir_sims = simulator$report() ``` -## For Developers - -Developers and contributors should clone this repository and call `make` at the command-line in the top level directory. The following `make` rules are available for getting more control over the build process. - -``` -make quick-install # for changes that only modify R source -make quick-doc-install # for changes that modify R source and roxygen comments -make quick-test # quick-doc-install + run-examples + run-tests -make run-examples # help file checks only (without package rebuild) -make run-tests # run scripts in tests (without package rebuild) -make full-install # for all changes, including changes to C++ source -make src-update # push changes to dev.cpp to macpan2.cpp (see below) -make enum-update # register new C++ engine functions on the R-side -make engine-doc-update # generate roxygen comments from comments in dev.cpp -make doc-update # roxygenize -make pkg-build # build the R package -make pkg-install # install the R package from the build -make pkg-check # R package checks -``` - -In most R packages with compiled code, developers edit the source files to be compiled in the `src` directory. In `macpan2` there is a single file in that directory called `macpan2`, which is generated automatically from the file `misc/dev/dev.cpp`. This setup allows for quicker C++ development cycles, because developers can edit `misc/dev/dev.cpp` and then use this file in tests without needing to re-install the package with the new source. In particular, the above hello-world example could use `dev.cpp` as follows. - -``` -library(macpan2) -macpan2:::dev_compile() ## compile dev.cpp -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -N = 100 -simulator = sir$simulators$tmb(time_steps = 100 - , state = c(S = N - 1, I = 1, R = 0) - , flow = c(foi = 0, gamma = 0.1) - , N = N - , beta = 0.2 - , .tmb_cpp = "dev" ## use dev.cpp -) -sir_sims = simulator$report() -``` - -To update `src/macpan2` to the state of `misc/dev/dev.cpp` one may run `make src-update`. - -Running with `misc/dev/dev.cpp` will print out debugging information in a verbose manner, whereas `src/macpan2.cpp` will not. The `src-update` make rule removes the `#define MP_VERBOSE` flag at the top of the file. - ## Product Management * [Roadmap](https://github.com/orgs/canmod/projects/2) diff --git a/_pkgdown.yml b/_pkgdown.yml index 0b64d5dc..998aaf5d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -34,6 +34,7 @@ articles: desc: Vignettes aimed at package developers contents: - development_patterns + - debugging - composing_simulation_models - elementwise_binary_operators - state_dependent_rates diff --git a/inst/starter_models/sir/settings.json b/inst/starter_models/sir/settings.json index afe3a607..1c7e1e77 100644 --- a/inst/starter_models/sir/settings.json +++ b/inst/starter_models/sir/settings.json @@ -3,5 +3,6 @@ "null_partition" : "Null", "state_variables" : ["S", "I", "R"], "flow_variables" : ["foi", "gamma"], - "infection_flow_variables" : ["foi"] + "infection_flow_variables" : ["foi"], + "infectious_state_variables" : ["I"] } diff --git a/man/LogFile.Rd b/man/LogFile.Rd index 3e2abb30..0570e97b 100644 --- a/man/LogFile.Rd +++ b/man/LogFile.Rd @@ -7,8 +7,20 @@ LogFile(directory = tempdir()) } \arguments{ -\item{directory}{Directory within which to store a log file for a -model object.} +\item{directory}{Directory within which to try to store a log file for a +model object. If appropriate file access is not available, a temporary +file will be used instead.} +} +\value{ +Object of class \code{LogFile} with the following methods. +\itemize{ +\item \verb{$log()} -- Character vector containing the lines in the log file. +\item \verb{$data_arg()} -- List containing the components of the \code{TMB} data +structure related to log files. +\item \verb{$copy(...)} -- Make a copy of the log file at \code{file.path(...)}. +\item \verb{$err_msg()} -- Return the current error message in the log file, if any. +\item Other methods inherited from \code{\link{Files}} +} } \description{ Log File diff --git a/man/Method.Rd b/man/Method.Rd index c02d36e4..87627c33 100644 --- a/man/Method.Rd +++ b/man/Method.Rd @@ -4,7 +4,7 @@ \alias{Method} \title{Engine Method Class} \usage{ -Method(name, mat_args, const_args) +Method(name, mat_args, int_vec_args) } \arguments{ \item{name}{Method name.} @@ -12,7 +12,7 @@ Method(name, mat_args, const_args) \item{mat_args}{Character vector of names of matrices that will be used by the method to produce an output matrix.} -\item{const_args}{Character vector of names of constants that will be used +\item{int_vec_args}{Character vector of names of constants that will be used by the method to produce an output matrix.} } \description{ diff --git a/misc/build/run_vignette_code.R b/misc/build/run_vignette_code.R index 160cda9e..ad3666a3 100644 --- a/misc/build/run_vignette_code.R +++ b/misc/build/run_vignette_code.R @@ -31,3 +31,4 @@ source_r_code_from_vignettes <- function(vignette_directory) { } source_r_code_from_vignettes("vignettes") +message("Vignette success!") diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 58908f5d..2662a774 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -342,7 +342,8 @@ class ExprEvaluator { error_code = code; expr_row = row; strcpy(error_message, message); - std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + // std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + // Rf_error(message); }; // evaluators @@ -2015,7 +2016,7 @@ class ExprEvaluator { REPORT(error); \ REPORT(expr_row); \ \ - logfile.open (LOG_FILE_NAME, std::ios_base::app); \ + logfile.open (log_file, std::ios_base::app); \ logfile << "Error code = " << error << std::endl; \ logfile << "Error message = " << err_msg << std::endl; \ logfile << "Expression row = " << expr_row << std::endl; \ @@ -2058,7 +2059,7 @@ void UpdateSimulationHistory( } -const char LOG_FILE_NAME[] = "macpan2.log"; +// const char LOG_FILE_NAME[] = "macpan2.log"; // "main" function template @@ -2068,8 +2069,12 @@ Type objective_function::operator() () std::cout << "============== objective_function =============" << std::endl; #endif + // Log file path + DATA_STRING(log_file); + std::ofstream logfile; - logfile.open (LOG_FILE_NAME); + logfile.open (log_file); + //logfile.open (LOG_FILE_NAME); logfile << "======== log file of MacPan2 ========\n"; logfile.close(); diff --git a/src/macpan2.cpp b/src/macpan2.cpp index b5b6190e..513ae8c4 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -343,7 +343,8 @@ class ExprEvaluator { error_code = code; expr_row = row; strcpy(error_message, message); - std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + // std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + // Rf_error(message); }; // evaluators @@ -2016,7 +2017,7 @@ class ExprEvaluator { REPORT(error); \ REPORT(expr_row); \ \ - logfile.open (LOG_FILE_NAME, std::ios_base::app); \ + logfile.open (log_file, std::ios_base::app); \ logfile << "Error code = " << error << std::endl; \ logfile << "Error message = " << err_msg << std::endl; \ logfile << "Expression row = " << expr_row << std::endl; \ @@ -2059,7 +2060,7 @@ void UpdateSimulationHistory( } -const char LOG_FILE_NAME[] = "macpan2.log"; +// const char LOG_FILE_NAME[] = "macpan2.log"; // "main" function template @@ -2069,8 +2070,12 @@ Type objective_function::operator() () std::cout << "============== objective_function =============" << std::endl; #endif + // Log file path + DATA_STRING(log_file); + std::ofstream logfile; - logfile.open (LOG_FILE_NAME); + logfile.open (log_file); + //logfile.open (LOG_FILE_NAME); logfile << "======== log file of MacPan2 ========\n"; logfile.close(); diff --git a/tests/testthat/test-tmb-simulator.R b/tests/testthat/test-tmb-simulator.R index a8ee75df..6eb58be0 100644 --- a/tests/testthat/test-tmb-simulator.R +++ b/tests/testthat/test-tmb-simulator.R @@ -5,6 +5,6 @@ test_that("informative messages are given when non-existant functions or variabl ) expect_error( engine_eval(~ x), - "that were not found in the list of available variables" + "that were not found in the list of available symbols" ) }) From 5f72d45ffb486dd3b1122c08c17ae7f2d5012afd Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Sep 2023 14:33:08 -0400 Subject: [PATCH 014/332] can use ints to subset matrices --- DESCRIPTION | 2 +- Makefile | 2 +- NAMESPACE | 6 +- R/compartmental.R | 2 +- R/const_int_vec.R | 6 +- R/dev_tools.R | 1 - R/engine_eval.R | 2 +- R/engine_methods.R | 87 ++++++++++++++++++++++++----- R/expr_list.R | 4 +- R/mats_list.R | 21 ++----- R/objective_function.R | 2 +- R/parse_expr.R | 4 +- R/tmb_model.R | 41 ++++++-------- R/tmb_model_editors.R | 4 +- R/validity.R | 2 +- R/zzz.R | 1 + man/CompartmentalSimulator.Rd | 2 +- man/{ConstIntVecs.Rd => IntVecs.Rd} | 6 +- man/Method.Rd | 8 ++- man/TMBModel.Rd | 9 ++- man/TMBSimulator.Rd | 6 +- man/engine_eval.Rd | 2 +- man/parse_expr_list.Rd | 4 +- misc/dev/dev.cpp | 80 ++++++++++++++++++++------ src/macpan2.cpp | 80 ++++++++++++++++++++------ 25 files changed, 263 insertions(+), 121 deletions(-) rename man/{ConstIntVecs.Rd => IntVecs.Rd} (83%) diff --git a/DESCRIPTION b/DESCRIPTION index 569eb0fe..d084e66d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: macpan2 Title: Fast and Flexible Compartmental Modelling -Version: 0.0.3 +Version: 1.0.0 Authors@R: c( person("Steve Walker", email="swalk@mcmaster.ca", role=c("cre", "aut")), person("Darren Flynn-Primrose", role="aut"), diff --git a/Makefile b/Makefile index 26bed5fd..ada46359 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SED_RE = \(\,\)*[ ]*\/\/[ ]*\(.*\) ALIAS_RE = [ ]*MP2_\(.*\)\: \(.*\)(\(.*\)) ROXY_RE = ^.*\(\#'.*\)$ VERSION := $(shell sed -n '/^Version: /s///p' DESCRIPTION) -TEST := "testthat::test_package(\"macpan2\", reporter = \"progress\")" +TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") all: make src-update diff --git a/NAMESPACE b/NAMESPACE index 380f4069..636ad213 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,7 @@ S3method(c,String) S3method(c,StringData) -S3method(names,ConstIntVecs) +S3method(names,IntVecs) S3method(names,MatsList) S3method(names,MethList) S3method(print,MathExpression) @@ -30,13 +30,15 @@ export(CSVReader) export(Collection) export(Compartmental) export(CompartmentalSimulator) -export(ConstIntVecs) export(DerivationExtractor) export(Derivations2ExprList) +export(EngineMethods) export(Euler) export(ExprList) export(Files) export(FlowExpander) +export(FromRows) +export(IntVecs) export(JSONReader) export(Log) export(LogFile) diff --git a/R/compartmental.R b/R/compartmental.R index 4e71a4a8..4a562df8 100644 --- a/R/compartmental.R +++ b/R/compartmental.R @@ -77,7 +77,7 @@ CompartmentalSimulator = function(model_directory, time_steps , .mats_to_save = .mats_to_return , .mats_to_return = "state" , .dimnames = list() - , .tmb_cpp = "macpan2" + , .tmb_cpp = getOption("macpan2_dll") #"macpan2" , .initialize_ad_fun = TRUE ) { compartmental = Compartmental(model_directory) diff --git a/R/const_int_vec.R b/R/const_int_vec.R index 0c7b8bfd..555414a0 100644 --- a/R/const_int_vec.R +++ b/R/const_int_vec.R @@ -4,7 +4,7 @@ #' #' @param ... Named arguments, each of which can be coerced to an integer vector. #' @export -ConstIntVecs = function(...) { +IntVecs = function(...) { self = Base() # Args @@ -22,8 +22,8 @@ ConstIntVecs = function(...) { } self$const_names = function() names(self$list) - return_object(self, "ConstIntVecs") + return_object(self, "IntVecs") } #' @export -names.ConstIntVecs = function(x) names(x$list) +names.IntVecs = function(x) names(x$list) diff --git a/R/dev_tools.R b/R/dev_tools.R index bba41ef9..364b28c2 100644 --- a/R/dev_tools.R +++ b/R/dev_tools.R @@ -6,7 +6,6 @@ dev_in_vig = function() "vignette_status.Rmd" %in% list.files(full.names = FALSE dev_file = function(suffix = "", ext = "cpp") { cpp = function(path) { pp = file.path(path, sprintf("dev%s.%s", suffix, ext)) - print("YESYYSYSE") print(pp) print(file.exists(pp)) print(getwd()) diff --git a/R/engine_eval.R b/R/engine_eval.R index 46e2eecc..f88a1b36 100644 --- a/R/engine_eval.R +++ b/R/engine_eval.R @@ -34,7 +34,7 @@ #' , .matrix_to_return = "x" #' ) #' @importFrom stats as.formula -engine_eval = function(e, ..., .matrix_to_return, .tmb_cpp = "macpan2", .structure_labels = NullLabels()) { +engine_eval = function(e, ..., .matrix_to_return, .tmb_cpp = getOption("macpan2_dll"), .structure_labels = NullLabels()) { dot_mats = list(...) ## force two-sided formula for compliance with TMBSimulator diff --git a/R/engine_methods.R b/R/engine_methods.R index f2e894a3..58101388 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -1,3 +1,69 @@ +#' @export +EngineMethods = function(...) { + self = Base() + self$method_exprs = list(...) + + self$methods = function() { + method_objs = list() + for (meth_nm in names(self$method_exprs)) { + m = self$method_exprs[[meth_nm]] + args = c( + as.list(c(meth_nm, m$mat, m$id_nm)), + init_mats = m$init_mats, + int_vecs = self$int_vecs() + ) + method_objs[[meth_nm]] = do.call(m$constructor, args) + } + do.call(MethList, method_objs) + } + self$int_vecs = function() { + int_vec_objs = list() + for (meth_nm in names(self$method_exprs)) { + m = self$method_exprs[[meth_nm]] + int_vec_objs[[m$id_nm]] = m$indices() + } + do.call(IntVecs, int_vec_objs) + } + self$refresh_init_mats = function(init_mats) { + for (meth_nm in names(self$method_exprs)) { + m = self$method_exprs[[meth_nm]]$init_mats = init_mats + } + } + return_object(self, "EngineMethods") +} + +MethodRowIndexer = NULL + +#' @export +FromRows = function(mat + , by + , id_nm + , init_mats = MatsList() + ) { + self = Base() + + self$mat = mat + self$by = by + self$id_nm = id_nm + self$init_mats = init_mats + + self$call = deparse1(match.call()) + self$constructor = MethodRowIndexer + + self$indices = function() { + l = list() + l[[self$id_nm]] = get_indices(self$by + , vec = self$init_mats$rownames()[[self$mat]] + , vec_type = "variables" + , expr_as_string = self$call + , zero_based = TRUE + ) + l + } + return_object(self, "FromRows") +} + + #' Method List #' #' @param ... List of \code{\link{Method}} objects. @@ -50,13 +116,15 @@ names.MethList = function(x) { #' @param int_vec_args Character vector of names of constants that will be used #' by the method to produce an output matrix. #' @export -Method = function(name, mat_args, int_vec_args) { +Method = function(name, mat_args, int_vec_args, init_mats = MatsList(), int_vecs = IntVecs()) { self = Base() # Args self$name = name self$mat_args = mat_args self$int_vec_args = int_vec_args + self$init_mats = init_mats + self$int_vecs = int_vecs # Static ## abstract -- instantiate with implementation classes (e.g. MethodRowIndexer) @@ -64,7 +132,7 @@ Method = function(name, mat_args, int_vec_args) { # Private self$.mat_ids = function() match(self$mat_args, names(self$init_mats)) - 1L - self$.int_vec_ids = function() match(self$int_vec_args, names(self$const_int_vecs)) - 1L + self$.int_vec_ids = function() match(self$int_vec_args, names(self$int_vecs)) - 1L # Standard Methods self$data_arg = function() { @@ -79,9 +147,6 @@ Method = function(name, mat_args, int_vec_args) { ) } - self$init_mats = MatsList() - self$const_int_vecs = ConstIntVecs() - return_object(self, "Method") } @@ -92,19 +157,11 @@ mk_meth_cls = function(cls_nm, meth_type_id) { force(pf) force(cls_nm) force(meth_type_id) - f = function(name, mat_args, const_args) { - self = Method(name, mat_args, const_args) + f = function(name, mat_args, const_args, init_mats = MatsList(), int_vecs = IntVecs()) { + self = Method(name, mat_args, const_args, init_mats, int_vecs) self$meth_type_id = meth_type_id return_object(self, cls_nm) } assign(cls_nm, f, envir = pf) } for (i in seq_along(meth_cls_types)) mk_meth_cls(meth_cls_types[i], i) - - -#' -EngineMethods = function(...) { - meth_exprs = list(...) - - -} diff --git a/R/expr_list.R b/R/expr_list.R index 369710bf..cdda3ca9 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -14,7 +14,7 @@ ExprListUtils = function() { , valid_vars = self$.init_valid_vars() , valid_literals = .existing_literals , offset = .offset - , valid_methods = self$meth_list + , valid_methods = self$engine_methods$methods() ) } self$.set_name_prefix = function(x, prefix) { @@ -222,7 +222,7 @@ ExprList = function( # Composition self$init_mats = MatsList() - self$meth_list = MethList() + self$engine_methods = EngineMethods() return_object(self, "ExprList") } diff --git a/R/mats_list.R b/R/mats_list.R index 27bbf882..f295293d 100644 --- a/R/mats_list.R +++ b/R/mats_list.R @@ -44,12 +44,7 @@ MatsList = function(... , .mats_to_return = character(0L) , .dimnames = list() , .structure_labels = NullLabels() - #, .init_saved_dims = list() ) { - # self = EditableArgs(MatsList - # , lapply(list(...), as.matrix) - # , list() - # ) self = Base() ## Args @@ -65,7 +60,6 @@ MatsList = function(... self$.mats_to_return = .mats_to_return self$.dimnames = .dimnames self$.structure_labels = .structure_labels - #self$.init_saved_dims = .init_saved_dims # Static self$.initial_mats = lapply(list(...), as.matrix) @@ -73,16 +67,6 @@ MatsList = function(... self$mats_save_hist = function() names(self$.initial_mats) %in% self$.mats_to_save self$mats_return = function() names(self$.initial_mats) %in% self$.mats_to_return - # self$mats_save_dims = function() { - # msh = self$mats_save_hist() - # msd = setNames( - # lapply(self$.initial_mats[msh], dim), - # self$.names()[msh] - # ) - # for (m in names(self$.init_saved_dims)) { - # - # } - # } ## Standard methods self$get = function(variable_name) { @@ -133,6 +117,10 @@ MatsList = function(... self$mat_dims = function() { data.frame(mat = self$.names(), nrow = self$.nrow, ncol = self$.ncol) } + + self$dimnames = function() self$.dimnames + self$rownames = function() lapply(self$dimnames(), getElement, 1L) + self$colnames = function() lapply(self$dimnames(), getElement, 2L) self$data_arg = function() { r = list( mats = self$.mats(), @@ -188,4 +176,3 @@ MatsList = function(... #' @export names.MatsList = function(x) x$.names() - diff --git a/R/objective_function.R b/R/objective_function.R index 2592a35d..8ef4c360 100644 --- a/R/objective_function.R +++ b/R/objective_function.R @@ -61,7 +61,7 @@ ObjectiveFunction = function(obj_fn_expr) { ## Composition self$init_mats = MatsList() - self$meth_list = MethList() + self$engine_methods = EngineMethods() return_object(self, "ObjectiveFunction") } diff --git a/R/parse_expr.R b/R/parse_expr.R index 6e31aa49..4343c0fa 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -330,7 +330,7 @@ empty_matrix = matrix(numeric(0L), 0L, 0L) #' from a related expression list. Additional literals in the expressions #' themselves will be discovered and added to this list. #' @param valid_methods \code{\link{MethList}} object. -#' @param valid_int_vecs \code{\link{ConstIntVecs}} object. +#' @param valid_int_vecs \code{\link{IntVecs}} object. #' @param offset The zero-based row index for the first row of the table. #' This is useful when combining tables. #' @@ -339,7 +339,7 @@ parse_expr_list = function(expr_list , valid_vars , valid_literals = numeric(0L) , valid_methods = MethList() - , valid_int_vecs = ConstIntVecs() + , valid_int_vecs = IntVecs() , offset = 0L ) { eval_env = nlist( diff --git a/R/tmb_model.R b/R/tmb_model.R index 0d279364..ae0444c0 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -10,7 +10,7 @@ #' @param obj_fn An object of class \code{\link{ObjectiveFunction}}. #' @param time_steps An object of class \code{\link{Time}}. #' @param meth_list An object of class \code{\link{MethList}}. -#' @param const_int_vecs An object of class \code{\link{ConstIntVecs}}. +#' @param int_vecs An object of class \code{\link{IntVecs}}. #' @param log_file An object of class \code{\link{LogFile}}. #' #' @return Object of class \code{TMBModel} with the following methods. @@ -75,8 +75,7 @@ TMBModel = function( , random = OptParamsList() , obj_fn = ObjectiveFunction(~0) , time_steps = Time(0L) - , meth_list = MethList() - , const_int_vecs = ConstIntVecs() + , engine_methods = EngineMethods() , log_file = LogFile() ) { ## Inheritance @@ -89,8 +88,9 @@ TMBModel = function( self$random = random self$obj_fn = obj_fn self$time_steps = time_steps - self$meth_list = meth_list - self$const_int_vecs = const_int_vecs + #self$meth_list = meth_list + #self$int_vecs = int_vecs + self$engine_methods = engine_methods self$log_file = log_file ## Standard Methods @@ -103,8 +103,8 @@ TMBModel = function( self$random$data_arg("r"), self$obj_fn$data_arg(existing_literals), self$time_steps$data_arg(), - self$meth_list$data_arg(), - self$const_int_vecs$data_arg(), + self$engine_methods$methods()$data_arg(), + self$engine_methods$int_vecs()$data_arg(), self$log_file$data_arg() ) } @@ -120,7 +120,7 @@ TMBModel = function( if (length(self$random$vector()) == 0L) return(NULL) return("random") } - self$make_ad_fun_arg = function(tmb_cpp = "macpan2") { + self$make_ad_fun_arg = function(tmb_cpp = getOption("macpan2_dll")) { list( data = self$data_arg(), parameters = self$param_arg(), @@ -128,11 +128,11 @@ TMBModel = function( DLL = tmb_cpp ) } - self$ad_fun = function(tmb_cpp = "macpan2") { + self$ad_fun = function(tmb_cpp = getOption("macpan2_dll")) { do.call(TMB::MakeADFun, self$make_ad_fun_arg(tmb_cpp)) } - self$simulator = function(tmb_cpp = "macpan2", initialize_ad_fun = TRUE) { + self$simulator = function(tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE) { TMBSimulator(self, tmb_cpp = tmb_cpp, initialize_ad_fun = initialize_ad_fun) } @@ -151,7 +151,7 @@ TMBModel = function( self$obj_fn$init_mats = init_mats self$params$init_mats = init_mats self$random$init_mats = init_mats - for (m in self$meth_list$methods) m$init_mats = init_mats + self$engine_methods$refresh_init_mats(init_mats) } self$refresh_expr_list = function(expr_list) { self$expr_list = expr_list @@ -169,19 +169,14 @@ TMBModel = function( self$random = random self$refresh_init_mats(self$init_mats) } - self$refresh_const_int_vecs = function(const_int_vecs) { - self$const_int_vecs = const_int_vecs - for (m in self$meth_list$methods) m$const_int_vecs = const_int_vecs - } - self$refresh_meth_list = function(meth_list) { - self$meth_list = meth_list - self$expr_list$meth_list = meth_list - self$obj_fn$meth_list = meth_list - self$refresh_const_int_vecs(self$const_int_vecs) + self$refresh_engine_methods = function(engine_methods) { + self$engine_methods = engine_methods + self$expr_list$engine_methods = engine_methods + self$obj_fn$engine_methods = engine_methods self$refresh_init_mats(self$init_mats) } self$refresh_init_mats(self$init_mats) - self$refresh_meth_list(self$meth_list) + self$refresh_engine_methods(self$engine_methods) return_object( valid$tmb_model$assert(self), @@ -239,7 +234,7 @@ TMBSimulationUtils = function() { c("matrix", "time", "row", "col", "value") ) ## get raw simulation output from TMB and supply column names (which don't exist on the TMB side) r$matrix = self$matrix_names()[r$matrix + 1L] ## replace matrix indices with matrix names - dn = self$tmb_model$init_mats$.dimnames ## get the row and column names of matrices with such names + dn = self$tmb_model$init_mats$dimnames() ## get the row and column names of matrices with such names for (mat in names(dn)) { i = r$matrix == mat @@ -335,7 +330,7 @@ TMBSimulationUtils = function() { #' #' @importFrom MASS mvrnorm #' @export -TMBSimulator = function(tmb_model, tmb_cpp = "macpan2", initialize_ad_fun = TRUE) { +TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE) { self = TMBSimulationUtils() ## Args diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index 7863ece5..1fbd5853 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -180,7 +180,7 @@ TMBSimulatorReplacer = function(simulator) { invisible(self$simulator) } self$params_frame = function(frame) { - new_params = OptParamsFrame(frame, self$model$init_mats$.dimnames) + new_params = OptParamsFrame(frame, self$model$init_mats$dimnames()) self$model$refresh_params(new_params) self$simulator$cache$invalidate() valid$consistency_params_mats$check(self$model) @@ -190,7 +190,7 @@ TMBSimulatorReplacer = function(simulator) { self$params_frame(data.frame(default, mat, row, col)) } self$random_frame = function(frame) { - new_random = OptParamsFrame(frame, self$model$init_mats$.dimnames) + new_random = OptParamsFrame(frame, self$model$init_mats$dimnames()) self$model$refresh_random(new_random) self$simulator$cache$invalidate() valid$consistency_random_mats$check(self$model) diff --git a/R/validity.R b/R/validity.R index 7a384ebc..ba106872 100644 --- a/R/validity.R +++ b/R/validity.R @@ -387,7 +387,7 @@ TMBAdaptorValidity <- function() { self$all_int, self$component_lengths("eval_schedule", 3L, 3L), self$component_ranges("p_table_x", 0L, Inf), - self$component_ranges("p_table_n", -1L, Inf), + self$component_ranges("p_table_n", -2L, Inf), self$component_ranges("p_table_i", -1L, Inf), self$component_ranges("expr_output_id", 0L, Inf), self$component_ranges("expr_sim_block", 0L, 1L), diff --git a/R/zzz.R b/R/zzz.R index 8305f0f7..2fd2198f 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,2 +1,3 @@ .onLoad <- function(lib, pkg) { + options(macpan2_dll = "macpan2") } diff --git a/man/CompartmentalSimulator.Rd b/man/CompartmentalSimulator.Rd index 5e7c367e..a4ab38f8 100644 --- a/man/CompartmentalSimulator.Rd +++ b/man/CompartmentalSimulator.Rd @@ -10,7 +10,7 @@ CompartmentalSimulator( .mats_to_save = .mats_to_return, .mats_to_return = "state", .dimnames = list(), - .tmb_cpp = "macpan2", + .tmb_cpp = getOption("macpan2_dll"), .initialize_ad_fun = TRUE ) } diff --git a/man/ConstIntVecs.Rd b/man/IntVecs.Rd similarity index 83% rename from man/ConstIntVecs.Rd rename to man/IntVecs.Rd index 33404912..90450862 100644 --- a/man/ConstIntVecs.Rd +++ b/man/IntVecs.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/const_int_vec.R -\name{ConstIntVecs} -\alias{ConstIntVecs} +\name{IntVecs} +\alias{IntVecs} \title{Constant Integer Vectors} \usage{ -ConstIntVecs(...) +IntVecs(...) } \arguments{ \item{...}{Named arguments, each of which can be coerced to an integer vector.} diff --git a/man/Method.Rd b/man/Method.Rd index 87627c33..3b86bfa0 100644 --- a/man/Method.Rd +++ b/man/Method.Rd @@ -4,7 +4,13 @@ \alias{Method} \title{Engine Method Class} \usage{ -Method(name, mat_args, int_vec_args) +Method( + name, + mat_args, + int_vec_args, + init_mats = MatsList(), + int_vecs = IntVecs() +) } \arguments{ \item{name}{Method name.} diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index c0b57724..7f6d9bb4 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -11,8 +11,7 @@ TMBModel( random = OptParamsList(), obj_fn = ObjectiveFunction(~0), time_steps = Time(0L), - meth_list = MethList(), - const_int_vecs = ConstIntVecs(), + engine_methods = EngineMethods(), log_file = LogFile() ) } @@ -29,11 +28,11 @@ TMBModel( \item{time_steps}{An object of class \code{\link{Time}}.} -\item{meth_list}{An object of class \code{\link{MethList}}.} +\item{log_file}{An object of class \code{\link{LogFile}}.} -\item{const_int_vecs}{An object of class \code{\link{ConstIntVecs}}.} +\item{meth_list}{An object of class \code{\link{MethList}}.} -\item{log_file}{An object of class \code{\link{LogFile}}.} +\item{int_vecs}{An object of class \code{\link{IntVecs}}.} } \value{ Object of class \code{TMBModel} with the following methods. diff --git a/man/TMBSimulator.Rd b/man/TMBSimulator.Rd index 0f4172e6..e758b60d 100644 --- a/man/TMBSimulator.Rd +++ b/man/TMBSimulator.Rd @@ -4,7 +4,11 @@ \alias{TMBSimulator} \title{TMB Simulator} \usage{ -TMBSimulator(tmb_model, tmb_cpp = "macpan2", initialize_ad_fun = TRUE) +TMBSimulator( + tmb_model, + tmb_cpp = getOption("macpan2_dll"), + initialize_ad_fun = TRUE +) } \arguments{ \item{tmb_model}{An object of class \code{\link{TMBModel}}.} diff --git a/man/engine_eval.Rd b/man/engine_eval.Rd index 7a70a8e4..c2ae2a2f 100644 --- a/man/engine_eval.Rd +++ b/man/engine_eval.Rd @@ -8,7 +8,7 @@ engine_eval( e, ..., .matrix_to_return, - .tmb_cpp = "macpan2", + .tmb_cpp = getOption("macpan2_dll"), .structure_labels = NullLabels() ) } diff --git a/man/parse_expr_list.Rd b/man/parse_expr_list.Rd index c1272378..d00beb99 100644 --- a/man/parse_expr_list.Rd +++ b/man/parse_expr_list.Rd @@ -9,7 +9,7 @@ parse_expr_list( valid_vars, valid_literals = numeric(0L), valid_methods = MethList(), - valid_int_vecs = ConstIntVecs(), + valid_int_vecs = IntVecs(), offset = 0L ) } @@ -25,7 +25,7 @@ themselves will be discovered and added to this list.} \item{valid_methods}{\code{\link{MethList}} object.} -\item{valid_int_vecs}{\code{\link{ConstIntVecs}} object.} +\item{valid_int_vecs}{\code{\link{IntVecs}} object.} \item{offset}{The zero-based row index for the first row of the table. This is useful when combining tables.} diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 2662a774..8c214293 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -1,4 +1,5 @@ #define MP_VERBOSE +#define TMB_LIB_INIT R_init_macpan2 #define EIGEN_PERMANENTLY_DISABLE_STUPID_WARNINGS #include #include @@ -9,7 +10,6 @@ #include // isnan() is defined #include // https://github.com/kaskr/adcomp/wiki/Development#distributing-code -#define TMB_LIB_INIT R_init_macpan2 #include #include @@ -226,14 +226,23 @@ struct ListOfMatrices { }; +// class for lists of integer vectors that have been +// 'flattened' into two integer vectors -- one +// containing the concatenation of the vectors and +// another containing the lengths of each concatenated +// vector class ListOfIntVecs { public: + // this nestedVector will contain examples of unflattened + // integer vectors std::vector> nestedVector; // Default constructor ListOfIntVecs() {} // Constructor that takes all_ints and vec_lens vectors + // all_ints: concatenated integer vectors + // vec_lens: lengths of each concatenated integer vectors ListOfIntVecs(const std::vector& all_ints, const std::vector& vec_lens) { size_t totalElements = 0; for (int size : vec_lens) { @@ -255,7 +264,7 @@ class ListOfIntVecs { } } - // Overload [] operator to access the inner vectors by index + // Overload [] operator to access the unflattened vectors by index std::vector& operator[](size_t index) { if (index < nestedVector.size()) { return nestedVector[index]; @@ -265,7 +274,7 @@ class ListOfIntVecs { } } - // Method to return the number of vector objects in the struct + // Method to return the number of unflattened vectors size_t size() const { return nestedVector.size(); } @@ -285,8 +294,18 @@ class ListOfIntVecs { return result; } -}; + // Method to print each vector in the list + void printVectors() const { + for (const std::vector& innerVector : nestedVector) { + std::cout << "Vector:"; + for (int element : innerVector) { + std::cout << " " << element; + } + std::cout << std::endl; + } + } +}; @@ -342,8 +361,6 @@ class ExprEvaluator { error_code = code; expr_row = row; strcpy(error_message, message); - // std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; - // Rf_error(message); }; // evaluators @@ -360,26 +377,55 @@ class ExprEvaluator { vector v; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars - int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2, curr_meth_id; + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size; + int sz, start, err_code, err_code1, err_code2, curr_meth_id; + size_t numMats; + size_t numIntVecs; + std::vector curr_meth_mat_id_vec; + std::vector curr_meth_int_id_vec; + vector> meth_args(meth_mats.size()); + ListOfIntVecs meth_int_args; if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. switch (table_n[row]) { case -2: // methods (pre-processed matrices) + // Access and manipulate meth_mats + numMats = meth_mats.size(); + numIntVecs = meth_int_vecs.size(); + // for (size_t i = 0; i < numMats; ++i) { + // curr_meth_mat_id_vec = meth_mats[i]; + // + // // Print the elements of the vector + // std::cout << "single vector: "; + // for (int element : curr_meth_mat_id_vec) { + // std::cout << element << ' '; + // } + // std::cout << std::endl; + // } std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; - curr_meth_id = table_x[row]+1; - // v = meth_mats.size(); - - // vector > int_vec_args(meth_int_vecs.v_vectors.size()); - // for (int i=0; i::Zero(meth_int_args[0].size(), meth_args[0].cols()); // m1 = matrix::Zero(v.size(), m.cols()); - // for (int i=0; i #include @@ -10,7 +11,6 @@ #include // isnan() is defined #include // https://github.com/kaskr/adcomp/wiki/Development#distributing-code -#define TMB_LIB_INIT R_init_macpan2 #include #include @@ -227,14 +227,23 @@ struct ListOfMatrices { }; +// class for lists of integer vectors that have been +// 'flattened' into two integer vectors -- one +// containing the concatenation of the vectors and +// another containing the lengths of each concatenated +// vector class ListOfIntVecs { public: + // this nestedVector will contain examples of unflattened + // integer vectors std::vector> nestedVector; // Default constructor ListOfIntVecs() {} // Constructor that takes all_ints and vec_lens vectors + // all_ints: concatenated integer vectors + // vec_lens: lengths of each concatenated integer vectors ListOfIntVecs(const std::vector& all_ints, const std::vector& vec_lens) { size_t totalElements = 0; for (int size : vec_lens) { @@ -256,7 +265,7 @@ class ListOfIntVecs { } } - // Overload [] operator to access the inner vectors by index + // Overload [] operator to access the unflattened vectors by index std::vector& operator[](size_t index) { if (index < nestedVector.size()) { return nestedVector[index]; @@ -266,7 +275,7 @@ class ListOfIntVecs { } } - // Method to return the number of vector objects in the struct + // Method to return the number of unflattened vectors size_t size() const { return nestedVector.size(); } @@ -286,8 +295,18 @@ class ListOfIntVecs { return result; } -}; + // Method to print each vector in the list + void printVectors() const { + for (const std::vector& innerVector : nestedVector) { + std::cout << "Vector:"; + for (int element : innerVector) { + std::cout << " " << element; + } + std::cout << std::endl; + } + } +}; @@ -343,8 +362,6 @@ class ExprEvaluator { error_code = code; expr_row = row; strcpy(error_message, message); - // std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; - // Rf_error(message); }; // evaluators @@ -361,26 +378,55 @@ class ExprEvaluator { vector v; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars - int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2, curr_meth_id; + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size; + int sz, start, err_code, err_code1, err_code2, curr_meth_id; + size_t numMats; + size_t numIntVecs; + std::vector curr_meth_mat_id_vec; + std::vector curr_meth_int_id_vec; + vector> meth_args(meth_mats.size()); + ListOfIntVecs meth_int_args; if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. switch (table_n[row]) { case -2: // methods (pre-processed matrices) + // Access and manipulate meth_mats + numMats = meth_mats.size(); + numIntVecs = meth_int_vecs.size(); + // for (size_t i = 0; i < numMats; ++i) { + // curr_meth_mat_id_vec = meth_mats[i]; + // + // // Print the elements of the vector + // std::cout << "single vector: "; + // for (int element : curr_meth_mat_id_vec) { + // std::cout << element << ' '; + // } + // std::cout << std::endl; + // } std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; - curr_meth_id = table_x[row]+1; - // v = meth_mats.size(); - - // vector > int_vec_args(meth_int_vecs.v_vectors.size()); - // for (int i=0; i::Zero(meth_int_args[0].size(), meth_args[0].cols()); // m1 = matrix::Zero(v.size(), m.cols()); - // for (int i=0; i Date: Mon, 18 Sep 2023 15:05:18 -0400 Subject: [PATCH 015/332] define engine methods in c++ --- Makefile | 9 +- NAMESPACE | 1 - R/alt_model_constructors.R | 27 ++-- R/case_changes.R | 17 +++ R/compartmental.R | 3 - R/engine_methods.R | 159 +++++++++++------------ R/enum_methods.R | 141 +++++++++++++++++++++ R/expr_list.R | 2 +- R/parse_expr.R | 5 +- R/simulators.R | 2 +- R/tmb_model.R | 43 +------ man/EngineMethods.Rd | 21 ++++ man/MethListFromEngineMethods.Rd | 19 +++ man/MethodPrototype.Rd | 23 ++++ man/MethodTypeUtils.Rd | 12 ++ man/TMBModel.Rd | 6 +- man/mk_meth_cls.Rd | 17 +++ misc/dev/dev.cpp | 208 ++++++++++++++++++++++++------- src/macpan2.cpp | 208 ++++++++++++++++++++++++------- 19 files changed, 687 insertions(+), 236 deletions(-) create mode 100644 R/case_changes.R create mode 100644 R/enum_methods.R create mode 100644 man/EngineMethods.Rd create mode 100644 man/MethListFromEngineMethods.Rd create mode 100644 man/MethodPrototype.Rd create mode 100644 man/MethodTypeUtils.Rd create mode 100644 man/mk_meth_cls.Rd diff --git a/Makefile b/Makefile index ada46359..f20d0966 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") all: make src-update make enum-update + make enum-meth-update make engine-doc-update make doc-update make pkg-build @@ -20,6 +21,7 @@ all: full-install: make src-update make enum-update + make enum-meth-update make engine-doc-update make doc-update make pkg-build @@ -29,7 +31,7 @@ full-install: # Use this rule if (1) you are in a development cycle, (2) you # haven't updated macpan.cpp (but have perhaps modified dev.cpp) # and (3) do not require a roxygen update. -quick-install: enum-update +quick-install: enum-update enum-meth-update R CMD INSTALL --no-multiarch --install-tests . install-deps: @@ -81,6 +83,11 @@ R/enum.R: misc/dev/dev.cpp misc/build/enum_tail.R echo "valid_funcs = setNames(as.list(valid_funcs), valid_funcs)" >> $@ +enum-meth-update:: R/enum_methods.R +R/enum_methods.R: misc/dev/dev.cpp misc/build/method_head.R misc/build/build_from_enum_methods.R + Rscript misc/build/build_from_enum_methods.R + + src-update:: src/macpan2.cpp src/macpan2.cpp: misc/dev/dev.cpp echo "// Auto-generated - do not edit by hand" > $@ diff --git a/NAMESPACE b/NAMESPACE index 636ad213..fe9ec407 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,7 +37,6 @@ export(Euler) export(ExprList) export(Files) export(FlowExpander) -export(FromRows) export(IntVecs) export(JSONReader) export(Log) diff --git a/R/alt_model_constructors.R b/R/alt_model_constructors.R index cb793e9d..a46649df 100644 --- a/R/alt_model_constructors.R +++ b/R/alt_model_constructors.R @@ -1,8 +1,9 @@ StandardExprAlt = function(model){ - self = macpan2:::Indices(model) + self = Indices(model) self$.init_derivations_list = function(){ - #This expression is only required to ensure every model has at least one expression. - #There is currently a bug where it is not possible to create a simulator for a model with no expressions at all + # This expression is only required to ensure every model has at least + # one expression. There is currently a bug where it is not possible to + # create a simulator for a model with no expressions at all. hack_expression_list = list( output_names = "state", expression = "1*state", @@ -35,7 +36,7 @@ Derivations2ExprListAlt = function(user_expr, standard_expr) { self$.user_expr_list = user_expr$expand_vector_expressions() self$.expression_formatter = function(expression_list_element){ - macpan2:::two_sided( + two_sided( expression_list_element$output_names, expression_list_element$expression ) @@ -102,16 +103,16 @@ Derivations2ExprListAlt = function(user_expr, standard_expr) { ModelAlt = function(definition) { # Inheritance - self = oor:::Base() + self = Base() # Args / Composition self$def = definition ## ModelFiles object # Compositions - self$settings = macpan2:::Settings(self) - self$variables = macpan2:::Variables(self) - self$labels = macpan2:::VariableLabels(self$variables) - self$indices = macpan2:::VariableIndices(self$labels) + self$settings = Settings(self) + self$variables = Variables(self) + self$labels = VariableLabels(self$variables) + self$indices = VariableIndices(self$labels) # Standard Methods self$flows = function() self$def$flows() @@ -149,7 +150,7 @@ ModelAlt = function(definition) { # so that when the model definition files change # on disk, the caches that depend on these files # are invalidated. - self$def$cache = macpan2:::CacheList( + self$def$cache = CacheList( self$variables$cache, self$labels$cache, self$indices$flow ## invalidate method outside of the cache for convenience @@ -157,8 +158,8 @@ ModelAlt = function(definition) { # Validate and Return (self - |> macpan2:::assert_variables() - |> oor:::return_object("Model") + |> assert_variables() + |> return_object("Model") ) } @@ -179,7 +180,7 @@ SimulatorConstructor = function(model_directory, integration_method = RK4, ...){ model = CompartmentalAlt(model_directory) model_simulator = model$simulators$tmb(..., .bundle_compartmental_model = TRUE) - + expanded_flows = model$flows_expanded() diff --git a/R/case_changes.R b/R/case_changes.R new file mode 100644 index 00000000..bddc837d --- /dev/null +++ b/R/case_changes.R @@ -0,0 +1,17 @@ +var_case_to_cls_case = function(...) { + x = unlist(list(...), recursive = TRUE, use.names = FALSE) + y = strsplit(x, "_") + first_letters = (y + |> lapply(substring, 1L, 1L) + |> lapply(toupper) + ) + remaining_letters = (y + |> lapply(substring, 2L) + ) + mapply(paste + , first_letters + , remaining_letters + , MoreArgs = list(sep = "") + , SIMPLIFY = FALSE + ) |> vapply(paste, character(1L), collapse = "") +} diff --git a/R/compartmental.R b/R/compartmental.R index 4a562df8..5563a043 100644 --- a/R/compartmental.R +++ b/R/compartmental.R @@ -148,13 +148,10 @@ CompartmentalMatsList = function( , per_capita_inflow_to = indices$flow$per_capita_inflow$to() , per_capita_inflow_flow = indices$flow$per_capita_inflow$flow() , per_capita_outflow_from = indices$flow$per_capita_outflow$from() - #, per_capita_outflow_to = indices$flow$per_capita_outflow$to() , per_capita_outflow_flow = indices$flow$per_capita_outflow$flow() - #, absolute_inflow_from = indices$flow$absolute_inflow$from() , absolute_inflow_to = indices$flow$absolute_inflow$to() , absolute_inflow_flow = indices$flow$absolute_inflow$flow() , absolute_outflow_from = indices$flow$absolute_outflow$from() - #, absolute_outflow_to = indices$flow$absolute_outflow$to() , absolute_outflow_flow = indices$flow$absolute_outflow$flow() , per_capita = empty_matrix , absolute = empty_matrix diff --git a/R/engine_methods.R b/R/engine_methods.R index 58101388..cff2e425 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -1,66 +1,53 @@ +#' Engine Methods +#' +#' List of methods for pre-processing matrices before returning or +#' assigning them. One benefit of these methods is that they can make use +#' of user-supplied integer vectors that are stored in C++ as integers. Another +#' benefit can be readability but this is subjective. +#' +#' @param exprs A named `list` of formulas that can be matched to one of the +#' method prototypes (TODO: describe these). +#' @param int_vecs An \code{\link{IntVecs}} object containing integer vectors +#' that can be used by the methods. +#' #' @export -EngineMethods = function(...) { +EngineMethods = function(exprs = list(), int_vecs = IntVecs()) { self = Base() - self$method_exprs = list(...) - - self$methods = function() { - method_objs = list() - for (meth_nm in names(self$method_exprs)) { - m = self$method_exprs[[meth_nm]] - args = c( - as.list(c(meth_nm, m$mat, m$id_nm)), - init_mats = m$init_mats, - int_vecs = self$int_vecs() - ) - method_objs[[meth_nm]] = do.call(m$constructor, args) - } - do.call(MethList, method_objs) - } - self$int_vecs = function() { - int_vec_objs = list() - for (meth_nm in names(self$method_exprs)) { - m = self$method_exprs[[meth_nm]] - int_vec_objs[[m$id_nm]] = m$indices() - } - do.call(IntVecs, int_vec_objs) - } + + # Args + self$exprs = exprs + self$int_vecs = int_vecs + + # Composition + self$method_types = MethodTypes() + self$meth_list = MethListFromEngineMethods(self) + + # Refreshments self$refresh_init_mats = function(init_mats) { - for (meth_nm in names(self$method_exprs)) { - m = self$method_exprs[[meth_nm]]$init_mats = init_mats + for (meth_nm in names(self$meth_list)) { + self$meth_list$methods[[meth_nm]]$init_mats = init_mats } } return_object(self, "EngineMethods") } -MethodRowIndexer = NULL - -#' @export -FromRows = function(mat - , by - , id_nm - , init_mats = MatsList() - ) { - self = Base() - - self$mat = mat - self$by = by - self$id_nm = id_nm - self$init_mats = init_mats - - self$call = deparse1(match.call()) - self$constructor = MethodRowIndexer - - self$indices = function() { - l = list() - l[[self$id_nm]] = get_indices(self$by - , vec = self$init_mats$rownames()[[self$mat]] - , vec_type = "variables" - , expr_as_string = self$call - , zero_based = TRUE - ) - l +#' Construct a Method List from Engine Methods +#' +#' Alternative constuctor for `MethList`. +#' +#' The engine interacts with `MethList` objects, but the user interacts +#' with `EngineMethods` objects. This allows developers to convert between +#' the two. +#' +#' @param engine_methods An object of class EngineMethods +#' @nord +MethListFromEngineMethods = function(engine_methods) { + l = list() + for (nm in names(engine_methods$exprs)) { + l[[nm]] = engine_methods$method_types$make_method(engine_methods$exprs[[nm]], nm) + l[[nm]]$int_vecs = engine_methods$int_vecs } - return_object(self, "FromRows") + do.call(MethList, l) } @@ -77,13 +64,8 @@ MethList = function(...) { # Static self$.null_data_arg = setNames( rep(list(integer()), 5), - sprintf("meth_%s", c( - "type_id" - , "n_mats" - , "n_int_vecs" - , "mat_id" - , "int_vec_id" - ) + sprintf("meth_%s", + c("type_id", "n_mats", "n_int_vecs", "mat_id", "int_vec_id") ) ) @@ -91,7 +73,11 @@ MethList = function(...) { l = self$.null_data_arg new_args = method_apply(self$methods, "data_arg") for (a in names(l)) { - l[[a]] = c(l[[a]], unlist(lapply(new_args, `[[`, a), recursive = FALSE, use.names = FALSE)) + l[[a]] = c(l[[a]], unlist( + lapply(new_args, `[[`, a), + recursive = FALSE + , use.names = FALSE + )) } l } @@ -133,9 +119,42 @@ Method = function(name, mat_args, int_vec_args, init_mats = MatsList(), int_vecs # Private self$.mat_ids = function() match(self$mat_args, names(self$init_mats)) - 1L self$.int_vec_ids = function() match(self$int_vec_args, names(self$int_vecs)) - 1L + self$.check_args = function() { + missing_mats = self$mat_args[!self$mat_args %in% names(self$init_mats)] + missing_int_vecs = self$int_vec_args[!self$int_vec_args %in% names(self$int_vecs)] + if (length(missing_mats) != 0L) { + mixup = missing_mats[missing_mats %in% names(self$int_vecs)] + if (length(mixup) != 0L) { + stop( + "\nThe ", self$name, " method had the following integer vectors passed" + , "\nto arguments that require matrices:\n" + , paste(mixup, collapse = "\n") + ) + } + stop( + "\nThe ", self$name, " method could not find the following matrices:\n" + , paste(missing_mats, collapse = "\n") + ) + } + if (length(missing_int_vecs) != 0L) { + mixup = missing_int_vecs[missing_int_vecs %in% names(self$init_mats)] + if (length(mixup) != 0L) { + stop( + "\nThe ", self$name, " method had the following matrices passed" + , "\nto arguments that require integer vectors:\n" + , paste(mixup, collapse = "\n") + ) + } + stop( + "\nThe ", self$name, " method coult not find the following integer vectors:\n" + , paste(missing_int_vecs, collapse = "\n") + ) + } + } # Standard Methods self$data_arg = function() { + self$.check_args() list( ## these must be length-1 integer vectors meth_type_id = self$meth_type_id @@ -149,19 +168,3 @@ Method = function(name, mat_args, int_vec_args, init_mats = MatsList(), int_vecs return_object(self, "Method") } - -meth_cls_types = c("MethodRowIndexer") - -mk_meth_cls = function(cls_nm, meth_type_id) { - pf = parent.frame() - force(pf) - force(cls_nm) - force(meth_type_id) - f = function(name, mat_args, const_args, init_mats = MatsList(), int_vecs = IntVecs()) { - self = Method(name, mat_args, const_args, init_mats, int_vecs) - self$meth_type_id = meth_type_id - return_object(self, cls_nm) - } - assign(cls_nm, f, envir = pf) -} -for (i in seq_along(meth_cls_types)) mk_meth_cls(meth_cls_types[i], i) diff --git a/R/enum_methods.R b/R/enum_methods.R new file mode 100644 index 00000000..01a1f2b0 --- /dev/null +++ b/R/enum_methods.R @@ -0,0 +1,141 @@ +## Auto-generated - do not edit by hand +#' Method Prototype +#' +#' Define a method type using a prototype. These prototypes can be compared +#' with methods defined in R to see if they are consistent with a method +#' type that has been defined in C++. All +#' arguments are automatically derived through comments in the C++ code where +#' the method types are defined. +#' +#' @param formula Formula for defining a method type using a prototype. +#' @param mat_arg_nms Character vector naming the matrix-valued arguments. +#' @param int_vec_arg_nms Character vector naming the integer-vector-valued +#' arguments. +#' +#' @nord +MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { + self = Base() + self$formula = formula + self$mat_arg_nms = mat_arg_nms + self$int_vec_arg_nms = int_vec_arg_nms + self$mat_args = function(other_formula) { + stopifnot(self$consistent(other_formula)) + that = self$.concat_parse_table(other_formula) + this = self$.concat_parse_table(self$formula) + which_mats = this$x %in% self$mat_arg_nms + that$x[which_mats] + } + self$int_vec_args = function(other_formula) { + stopifnot(self$consistent(other_formula)) + that = self$.concat_parse_table(other_formula) + this = self$.concat_parse_table(self$formula) + which_int_vecs = this$x %in% self$int_vec_arg_nms + that$x[which_int_vecs] + } + self$as_character = function() deparse(self$formula) + self$is_assignment = function(other_formula) { + (length(self$formula) == 3L) & (length(other_formula) == 3L) + } + self$is_return = function(other_formula) { + (length(self$formula) == 2L) & (length(other_formula) == 2L) + } + self$parse_table = function() { + method_parser(self$formula) + } + self$consistent = function(other_formula) { + this = self$.concat_parse_table(self$formula) + that = self$.concat_parse_table(other_formula) + this_funs = this$x[this$n > 0L] + that_funs = that$x[that$n > 0L] + good_n_sig = identical(this$n, that$n) + good_fun_names = identical(this_funs, that_funs) + good_n_sig & good_fun_names + } + + ## Private + self$.lhs = function(two_sided_formula) two_sided_formula[-3L] + self$.rhs = function(two_sided_formula) two_sided_formula[-2L] + self$.concat_parse_table = function(formula) { + if (length(formula) == 2L) return(method_parser(formula)) + rbind(method_parser(self$.lhs(formula)), method_parser(self$.rhs(formula))) + } + return_object(self, "MethodPrototype") +} + +#' Make Method Class +#' +#' Place a method object in the package namespace for a given method type +#' defined in the C++ code. +#' +#' @param cls_nm Character string giving the name of the class. +#' @param meth_type_id Integer giving the associated ID of the method type. +#' +#' @nord +mk_meth_cls = function(cls_nm, meth_type_id) { + pf = parent.frame() + force(pf) + force(cls_nm) + force(meth_type_id) + f = function(name, mat_args, const_args, init_mats = MatsList(), int_vecs = IntVecs()) { + self = Method(name, mat_args, const_args, init_mats, int_vecs) + self$meth_type_id = meth_type_id + return_object(self, cls_nm) + } + assign(cls_nm, f, envir = pf) +} + +#' Method Type Utilities +#' +#' This class is here so that `MethodTypes`, which is automatically generated +#' from the C++ code, can inherit from it. +#' +#' @nord +MethodTypeUtils = function() { + self = Base() + + # return all prototypes as character strings + self$all_prototype_formulas = function() { + l = list() + for (m in self$method_ordering) { + l[[m]] = self[[m]]$as_character() + } + unlist(l, use.names = FALSE) + } + + # construct an object of class `Method` + # + # @param formula -- a user-supplied formula for defining a method. this + # formula gets compared with the prototypes to decide what method type to + # use for this user-defined method. + # @param meth_nm -- character string giving the method name. + self$make_method = function(formula, meth_nm) { + for (meth_type_nm in self$method_ordering) { + if (self[[meth_type_nm]]$consistent(formula)) { + method_cls = mk_meth_cls( + cls_nm = var_case_to_cls_case(meth_type_nm), + meth_type_id = match(meth_type_nm, self$method_ordering) + ) + method = method_cls(meth_nm + , self[[meth_type_nm]]$mat_args(formula) + , self[[meth_type_nm]]$int_vec_args(formula) + ) + return(method) + } + } + stop( + "\nThe following engine method formula ...\n\n", deparse(formula), "\n\n" + , "... is inconsistent with all of the available prototypes:\n\n" + , paste0(self$all_prototype_formulas(), collapse = "\n") + ) + } + + return_object(self, "MethodTypesUtil") +} +MethodTypes = function() { + self = MethodTypeUtils() + self$method_ordering = c("meth_from_rows", "meth_matmult_to_rows", "meth_group_sums") + self$meth_from_rows = MethodPrototype(~ Y[i], "Y", "i") + self$meth_matmult_to_rows = MethodPrototype(Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j")) + self$meth_group_sums = MethodPrototype(~ groupSums(Y, i, n), "Y", c("i", "n")) + return_object(self, "MethodTypes") +} diff --git a/R/expr_list.R b/R/expr_list.R index cdda3ca9..961b8879 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -14,7 +14,7 @@ ExprListUtils = function() { , valid_vars = self$.init_valid_vars() , valid_literals = .existing_literals , offset = .offset - , valid_methods = self$engine_methods$methods() + , valid_methods = self$engine_methods$meth_list ) } self$.set_name_prefix = function(x, prefix) { diff --git a/R/parse_expr.R b/R/parse_expr.R index 4343c0fa..a03d0a6b 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -221,11 +221,11 @@ finalizer_index = function(x) { valid_literals = c(valid_literals, new_valid_literals) } if (any(is_meth)) { - get_indices(x_char[is_meth] + x_int[is_meth] = get_indices(x_char[is_meth] , vec = valid_methods , vec_type = "methods" , expr_as_string = x$input_expr_as_string - , zero_based = FALSE + , zero_based = TRUE ) } x$x = as.integer(x_int) @@ -237,6 +237,7 @@ finalizer_index = function(x) { } parse_expr = make_expr_parser(finalizer = finalizer_index) +method_parser = make_expr_parser("method_parser", finalizer_char) get_indices = function(x, vec, vec_type, expr_as_string, zero_based = FALSE) { if (!is.character(vec)) vec = names(vec) diff --git a/R/simulators.R b/R/simulators.R index 5ff0dd39..e4c9b68a 100644 --- a/R/simulators.R +++ b/R/simulators.R @@ -56,7 +56,7 @@ Simulators = function(model) { , .dimnames = list() , .tmb_cpp = "macpan2" , .initialize_ad_fun = TRUE - , .bundle_compartmental_model = FALSE + , .bundle_compartmental_model = TRUE ## TODO: always be TRUE? ) { tmb_simulator = TMBModel( init_mats = CompartmentalMatsList(self$model, state, flow diff --git a/R/tmb_model.R b/R/tmb_model.R index ae0444c0..c8c8db3b 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -9,8 +9,7 @@ #' @param random An object of class \code{\link{OptParamsList}}. #' @param obj_fn An object of class \code{\link{ObjectiveFunction}}. #' @param time_steps An object of class \code{\link{Time}}. -#' @param meth_list An object of class \code{\link{MethList}}. -#' @param int_vecs An object of class \code{\link{IntVecs}}. +#' @param engine_methods An object of class \code{\link{EngineMethods}}. #' @param log_file An object of class \code{\link{LogFile}}. #' #' @return Object of class \code{TMBModel} with the following methods. @@ -88,8 +87,6 @@ TMBModel = function( self$random = random self$obj_fn = obj_fn self$time_steps = time_steps - #self$meth_list = meth_list - #self$int_vecs = int_vecs self$engine_methods = engine_methods self$log_file = log_file @@ -103,8 +100,8 @@ TMBModel = function( self$random$data_arg("r"), self$obj_fn$data_arg(existing_literals), self$time_steps$data_arg(), - self$engine_methods$methods()$data_arg(), - self$engine_methods$int_vecs()$data_arg(), + self$engine_methods$meth_list$data_arg(), + self$engine_methods$int_vecs$data_arg(), self$log_file$data_arg() ) } @@ -191,40 +188,6 @@ TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { return_object(self, "TMBCompartmentalSimulator") } -# SimulatorsList = function() { -# self = Base() -# self$.simulators = list() -# self$simulators = function() self$.simulators -# self$add = function(simulator) { -# if (!any(vapply(self$.simulators, identical, logical, simulator))) { -# self$.simulators = append(self$.simulators, simulator) -# } -# } -# return_object(self, "SimulatorsList") -# } - -## The copied method acts on the source when called by the target. -## This is useful when the target has the source as a composed object. -# copy_method = function( -# method ## string giving the name of the method to transfer -# , source ## source object to donate the method -# , target ## target object to receive the method -# , return = TRUE ## should the return value in the source be returned in the target? -# ) { -# force(method); force(source); force(target) -# target[[method]] = function() { -# named_args = as.list(environment()) -# dot_args = try(list(...), silent = TRUE) -# if (inherits(dot_args, "try-error")) dot_args = list() -# args = c(named_args, dot_args) -# y = do.call(source[[method]], args) -# if (return) return(y) -# } -# formals(target[[method]]) = formals(source[[method]]) -# } -# copy_methods = function(methods, source, target, return = TRUE) { -# for (method in methods) copy_method(method, source, target, return) -# } TMBSimulationUtils = function() { self = Base() diff --git a/man/EngineMethods.Rd b/man/EngineMethods.Rd new file mode 100644 index 00000000..1aeb3a7c --- /dev/null +++ b/man/EngineMethods.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/engine_methods.R +\name{EngineMethods} +\alias{EngineMethods} +\title{Engine Methods} +\usage{ +EngineMethods(exprs = list(), int_vecs = IntVecs()) +} +\arguments{ +\item{exprs}{A named \code{list} of formulas that can be matched to one of the +method prototypes (TODO: describe these).} + +\item{int_vecs}{An \code{\link{IntVecs}} object containing integer vectors +that can be used by the methods.} +} +\description{ +List of methods for pre-processing matrices before returning or +assigning them. One benefit of these methods is that they can make use +of user-supplied integer vectors that are stored in C++ as integers. Another +benefit can be readability but this is subjective. +} diff --git a/man/MethListFromEngineMethods.Rd b/man/MethListFromEngineMethods.Rd new file mode 100644 index 00000000..fb97310d --- /dev/null +++ b/man/MethListFromEngineMethods.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/engine_methods.R +\name{MethListFromEngineMethods} +\alias{MethListFromEngineMethods} +\title{Construct a Method List from Engine Methods} +\usage{ +MethListFromEngineMethods(engine_methods) +} +\arguments{ +\item{engine_methods}{An object of class EngineMethods} +} +\description{ +Alternative constuctor for \code{MethList}. +} +\details{ +The engine interacts with \code{MethList} objects, but the user interacts +with \code{EngineMethods} objects. This allows developers to convert between +the two. +} diff --git a/man/MethodPrototype.Rd b/man/MethodPrototype.Rd new file mode 100644 index 00000000..9c3b8c25 --- /dev/null +++ b/man/MethodPrototype.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enum_methods.R +\name{MethodPrototype} +\alias{MethodPrototype} +\title{Method Prototype} +\usage{ +MethodPrototype(formula, mat_arg_nms, int_vec_arg_nms) +} +\arguments{ +\item{formula}{Formula for defining a method type using a prototype.} + +\item{mat_arg_nms}{Character vector naming the matrix-valued arguments.} + +\item{int_vec_arg_nms}{Character vector naming the integer-vector-valued +arguments.} +} +\description{ +Define a method type using a prototype. These prototypes can be compared +with methods defined in R to see if they are consistent with a method +type that has been defined in C++. All +arguments are automatically derived through comments in the C++ code where +the method types are defined. +} diff --git a/man/MethodTypeUtils.Rd b/man/MethodTypeUtils.Rd new file mode 100644 index 00000000..0e535e7f --- /dev/null +++ b/man/MethodTypeUtils.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enum_methods.R +\name{MethodTypeUtils} +\alias{MethodTypeUtils} +\title{Method Type Utilities} +\usage{ +MethodTypeUtils() +} +\description{ +This class is here so that \code{MethodTypes}, which is automatically generated +from the C++ code, can inherit from it. +} diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index 7f6d9bb4..1a042c18 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -28,11 +28,9 @@ TMBModel( \item{time_steps}{An object of class \code{\link{Time}}.} -\item{log_file}{An object of class \code{\link{LogFile}}.} - -\item{meth_list}{An object of class \code{\link{MethList}}.} +\item{engine_methods}{An object of class \code{\link{EngineMethods}}.} -\item{int_vecs}{An object of class \code{\link{IntVecs}}.} +\item{log_file}{An object of class \code{\link{LogFile}}.} } \value{ Object of class \code{TMBModel} with the following methods. diff --git a/man/mk_meth_cls.Rd b/man/mk_meth_cls.Rd new file mode 100644 index 00000000..a3d4e6d6 --- /dev/null +++ b/man/mk_meth_cls.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enum_methods.R +\name{mk_meth_cls} +\alias{mk_meth_cls} +\title{Make Method Class} +\usage{ +mk_meth_cls(cls_nm, meth_type_id) +} +\arguments{ +\item{cls_nm}{Character string giving the name of the class.} + +\item{meth_type_id}{Integer giving the associated ID of the method type.} +} +\description{ +Place a method object in the package namespace for a given method type +defined in the C++ code. +} diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 8c214293..fa91e3ff 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -109,9 +109,39 @@ enum macpan2_func { }; enum macpan2_meth { - METH_ROW_EXTRACT = 1 + METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" + , METH_MATMULT_TO_ROWS = 2 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") + , METH_GROUP_SUMS = 3 // ~ groupSums(Y, i, n), "Y", c("i", "n") }; +void printIntVector(const std::vector& intVector) { + for (int element : intVector) { + std::cout << element << ' '; + } + std::cout << std::endl; +} + +void printIntVectorWithLabel(const std::vector& intVector, const std::string& label) { + std::cout << label << ": "; + printIntVector(intVector); +} + +// Define the function to print a single matrix here +template +void printMatrix(const matrix& mat) { + // Implement the logic to print a matrix here + // You can use a loop to iterate through rows and columns + // and print each element. + // Example: + for (int i = 0; i < mat.rows(); ++i) { + for (int j = 0; j < mat.cols(); ++j) { + std::cout << mat.coeff(i, j) << ' '; + } + std::cout << std::endl; + } +} + + // Helper function template int CheckIndices( @@ -208,21 +238,35 @@ struct ListOfMatrices { return *this; } - // Square bracket operator to get more than one matrix back ListOfMatrices operator[](const std::vector& indices) const { + std::cout << "hereherher 1" << std::endl; ListOfMatrices result; - + //result.m_matrices.clear(); // Ensure the result vector is empty + //result.m_matrices.reserve(indices.size()); // Reserve memory for expected matrices + std::cout << "hereherher 1" << std::endl; for (int index : indices) { if (index >= 0 && index < m_matrices.size()) { - result.m_matrices.push_back(m_matrices[index]); + result.m_matrices[index] = m_matrices[index]; } else { // Handle out-of-range index or negative index as needed throw std::out_of_range("Index out of range"); } } + std::cout << "hereherher 2" << std::endl; return result; } + + // Method to print specific matrices in the list + void printMatrices(const std::vector& indices, const std::string& label) const { + std::cout << label << ": " << std::endl; + for (int index : indices) { + const matrix& mat = m_matrices[index]; + std::cout << " inner matrix:" << std::endl; + printMatrix(mat); + } + } + }; @@ -251,7 +295,7 @@ class ListOfIntVecs { if (all_ints.size() != totalElements) { // Handle mismatched sizes as needed - throw std::invalid_argument("all_ints and vec_lens sizes do not match."); + Rf_error("all_ints and vec_lens sizes do not match."); } size_t xIndex = 0; @@ -270,7 +314,7 @@ class ListOfIntVecs { return nestedVector[index]; } else { // Handle out-of-range access here (you can throw an exception or handle it as needed) - throw std::out_of_range("Index out of range"); + Rf_error("Index out of range"); } } @@ -288,7 +332,7 @@ class ListOfIntVecs { result.nestedVector.push_back(nestedVector[static_cast(index)]); } else { // Handle out-of-range index or negative index as needed - throw std::out_of_range("Index out of range"); + Rf_error("Index out of range"); } } @@ -296,17 +340,52 @@ class ListOfIntVecs { } // Method to print each vector in the list - void printVectors() const { + void printVectors(const std::string& label) const { + std::cout << label << ": " << std::endl; for (const std::vector& innerVector : nestedVector) { - std::cout << "Vector:"; + std::cout << " inner vector: "; for (int element : innerVector) { std::cout << " " << element; } std::cout << std::endl; } } + }; +vector getNthIntVec( + int vec_number, + int curr_meth_id, + ListOfIntVecs& valid_int_vecs, + ListOfIntVecs& meth_int_vecs + ) { + vector result = valid_int_vecs[meth_int_vecs[curr_meth_id]][vec_number]; + return result; +} + + +int getNthMatIndex( + int mat_number, + int curr_meth_id, + ListOfIntVecs& meth_mats +) { + int result = meth_mats[curr_meth_id][mat_number]; + return result; +} + + +template +matrix getNthMat( + int mat_number, + int curr_meth_id, + ListOfMatrices& valid_vars, + ListOfIntVecs& meth_mats + +) { + int i = getNthMatIndex(mat_number, curr_meth_id, meth_mats); + matrix result = valid_vars.m_matrices[i]; + return result; +} template @@ -372,61 +451,98 @@ class ExprEvaluator { ) { - // Variables to use locally in function bodies - matrix m, m1, m2; // return values - vector v; + // Variables to use locally in 'macpan2 function' and + // 'macpan2 method' bodies -- scare quotes to clarify that + // these are not real functions and methods in either the + // c++ or r sense. + matrix m, m1, m2, m3, m4, m5; // return values + vector v, v1, v2, v3, v4, v5; // method integer vectors + vector u; + matrix Y, X, A; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size; int sz, start, err_code, err_code1, err_code2, curr_meth_id; - size_t numMats; - size_t numIntVecs; + // size_t numMats; + // size_t numIntVecs; std::vector curr_meth_mat_id_vec; std::vector curr_meth_int_id_vec; vector> meth_args(meth_mats.size()); ListOfIntVecs meth_int_args; - if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. + // Check if error has already happened at some point + // of the recursive call. + if (GetErrorCode()) return m; switch (table_n[row]) { case -2: // methods (pre-processed matrices) - // Access and manipulate meth_mats - numMats = meth_mats.size(); - numIntVecs = meth_int_vecs.size(); - // for (size_t i = 0; i < numMats; ++i) { - // curr_meth_mat_id_vec = meth_mats[i]; - // - // // Print the elements of the vector - // std::cout << "single vector: "; - // for (int element : curr_meth_mat_id_vec) { - // std::cout << element << ' '; - // } - // std::cout << std::endl; - // } - std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; + + std::cout << "---------------" << std::endl; + std::cout << "IN METHODS CASE" << std::endl; + std::cout << "---------------" << std::endl; curr_meth_id = table_x[row]; - curr_meth_mat_id_vec = meth_mats[curr_meth_id]; - curr_meth_int_id_vec = meth_int_vecs[curr_meth_id]; - std::cout << "curr_meth_id: " << curr_meth_id << std::endl; - std::cout << "meth_type_id: " << meth_type_id[curr_meth_id] << std::endl; + switch(meth_type_id[curr_meth_id]) { + case METH_FROM_ROWS: + std::cout << "-----------------" << std::endl; + std::cout << "IN METH_FROM_ROWS" << std::endl; + std::cout << "-----------------" << std::endl; + m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); + printMatrix(m); + v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); + printIntVectorWithLabel(v, ""); + m1 = matrix::Zero(v.size(), m.cols()); + for (int i=0; i::Zero(v1.size(), m1.cols()); + + std::cout << "Y index" << matIndex << std::endl; + std::cout << "A" << m << std::endl; + std::cout << "X" << m1 << std::endl; + printIntVectorWithLabel(v, "i"); + printIntVectorWithLabel(v1, "j"); + + for (int i=0; i::Zero(meth_int_args[0].size(), meth_args[0].cols()); - // m1 = matrix::Zero(v.size(), m.cols()); - for (int i=0; i::Zero(rows, 1); + + for (int i = 0; i < m.rows(); i++) { + rowIndex = v[i]; + m1.coeffRef(rowIndex,0) += m.coeff(i,0); + } return m1; + default: SetError(254, "invalid method in arithmetic expression", row); return m; diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 03f3b192..3a0e4e9f 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -110,9 +110,39 @@ enum macpan2_func { }; enum macpan2_meth { - METH_ROW_EXTRACT = 1 + METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" + , METH_MATMULT_TO_ROWS = 2 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") + , METH_GROUP_SUMS = 3 // ~ groupSums(Y, i, n), "Y", c("i", "n") }; +void printIntVector(const std::vector& intVector) { + for (int element : intVector) { + std::cout << element << ' '; + } + std::cout << std::endl; +} + +void printIntVectorWithLabel(const std::vector& intVector, const std::string& label) { + std::cout << label << ": "; + printIntVector(intVector); +} + +// Define the function to print a single matrix here +template +void printMatrix(const matrix& mat) { + // Implement the logic to print a matrix here + // You can use a loop to iterate through rows and columns + // and print each element. + // Example: + for (int i = 0; i < mat.rows(); ++i) { + for (int j = 0; j < mat.cols(); ++j) { + std::cout << mat.coeff(i, j) << ' '; + } + std::cout << std::endl; + } +} + + // Helper function template int CheckIndices( @@ -209,21 +239,35 @@ struct ListOfMatrices { return *this; } - // Square bracket operator to get more than one matrix back ListOfMatrices operator[](const std::vector& indices) const { + std::cout << "hereherher 1" << std::endl; ListOfMatrices result; - + //result.m_matrices.clear(); // Ensure the result vector is empty + //result.m_matrices.reserve(indices.size()); // Reserve memory for expected matrices + std::cout << "hereherher 1" << std::endl; for (int index : indices) { if (index >= 0 && index < m_matrices.size()) { - result.m_matrices.push_back(m_matrices[index]); + result.m_matrices[index] = m_matrices[index]; } else { // Handle out-of-range index or negative index as needed throw std::out_of_range("Index out of range"); } } + std::cout << "hereherher 2" << std::endl; return result; } + + // Method to print specific matrices in the list + void printMatrices(const std::vector& indices, const std::string& label) const { + std::cout << label << ": " << std::endl; + for (int index : indices) { + const matrix& mat = m_matrices[index]; + std::cout << " inner matrix:" << std::endl; + printMatrix(mat); + } + } + }; @@ -252,7 +296,7 @@ class ListOfIntVecs { if (all_ints.size() != totalElements) { // Handle mismatched sizes as needed - throw std::invalid_argument("all_ints and vec_lens sizes do not match."); + Rf_error("all_ints and vec_lens sizes do not match."); } size_t xIndex = 0; @@ -271,7 +315,7 @@ class ListOfIntVecs { return nestedVector[index]; } else { // Handle out-of-range access here (you can throw an exception or handle it as needed) - throw std::out_of_range("Index out of range"); + Rf_error("Index out of range"); } } @@ -289,7 +333,7 @@ class ListOfIntVecs { result.nestedVector.push_back(nestedVector[static_cast(index)]); } else { // Handle out-of-range index or negative index as needed - throw std::out_of_range("Index out of range"); + Rf_error("Index out of range"); } } @@ -297,17 +341,52 @@ class ListOfIntVecs { } // Method to print each vector in the list - void printVectors() const { + void printVectors(const std::string& label) const { + std::cout << label << ": " << std::endl; for (const std::vector& innerVector : nestedVector) { - std::cout << "Vector:"; + std::cout << " inner vector: "; for (int element : innerVector) { std::cout << " " << element; } std::cout << std::endl; } } + }; +vector getNthIntVec( + int vec_number, + int curr_meth_id, + ListOfIntVecs& valid_int_vecs, + ListOfIntVecs& meth_int_vecs + ) { + vector result = valid_int_vecs[meth_int_vecs[curr_meth_id]][vec_number]; + return result; +} + + +int getNthMatIndex( + int mat_number, + int curr_meth_id, + ListOfIntVecs& meth_mats +) { + int result = meth_mats[curr_meth_id][mat_number]; + return result; +} + + +template +matrix getNthMat( + int mat_number, + int curr_meth_id, + ListOfMatrices& valid_vars, + ListOfIntVecs& meth_mats + +) { + int i = getNthMatIndex(mat_number, curr_meth_id, meth_mats); + matrix result = valid_vars.m_matrices[i]; + return result; +} template @@ -373,61 +452,98 @@ class ExprEvaluator { ) { - // Variables to use locally in function bodies - matrix m, m1, m2; // return values - vector v; + // Variables to use locally in 'macpan2 function' and + // 'macpan2 method' bodies -- scare quotes to clarify that + // these are not real functions and methods in either the + // c++ or r sense. + matrix m, m1, m2, m3, m4, m5; // return values + vector v, v1, v2, v3, v4, v5; // method integer vectors + vector u; + matrix Y, X, A; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size; int sz, start, err_code, err_code1, err_code2, curr_meth_id; - size_t numMats; - size_t numIntVecs; + // size_t numMats; + // size_t numIntVecs; std::vector curr_meth_mat_id_vec; std::vector curr_meth_int_id_vec; vector> meth_args(meth_mats.size()); ListOfIntVecs meth_int_args; - if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. + // Check if error has already happened at some point + // of the recursive call. + if (GetErrorCode()) return m; switch (table_n[row]) { case -2: // methods (pre-processed matrices) - // Access and manipulate meth_mats - numMats = meth_mats.size(); - numIntVecs = meth_int_vecs.size(); - // for (size_t i = 0; i < numMats; ++i) { - // curr_meth_mat_id_vec = meth_mats[i]; - // - // // Print the elements of the vector - // std::cout << "single vector: "; - // for (int element : curr_meth_mat_id_vec) { - // std::cout << element << ' '; - // } - // std::cout << std::endl; - // } - std::cout << "--------------" << "IN METHODS CASE" << std::endl << "--------------" << std::endl; + + std::cout << "---------------" << std::endl; + std::cout << "IN METHODS CASE" << std::endl; + std::cout << "---------------" << std::endl; curr_meth_id = table_x[row]; - curr_meth_mat_id_vec = meth_mats[curr_meth_id]; - curr_meth_int_id_vec = meth_int_vecs[curr_meth_id]; - std::cout << "curr_meth_id: " << curr_meth_id << std::endl; - std::cout << "meth_type_id: " << meth_type_id[curr_meth_id] << std::endl; + switch(meth_type_id[curr_meth_id]) { + case METH_FROM_ROWS: + std::cout << "-----------------" << std::endl; + std::cout << "IN METH_FROM_ROWS" << std::endl; + std::cout << "-----------------" << std::endl; + m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); + printMatrix(m); + v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); + printIntVectorWithLabel(v, ""); + m1 = matrix::Zero(v.size(), m.cols()); + for (int i=0; i::Zero(v1.size(), m1.cols()); + + std::cout << "Y index" << matIndex << std::endl; + std::cout << "A" << m << std::endl; + std::cout << "X" << m1 << std::endl; + printIntVectorWithLabel(v, "i"); + printIntVectorWithLabel(v1, "j"); + + for (int i=0; i::Zero(meth_int_args[0].size(), meth_args[0].cols()); - // m1 = matrix::Zero(v.size(), m.cols()); - for (int i=0; i::Zero(rows, 1); + + for (int i = 0; i < m.rows(); i++) { + rowIndex = v[i]; + m1.coeffRef(rowIndex,0) += m.coeff(i,0); + } return m1; + default: SetError(254, "invalid method in arithmetic expression", row); return m; From ac4cb4c86d902543df1a279c8c91d7427807672a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 18 Sep 2023 15:23:23 -0400 Subject: [PATCH 016/332] missed doc entry --- R/engine_methods.R | 2 ++ R/enum_methods.R | 3 ++- man/Method.Rd | 4 ++++ misc/dev/dev.cpp | 9 +++++++++ src/macpan2.cpp | 9 +++++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/R/engine_methods.R b/R/engine_methods.R index cff2e425..5b3414ae 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -101,6 +101,8 @@ names.MethList = function(x) { #' the method to produce an output matrix. #' @param int_vec_args Character vector of names of constants that will be used #' by the method to produce an output matrix. +#' @param init_mats Object of class \code{\link{MatsList}}. +#' @param int_vecs = Object of class \code{\link{IntVecs}}. #' @export Method = function(name, mat_args, int_vec_args, init_mats = MatsList(), int_vecs = IntVecs()) { self = Base() diff --git a/R/enum_methods.R b/R/enum_methods.R index 01a1f2b0..67532226 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -133,9 +133,10 @@ MethodTypeUtils = function() { } MethodTypes = function() { self = MethodTypeUtils() - self$method_ordering = c("meth_from_rows", "meth_matmult_to_rows", "meth_group_sums") + self$method_ordering = c("meth_from_rows", "meth_matmult_to_rows", "meth_group_sums", "meth_time_block") self$meth_from_rows = MethodPrototype(~ Y[i], "Y", "i") self$meth_matmult_to_rows = MethodPrototype(Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j")) self$meth_group_sums = MethodPrototype(~ groupSums(Y, i, n), "Y", c("i", "n")) + self$meth_time_block = MethodPrototype(~ time_block(Y, t, n)) return_object(self, "MethodTypes") } diff --git a/man/Method.Rd b/man/Method.Rd index 3b86bfa0..45106b60 100644 --- a/man/Method.Rd +++ b/man/Method.Rd @@ -20,6 +20,10 @@ the method to produce an output matrix.} \item{int_vec_args}{Character vector of names of constants that will be used by the method to produce an output matrix.} + +\item{init_mats}{Object of class \code{\link{MatsList}}.} + +\item{int_vecs}{= Object of class \code{\link{IntVecs}}.} } \description{ Engine methods allow users to interact with the engine diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index fa91e3ff..88d45c99 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -112,6 +112,7 @@ enum macpan2_meth { METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" , METH_MATMULT_TO_ROWS = 2 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") , METH_GROUP_SUMS = 3 // ~ groupSums(Y, i, n), "Y", c("i", "n") + , METH_TIME_BLOCK = 4 // ~ time_block(Y, t, n) }; void printIntVector(const std::vector& intVector) { @@ -543,6 +544,14 @@ class ExprEvaluator { } return m1; + case METH_TIME_BLOCK: + // ~ time_block(Y, t, n) + m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); // Y -- row-binded blocks, each corresponding to a change-point + v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); // t -- change-point times + rows = getNthIntVec(1, curr_meth_id, valid_int_vecs, meth_int_vecs)[0]; // n -- block size + m1 = matrix::Zero(rows, m.cols()); + + default: SetError(254, "invalid method in arithmetic expression", row); return m; diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 3a0e4e9f..9eb05bef 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -113,6 +113,7 @@ enum macpan2_meth { METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" , METH_MATMULT_TO_ROWS = 2 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") , METH_GROUP_SUMS = 3 // ~ groupSums(Y, i, n), "Y", c("i", "n") + , METH_TIME_BLOCK = 4 // ~ time_block(Y, t, n) }; void printIntVector(const std::vector& intVector) { @@ -544,6 +545,14 @@ class ExprEvaluator { } return m1; + case METH_TIME_BLOCK: + // ~ time_block(Y, t, n) + m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); // Y -- row-binded blocks, each corresponding to a change-point + v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); // t -- change-point times + rows = getNthIntVec(1, curr_meth_id, valid_int_vecs, meth_int_vecs)[0]; // n -- block size + m1 = matrix::Zero(rows, m.cols()); + + default: SetError(254, "invalid method in arithmetic expression", row); return m; From 2eac8800771fc0219b66f3900dca43fbfaaea653 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Sep 2023 13:40:07 -0400 Subject: [PATCH 017/332] time-based methods --- R/enum_methods.R | 11 ++-- misc/dev/dev.cpp | 147 ++++++++++++++++++++++++++++++++++++----------- src/macpan2.cpp | 147 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 237 insertions(+), 68 deletions(-) diff --git a/R/enum_methods.R b/R/enum_methods.R index 67532226..4d7f5e6f 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -132,11 +132,14 @@ MethodTypeUtils = function() { return_object(self, "MethodTypesUtil") } MethodTypes = function() { - self = MethodTypeUtils() - self$method_ordering = c("meth_from_rows", "meth_matmult_to_rows", "meth_group_sums", "meth_time_block") + self = MethodTypeUtils() + self$method_ordering = c("meth_from_rows", "meth_to_rows", "meth_rows_to_rows", "meth_mat_mult_to_rows", "meth_tv_mat_mult_to_rows", "meth_group_sums", "meth_tv_mat") self$meth_from_rows = MethodPrototype(~ Y[i], "Y", "i") - self$meth_matmult_to_rows = MethodPrototype(Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j")) + self$meth_to_rows = MethodPrototype(Y[i] ~ X, c("Y", "X"), "i") + self$meth_rows_to_rows = MethodPrototype(Y[i] ~ X[j], c("Y", "X"), c("i", "j")) + self$meth_mat_mult_to_rows = MethodPrototype(Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j")) + self$meth_tv_mat_mult_to_rows = MethodPrototype(Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer")) self$meth_group_sums = MethodPrototype(~ groupSums(Y, i, n), "Y", c("i", "n")) - self$meth_time_block = MethodPrototype(~ time_block(Y, t, n)) + self$meth_tv_mat = MethodPrototype(~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer")) return_object(self, "MethodTypes") } diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 88d45c99..878a204b 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -110,9 +110,12 @@ enum macpan2_func { enum macpan2_meth { METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" - , METH_MATMULT_TO_ROWS = 2 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") - , METH_GROUP_SUMS = 3 // ~ groupSums(Y, i, n), "Y", c("i", "n") - , METH_TIME_BLOCK = 4 // ~ time_block(Y, t, n) + , METH_TO_ROWS = 2 // Y[i] ~ X, c("Y", "X"), "i" + , METH_ROWS_TO_ROWS = 3 // Y[i] ~ X[j], c("Y", "X"), c("i", "j") + , METH_MAT_MULT_TO_ROWS = 4 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") + , METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") + , METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") + , METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") }; void printIntVector(const std::vector& intVector) { @@ -240,11 +243,9 @@ struct ListOfMatrices { } ListOfMatrices operator[](const std::vector& indices) const { - std::cout << "hereherher 1" << std::endl; ListOfMatrices result; //result.m_matrices.clear(); // Ensure the result vector is empty //result.m_matrices.reserve(indices.size()); // Reserve memory for expected matrices - std::cout << "hereherher 1" << std::endl; for (int index : indices) { if (index >= 0 && index < m_matrices.size()) { result.m_matrices[index] = m_matrices[index]; @@ -253,7 +254,6 @@ struct ListOfMatrices { throw std::out_of_range("Index out of range"); } } - std::cout << "hereherher 2" << std::endl; return result; } @@ -352,6 +352,16 @@ class ListOfIntVecs { } } + // Method to set the value of the n'th integer vector + void setNthIntVec(size_t vec_number, const std::vector& new_vector) { + if (vec_number < nestedVector.size()) { + nestedVector[vec_number] = new_vector; + } else { + // Handle out-of-range access here + Rf_error("Index out of range"); + } + } + }; vector getNthIntVec( @@ -365,11 +375,37 @@ vector getNthIntVec( } +void setNthIntVec( + int vec_number, + int curr_meth_id, + ListOfIntVecs& valid_int_vecs, + ListOfIntVecs& meth_int_vecs, + const std::vector& new_vector +) { + vec_number = meth_int_vecs[curr_meth_id][vec_number]; + if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { + valid_int_vecs.setNthIntVec(vec_number, new_vector); + } + //curr_meth_int_vecs = std::vector + //if (curr_meth_id >= 0 && curr_meth_id < curr_meth_int_vecs.size()) { + // curr_meth_int_vecs = meth_int_vecs[curr_meth_id]; + //} else { + // Rf_error("curr_meth_id is out of range."); + //} + //if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { + // valid_int_vecs.setNthIntVec(vec_number, new_vector); + //} else { + // Rf_error("vec_number is out of range."); + //} +} + + int getNthMatIndex( int mat_number, int curr_meth_id, ListOfIntVecs& meth_mats ) { + //printIntVectorWithLabel(meth_mats[curr_meth_id], "mat index: "); int result = meth_mats[curr_meth_id][mat_number]; return result; } @@ -462,7 +498,7 @@ class ExprEvaluator { matrix Y, X, A; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars - int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size; + int rows, cols, lag, rowIndex, colIndex, matIndex, grpIndex, reps, cp, off, size; int sz, start, err_code, err_code1, err_code2, curr_meth_id; // size_t numMats; // size_t numIntVecs; @@ -478,30 +514,37 @@ class ExprEvaluator { switch (table_n[row]) { case -2: // methods (pre-processed matrices) - std::cout << "---------------" << std::endl; - std::cout << "IN METHODS CASE" << std::endl; - std::cout << "---------------" << std::endl; + //std::cout << "---------------" << std::endl; + //std::cout << "IN METHODS CASE" << std::endl; + //std::cout << "---------------" << std::endl; + + // METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" + // METH_TO_ROWS = 2 // Y[i] ~ X, c("Y", "X"), "i" + // METH_ROWS_TO_ROWS = 3 // Y[i] ~ X[j], c("Y", "X"), c("i", "j") + // METH_MAT_MULT_TO_ROWS = 4 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") + // METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") + // METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") + // METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") curr_meth_id = table_x[row]; switch(meth_type_id[curr_meth_id]) { case METH_FROM_ROWS: - std::cout << "-----------------" << std::endl; - std::cout << "IN METH_FROM_ROWS" << std::endl; - std::cout << "-----------------" << std::endl; + //std::cout << "-----------------" << std::endl; + //std::cout << "IN METH_FROM_ROWS" << std::endl; + //std::cout << "-----------------" << std::endl; m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); - printMatrix(m); + //printMatrix(m); v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); - printIntVectorWithLabel(v, ""); + //printIntVectorWithLabel(v, ""); m1 = matrix::Zero(v.size(), m.cols()); for (int i=0; i::Zero(v1.size(), m1.cols()); - std::cout << "Y index" << matIndex << std::endl; - std::cout << "A" << m << std::endl; - std::cout << "X" << m1 << std::endl; - printIntVectorWithLabel(v, "i"); - printIntVectorWithLabel(v1, "j"); + //std::cout << "Y index" << matIndex << std::endl; + //std::cout << "A" << m << std::endl; + //std::cout << "X" << m1 << std::endl; + //printIntVectorWithLabel(v, "i"); + //printIntVectorWithLabel(v1, "j"); for (int i=0; i::Zero(rows, m.cols()); + cols = m.cols(); + u = getNthIntVec(2, curr_meth_id, valid_int_vecs, meth_int_vecs); // i -- time-group pointer + + //std::cout << "m: " << m << std::endl; + //printIntVectorWithLabel(v, "v"); + //printIntVectorWithLabel(u, "u"); + //std::cout << "rows: " << rows << std::endl; + //std::cout << "cols: " << cols << std::endl; + + //printIntVectorWithLabel(v, "v"); + off = u[0]; + grpIndex = off + 1; + //std::cout << "off: " << off << std::endl; + if (grpIndex < v.size()) { + if (v[grpIndex] == t) { + //std::cout << "here" << std::endl; + //std::cout << "off: " << off << std::endl; + u[0] = grpIndex; + //printIntVectorWithLabel(u, "u"); + setNthIntVec(2, curr_meth_id, valid_int_vecs, meth_int_vecs, u); + } + } + //std::cout << "t: " << t << std::endl; + //std::cout << "cp: " << cp << std::endl; + + //cp = v[u[0] + 1]; + //if (cp == t) { + // u[0] = u[0] + 1; + //} + //std::cout << "off: " << off << std::endl; + return m.block(rows * off, 0, rows, cols); + + // m = args[0]; + // off = CppAD::Integer(args[0].coeff(0, 0)); + // cp = CppAD::Integer(args[1].coeff(off + 1, 0)); + // if (cp == t) { + // m.coeffRef(0,0) = off + 1; + // } + default: diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 9eb05bef..73ce3179 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -111,9 +111,12 @@ enum macpan2_func { enum macpan2_meth { METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" - , METH_MATMULT_TO_ROWS = 2 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") - , METH_GROUP_SUMS = 3 // ~ groupSums(Y, i, n), "Y", c("i", "n") - , METH_TIME_BLOCK = 4 // ~ time_block(Y, t, n) + , METH_TO_ROWS = 2 // Y[i] ~ X, c("Y", "X"), "i" + , METH_ROWS_TO_ROWS = 3 // Y[i] ~ X[j], c("Y", "X"), c("i", "j") + , METH_MAT_MULT_TO_ROWS = 4 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") + , METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") + , METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") + , METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") }; void printIntVector(const std::vector& intVector) { @@ -241,11 +244,9 @@ struct ListOfMatrices { } ListOfMatrices operator[](const std::vector& indices) const { - std::cout << "hereherher 1" << std::endl; ListOfMatrices result; //result.m_matrices.clear(); // Ensure the result vector is empty //result.m_matrices.reserve(indices.size()); // Reserve memory for expected matrices - std::cout << "hereherher 1" << std::endl; for (int index : indices) { if (index >= 0 && index < m_matrices.size()) { result.m_matrices[index] = m_matrices[index]; @@ -254,7 +255,6 @@ struct ListOfMatrices { throw std::out_of_range("Index out of range"); } } - std::cout << "hereherher 2" << std::endl; return result; } @@ -353,6 +353,16 @@ class ListOfIntVecs { } } + // Method to set the value of the n'th integer vector + void setNthIntVec(size_t vec_number, const std::vector& new_vector) { + if (vec_number < nestedVector.size()) { + nestedVector[vec_number] = new_vector; + } else { + // Handle out-of-range access here + Rf_error("Index out of range"); + } + } + }; vector getNthIntVec( @@ -366,11 +376,37 @@ vector getNthIntVec( } +void setNthIntVec( + int vec_number, + int curr_meth_id, + ListOfIntVecs& valid_int_vecs, + ListOfIntVecs& meth_int_vecs, + const std::vector& new_vector +) { + vec_number = meth_int_vecs[curr_meth_id][vec_number]; + if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { + valid_int_vecs.setNthIntVec(vec_number, new_vector); + } + //curr_meth_int_vecs = std::vector + //if (curr_meth_id >= 0 && curr_meth_id < curr_meth_int_vecs.size()) { + // curr_meth_int_vecs = meth_int_vecs[curr_meth_id]; + //} else { + // Rf_error("curr_meth_id is out of range."); + //} + //if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { + // valid_int_vecs.setNthIntVec(vec_number, new_vector); + //} else { + // Rf_error("vec_number is out of range."); + //} +} + + int getNthMatIndex( int mat_number, int curr_meth_id, ListOfIntVecs& meth_mats ) { + //printIntVectorWithLabel(meth_mats[curr_meth_id], "mat index: "); int result = meth_mats[curr_meth_id][mat_number]; return result; } @@ -463,7 +499,7 @@ class ExprEvaluator { matrix Y, X, A; matrix timeIndex; // for rbind_time Type sum, s, eps, var, by; // intermediate scalars - int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size; + int rows, cols, lag, rowIndex, colIndex, matIndex, grpIndex, reps, cp, off, size; int sz, start, err_code, err_code1, err_code2, curr_meth_id; // size_t numMats; // size_t numIntVecs; @@ -479,30 +515,37 @@ class ExprEvaluator { switch (table_n[row]) { case -2: // methods (pre-processed matrices) - std::cout << "---------------" << std::endl; - std::cout << "IN METHODS CASE" << std::endl; - std::cout << "---------------" << std::endl; + //std::cout << "---------------" << std::endl; + //std::cout << "IN METHODS CASE" << std::endl; + //std::cout << "---------------" << std::endl; + + // METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" + // METH_TO_ROWS = 2 // Y[i] ~ X, c("Y", "X"), "i" + // METH_ROWS_TO_ROWS = 3 // Y[i] ~ X[j], c("Y", "X"), c("i", "j") + // METH_MAT_MULT_TO_ROWS = 4 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") + // METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") + // METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") + // METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") curr_meth_id = table_x[row]; switch(meth_type_id[curr_meth_id]) { case METH_FROM_ROWS: - std::cout << "-----------------" << std::endl; - std::cout << "IN METH_FROM_ROWS" << std::endl; - std::cout << "-----------------" << std::endl; + //std::cout << "-----------------" << std::endl; + //std::cout << "IN METH_FROM_ROWS" << std::endl; + //std::cout << "-----------------" << std::endl; m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); - printMatrix(m); + //printMatrix(m); v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); - printIntVectorWithLabel(v, ""); + //printIntVectorWithLabel(v, ""); m1 = matrix::Zero(v.size(), m.cols()); for (int i=0; i::Zero(v1.size(), m1.cols()); - std::cout << "Y index" << matIndex << std::endl; - std::cout << "A" << m << std::endl; - std::cout << "X" << m1 << std::endl; - printIntVectorWithLabel(v, "i"); - printIntVectorWithLabel(v1, "j"); + //std::cout << "Y index" << matIndex << std::endl; + //std::cout << "A" << m << std::endl; + //std::cout << "X" << m1 << std::endl; + //printIntVectorWithLabel(v, "i"); + //printIntVectorWithLabel(v1, "j"); for (int i=0; i::Zero(rows, m.cols()); + cols = m.cols(); + u = getNthIntVec(2, curr_meth_id, valid_int_vecs, meth_int_vecs); // i -- time-group pointer + + //std::cout << "m: " << m << std::endl; + //printIntVectorWithLabel(v, "v"); + //printIntVectorWithLabel(u, "u"); + //std::cout << "rows: " << rows << std::endl; + //std::cout << "cols: " << cols << std::endl; + + //printIntVectorWithLabel(v, "v"); + off = u[0]; + grpIndex = off + 1; + //std::cout << "off: " << off << std::endl; + if (grpIndex < v.size()) { + if (v[grpIndex] == t) { + //std::cout << "here" << std::endl; + //std::cout << "off: " << off << std::endl; + u[0] = grpIndex; + //printIntVectorWithLabel(u, "u"); + setNthIntVec(2, curr_meth_id, valid_int_vecs, meth_int_vecs, u); + } + } + //std::cout << "t: " << t << std::endl; + //std::cout << "cp: " << cp << std::endl; + + //cp = v[u[0] + 1]; + //if (cp == t) { + // u[0] = u[0] + 1; + //} + //std::cout << "off: " << off << std::endl; + return m.block(rows * off, 0, rows, cols); + + // m = args[0]; + // off = CppAD::Integer(args[0].coeff(0, 0)); + // cp = CppAD::Integer(args[1].coeff(off + 1, 0)); + // if (cp == t) { + // m.coeffRef(0,0) = off + 1; + // } + default: From 907c3a5d2d02a42ef97b112256871757473955c2 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Sep 2023 14:25:14 -0400 Subject: [PATCH 018/332] fix issues with vignettes introduced by adreport flag o this will break saved objects so we probably shouldn't merge this into master o instead we could merge this into refactorcpp --- R/tmb_model.R | 2 +- misc/dev/dev.cpp | 7 ++++++- vignettes/calibration.Rmd | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/R/tmb_model.R b/R/tmb_model.R index 7c1bbe2c..316a719f 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -740,7 +740,7 @@ TMBModel = function( random = OptParamsList(), obj_fn = ObjectiveFunction(~0), time_steps = Time(0L), - do_pred_sdreport = 1L + do_pred_sdreport = TRUE ) { ## Inheritance self = Base() diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 1f7440c5..43c5c11d 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -1990,6 +1990,9 @@ Type objective_function::operator() () DATA_IVECTOR(o_table_x); DATA_IVECTOR(o_table_i); + // Flags + DATA_INTEGER(values_adreport); + // DATA_STRUCT(settings, settings_struct); #ifdef MP_VERBOSE @@ -2309,7 +2312,9 @@ Type objective_function::operator() () } REPORT(values) - ADREPORT(values) + if (values_adreport == 1) { + ADREPORT(values) + } // 7 Calc the return of the objective function diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index 97466c61..20297f0f 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -612,7 +612,7 @@ If you get error messages when running the objective function, `sir_simulator$re fit <- sir_simulator$optimize$nlminb() ``` -## Measles Data +## Measles Data -- In-Progress Here is a reasonably difficult problem -- fit an SIR model to weekly measles incidence data from London UK over about six decades. @@ -702,7 +702,7 @@ plot(measles$date, simulated_incidence, type = "l", xlab = "time") It looks nothing like the observed measles series, but illustrates the ability to generate complex incidence patterns not present in the simple SIR model without radial basis functions and waning immunity. -```{r rbf_model, echo=FALSE} +```{r rbf_model, echo=FALSE, eval=FALSE} simulator = ("https://github.com" |> file.path("canmod/macpan2") |> file.path("raw/main") @@ -752,12 +752,12 @@ simulator$optimize$nlminb(control = list(eval.max = 10000, iter.max = 10000, tra -```{r rbf_opt_get} +```{r rbf_opt_get, eval = FALSE} simulator$optimization_history$get()[[3]] ## the 3 is there because we tried two other times ``` Here the red data are fitted and black observed. -```{r plot_rbf_res, fig.width=6} +```{r plot_rbf_res, fig.width=6, eval = FALSE} simulated_incidence = filter(simulator$report(.phases = "during"), matrix == "incidence")$value plot(measles$date, measles$cases, xlab = "time", type = "l") lines(measles$date, simulated_incidence, col = 2) From a5e487600ce1c3886e0ace97845bd0bc983b1188 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Sep 2023 15:01:47 -0400 Subject: [PATCH 019/332] code doc mismatches --- man/TMBModel.Rd | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index 0b60b6bd..9c48ec22 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -11,9 +11,9 @@ TMBModel( random = OptParamsList(), obj_fn = ObjectiveFunction(~0), time_steps = Time(0L), - do_pred_sdreport = TRUE, engine_methods = EngineMethods(), - log_file = LogFile() + log_file = LogFile(), + do_pred_sdreport = TRUE ) } \arguments{ @@ -29,12 +29,11 @@ TMBModel( \item{time_steps}{An object of class \code{\link{Time}}.} -\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} - \item{engine_methods}{An object of class \code{\link{EngineMethods}}.} \item{log_file}{An object of class \code{\link{LogFile}}.} +\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} } \value{ Object of class \code{TMBModel} with the following methods. From 92eca800bcd1cbfff62a88b917b228e652b46ef7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Sep 2023 15:01:03 -0400 Subject: [PATCH 020/332] make specification of state and flow vectors with missing elements more robust --- R/compartmental.R | 4 ++-- R/name_utils.R | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/R/compartmental.R b/R/compartmental.R index 5563a043..e79a1572 100644 --- a/R/compartmental.R +++ b/R/compartmental.R @@ -134,8 +134,8 @@ CompartmentalMatsList = function( indices = model$indices MatsList( - state = as.matrix(state)[settings$state(), , drop = FALSE] - , flow = as.matrix(flow)[settings$flow(), , drop = FALSE] + state = to_matrix_with_rownames(state, settings$state()) + , flow = to_matrix_with_rownames(flow, settings$flow()) , ... , state_length = length(state) , per_capita_from = indices$flow$per_capita$from() diff --git a/R/name_utils.R b/R/name_utils.R index a5e954ab..0a7b614a 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -111,3 +111,20 @@ frame_to_part = function(frame) { } y } + +to_matrix_with_rownames = function(x, nms) { + x = as.matrix(x) + if (!all(rownames(x) %in% nms)) { + stop("Matrix rownames are not all in supplied rownames.") + } + missing_nms = nms[!nms %in% rownames(x)] + if (length(missing_nms) > 0L) { + y = matrix(NA + , length(missing_nms) + , ncol(x) + , dimnames = list(missing_nms, colnames(x)) + ) + x = rbind(x, y) + } + x[nms, , drop = FALSE] +} From d289c4678b0d6353f5f9b248d5e5ed9e5e04e8d1 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 25 Sep 2023 17:22:18 -0400 Subject: [PATCH 021/332] broken experimental brach --- Makefile | 12 +- NAMESPACE | 2 + R/alt_model_constructors.R | 12 +- R/const_int_vec.R | 2 - R/enum_methods.R | 32 +++--- R/flow_expander.R | 14 +-- R/formula.R | 64 +++++++++++ R/formula_utils.R | 96 ++++++++++++++-- R/name_utils.R | 8 ++ R/names_and_labels.R | 3 +- R/simulation_methods.R | 12 +- _pkgdown.yml | 2 +- inst/starter_models/age/flows.csv | 36 +++--- inst/starter_models/age/variables.csv | 2 +- inst/starter_models/sir/variables.csv | 1 + man/SimulatorConstructor.Rd | 2 +- misc/dev/dev.cpp | 22 ++++ src/macpan2.cpp | 22 ++++ tests/testthat/test-dev-compile.R | 13 +++ tests/testthat/test-example-models.R | 130 +++++++++++++++------- tests/testthat/test-expand-flows.R | 3 + tests/testthat/test-formula-utils.R | 25 +++++ tests/testthat/test-labelled-partitions.R | 4 +- tests/testthat/test-no-derivations.R | 20 ++++ vignettes/flow_types.Rmd | 41 ++++++- 25 files changed, 458 insertions(+), 122 deletions(-) create mode 100644 R/formula.R create mode 100644 tests/testthat/test-dev-compile.R create mode 100644 tests/testthat/test-expand-flows.R create mode 100644 tests/testthat/test-formula-utils.R create mode 100644 tests/testthat/test-no-derivations.R diff --git a/Makefile b/Makefile index f20d0966..628a8d52 100644 --- a/Makefile +++ b/Makefile @@ -108,17 +108,17 @@ doc-update: R/*.R misc/dev/dev.cpp echo "suppressWarnings(roxygen2::roxygenize(\".\",roclets = c(\"collate\", \"rd\", \"namespace\")))" | R --slave -pkg-build:: macpan2_$(VERSION).tar.gz -macpan2_$(VERSION).tar.gz: DESCRIPTION man/*.Rd R/*.R src/*.cpp tests/testthat/test-*.R tests/testthat.R inst/starter_models/**/*.csv inst/starter_models/**/*.json +pkg-build:: ../macpan2_$(VERSION).tar.gz +../macpan2_$(VERSION).tar.gz: DESCRIPTION man/*.Rd R/*.R src/*.cpp tests/testthat/test-*.R tests/testthat.R inst/starter_models/**/*.csv inst/starter_models/**/*.json R CMD build . -pkg-check: macpan2_$(VERSION).tar.gz - R CMD check macpan2_$(VERSION).tar.gz +pkg-check: ../macpan2_$(VERSION).tar.gz + R CMD check ../macpan2_$(VERSION).tar.gz -pkg-install: macpan2_$(VERSION).tar.gz - R CMD INSTALL --no-multiarch --install-tests macpan2_$(VERSION).tar.gz +pkg-install: ../macpan2_$(VERSION).tar.gz + R CMD INSTALL --no-multiarch --install-tests ../macpan2_$(VERSION).tar.gz compile-dev: misc/dev/dev.cpp diff --git a/NAMESPACE b/NAMESPACE index fe9ec407..98c9f713 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,6 +37,7 @@ export(Euler) export(ExprList) export(Files) export(FlowExpander) +export(Formula) export(IntVecs) export(JSONReader) export(Log) @@ -79,6 +80,7 @@ export(engine_eval) export(finalizer_char) export(finalizer_index) export(initial_valid_vars) +export(labelled_zero_vector) export(make_expr_parser) export(model_starter) export(nlist) diff --git a/R/alt_model_constructors.R b/R/alt_model_constructors.R index a46649df..5e875ce4 100644 --- a/R/alt_model_constructors.R +++ b/R/alt_model_constructors.R @@ -70,7 +70,6 @@ Derivations2ExprListAlt = function(user_expr, standard_expr) { self$expr_list_per_phase = function( phase = c("before", "during", "after", "during_pre_update", "during_update", "during_post_update") ) { - #browser() phases = match.arg(phase) if (phases == "during") { phases = c("during_pre_update", "during_update", "during_post_update") @@ -138,7 +137,7 @@ ModelAlt = function(definition) { missing_fields = which(lapply(optional_fields, is_missing) == TRUE) return(cbind(self$flows(), default_entries[,missing_fields])) } - self$derivations = self$def$derivations ## look like a field but actually method forwarding + self$derivations = self$def$derivations self$expr_list = function() { Derivations2ExprListAlt(UserExpr(self), StandardExprAlt(self))$expr_list() } @@ -174,17 +173,12 @@ CompartmentalAlt = function(model_directory){ #' model definition files. #' @param integration_method One of the functions described in #' \link{integration_methods}, used to integrate the dynamical system. -#' @param ... Arguments to pass to \code{\link{TMBModel}}. +#' @param ... Arguments to pass to the simulator constructor. #' @export SimulatorConstructor = function(model_directory, integration_method = RK4, ...){ model = CompartmentalAlt(model_directory) - model_simulator = model$simulators$tmb(..., .bundle_compartmental_model = TRUE) - - - expanded_flows = model$flows_expanded() - - model_simulator = do.call(integration_method, list(model_simulator)) + model_simulator = model$simulators$tmb(...) |> integration_method() return(model_simulator) } diff --git a/R/const_int_vec.R b/R/const_int_vec.R index 555414a0..788a9742 100644 --- a/R/const_int_vec.R +++ b/R/const_int_vec.R @@ -13,8 +13,6 @@ IntVecs = function(...) { # Standard Methods self$data_arg = function() { - # DATA_IVECTOR(const_int_vec); - # DATA_IVECTOR(const_n_int_vecs); list( const_int_vec = unlist(self$list, use.names = FALSE), const_n_int_vecs = unlist(lapply(self$list, length), use.names = FALSE) diff --git a/R/enum_methods.R b/R/enum_methods.R index 4d7f5e6f..cd36781e 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -1,4 +1,5 @@ ## Auto-generated - do not edit by hand + #' Method Prototype #' #' Define a method type using a prototype. These prototypes can be compared @@ -33,18 +34,18 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { that$x[which_int_vecs] } self$as_character = function() deparse(self$formula) - self$is_assignment = function(other_formula) { - (length(self$formula) == 3L) & (length(other_formula) == 3L) + self$is_setter = function(other_formula) { + two_sided(self$formula) & two_sided(other_formula) } - self$is_return = function(other_formula) { - (length(self$formula) == 2L) & (length(other_formula) == 2L) + self$is_getter = function(other_formula) { + one_sided(self$formula) & one_sided(other_formula) } self$parse_table = function() { method_parser(self$formula) } self$consistent = function(other_formula) { - this = self$.concat_parse_table(self$formula) - that = self$.concat_parse_table(other_formula) + this = concat_parse_table(self$formula) + that = concat_parse_table(other_formula) this_funs = this$x[this$n > 0L] that_funs = that$x[that$n > 0L] good_n_sig = identical(this$n, that$n) @@ -52,13 +53,6 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { good_n_sig & good_fun_names } - ## Private - self$.lhs = function(two_sided_formula) two_sided_formula[-3L] - self$.rhs = function(two_sided_formula) two_sided_formula[-2L] - self$.concat_parse_table = function(formula) { - if (length(formula) == 2L) return(method_parser(formula)) - rbind(method_parser(self$.lhs(formula)), method_parser(self$.rhs(formula))) - } return_object(self, "MethodPrototype") } @@ -129,11 +123,20 @@ MethodTypeUtils = function() { ) } + self$could_make_method = function(formula) { + v = setNames(logical(length(self$method_ordering)), self$method_ordering) + for (meth_type_nm in self$method_ordering) { + v[meth_type_nm] = self[[meth_type_nm]]$consistent(formula) + } + any(v) + } + return_object(self, "MethodTypesUtil") } + MethodTypes = function() { self = MethodTypeUtils() - self$method_ordering = c("meth_from_rows", "meth_to_rows", "meth_rows_to_rows", "meth_mat_mult_to_rows", "meth_tv_mat_mult_to_rows", "meth_group_sums", "meth_tv_mat") + self$method_ordering = c("meth_from_rows", "meth_to_rows", "meth_rows_to_rows", "meth_mat_mult_to_rows", "meth_tv_mat_mult_to_rows", "meth_group_sums", "meth_tv_mat", "meth_rows_times_rows") self$meth_from_rows = MethodPrototype(~ Y[i], "Y", "i") self$meth_to_rows = MethodPrototype(Y[i] ~ X, c("Y", "X"), "i") self$meth_rows_to_rows = MethodPrototype(Y[i] ~ X[j], c("Y", "X"), c("i", "j")) @@ -141,5 +144,6 @@ MethodTypes = function() { self$meth_tv_mat_mult_to_rows = MethodPrototype(Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer")) self$meth_group_sums = MethodPrototype(~ groupSums(Y, i, n), "Y", c("i", "n")) self$meth_tv_mat = MethodPrototype(~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer")) + self$meth_rows_times_rows = MethodPrototype(~ A[i] * X[j], c("A", "X"), c("i", "j")) return_object(self, "MethodTypes") } diff --git a/R/flow_expander.R b/R/flow_expander.R index a14c8216..db2eaf82 100644 --- a/R/flow_expander.R +++ b/R/flow_expander.R @@ -41,16 +41,7 @@ FlowExpander = function(model) { self$f = self$.add_missing_columns(model$flows()) self$.namer = Namer() self$.filter = FilterBlanksNotSpecial() - - ## only rate makes sense now, but adding others to prepare for them - valid_flow_types = sprintf("%s_variables", c("flow")) - flow_settings = self$s[valid_flow_types] - ## TODO: add check to enforce that no variable is in more than one type?? - ## TODO: make sure that flow_settings is a valid character vector even - ## though many of these types will not be present in any given model - self$.flow_variables = do.call(c, flow_settings) - - + self$.flow_variables = self$s$flow_variables self$filter_flow = function(flow_number, component_type) { filter_partitions = paste(component_type, "partition", sep = "_") @@ -62,7 +53,6 @@ FlowExpander = function(model) { , to = self$s$state_variables , flow = self$s$flow_variables ) - #namer$v_to_psl_with_pn(partition_set, s$required_partitions) partition_set[ self$.namer$v_to_psl_with_pn(partition_set, self$s$required_partitions) %in% component_filter @@ -98,6 +88,8 @@ FlowExpander = function(model) { z } + ## take a single row in flows.csv and return several rows that each + ## represent a single flow self$expand_flow = function(flow_number) { from_to = self$matching_flow(flow_number, c("from", "to")) from_flow = self$matching_flow(flow_number, c("from", "flow")) diff --git a/R/formula.R b/R/formula.R new file mode 100644 index 00000000..fd5b1b3a --- /dev/null +++ b/R/formula.R @@ -0,0 +1,64 @@ +#' @export +Formula = function(formula, name = "", mats_list = MatsList()) { + self = Base() + self$formula = formula + self$name = name + self$method_types = MethodTypes() + self$lhs = function() lhs(self$formula) + self$rhs = function() rhs(self$formula) + self$could_be_getter = function() { + self$method_types$could_make_method(self$rhs()) + } + self$could_be_setter = function() { + self$method_types$could_make_method(self$formula) + } + self$method_name = function() { + pt = method_parser(self$lhs()) + name = paste0(rev(pt$x[pt$n == 0]), collapse = "_") + if (self$could_be_setter()) { + return(sprintf("set_%s", name)) + } + sprintf("get_%s", name) + } + self$setter = function() { + try(self$method_types$make_method(self$formula), silent = TRUE) + } + self$getter = function() { + try(self$method_types$make_method(self$rhs()), silent = TRUE) + } + self$mat_args = function() { + meth = self$setter() + if (inherits(meth, "try-error")) meth = self$getter() + if (!inherits(meth, "try-error")) return(meth$mat_args()) + c(mat_vec_nms(self$rhs()), mat_vec_nms(self$lhs())) |> unique() + } + self$int_vec_args = function() { + meth = self$setter() + if (inherits(meth, "try-error")) meth = self$getter() + if (!inherits(meth, "try-error")) return(meth$int_vec_args()) + character(0L) + } + self$expr_list = function() { + if (self$could_be_setter()) { + f = two_sided("dummy", self$method_name()) + } else if (self$could_be_getter()) { + f = two_sided(lhs_char(self$formula), self$method_name()) + } else if (is_two_sided(self$formula)) { + f = to_assign(self$formula) + } else { + stop("Invalid formula") + } + setNames(list(f), self$name) + } + self$meth_list = function() { + if (self$could_be_setter()) { + f = self$formula + } else if (self$could_be_getter()) { + f = self$rhs() + } else { + return(list()) + } + setNames(list(f), self$method_name()) + } + return_object(self, "Formula") +} diff --git a/R/formula_utils.R b/R/formula_utils.R index 746dcfc3..0b7e8cb8 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -1,7 +1,23 @@ +#' To Special Vectors +#' +#' Takes a formula that refers to components of special vectors (e.g. +#' `state`, `flow`, `trans`), and returns a formula that can be processed +#' by the engine. +#' +#' @param formula A formula +#' @param component_list Named list of character vectors, with names +#' giving special vectors and character vectors giving the names of the +#' components of those special vectors. For example, +#' `list(state = c("S", "I"), flow = "infection")`. +#' @param matrix_list Character vector of the names of matrices and methods. +#' @param component_vec_by Named character vector, with names giving the +#' special vectors. The values associated with these names gives the vector +#' that will be accessed by the component names. +#' @noRd to_special_vecs = function( formula, component_list, matrix_list, - component_vec_by = c(state = "state", flow = "flow" - )) { + component_vec_by = c(state = "state", flow = "flow") + ) { arg_signature = c( unlist(component_list, use.names = FALSE, recursive = FALSE), matrix_list @@ -12,7 +28,7 @@ to_special_vecs = function( hh = MathExpressionFromStrings(form_parts[[2L]], arg_signature) for (nm in names(component_list)) { - ## this ...RAW...INDICES... token probably won't clash ... right?? + ## this ...RAW...INDICES... token won't clash ## it just means that if you ## name a new special vector (e.g. state, flow) it cannot be called ## ...RAW...INDICES..., which seems fine @@ -28,13 +44,11 @@ to_special_vecs = function( )) lhs = do.call(hh$symbolic$evaluate, args) rhs = do.call(ee$symbolic$evaluate, args) - ("%s ~ %s" - |> sprintf(lhs, rhs) - |> as.formula() - ) + two_sided(lhs, rhs) } -to_assign = function(formula) { + +to_assign = function(formula, dummy = "dummy") { lhs = formula[[2L]] rhs = formula[[3L]] lhs_char = vapply(lhs, deparse1, character(1L)) @@ -42,7 +56,8 @@ to_assign = function(formula) { if (length(lhs_char) == 3L) lhs_char = append(lhs_char, "0") rhs_char = deparse1(rhs) args = as.list(c( - "dummy ~ assign(%s, %s, %s, %s)", + "%s ~ assign(%s, %s, %s, %s)", + dummy, lhs_char[-1L], rhs_char )) @@ -56,3 +71,66 @@ one_sided = function(rhs) { two_sided = function(lhs, rhs) { as.formula(sprintf("%s ~ %s", as.character(lhs), as.character(rhs))) } + +## how many sides does a formula have? +is_zero_sided = function(formula) { + (length(formula) == 1L) & inherits(formula, "formula") +} +is_one_sided = function(formula) { + (length(formula) == 2L) & inherits(formula, "formula") +} +is_two_sided = function(formula) { + (length(formula) == 3L) & inherits(formula, "formula") +} + +rhs = function(formula) { + if (is_two_sided(formula)) formula = formula[-2L] + formula +} +lhs = function(formula) { + if (is_two_sided(formula)) { + formula = formula[-3L] + } else if (is_one_sided(formula)) { + formula = formula[1L] + } + formula +} + +lhs_char = function(formula) { + if (is_two_sided(formula)) { + return(deparse(formula[[2L]], 500)) + } + "" +} + +# formula parsing in macpan2 works one side at a time. but sometimes +# it is helpful to parse two-sided formulas. this function does so +# by parsing each side at a time and rbinding the results. +concat_parse_table = function(formula) { + if (is_one_sided(formula)) return(method_parser(formula)) + rbind(method_parser(lhs(formula)), method_parser(rhs(formula))) +} + +# When looking at a formula without any additional information (e.g. +# no information on the matrices in the model), one can classify three +# types of components that make up a formula: +# 1. variables +# 2. functions +# 3. literals +# +# In the context of a model, variables can be things like matrices, +# methods, integer vectors. +# +# This function returns a list with three components each giving a list of +# the components of that type. +formula_components = function(formula) { + parse_table = concat_parse_table(formula) + is_var_or_lit = parse_table$n == 0L + is_func = parse_table$n > 0L + is_lit = grepl("^[0-9]*\\.?[0-9]*$", parse_table$x) + list( + variables = parse_table$x[is_var_or_lit & !is_lit] |> unique(), + functions = parse_table$x[is_func] |> unique(), + literals = parse_table$x[is_var_or_lit & is_lit] |> as.numeric() |> unique() + ) +} diff --git a/R/name_utils.R b/R/name_utils.R index 0a7b614a..56ed27f9 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -128,3 +128,11 @@ to_matrix_with_rownames = function(x, nms) { } x[nms, , drop = FALSE] } + +#' @export +labelled_zero_vector = function(labels) { + (rep(0, length(labels)) + |> setNames(labels) + |> dput() + ) +} diff --git a/R/names_and_labels.R b/R/names_and_labels.R index e5cbf21f..63f3a063 100644 --- a/R/names_and_labels.R +++ b/R/names_and_labels.R @@ -13,7 +13,8 @@ valid_undotted = ValidityMessager( All( is.character, TestPipeline(Summarizer(valid_undotted_chars, all), TestTrue()) - ) + ), + "vector contained strings with dots" ) valid_dotted = ValidityMessager( All( diff --git a/R/simulation_methods.R b/R/simulation_methods.R index 8a153f6d..62b7e86c 100644 --- a/R/simulation_methods.R +++ b/R/simulation_methods.R @@ -35,7 +35,7 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ } args_per_capita = list( - as.formula("per_capita ~ state[per_capita_from]*flow[per_capita_flow]"), + per_capita ~ state[per_capita_from]*flow[per_capita_flow], .at = Inf, .phase = "during", .vec_by_flows = "flows", @@ -43,7 +43,7 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ ) args_absolute = list( - as.formula("absolute ~ flow[absolute_flow]"), + absolute ~ flow[absolute_flow], .at = Inf, .phase = "during", .vec_by_flows = "flows", @@ -51,7 +51,7 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ ) args_per_capita_inflow = list( - as.formula("per_capita_inflow ~ state[per_capita_inflow_from]*flow[per_capita_inflow_flow]"), + per_capita_inflow ~ state[per_capita_inflow_from]*flow[per_capita_inflow_flow], .at = Inf, .phase = "during", .vec_by_flows = "flows", @@ -59,7 +59,7 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ ) args_per_capita_outflow = list( - as.formula("per_capita_outflow ~ state[per_capita_outflow_from]*flow[per_capita_outflow_flow]"), + per_capita_outflow ~ state[per_capita_outflow_from]*flow[per_capita_outflow_flow], .at = Inf, .phase = "during", .vec_by_flows = "flow", @@ -67,7 +67,7 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ ) args_absolute_inflow = list( - as.formula("absolute_inflow ~ flow[absolute_inflow_flow]"), + absolute_inflow ~ flow[absolute_inflow_flow], .at = Inf, .phase = "during", .vec_by_flows = "flows", @@ -75,7 +75,7 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ ) args_absolute_outflow = list( - as.formula("absolute_outflow ~ flow[absolute_outflow_flow]"), + absolute_outflow ~ flow[absolute_outflow_flow], .at = Inf, .phase = "during", .vec_by_flows = "flows", diff --git a/_pkgdown.yml b/_pkgdown.yml index 998aaf5d..cde0d6c6 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -23,7 +23,7 @@ articles: - time_varying_parameters - calibration - example_models - - flow_types + - flows - title: Specs desc: Specification documents contents: diff --git a/inst/starter_models/age/flows.csv b/inst/starter_models/age/flows.csv index f75cc6d9..39e9b104 100644 --- a/inst/starter_models/age/flows.csv +++ b/inst/starter_models/age/flows.csv @@ -1,18 +1,18 @@ -from,to,flow,type,from_partition,to_partition,flow_partition,from_to_partition,from_flow_partition,to_flow_partition -,age_0_19,birth_rate_0_19,per_capita_inflow,Age,Age,Age,,,Null -,age_0_19,birth_rate_20_39,per_capita_inflow,Age,Age,Age,,,Null -,age_0_19,birth_rate_40_59,per_capita_inflow,Age,Age,Age,,,Null -,age_0_19,birth_rate_60_79,per_capita_inflow,Age,Age,Age,,,Null -,age_0_19,birth_rate_80_99,per_capita_inflow,Age,Age,Age,,,Null -,age_0_19,birth_rate_100_plus,per_capita_inflow,Age,Age,Age,,,Null -age_0_19,,death_rate_0_19,per_capita_outflow,Age,Age,Age,,,Null -age_20_39,,death_rate_20_39,per_capita_outflow,Age,Age,Age,,,Null -age_40_59,,death_rate_40_59,per_capita_outflow,Age,Age,Age,,,Null -age_60_79,,death_rate_60_79,per_capita_outflow,Age,Age,Age,,,Null -age_80_99,,death_rate_80_99,per_capita_outflow,Age,Age,Age,,,Null -age_100_plus,,death_rate_100_plus,per_capita_outflow,Age,Age,Age,,,Null -age_0_19,age_20_39,age_rate,per_capita,Age,Age,Age,,,Null -age_20_39,age_40_59,age_rate,per_capita,Age,Age,Age,,,Null -age_40_59,age_60_79,age_rate,per_capita,Age,Age,Age,,,Null -age_60_79,age_80_99,age_rate,per_capita,Age,Age,Age,,,Null -age_80_99,age_100_plus,age_rate,per_capita,Age,Age,Age,,,Null +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +age_0_19 ,age_0_19 ,birth_rate_0_19 ,per_capita_inflow ,Age ,Age ,Age , , ,Null +age_20_39 ,age_0_19 ,birth_rate_20_39 ,per_capita_inflow ,Age ,Age ,Age , , ,Null +age_40_59 ,age_0_19 ,birth_rate_40_59 ,per_capita_inflow ,Age ,Age ,Age , , ,Null +age_60_79 ,age_0_19 ,birth_rate_60_79 ,per_capita_inflow ,Age ,Age ,Age , , ,Null +age_80_99 ,age_0_19 ,birth_rate_80_99 ,per_capita_inflow ,Age ,Age ,Age , , ,Null +age_100_plus ,age_0_19 ,birth_rate_100_plus ,per_capita_inflow ,Age ,Age ,Age , , ,Null +age_0_19 ,age_0_19 ,death_rate_0_19 ,per_capita_outflow ,Age ,Null ,Age , , ,Null +age_20_39 ,age_20_39 ,death_rate_20_39 ,per_capita_outflow ,Age ,Null ,Age , , ,Null +age_40_59 ,age_40_59 ,death_rate_40_59 ,per_capita_outflow ,Age ,Null ,Age , , ,Null +age_60_79 ,age_60_79 ,death_rate_60_79 ,per_capita_outflow ,Age ,Null ,Age , , ,Null +age_80_99 ,age_80_99 ,death_rate_80_99 ,per_capita_outflow ,Age ,Null ,Age , , ,Null +age_100_plus ,age_100_plus ,death_rate_100_plus ,per_capita_outflow ,Age ,Null ,Age , , ,Null +age_0_19 ,age_20_39 ,age_rate ,per_capita ,Age ,Age ,Age , , ,Null +age_20_39 ,age_40_59 ,age_rate ,per_capita ,Age ,Age ,Age , , ,Null +age_40_59 ,age_60_79 ,age_rate ,per_capita ,Age ,Age ,Age , , ,Null +age_60_79 ,age_80_99 ,age_rate ,per_capita ,Age ,Age ,Age , , ,Null +age_80_99 ,age_100_plus ,age_rate ,per_capita ,Age ,Age ,Age , , ,Null diff --git a/inst/starter_models/age/variables.csv b/inst/starter_models/age/variables.csv index 72091c2f..aa71db06 100644 --- a/inst/starter_models/age/variables.csv +++ b/inst/starter_models/age/variables.csv @@ -17,4 +17,4 @@ death_rate_20_39 death_rate_40_59 death_rate_60_79 death_rate_80_99 -death_rate_100_plus +death_rate_100_plus \ No newline at end of file diff --git a/inst/starter_models/sir/variables.csv b/inst/starter_models/sir/variables.csv index edcfabd9..3895a36a 100644 --- a/inst/starter_models/sir/variables.csv +++ b/inst/starter_models/sir/variables.csv @@ -6,3 +6,4 @@ N beta foi gamma +per_capita_transmission diff --git a/man/SimulatorConstructor.Rd b/man/SimulatorConstructor.Rd index 38438f19..93320297 100644 --- a/man/SimulatorConstructor.Rd +++ b/man/SimulatorConstructor.Rd @@ -13,7 +13,7 @@ model definition files.} \item{integration_method}{One of the functions described in \link{integration_methods}, used to integrate the dynamical system.} -\item{...}{Arguments to pass to \code{\link{TMBModel}}.} +\item{...}{Arguments to pass to the simulator constructor.} } \description{ Simulator Constructor diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index a9c70470..d634c33f 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -116,6 +116,7 @@ enum macpan2_meth { , METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") , METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") , METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") + , METH_ROWS_TIMES_ROWS = 8 // ~ A[i] * X[j], c("A", "X"), c("i", "j") }; void printIntVector(const std::vector& intVector) { @@ -634,6 +635,27 @@ class ExprEvaluator { // } + case METH_ROWS_TIMES_ROWS: + m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); + m1 = getNthMat(1, curr_meth_id, valid_vars, meth_mats); + u = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); + v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); + + if (u.size() != v.size()) { + SetError(201, "The two operands do not have the same number of rows", row); + return m2; + } + if (m.cols() != m1.cols()) { + SetError(202, "The two operands do not have the same number of columns", row); + return m2; + } + m2 = matrix::Zero(u.size(), m.cols()); + m3 = matrix::Zero(v.size(), m1.cols()); + for (int i=0; i& intVector) { @@ -635,6 +636,27 @@ class ExprEvaluator { // } + case METH_ROWS_TIMES_ROWS: + m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); + m1 = getNthMat(1, curr_meth_id, valid_vars, meth_mats); + u = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); + v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); + + if (u.size() != v.size()) { + SetError(201, "The two operands do not have the same number of rows", row); + return m2; + } + if (m.cols() != m1.cols()) { + SetError(202, "The two operands do not have the same number of columns", row); + return m2; + } + m2 = matrix::Zero(u.size(), m.cols()); + m3 = matrix::Zero(v.size(), m1.cols()); + for (int i=0; i macpan2:::to_assign() + + +test_that("out-of-context formula parsing works", { + expect_equal( + macpan2:::formula_components(this[is] ~ a(test, 3) / of_the_number + 2.), + list( + variables = c("this", "is", "of_the_number", "test"), + functions = c("~", "[", "+", "/", "a"), + literals = c(2, 3) + ) + ) + expect_equal( + macpan2:::formula_components(~ 1.35 / a(b)), + list( + variables = "b", + functions = c("~", "/", "a"), + literals = 1.35 + ) + ) +}) diff --git a/tests/testthat/test-labelled-partitions.R b/tests/testthat/test-labelled-partitions.R index d136a1c4..16fbe4c0 100644 --- a/tests/testthat/test-labelled-partitions.R +++ b/tests/testthat/test-labelled-partitions.R @@ -59,7 +59,7 @@ test_that("labels, name, and names conversion is correct", { expect_identical(to_names("A"), "A") expect_identical(to_names(c("A", "B")), c("A", "B")) expect_identical(to_names("A.B"), c("A", "B")) - expect_error(to_names(c("A.B", "C.D")), "Error in valid_undotted") + expect_error(to_names(c("A.B", "C.D")), "vector contained strings with dots") expect_identical(to_names(p$.partition), c("A", "B")) expect_identical(to_names(dotted_scalar), c("a", "z")) expect_identical(to_names(undotted_vector), c("a", "z")) @@ -70,7 +70,7 @@ test_that("labels, name, and names conversion is correct", { expect_identical(to_name(p), "A.B") - expect_error(to_name(c("a.z", "b.y")), "Error in valid_undotted") + expect_error(to_name(c("a.z", "b.y")), "vector contained strings with dots") expect_identical(to_name("a.z"), "a.z") expect_identical(to_name(c("a", "z")), "a.z") expect_identical(to_name(p$.partition), "A.B") diff --git a/tests/testthat/test-no-derivations.R b/tests/testthat/test-no-derivations.R new file mode 100644 index 00000000..62dabb13 --- /dev/null +++ b/tests/testthat/test-no-derivations.R @@ -0,0 +1,20 @@ +library(macpan2) +library(dplyr) +library(ggplot2) +library(oor) +age = Compartmental(system.file("starter_models", "age", package = "macpan2")) +s = age$simulators$tmb(time_steps = 100L + , state = c(age_0_19 = 100, age_20_39 = 0, age_40_59 = 0, age_60_79 = 0, age_80_99 = 0, age_100_plus = 0) + , flow = c( + birth_rate_0_19 = 0, birth_rate_20_39 = 0.09, birth_rate_40_59 = 0.02, + birth_rate_60_79 = 0, birth_rate_80_99 = 0, birth_rate_100_plus = 0, + death_rate_0_19 = 0.01, death_rate_20_39 = 0.03, death_rate_40_59 = 0.05, + death_rate_60_79 = 0.1, death_rate_80_99 = 0.2, death_rate_100_plus = 0.5, + age_rate = 1 / 20 + ) +) +(s$report() + |> filter(matrix == "state", row != "dead") + |> ggplot() + + geom_line(aes(time, value, colour = row)) +) diff --git a/vignettes/flow_types.Rmd b/vignettes/flow_types.Rmd index 8c83083b..6e448dc5 100644 --- a/vignettes/flow_types.Rmd +++ b/vignettes/flow_types.Rmd @@ -1,10 +1,10 @@ --- -title: "Flow Types" +title: "Flows" output: rmarkdown::html_vignette: toc: true vignette: > - %\VignetteIndexEntry{Flow Types} + %\VignetteIndexEntry{Flows} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: @@ -18,6 +18,7 @@ library(macpan2) library(macpan2helpers) library(dplyr) library(ggplot2) +library(igraph) knitr::opts_chunk$set( collapse = TRUE, comment = "#>" @@ -25,6 +26,42 @@ knitr::opts_chunk$set( cat_file = function(...) cat(readLines(file.path(...)), sep = "\n") ``` +## Structured Flows + + + +```{r, echo=FALSE, fig.width=7} +d = system.file("starter_models", "sir_vax", package = "macpan2") +model = Compartmental(d) +e = model$flows_expanded() +v = data.frame(id = model$labels$state()) + +|> graph.data.frame(vertices = )) +coords = layout_(g, on_grid(width = 3, height = 2)) +par(mar=c(0,0,0,0)) +plot(g, layout = coords, asp = 0.2) +# , vertex.color = "lightgrey" +# , vertex.shape = "rectangle" +# , vertex.size = 60 +# , vertex.size2 = 40 +# , asp = 0.5 +# ) +``` + +```{r} +cat_file(d, "flows.csv") +cat_file(d, "variables.csv") +cat_file(d, "derivations.json") +``` + + + +The flows +```{r} +model$flows() +model$flows_explicit() +``` + ## Types of Flows The `Macpan2` library allows for six different types of flows. They are: From 16c9d76da79849dc2d66b67f9d51a774d3f9196a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 10 Oct 2023 10:05:31 -0400 Subject: [PATCH 022/332] working away --- DESCRIPTION | 8 +- Makefile | 8 +- NAMESPACE | 4 + R/connection.R | 239 +++++++++++++++++++++++++++++++++++++++ R/derivations.R | 203 +++++++++++++++++++++++++++++++++ R/enum_methods.R | 8 +- R/files.R | 3 + R/files_to_models.R | 114 +++++++++++++++++-- R/flow_expander.R | 33 +++++- R/labelled_partitions.R | 83 ++------------ R/macpan2.R | 1 + R/model_data_structure.R | 35 ++++-- R/model_files.R | 2 + R/name_utils.R | 1 + R/namer.R | 10 -- R/names_and_labels.R | 55 ++++++--- R/parse_expr.R | 22 +++- R/settings.R | 1 + R/tmb_model_editors.R | 4 +- R/variables.R | 23 ++-- R/zzz.R | 5 +- man/macpan2-package.Rd | 1 + 22 files changed, 720 insertions(+), 143 deletions(-) create mode 100644 R/connection.R create mode 100644 R/derivations.R diff --git a/DESCRIPTION b/DESCRIPTION index d084e66d..b54c9b8f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,8 @@ Imports: TMB, oor, jsonlite, - MASS + MASS, + memoise Suggests: covr, knitr, @@ -41,10 +42,11 @@ LinkingTo: Rcpp VignetteBuilder: knitr -Additional_repositories: https://canmod.github.io/drat +Remotes: + canmod/oor@valitidy Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.2.3.9000 URL: https://canmod.github.io/macpan2/, https://github.com/canmod/macpan2 diff --git a/Makefile b/Makefile index 628a8d52..e4d84897 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ ROXY_RE = ^.*\(\#'.*\)$ VERSION := $(shell sed -n '/^Version: /s///p' DESCRIPTION) TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") + all: make src-update make enum-update @@ -16,6 +17,11 @@ all: make pkg-install make pkg-check + +install-deps: + Rscript -e "remotes::install_github('canmod/oor@validity')" + + # Use this rule if you are doing R development or want to promote # dev.cpp to macpan2.cpp before doing R development. full-install: @@ -34,8 +40,6 @@ full-install: quick-install: enum-update enum-meth-update R CMD INSTALL --no-multiarch --install-tests . -install-deps: - Rscript -e "remotes::install_github('canmod/oor')" quick-doc-install: R/*.R misc/dev/dev.cpp make engine-doc-update diff --git a/NAMESPACE b/NAMESPACE index 98c9f713..a5d18b06 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -31,12 +31,14 @@ export(Collection) export(Compartmental) export(CompartmentalSimulator) export(DerivationExtractor) +export(Derivations) export(Derivations2ExprList) export(EngineMethods) export(Euler) export(ExprList) export(Files) export(FlowExpander) +export(Flows) export(Formula) export(IntVecs) export(JSONReader) @@ -69,6 +71,7 @@ export(TMBModel) export(TMBSimulator) export(TXTReader) export(Time) +export(Trans) export(Transform) export(UserExpr) export(all_consistent) @@ -97,6 +100,7 @@ export(union_vars) importFrom(MASS,mvrnorm) importFrom(TMB,MakeADFun) importFrom(jsonlite,fromJSON) +importFrom(memoise,memoise) importFrom(oor,All) importFrom(oor,Any) importFrom(oor,Base) diff --git a/R/connection.R b/R/connection.R new file mode 100644 index 00000000..cd70193f --- /dev/null +++ b/R/connection.R @@ -0,0 +1,239 @@ +Connection = function(row, variables + , from_ref = "from", to_ref = "to", conn_ref = "conn", type_ref = "type" + , from_set = "all", to_set = "all", conn_set = "all" + , filter_ref = "filter", join_ref = "join" +) { + self = Base() + self$row = row + self$variables = variables + self$from_ref = from_ref + self$to_ref = to_ref + self$from_set = from_set + self$to_set = to_set + self$conn_set = conn_set + self$conn_ref = conn_ref + self$filter_ref = filter_ref + self$join_ref = join_ref + self$type_ref = type_ref + + self$from_universe = variables[[self$from_set]] + self$to_universe = variables[[self$to_set]] + self$conn_universe = variables[[self$conn_set]] + + self$from = self$row[[self$from_ref]] + self$to = self$row[[self$to_ref]] + self$conn = self$row[[self$conn_ref]] + + self$from_filter = function() { + x = self$row[[sprintf("%s_%s", self$from_ref, self$filter_ref)]] + if (isTRUE(nchar(x) == 0L)) x = character() + if (is.null(x)) x = self$req_part() + x + } + self$to_filter = function() { + x = self$row[[sprintf("%s_%s", self$to_ref, self$filter_ref)]] + if (isTRUE(nchar(x) == 0L)) x = character() + if (is.null(x)) x = self$req_part() + x + } + self$conn_filter = function() { + x = self$row[[sprintf("%s_%s", self$conn_ref, self$filter_ref)]] + if (isTRUE(nchar(x) == 0L)) x = character() + if (is.null(x)) x = self$req_part() + x + } + + self$from_to_join = function() { + x = self$row[[sprintf("%s_%s_%s", self$from_ref, self$to_ref, self$join_ref)]] + if (isTRUE(nchar(x) == 0L) | is.null(x)) x = character() + x + } + self$from_conn_join = function() { + x = self$row[[sprintf("%s_%s_%s", self$from_ref, self$conn_ref, self$join_ref)]] + if (isTRUE(nchar(x) == 0L) | is.null(x)) x = character() + x + } + self$to_conn_join = function() { + x = self$row[[sprintf("%s_%s_%s", self$to_ref, self$conn_ref, self$join_ref)]] + if (isTRUE(nchar(x) == 0L) | is.null(x)) x = character() + x + } + + self$null_part = function() self$variables$model$settings$null() + self$req_part = function() self$variables$model$settings$names() + + self$from_vars = function() { + labelled_frame( + self$from_universe()$filter(self$from, .wrt = self$from_filter()), + self$from_ref + ) + } + self$to_vars = function() { + labelled_frame( + self$to_universe()$filter(self$to, .wrt = self$to_filter()), + self$to_ref + ) + } + self$conn_vars = function() { + f = labelled_frame( + self$conn_universe()$filter(self$conn, .wrt = self$conn_filter()), + self$conn_ref + ) + if (length(self$type_ref) == 1L) f$type = self$row[[self$type_ref]] + f + } + self$from_to_merge = function() { + output_cols = c(self$from_ref, self$to_ref) + if (identical(self$from_to_join(), self$null_part())) return(empty_frame(output_cols)) + connection_merge( + self$from_vars(), + self$to_vars(), + by = self$from_to_join(), + output_cols = output_cols + ) + } + self$from_conn_merge = function() { + output_cols = c(self$from_ref, self$conn_ref, self$type_ref) + if (identical(self$from_conn_join(), self$null_part())) return(empty_frame(output_cols)) + connection_merge( + self$from_vars(), + self$conn_vars(), + by = self$from_conn_join(), + output_cols = output_cols + ) + } + self$to_conn_merge = function() { + output_cols = c(self$to_ref, self$conn_ref, self$type_ref) + if (identical(self$to_conn_join(), self$null_part())) return(empty_frame(output_cols)) + connection_merge( + self$to_vars(), + self$conn_vars(), + by = self$to_conn_join(), + output_cols = output_cols + ) + } + + self$frame = function() { + from_to = self$from_to_merge() + from_conn = self$from_conn_merge() + to_conn = self$to_conn_merge() + out_cols = c(self$from_ref, self$to_ref, self$conn_ref, self$type_ref) + col_order = c(self$from_ref, self$to_ref, self$conn_ref, self$type_ref) + if (nrow(from_conn) != 0L) { + if (nrow(from_to) == 0L) return(enforce_schema(from_conn, out_cols)) + from_conn = merge(from_to, from_conn, by = self$from_ref, sort = FALSE)[col_order] + if (nrow(to_conn) == 0L) return(enforce_schema(from_conn, out_cols)) + } + if (nrow(to_conn) != 0L) { + if (nrow(from_to) == 0L) return(enforce_schema(to_conn, out_cols)) + to_conn = merge(from_to, to_conn, by = self$to_ref, sort = FALSE)[col_order] + if (nrow(from_conn) == 0L) return(enforce_schema(from_conn, out_cols)) + } + unique(rbind(from_conn, to_conn)) + } + + return_object(self, "Connection") +} + +connection_merge = function(x, y, by, output_cols) { + merge(x, y, by = by, sort = FALSE)[, output_cols] +} +connection_merge = memoise(connection_merge) + + + +#' @export +Flows = function(flows, variables) { + self = Base() + self$flows = flows + self$variables = variables + self$connections = list() + for (i in seq_row(self$flows)) { + self$connections[[i]] = Connection( + row = self$flows[i, , drop = FALSE] + , variables = self$variables + , conn_ref = "flow" + , from_set = "state", to_set = "state", conn_set = "flow" + , filter_ref = "partition" + , join_ref = "partition" + ) + } + self$frame = function() { + if (ncol(self$flows) < 5) return(self$flows) + do.call(rbind, method_apply(self$connections, "frame")) + } + return_object(self, "Flow") +} + +#' @export +Trans = function(trans, variables) { + self = Base() + self$trans = process_trans_frame(trans) + self$variables = variables + self$connections = list() + for (i in seq_row(self$trans)) { + self$connections[[i]] = Connection( + row = self$trans[i, , drop = FALSE] + , variables = self$variables + , from_ref = "state" + , to_ref = "flow" + , conn_ref = "pop" + , type_ref = "type" + , from_set = "state", to_set = "flow", conn_set = "all" + , filter_ref = "partition" + , join_ref = "partition" + ) + } + self$frame = function() { + if (ncol(self$trans) < 4) return(self$trans) + do.call(rbind, method_apply(self$connections, "frame")) + } + return_object(self, "Trans") +} + +process_trans_frame = function(trans) { + if (is.null(trans)) trans = empty_frame("state", "flow", "pop", "type") + trans +} + +labelled_frame = function(partition, label_name = "label") { + f = partition$frame() + f[[label_name]] = partition$labels() + f +} + +if (FALSE) { + library(macpan2) + m = Compartmental("inst/starter_models/minimal_all_index_cases") + m$flows_expanded() + + + + m = Compartmental("../macpan2/inst/starter_models/sir_vax/") + m$flows_info$frame() + m$flows_info$connections[[5L]]$frame() + m$flows_expanded() + m = Compartmental("../macpan2/inst/starter_models/sir_symp/") + m = Compartmental("../macpan2/inst/starter_models/sir/") + xx = Flows(m$flows_explicit(), m$variables) + xx$frame() + xx$connections[[1]]$row + yy = xx$connections[[1]] + debug(yy$from_to_merge) + debug(yy$from_vars) + yy$frame() + cc = Connection( + row = m$flows()[2,,drop=FALSE] + , variables = m$variables + , conn_ref = "flow" + , filter_ref = "partition" + , join_ref = "partition" + ) + cc$from_vars() + cc$to_vars() + cc$conn_vars() + cc$from_to_merge() + cc$from_conn_merge() + cc$to_conn_merge() + cc$frame() +} diff --git a/R/derivations.R b/R/derivations.R new file mode 100644 index 00000000..e15016f3 --- /dev/null +++ b/R/derivations.R @@ -0,0 +1,203 @@ +# Derivation Grouping +# +# Group together derivations into those with identical filtering and +# grouping specs. This class avoids filtering Partitions repeatedly in the +# same way, which is helpful for product models e.g. several derivations +# are age-stratified in the same way. +# +# @param model Object of class Model +DerivationGrouping = function(model) { + self = Base() + self$model = model + + ## number of derivations in the model + self$n_derivations = function() length(self$model$def$derivations()) + + ## list of spec lists, which are lists containing any of the following fields: + ## "filter_partition", "filter_names", "group_partition", "group_names" + self$specs = function() { + get_fields = function(x) { + fields = c("filter_partition", "filter_names", "group_partition", "group_names") + l = setNames(vector(mode = "list", length = length(fields)), fields) + for (f in fields) l[[f]] = x[[f]] + l + } + lapply(self$model$def$derivations(), get_fields) + } + + ## list of unique specs + self$unique_specs = function() unique(self$specs()) + + ## integer vector of length self$n_derivations, giving the id for each + ## derivation into self$unique_specs() + self$spec_map = function() { + l = integer(self$n_derivations()) + s = self$specs() + us = self$unique_specs() + for (i in seq_along(s)) for (j in seq_along(us)) { + if (identical(s[[i]], us[[j]])) l[i] = j + } + l + } + + ## list of unique partitions + self$unique_var_lists = function() { + all_vars = self$model$variables$all() + specs = self$unique_specs() + l = list() + for (i in seq_along(specs)) { + if (is.null(specs[[i]]$filter_partition)) { + filtered_vars = all_vars + } else { + filtered_vars = all_vars$filter( + specs[[i]]$filter_names, + .wrt = specs[[i]]$filter_partition + ) + } + l[[i]] = list() + if (is.null(specs[[i]]$group_partition)) { + grp_nm = specs[[i]]$filter_partition + if (is.null(grp_nm)) grp_nm = "all" + l[[i]][[grp_nm]] = filtered_vars + } else { + for (g in specs[[i]]$group_names) { + l[[i]][[g]] = filtered_vars$filter(g + , .wrt = specs[[i]]$group_partition + , .comparison_function = all_consistent + ) + } + } + } + l + } + + ## get the variable list partitions by derivation_id and looking up the + ## result in self$unique_var_lists() using the self$spec_map(). the result + ## is a list with one element per grouping giving the partition for that + ## group + self$var_list = function(derivation_id) { + self$unique_var_lists()[[self$spec_map()[derivation_id]]] + } + + initialize_cache(self, "n_derivations", "specs", "unique_specs", "spec_map" + , "unique_var_lists" + ) + return_object(self, "DerivationGrouping") +} + +Derivation = function(derivation_id, model) { + self = Base() + self$model = model + self$derivation_grouping = DerivationGrouping(model) + self$derivation_id = derivation_id + self$component_list = function() { + list( + state = self$model$labels$state(), + flow = self$model$labels$flow() + ) + } + self$get = function() self$model$def$derivations()[[self$derivation_id]] + self$output_partition = function() { + p = self$get()$output_partition + if (is.null(p)) p = self$model$settings$name() + p + } + self$input_partition = function() { + p = self$get()$input_partition + if (is.null(p)) p = self$model$settings$name() + p + } + self$do_lookup_output_names = function() { + self$output_partition() != self$model$settings$name() + } + self$output_labels = function() { + if (!self$do_lookup_output_names()) return(self$get()$output_names) + l = self$get()$output_names + for (i in seq_along(self$var_list)) { + l[i] = self$var_list()[[i]]$filter(l[i], .wrt = self$output_partition())$labels() + } + l + } + self$arguments = function() { + args = self$get()$arguments + if (is.null(args)) args = character() + l = self$var_list() + if (length(args) == 0L) { + for (i in seq_along(l)) l[[i]] = args + } else { + for (i in seq_along(l)) { + l[[i]] = l[[i]]$filter(args, .wrt = self$input_partition())$labels() + } + } + l + } + self$argument_dots = function() { + arg_dots = self$get()$argument_dots + if (is.null(arg_dots)) arg_dots = character() + l = self$var_list() + if (length(arg_dots) == 0L) { + for (i in seq_along(l)) l[[i]] = arg_dots + } else { + for (i in seq_along(l)) { + l[[i]] = l[[i]]$filter(arg_dots, .wrt = self$input_partition())$labels() + } + } + l + } + self$all_arguments = function() { + mapply(c, self$arguments(), self$argument_dots(), SIMPLIFY = FALSE) + } + self$var_list = function() self$derivation_grouping$var_list(self$derivation_id) + self$math = function() { + args = c(self$get()$arguments, self$get()$argument_dots) + dots_true = !is.null(self$get()$argument_dots) + MathExpressionFromStrings(self$get()$expression, args, dots_true)$symbolic$evaluate + } + self$expr_list = function() { + l = setNames( + vector(mode = "list", length = length(self$output_labels())), + sprintf("derivation_%s_%s", self$derivation_id, self$output_labels()) + ) + for (i in seq_along(l)) { + l[[i]] = do.call(self$math(), as.list(self$all_arguments()[[i]])) + l[[i]] = (two_sided(self$output_labels()[i], l[[i]]) + |> to_special_vecs( + self$component_list() + , c("state", "flow", self$model$labels$other()) + , c(state = "state", flow = "flow") + ) + |> to_assign() + ) + } + l + } + initialize_cache(self, "get", "output_partition", "input_partition" + , "do_lookup_output_names", "output_labels", "arguments", "argument_dots" + , "all_arguments", "var_list", "math", "expr_list" + ) + return_object(self, "Derivation") +} + +#' @export +Derivations = function(model) { + self = Base() + self$model = model + self$derivation_grouping = DerivationGrouping(model) + self$derivations = lapply(1:self$derivation_grouping$n_derivations(), Derivation, self$model) + self$.expr_list_by_phase = function(phase) { + l = list() + for (i in 1:self$derivation_grouping$n_derivations()) { + if (self$derivations[[i]]$get()$simulation_phase == phase) { + l = append(l, self$derivations[[i]]$expr_list()) + } + } + l + } + self$before = function() self$.expr_list_by_phase("before") + self$during_pre_update = function() self$.expr_list_by_phase("during_pre_update") + self$during_update = function() self$.expr_list_by_phase("during_update") + self$during_post_update = function() self$.expr_list_by_phase("during_post_update") + self$during = function() c(self$during_pre_update(), self$during_update(), self$during_post_update()) + self$after = function() self$.expr_list_by_phase("after") + return_object(self, "Derivations") +} diff --git a/R/enum_methods.R b/R/enum_methods.R index cd36781e..0cbd3497 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -21,15 +21,15 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { self$int_vec_arg_nms = int_vec_arg_nms self$mat_args = function(other_formula) { stopifnot(self$consistent(other_formula)) - that = self$.concat_parse_table(other_formula) - this = self$.concat_parse_table(self$formula) + that = concat_parse_table(other_formula) + this = concat_parse_table(self$formula) which_mats = this$x %in% self$mat_arg_nms that$x[which_mats] } self$int_vec_args = function(other_formula) { stopifnot(self$consistent(other_formula)) - that = self$.concat_parse_table(other_formula) - this = self$.concat_parse_table(self$formula) + that = concat_parse_table(other_formula) + this = concat_parse_table(self$formula) which_int_vecs = this$x %in% self$int_vec_arg_nms that$x[which_int_vecs] } diff --git a/R/files.R b/R/files.R index db6c602e..c25c2ee7 100644 --- a/R/files.R +++ b/R/files.R @@ -109,6 +109,9 @@ Files = function(directory, ..., .cache = CacheList()) { modification_time = file.mtime(self$.readers[[component_name]]$file) if (is.na(modification_time)) { ff = self$.file_path(component_name) + if (nchar(ff) == 0L) { + stop("Cannot find a file for required component, ", component_name) + } if (!file.exists(ff)) { stop( "\nThe file, ", ff, " is not where it was.", diff --git a/R/files_to_models.R b/R/files_to_models.R index 2fc472e4..1c2bda61 100644 --- a/R/files_to_models.R +++ b/R/files_to_models.R @@ -1,3 +1,103 @@ +# init_mats = MatsList( +# state = c(S = 1 - 1e-5, E = 1e-5, I = 0, R = 0) +# , flow = c(total_foi = NA_real_, progression = 0.1, recovery = 0.1) +# , per_capita_transmission = 0.2 +# , per_capita = empty_matrix +# , per_capita_inflow = empty_matrix +# , per_capita_outflow = empty_matrix +# , absolute = empty_matrix +# , absolute_inflow = empty_matrix +# , absolute_outflow = empty_matrix +# , total_inflow = empty_matrix +# , total_outflow = empty_matrix +# , . = empty_matrix +# , .mats_to_save = c("state", "absolute") +# , .mats_to_return = c("state", "absolute") +# ), +# expr_list = ExprList( +# during = list( +# . ~ set_infection_flow +# , per_capita ~ get_per_capita +# , per_capita_inflow ~ get_per_capita_inflow +# , per_capita_outflow ~ get_per_capita_outflow +# , absolute ~ get_absolute +# , absolute_inflow ~ get_absolute_inflow +# , absolute_outflow ~ get_absolute_outflow +# , total_inflow ~ get_per_capita_state_in + get_per_capita_state_in + get_per_capita_inflow_state_in + get_absolute_inflow_state_in +# , total_outflow ~ get_per_capita_state_out + get_absolute_state_out + get_per_capita_outflow_state_out + get_absolute_outflow_state_out +# , state ~ state + total_inflow - total_outflow +# ) +# ), +# engine_methods = EngineMethods( +# exprs = list( +# set_infection_flow = flow[infection] ~ per_capita_transmission %*% state[infectious] +# , get_per_capita = ~ state[per_capita_from] * flow[per_capita_flow] +# , get_absolute = ~ flow[absolute_flow] +# , get_per_capita_inflow = ~ state[per_capita_inflow_from] * flow[per_capita_inflow_flow] +# , get_per_capita_outflow = ~ state[per_capita_outflow_from] * flow[per_capita_outflow_flow] +# , get_absolute_inflow = ~ flow[absolute_inflow_flow] +# , get_absolute_outflow = ~ flow[absolute_outflow_flow] +# , get_per_capita_state_in = ~ groupSums(per_capita, per_capita_to, state_length) +# , get_per_capita_state_in = ~ groupSums(absolute, absolute_to, state_length) +# , get_per_capita_inflow_state_in = ~ groupSums(per_capita_inflow, per_capita_inflow_to, state_length) +# , get_absolute_inflow_state_in = ~ groupSums(absolute_inflow, absolute_inflow_to, state_length) +# , get_per_capita_state_out = ~ groupSums(per_capita, per_capita_from, state_length) +# , get_absolute_state_out = ~ groupSums(absolute, absolute_from, state_length) +# , get_per_capita_outflow_state_out = ~ groupSums(per_capita_outflow, per_capita_outflow_from, state_length) +# , get_absolute_outflow_state_out = ~ groupSums(absolute_outflow, absolute_outflow_from, state_length) +# ), +# int_vecs = IntVecs( +# state_length = length(model$labels$state()) +# +# , per_capita_from = indices$flow$per_capita$from() +# , per_capita_to = indices$flow$per_capita$to() +# , per_capita_flow = indices$flow$per_capita$flow() +# +# , absolute_from = indices$flow$absolute$from() +# , absolute_to = indices$flow$absolute$to() +# , absolute_flow = indices$flow$absolute$flow() +# +# , per_capita_inflow_from = indices$flow$per_capita_inflow$from() +# , per_capita_inflow_to = indices$flow$per_capita_inflow$to() +# , per_capita_inflow_flow = indices$flow$per_capita_inflow$flow() +# +# , per_capita_outflow_from = indices$flow$per_capita_outflow$from() +# , per_capita_outflow_flow = indices$flow$per_capita_outflow$flow() +# +# , absolute_inflow_to = indices$flow$absolute_inflow$to() +# , absolute_inflow_flow = indices$flow$absolute_inflow$flow() +# +# , absolute_outflow_from = indices$flow$absolute_outflow$from() +# , absolute_outflow_flow = indices$flow$absolute_outflow$flow() +# +# +# ) +# ), +# time_steps = Time(100L) +# ) +# + +TransmissionExprs = function(model) { + self = Base() + self$model = model + self$expr_list = function() list(. ~ set_infection_flow) + self$engine_method_exprs = function() { + list( + set_infection_flow = flow[infection] ~ beta_matrix %*% state[infectious] + ) + } + self$engine_method_int_vecs = function() { + list( + infection = model$indices$transmission$infection_flow() + , infectious = model$indices$transmission$infectious_state() + + ) + } + return_object(self, "TransmissionExprs") +} + + + DerivationUtils = function(model) { self = Base() @@ -172,7 +272,7 @@ DerivationExtractor = function(model){ # TODO: change this name to `extract` self$extract_derivations = function(){ - derivation_list = self$model$derivations() + derivation_list = self$model$def$derivations() return(lapply(derivation_list, self$extract_derivation)) } @@ -199,22 +299,22 @@ Scalar2Vector = function(derivation_extractor){ self$model = derivation_extractor$model self$extracted_derivations = derivation_extractor$extract_derivations() self$.state_pointer = function(scalar_name){ - return(as.numeric(which(scalar_name == self$model$def$settings()[["state_variables"]])) - 1) + return(as.numeric(which(scalar_name == self$model$labels$state())) - 1) } self$.state_replacer = function(scalar_name){ return(paste0("state[", paste0(self$.state_pointer(scalar_name), "]"))) } self$.flow_pointer = function(scalar_name){ - return(as.numeric(which(scalar_name == self$model$def$settings()[["flow_variables"]])) - 1) + return(as.numeric(which(scalar_name == self$model$labels$flow())) - 1) } self$.flow_replacer = function(scalar_name){ return(paste0("flow[", paste0(self$.flow_pointer(scalar_name), "]"))) } self$.replacer = function(scalar_name){ - if (any(scalar_name == self$model$def$settings()[["state_variables"]])) { + if (any(scalar_name == self$model$labels$state())) { return(self$.state_replacer(scalar_name)) } - else if (any(scalar_name == self$model$def$settings()[["flow_variables"]])) { + else if (any(scalar_name == self$model$labels$flow())) { return(self$.flow_replacer(scalar_name)) } else return(scalar_name) @@ -244,7 +344,7 @@ Scalar2Vector = function(derivation_extractor){ new_derivation$expression = paste0("assign(vect_name, vect_index, 0, ", paste0(extracted_derivation$expression, ")")) s = self$model$def$settings() for (i in 1:length(extracted_derivation$outputs)) { - if (any(extracted_derivation$outputs[[i]] == s[["state_variables"]])) { + if (any(extracted_derivation$outputs[[i]] == self$model$labels$state())) { new_derivation$outputs = c(new_derivation$outputs, "dummy") if (length(extracted_derivation$variables) != 0L) { new_derivation$variables = c( @@ -259,7 +359,7 @@ Scalar2Vector = function(derivation_extractor){ } if (length(extracted_derivation$variable_dots) != 0) new_derivation$variable_dots = c(new_derivation$variable_dots, list(extracted_derivation$variable_dots[[i]])) } - else if (any(extracted_derivation$outputs[[i]] == s[["flow_variables"]])) { + else if (any(extracted_derivation$outputs[[i]] == self$model$labels$flow())) { new_derivation$outputs = c(new_derivation$outputs, "dummy") if (length(extracted_derivation$variables) != 0L) new_derivation$variables = c(new_derivation$variables, list(c("flow", self$.flow_pointer(extracted_derivation$outputs[[i]]), extracted_derivation$variables[[i]]))) if (length(extracted_derivation$variable_dots) != 0) new_derivation$variable_dots = c(new_derivation$variable_dots, list(extracted_derivation$variable_dots[[i]])) diff --git a/R/flow_expander.R b/R/flow_expander.R index db2eaf82..16a95d7a 100644 --- a/R/flow_expander.R +++ b/R/flow_expander.R @@ -8,8 +8,13 @@ FlowExpander = function(model) { self = Base() model = model$freeze() + ## at a minimum, only from, to, flow, and type are required columns + ## in flows.csv files. this function adds the filtering and matching + ## columns self$.add_missing_columns = function(flows) { cols_on_file = names(flows) + + ## check that required columns are present required_cols = c("from", "to", "flow", "type") if (!all(required_cols %in% cols_on_file)) { stop( @@ -18,15 +23,31 @@ FlowExpander = function(model) { required_cols ) } + + ## if filtering columns are not present, create them and set them + ## equal to the required partition that is found in the settings file rp = StringUndottedVector(self$s$required_partitions)$dot()$value() - filtering_cols = c("from_partition", "to_partition", "flow_partition") + filtering_cols = c( + "from_partition", + "to_partition", + "flow_partition" + ) for (col in filtering_cols) { if (!col %in% cols_on_file) { flows[[col]] = rp } } + + ## if the matching columns are not present, create them and set them + ## equal to either blank or the null partition that is found in the + ## settings file. a blank value means 'match with everything' but a + ## null value means 'match with nothing' mp = c("", "", self$s$null_partition) - matching_cols = c("from_to_partition", "from_flow_partition", "to_flow_partition") + matching_cols = c( + "from_to_partition", + "from_flow_partition", + "to_flow_partition" + ) for (col in matching_cols) { if (!col %in% cols_on_file) { flows[[col]] = mp[col == matching_cols] @@ -43,11 +64,17 @@ FlowExpander = function(model) { self$.filter = FilterBlanksNotSpecial() self$.flow_variables = self$s$flow_variables + ## + ## @param flow_number -- integer giving the row number of flow.csv + ## @param component_type -- string of one of the three types of filters + ## ("from", "to", "flow") self$filter_flow = function(flow_number, component_type) { filter_partitions = paste(component_type, "partition", sep = "_") partition_label = self$f[flow_number, component_type] partition_set_name = self$f[flow_number, filter_partitions] - partition_set = self$.filter$filter(self$v, partition_set_name, partition_label) + #partition_set = self$.filter$filter(self$v, partition_set_name, partition_label) + variables = Partition(self$v) + partition_set = variables$filter(partition_label, .wrt = partition_set_name)$frame() component_filter = switch(component_type , from = self$s$state_variables , to = self$s$state_variables diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 910f4acb..81b762ba 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -148,6 +148,18 @@ empty_frame = function(...) { setNames(as.data.frame(matrix(character(), 0L, ncol)), colnames) } +enforce_schema = function(frame, ...) { + anchor = as.list(macpan2:::empty_frame(...)) + for (c in names(anchor)) { + if (c %in% names(frame)) { + anchor[[c]] = frame[[c]] + } else { + anchor[[c]] = rep("", nrow(frame)) + } + } + as.data.frame(anchor) +} + #' Union of Variables #' #' Take the union of a set of variable lists, each of which is represented @@ -224,74 +236,3 @@ NumericPartition = function(frame, numeric_vector) { } return_object(self, "NumericPartition") } - - - -if (FALSE) { - model_dirs = list.files(system.file("starter_models", package = "macpan2"), full.names = TRUE) - models = setNames(lapply(model_dirs, ModelFiles), basename(model_dirs)) - pp = Partition(models$seir_symp_vax$variables$all()) - qq = pp$filter("S", "E", "I", "R", .wrt = "Epi")$filter("unvax", .wrt = "Vax") - Partition(pp$select("Epi", "Vax")$dotted()) - pp$frame() - #pp$filter(qq$select("Epi", "Vax"), "foi.unvax" , .wrt = "Epi.Vax") - qq = pp$select("Epi")$filter("S", .wrt = "Epi") - pp$filter(qq) - pp$filter("S", "I", .wrt = "Epi")$filter("unstructured", "component", .wrt = "SympStruc") - pp$filter("I.component", .wrt = "Epi.SympStruc") - pp$name() - pp$names() - pp$labels() - pp$frame() - pp$dotted() - - pp$filter("S", .wrt = "Epi", .comparison_function = not_all_equal) - pp$filter_out("S", .wrt = "Epi") - seir = Partition(models$seir$variables$all()) - vax = Partition(models$vax$variables$all()) - - models$seir$settings()$required_partitions - models$seir$settings()$state_variables - - m = Model(models$seir_symp_vax) - m$variables$all() - m$variables$flow() - m$variables$state() - m$flows() - m$flows_expanded() - m$derivations() -} - -if (FALSE) { - make_expression = function(model, expr_id, grp_id) { - v = model$variables$all() - e = model$derivations()[[expr_id]] - if (!is.null(e$filter_partition)) { - v = v$filter(e$filter_names, .wrt = e$filter_partition) - } - if (!is.null(e$group_partition)) { - g = v$filter(e$group_names[grp_id], .wrt = e$group_partition) - o = v$filter(e$output_names[grp_id], .wrt = e$output_partition) - } else { - g = v - o = v$filter(e$output_names[1], .wrt = e$output_partition) - } - a = c(character(0L), e$arguments, e$argument_dots) - a - } -} - -if (FALSE) { - -i = 3 -j = 1 -ee = m$derivations()[[i]] -vv = m$variables$all() -gg = vv$filter(ee$group_names[j], .wrt = ee$group_partition) -oo = vv$filter(ee$output_names[j], .wrt = ee$output_partition) -##ii = gg$filter(ee$argument_dots, .wrt = ee$input_partition) -##ff = MathExpressionFromStrings(ee$expression, character(0L), include_dots = TRUE) -ii = gg$filter(ee$arguments, .wrt = ee$input_partition) -ff = MathExpressionFromStrings(ee$expression, ee$arguments) -do.call(ff$symbolic$evaluate, as.list(ii$labels())) -} diff --git a/R/macpan2.R b/R/macpan2.R index 535c962c..2eac4845 100644 --- a/R/macpan2.R +++ b/R/macpan2.R @@ -6,4 +6,5 @@ ##' package on compartmental epidemic modelling for ##' forecasting and analysis of infectious diseases. ##' +##' @keywords internal "_PACKAGE" diff --git a/R/model_data_structure.R b/R/model_data_structure.R index 2162887e..897faf21 100644 --- a/R/model_data_structure.R +++ b/R/model_data_structure.R @@ -17,13 +17,12 @@ Model = function(definition) { self$variables = Variables(self) self$labels = VariableLabels(self$variables) self$indices = VariableIndices(self$labels) + self$flows_info = Flows(self$def$flows(), self$variables) + self$trans_info = Trans(self$def$trans(), self$variables) # Standard Methods self$flows = function() self$def$flows() - self$flows_expanded = function() { - expander = FlowExpander(self$def) - expander$expand_flows() - } + self$flows_expanded = function() self$flows_info$frame() #self$expander$expand_flows() self$flows_explicit = function() { optional_fields = c("from_partition", "to_partition", "flow_partition", "from_to_partition", "from_flow_partition", "to_flow_partition") @@ -31,10 +30,17 @@ Model = function(definition) { required_partition = self$settings$name() null_partition = self$settings$null() - default_entries = data.frame(required_partition, required_partition, required_partition, "", "", null_partition) + default_entries = data.frame(required_partition + , required_partition + , required_partition + , "", "" + , null_partition + ) names(default_entries) = optional_fields - default_entries = do.call("rbind", replicate(nrow(self$flows()), default_entries, simplify = FALSE)) + default_entries = do.call("rbind" + , replicate(nrow(self$flows()), default_entries, simplify = FALSE) + ) is_missing = function(field_name){ return(!any(names(self$flows()) == field_name)) @@ -42,13 +48,22 @@ Model = function(definition) { missing_fields = which(lapply(optional_fields, is_missing) == TRUE) return(cbind(self$flows(), default_entries[,missing_fields])) } - self$derivations = self$def$derivations ## look like a field but actually method forwarding + self$trans = function() self$def$trans() + self$trans_expanded = function() self$trans_info$frame() + self$trans_explicit = function() stop("under construction") + #self$derivations = self$def$derivations ## look like a field but actually method forwarding + self$derivations = Derivations(self) self$expr_list = function() { - Derivations2ExprList(UserExpr(self), StandardExpr(self))$expr_list() + self$derivations_2_expr_list = Derivations2ExprList( + UserExpr(self) + , StandardExpr(self) + ) + self$derivations_2_expr_list$expr_list() } # Composition self$simulators = Simulators(self) + self$expander = FlowExpander(self$def) # Set the cache in the underlying ModelFiles object # so that when the model definition files change @@ -81,8 +96,8 @@ assert_variables = function(model) { v = model$variables$all() AllValid( ValidityMessager(make_pipeline("required_partitions", v$names()), "Required partitions in Settings.json are not names of columns in Variables.csv"), - ValidityMessager(make_pipeline("state_variables", v$labels()), "State variables in Settings.json are not dot-separated concatentations of the required partitions in the Variables.csv"), - ValidityMessager(make_pipeline("flow_variables", v$labels()), "Flow variables in Settings.json are not expanded labels for variables in Flows.csv"), + #ValidityMessager(make_pipeline("state_variables", v$labels()), "State variables in Settings.json are not dot-separated concatentations of the required partitions in the Variables.csv"), + #ValidityMessager(make_pipeline("flow_variables", v$labels()), "Flow variables in Settings.json are not expanded labels for variables in Flows.csv"), ValidityMessager(make_pipeline("infectious_state_variables", v$labels()), "blah"), ValidityMessager(make_pipeline("infected_state_variables", v$labels()), "blah"), ValidityMessager(make_pipeline("infection_flow_variables", v$labels()), "blah"), diff --git a/R/model_files.R b/R/model_files.R index a24b9d21..b75e2cac 100644 --- a/R/model_files.R +++ b/R/model_files.R @@ -72,6 +72,7 @@ ModelFiles = function(model_directory , reader_spec("derivations.json", json_reader) , reader_spec("flows.csv", csv_reader) , reader_spec("settings.json", json_reader) + , reader_spec("trans.csv", csv_reader, optional = TRUE) , reader_spec("transmission_matrices.csv", csv_reader, optional = TRUE) , reader_spec("transmission_dimensions.csv", csv_reader, optional = TRUE) ) @@ -83,6 +84,7 @@ ModelFiles = function(model_directory self$variables = function() self$get("variables") self$derivations = function() self$get("derivations") self$flows = function() self$get("flows") + self$trans = function() self$get("trans", optional = TRUE) self$settings = function() self$get("settings") self$transmission_matrices = function() { self$get("transmission_matrices", optional = TRUE) diff --git a/R/name_utils.R b/R/name_utils.R index 56ed27f9..2af1c041 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -111,6 +111,7 @@ frame_to_part = function(frame) { } y } +frame_to_part = memoise(frame_to_part) to_matrix_with_rownames = function(x, nms) { x = as.matrix(x) diff --git a/R/namer.R b/R/namer.R index 8dff351b..0c414b5a 100644 --- a/R/namer.R +++ b/R/namer.R @@ -3,16 +3,6 @@ NameAndLabelUtilities = function() { self$.csv = function(x) paste0(as.character(x), collapse = ",") self$.dot = function(x) paste0(as.character(x), collapse = ".") - self$.tager = MathExpressionFromFunc(function(label, partition) partition[label]) - self$.tag = function(labels, partitions) { - mapply( - self$.tager$symbolic$evaluate, - labels, - partitions, - USE.NAMES = FALSE, - SIMPLIFY = TRUE - ) - } self$.mat = function(x) { m = vapply(x, as.character, character(nrow(x))) if (is.null(dim(m))) m = matrix(m, nrow = 1L) diff --git a/R/names_and_labels.R b/R/names_and_labels.R index 63f3a063..4a7db276 100644 --- a/R/names_and_labels.R +++ b/R/names_and_labels.R @@ -1,3 +1,4 @@ +valid_char = ValidityMessager(is.character) valid_undotted_chars = function(x) { #browser() if (length(x) == 0L) return(is.character(x)) @@ -55,11 +56,17 @@ test_is = local({ ) }) +valid_labels_dv = ValidityMessager(test_is$DottedVector) +valid_names_ds = ValidityMessager(test_is$DottedScalar) +valid_labels_um = ValidityMessager(test_is$UndottedMatrix) +valid_names_uv = ValidityMessager(test_is$UndottedVector) + #' Comparison Functions #' #' @param x \code{\link{character}} object #' @param y \code{\link{character}} object #' +#' @importFrom memoise memoise #' @name comparison NULL @@ -82,6 +89,28 @@ not_all_equal = function(x, y) !all_equal(x, y) #' @export all_not_equal = function(x, y) isTRUE(all(x != y)) +## used in $filter() of StringUndottedMatrix +character_comparison = function(x, y, comparison_function) { + z = logical(nrow(x)) + for (i in seq_row(x)) { + for (j in seq_row(y)) { + z[i] = comparison_function(x[i, , drop = TRUE], y[j, , drop = TRUE]) + if (z[i]) break + } + } + z +} + +## these functions are bottlenecks that +## get repeatedly called for the same inputs. +## so we use memoisation to solve this performance issue +## https://en.wikipedia.org/wiki/memoization +character_comparison = memoise(character_comparison) +# all_equal = memoise(all_equal) +# all_consistent = memoise(all_consistent) +# not_all_equal = memoise(not_all_equal) +# all_not_equal = memoise(all_not_equal) + seq_row = function(x) seq_len(nrow(x)) seq_col = function(x) seq_len(ncol(x)) @@ -94,9 +123,8 @@ seq_col = function(x) seq_len(ncol(x)) ## UndottedScalar, DottedMatrix String = function(x) { - valid = ValidityMessager(is.character) self = Base() - self$.value = valid$assert(x) + self$.value = valid_char$assert(x) self$value = function() self$.value return_object(self, "String") } @@ -209,16 +237,10 @@ StringUndottedMatrix = function(...) { StringDottedVector(v) } self$which_in = function(other, comparison_function) { - x = self$value() - y = other$undot()$value() - z = logical(nrow(x)) - for (i in seq_row(x)) { - for (j in seq_row(y)) { - z[i] = comparison_function(x[i,,drop = TRUE], y[j,,drop = TRUE]) - if (z[i]) break - } - } - z + character_comparison(self$value() + , other$undot()$value() + , comparison_function + ) } self$which_not_in = function(other, comparison_function) { x = self$value() @@ -306,9 +328,8 @@ c.StringData = function(...) { } StringDottedData = function(labels, names) { - valid_labels = ValidityMessager(test_is$DottedVector) - valid_names = ValidityMessager(test_is$DottedScalar) - self = StringData(valid_labels$assert(labels), valid_names$assert(names)) + + self = StringData(valid_labels_dv$assert(labels), valid_names_ds$assert(names)) if (any(duplicated(self$names()$undot()$value()))) { stop("String data cannot have duplicated names.") } @@ -338,9 +359,7 @@ StringDottedData = function(labels, names) { } StringUndottedData = function(labels, names) { - valid_labels = ValidityMessager(test_is$UndottedMatrix) - valid_names = ValidityMessager(test_is$UndottedVector) - self = StringData(valid_labels$assert(labels), valid_names$assert(names)) + self = StringData(valid_labels_um$assert(labels), valid_names_uv$assert(names)) if (any(duplicated(self$names()$value()))) { stop("String data cannot have duplicated names.") } diff --git a/R/parse_expr.R b/R/parse_expr.R index a03d0a6b..5572eeda 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -76,6 +76,7 @@ make_expr_parser = function( valid_vars = environment(x)$valid_vars, valid_literals = environment(x)$valid_literals, valid_methods = environment(x)$valid_methods, + valid_int_vecs = environment(x)$valid_int_vecs, offset = offset, input_expr_as_string = as.character(x)[2L] ) @@ -153,8 +154,9 @@ finalizer_index = function(x) { valid_funcs = x$valid_funcs valid_vars = x$valid_vars valid_methods = x$valid_methods + valid_int_vecs = x$valid_int_vecs valid_literals = as.numeric(x$valid_literals) - x$valid_funcs = x$valid_vars = x$valid_literals = x$valid_methods = NULL + x$valid_funcs = x$valid_vars = x$valid_literals = x$valid_methods = x$valid_int_vecs = NULL # remove the tilde function, which is in the first position, # and adjust the indices in i accordingly @@ -172,8 +174,9 @@ finalizer_index = function(x) { is_literal = unlist(lapply(x$x, is.numeric)) is_func = x$n != 0L is_meth = x_char %in% names(valid_methods) + is_int_vec = x_char %in% names(valid_int_vecs) is_var = x_char %in% names(valid_vars) - is_not_found = !(is_literal | is_func | is_meth | is_var) + is_not_found = !(is_literal | is_func | is_meth | is_int_vec | is_var) if (any(is_not_found)) { missing_items = x_char[is_not_found] @@ -195,6 +198,9 @@ finalizer_index = function(x) { # identify methods with -2 in the 'number of arguments' x$n[is_meth] = -2L + # identify integer vectors with -2 in the 'number of arguments' + x$n[is_int_vec] = -3L + # convert character identifiers to integers x_int = integer(length(x$x)) if (any(is_func)) { @@ -228,11 +234,19 @@ finalizer_index = function(x) { , zero_based = TRUE ) } + if (any(is_int_vec)) { + x_int[is_int_vec] = get_indices(x_char[is_int_vec] + , vec = valid_int_vecs + , vec_type = "int_vecs" + , expr_as_string = x$input_expr_as_string + , zero_based = TRUE + ) + } x$x = as.integer(x_int) x$i = as.integer(x$i) nlist( parse_table = as.data.frame(x), - valid_funcs, valid_vars, valid_methods, valid_literals + valid_funcs, valid_vars, valid_methods, valid_int_vecs, valid_literals ) } @@ -256,7 +270,7 @@ get_indices = function(x, vec, vec_type, expr_as_string, zero_based = FALSE) { ) } else if (vec_type == "methods") { pointers = "\nHelp for ?engine_methods is under construction." - } else if (vec_type == "integer") { + } else if (vec_type == "int_vecs") { pointers = paste( "\nPlease ensure that engine methods refer to the right integer vectors", sep = "" diff --git a/R/settings.R b/R/settings.R index 5663514f..5f35a8e0 100644 --- a/R/settings.R +++ b/R/settings.R @@ -5,6 +5,7 @@ Settings = function(model) { self$name = function() to_name(self$.settings()$required_partition) self$names = function() to_names(self$.settings()$required_partition) self$null = function() self$.settings()$null_partition + self$var_partitions = function() self$.settings()$var_partitions self$variable = function(type) { type_nm = sprintf("%s_variables", type) var_nms = self$.settings()[[type_nm]] diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index 1fbd5853..205e30c6 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -63,11 +63,11 @@ TMBSimulatorInserter = function(simulator) { if (inherits(self$model$init_mats$.structure_labels, "NullLabels")) { args = list(...) } else { - mat_names = self$model$init_mats$.names() + mat_names = names(self$model$init_mats) component_list = list( state = self$model$init_mats$.structure_labels$state(), flow = self$model$init_mats$.structure_labels$flow() - )# |> setNames(c(.vec_by_states, .vec_by_flows)) + ) args = (list(...) |> lapply(to_special_vecs, component_list, mat_names, component_vec_by) |> lapply(to_assign) diff --git a/R/variables.R b/R/variables.R index cff408a8..b9b4855b 100644 --- a/R/variables.R +++ b/R/variables.R @@ -5,7 +5,14 @@ Variables = function(model) { self$all = function() Partition(self$model$def$variables()) self$.type = function(type) { labels_this_type = self$model$settings$variable(type) + var_part = self$model$settings$var_partitions() wrt = self$model$settings$name() + if (is.null(var_part)) { + vars = self$all()$filter_ordered(labels_this_type, .wrt = wrt) + } else { + vars = self$all()$filter(labels_this_type, .wrt = var_part) + } + vars # if (length(var_nms) == 0L) { # warning( # "\nThere are no ", @@ -15,7 +22,6 @@ Variables = function(model) { # ) # return(NULL) # } - self$all()$filter_ordered(labels_this_type, .wrt = wrt) } self$flow = function() self$.type("flow") self$state = function() self$.type("state") @@ -66,14 +72,15 @@ VariableLabels = function(variables) { if (is.null(v)) return(v) v$labels() } - self$all = function() self$variables$all()$labels() - self$flow = function() self$variables$flow()$labels() - self$state = function() self$variables$state()$labels() - self$infectious_state = function() self$variables$infectious_state()$labels() - self$infected_state = function() self$variables$infected_state()$labels() - self$infection_flow = function() self$variables$infection_flow()$labels() + self$rp = function() self$variables$model$settings$names() + self$all = function() self$variables$all()$select(self$rp())$labels() + self$flow = function() self$variables$flow()$select(self$rp())$labels() + self$state = function() self$variables$state()$select(self$rp())$labels() + self$infectious_state = function() self$variables$infectious_state()$select(self$rp())$labels() + self$infected_state = function() self$variables$infected_state()$select(self$rp())$labels() + self$infection_flow = function() self$variables$infection_flow()$select(self$rp())$labels() self$other = function() setdiff(self$all(), c(self$state(), self$flow())) - initialize_cache(self, "all", "flow", "state", "infectious_state", "infection_flow", "other") + initialize_cache(self, "all", "flow", "state", "infectious_state", "infection_flow", "other", "rp") return_object(self, "VariableLabels") } diff --git a/R/zzz.R b/R/zzz.R index 2fd2198f..4e3872d9 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,3 +1,6 @@ .onLoad <- function(lib, pkg) { - options(macpan2_dll = "macpan2") + options( + macpan2_dll = "macpan2" + #, macpan2_memoise = TRUE + ) } diff --git a/man/macpan2-package.Rd b/man/macpan2-package.Rd index 41aee126..5e05e1c3 100644 --- a/man/macpan2-package.Rd +++ b/man/macpan2-package.Rd @@ -36,3 +36,4 @@ Other contributors: } } +\keyword{internal} From 69d2ad9615e69df6a550a92ee29b39588830b403 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 13 Oct 2023 08:28:13 -0400 Subject: [PATCH 023/332] assign by integer vectors --- R/component_deps.R | 78 ++++++++ R/connection.R | 11 +- R/expr_list.R | 120 ++++++------ R/labelled_partitions.R | 6 +- R/name_utils.R | 2 +- R/objective_function.R | 28 ++- R/opt_params.R | 3 + R/parse_expr.R | 5 +- R/tmb_model.R | 52 ++---- R/validity.R | 20 +- R/variables.R | 1 + man/parse_expr_list.Rd | 6 +- misc/dev/dev.cpp | 400 ++++++++++++++++++++++++++++------------ src/macpan2.cpp | 399 +++++++++++++++++++++++++++------------ 14 files changed, 763 insertions(+), 368 deletions(-) create mode 100644 R/component_deps.R diff --git a/R/component_deps.R b/R/component_deps.R new file mode 100644 index 00000000..95bbc175 --- /dev/null +++ b/R/component_deps.R @@ -0,0 +1,78 @@ +Dependencies = function(container, ...) { + self = Base() + self$container = container + self$component_classes = list(...) + self$rev_deps = function() { + l = list() + for (component in names(self$component_classes)) { + l[[component]] = list(simple = character(0L), custom = character(0L)) + contents = ls(self$container[[component]]) + for (d in setdiff(names(self$component_classes), component)) { + custom = sprintf("refresh_%s", component) %in% ls(self$container[[d]]) + right_cls = inherits(self$container[[d]][[component]], self$component_classes[[component]]) + containment = component %in% ls(self$container[[d]]) + if (custom) { + l[[component]]$custom = append(l[[component]]$custom, d) + } else if (right_cls & containment) { + l[[component]]$simple = append(l[[component]]$simple, d) + } + } + } + l + } + self$deps = function() { + l = list() + for (component in ls(self$component_classes)) { + l[[component]] = character(0L) + object = self$container[[component]] + for (d in ls(self$component_classes)) { + containment = d %in% ls(object) + right_cls = inherits(object[[d]], self$component_classes[[d]]) + custom_meth = sprintf("refresh_%s", d) %in% ls(object) + if ((containment & right_cls) | custom_meth) { + l[[component]] = append(l[[component]], d) + } + } + } + l + } + return_object(self, "Dependencies") +} + +Refresher = function(dependencies) { + self = Base() + self$dependencies = dependencies + components = names(self$dependencies$component_classes) + for (component in components) ComponentRefresher(self, component) + return_object(self, "Refresher") +} + +ComponentRefresher = function(refresher, component) { + self = Base() + self$refresher = refresher + self$component = component + self$refresher[[self$component]] = function(x) { + custom_refresher_name = sprintf("refresh_%s", self$component) + deps = self$refresher$dependencies$deps()[[self$component]] + rev_deps = self$refresher$dependencies$rev_deps()[[self$component]] + container = self$refresher$dependencies$container + + ## update of the component + container[[self$component]] = x + + ## place the updated component inside all other components + ## that have a 'simple' dependency on the focal component + for (d in rev_deps$simple) container[[d]][[self$component]] = x + + ## place the updated component inside all other components + ## have a dependency on the focal component, such that this + ## dependency is induced by a custom refresh method that is + ## called `refresh_{focal_component}` + for (d in rev_deps$custom) container[[d]][[custom_refresher_name]](x) + + ## update all components that the focal component depends on + ## (sounds expensive but we are only passing by reference) + for (d in deps) container$refresh[[d]](container[[d]]) + } + return_object(self, "ComponentRefresher") +} diff --git a/R/connection.R b/R/connection.R index cd70193f..d65f4a2b 100644 --- a/R/connection.R +++ b/R/connection.R @@ -131,6 +131,7 @@ Connection = function(row, variables } unique(rbind(from_conn, to_conn)) } + initialize_cache(self, "frame", "from_to_merge", "from_conn_merge", "to_conn_merge") return_object(self, "Connection") } @@ -138,7 +139,7 @@ Connection = function(row, variables connection_merge = function(x, y, by, output_cols) { merge(x, y, by = by, sort = FALSE)[, output_cols] } -connection_merge = memoise(connection_merge) +#connection_merge = memoise(connection_merge) @@ -168,7 +169,7 @@ Flows = function(flows, variables) { #' @export Trans = function(trans, variables) { self = Base() - self$trans = process_trans_frame(trans) + self$trans = enforce_schema(trans, "state", "flow", "pop", "type") self$variables = variables self$connections = list() for (i in seq_row(self$trans)) { @@ -191,16 +192,12 @@ Trans = function(trans, variables) { return_object(self, "Trans") } -process_trans_frame = function(trans) { - if (is.null(trans)) trans = empty_frame("state", "flow", "pop", "type") - trans -} - labelled_frame = function(partition, label_name = "label") { f = partition$frame() f[[label_name]] = partition$labels() f } +labelled_frame = memoise(labelled_frame) if (FALSE) { library(macpan2) diff --git a/R/expr_list.R b/R/expr_list.R index 961b8879..8845f99d 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -6,15 +6,18 @@ ExprListUtils = function() { self$.init_valid_vars = function() { initial_valid_vars(names(self$init_mats)) } - self$.parsed_expr_list = function(... + self$.parsed_expr_list = function( + .get_formula_side = rhs , .existing_literals = numeric(0L) - , .offset = 0L ) { - parse_expr_list(self$.all_rhs(self$expr_list()) - , valid_vars = self$.init_valid_vars() - , valid_literals = .existing_literals - , offset = .offset - , valid_methods = self$engine_methods$meth_list + (self$formula_list() + |> lapply(.get_formula_side) + |> parse_expr_list( + valid_vars = self$.init_valid_vars() + , valid_literals = .existing_literals + , valid_methods = self$engine_methods$meth_list + , valid_int_vecs = self$engine_methods$int_vecs + ) ) } self$.set_name_prefix = function(x, prefix) { @@ -23,15 +26,49 @@ ExprListUtils = function() { self$.does_assign = function(x) { raw_lhs = self$.lhs(x) } - self$.lhs = function(x) { - as.character(x[[2L]]) + self$.eval_schedule = function() { + c(length(self$before), length(self$during), length(self$after)) + } + + self$.expr_sim_block = function() { + as.integer(self$expr_nms() %in% self$.simulate_exprs) + } + + self$.expr_output_id = function() { + all_names = names(self$init_mats) + output_names = valid$engine_outputs(all_names)$assert( + self$.all_lhs(self$formula_list()) + ) + m = match(output_names, all_names) + if (any(is.na(m))) { + stop( + "\nThe following updated variables are not " + ) + } + as.integer(m - 1L) + } + self$.p_table = function() { + l = self$.parsed_expr_list(rhs) + p_table = l$parse_table[c("x", "n", "i")] |> as.list() + self$.set_name_prefix(p_table, "p_table_") + } + self$.a_table = function() { + l = self$.parsed_expr_list(lhs, self$.expr_literals()) + a_table = l$parse_table[c("x", "n", "i")] |> as.list() + self$.set_name_prefix(a_table, "a_table_") + } + self$.expr_literals = function() { + self$.parsed_expr_list(rhs)$valid_literals + } + self$.literals = function() { + self$.parsed_expr_list(lhs, self$.expr_literals())$valid_literals + } + self$.expr_num_p_table_rows = function() { + self$.parsed_expr_list(rhs)$num_p_table_rows } - self$.rhs = function(x) { - if (length(x) == 3L) e = x[c(1L, 3L)] else e = x - e + self$.assign_num_a_table_rows = function() { + self$.parsed_expr_list(lhs, self$.expr_literals())$num_p_table_rows } - self$.all_lhs = function(x) vapply(x, self$.lhs, character(1L)) - self$.all_rhs = function(x) lapply(x, self$.rhs) return_object(self, "ExprListUtils") } @@ -95,14 +132,16 @@ ExprList = function( , .simulate_exprs = character(0L) ) { self = ExprListUtils() - lhs = function(x) x[[2L]] valid_expr_list = ValidityMessager( All( is.list, ## list of ... MappedAllTest(Is("formula")), ## ... formulas that are ... - TestPipeline(MappedSummarizer(length), MappedAllTest(TestRange(3L, 3L))), ## ... two-sided formula - TestPipeline(MappedSummarizer(lhs, is.symbol), MappedAllTest(TestTrue())) ## ... only one symbol on the lhs + TestPipeline(MappedSummarizer(length), MappedAllTest(TestRange(3L, 3L))) ## ... two-sided formula + #TestPipeline(MappedSummarizer(lhs, is.symbol), MappedAllTest(TestTrue())) ## ... only one symbol on the lhs ), + ## TODO: fix this error message now that expressions can have formulas + ## on the left-hand-side. should also make a new test that actually + ## checks the requirements. "Model expressions must be two-sided assignment formulas,", "without subsetting on the left-hand-side", "(i.e. x ~ 1 is fine, but x[0] ~ 1 is not)." @@ -114,58 +153,23 @@ ExprList = function( self$after = valid_expr_list$assert(after) self$.simulate_exprs = valid$char$assert(.simulate_exprs) - self$expr_list = function() unname(c(self$before, self$during, self$after)) + self$formula_list = function() unname(c(self$before, self$during, self$after)) self$expr_nms = function() { nms = names(c(self$before, self$during, self$after)) - if (is.null(nms)) nms = rep("", length(self$expr_list())) + if (is.null(nms)) nms = rep("", length(self$formula_list())) nms } - self$.eval_schedule = function() { - c(length(self$before), length(self$during), length(self$after)) - } - - self$.expr_sim_block = function() { - as.integer(self$expr_nms() %in% self$.simulate_exprs) - } - - self$.expr_output_id = function() { - all_names = names(self$init_mats) - output_names = valid$engine_outputs(all_names)$assert( - self$.all_lhs(self$expr_list()) - ) - m = match(output_names, all_names) - if (any(is.na(m))) { - stop( - "\nThe following updated variables are not " - ) - } - as.integer(m - 1L) - } - self$.expr_num_p_table_rows = function(...) { - self$.parsed_expr_list(...)$num_p_table_rows - } - - ## list of three equal length integer vectors - ## p_table_x, p_table_n, p_table_i - self$.parse_table = function(...) { - l = as.list(self$.parsed_expr_list(...)$parse_table[c("x", "n", "i")]) - self$.set_name_prefix(l, "p_table_") - } - self$.literals = function(...) { - self$.parsed_expr_list(...)$valid_literals - } - self$data_arg = function() { - expr_output_id = self$.expr_output_id() r = c( list( - expr_output_id = as.integer(expr_output_id), expr_sim_block = as.integer(self$.expr_sim_block()), expr_num_p_table_rows = as.integer(self$.expr_num_p_table_rows()), + assign_num_a_table_rows = as.integer(self$.assign_num_a_table_rows()), eval_schedule = as.integer(self$.eval_schedule()) ), - self$.parse_table() + self$.p_table(), + self$.a_table() ) valid$expr_arg$assert(r) } @@ -190,7 +194,7 @@ ExprList = function( ) for (i in 1:3) { if (self$.eval_schedule()[i] > 0L) { - expr_strings = lapply(self$expr_list()[from[i]:to[i]], deparse) + expr_strings = lapply(self$formula_list()[from[i]:to[i]], deparse) tab_size = nchar(self$.eval_schedule()[i]) fmt = sprintf("%%%ii: %%s", tab_size) tab = paste0(rep(" ", tab_size), collapse = "") diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 81b762ba..402c4a4e 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -70,6 +70,7 @@ Partition = function(frame) { , .filter_type = "filter" ) } + self$filter = memoise(self$filter) self$filter_out = function(..., .wrt, .comparison_function = not_all_equal) { self$.filter(... , .wrt = .wrt @@ -100,6 +101,7 @@ Partition = function(frame) { } return_object(self, "Partition") } +Partition = memoise(Partition) NullPartition = function(...) { self = Base() @@ -149,7 +151,9 @@ empty_frame = function(...) { } enforce_schema = function(frame, ...) { - anchor = as.list(macpan2:::empty_frame(...)) + anchor = macpan2:::empty_frame(...) + if (is.null(frame)) return(anchor) + anchor = as.list(anchor) for (c in names(anchor)) { if (c %in% names(frame)) { anchor[[c]] = frame[[c]] diff --git a/R/name_utils.R b/R/name_utils.R index 2af1c041..137477d7 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -111,7 +111,7 @@ frame_to_part = function(frame) { } y } -frame_to_part = memoise(frame_to_part) +#frame_to_part = memoise(frame_to_part) to_matrix_with_rownames = function(x, nms) { x = as.matrix(x) diff --git a/R/objective_function.R b/R/objective_function.R index 8ef4c360..a2a140db 100644 --- a/R/objective_function.R +++ b/R/objective_function.R @@ -39,28 +39,24 @@ ObjectiveFunction = function(obj_fn_expr) { self$obj_fn_expr = obj_fn_expr ## Standard Methods - self$expr_list = function() list(self$obj_fn_expr) - self$.literals = function(.existing_literals) { - self$.parsed_expr_list( - names(self$init_mats), - .existing_literals = .existing_literals - )$valid_literals + self$formula_list = function() list(self$obj_fn_expr) + self$.literals = function() { + self$.parsed_expr_list(rhs, self$expr_list$.literals())$valid_literals } - self$.parse_table = function(.existing_literals) { - l = as.list(self$.parsed_expr_list( - names(self$init_mats), - .existing_literals = .existing_literals - )$parse_table) - self$.set_name_prefix(l[c("x", "n", "i")], "o_table_") + self$.o_table = function() { + l = self$.parsed_expr_list(rhs, self$expr_list$.literals()) + o_table = l$parse_table[c("x", "n", "i")] |> as.list() + self$.set_name_prefix(o_table, "o_table_") } - self$data_arg = function(.existing_literals) { - p = self$.parse_table(.existing_literals = .existing_literals) - p$literals = self$.literals(.existing_literals = .existing_literals) - p + self$data_arg = function() { + l = self$.o_table() + l$literals = self$.literals() + l } ## Composition self$init_mats = MatsList() + self$expr_list = ExprList() self$engine_methods = EngineMethods() return_object(self, "ObjectiveFunction") diff --git a/R/opt_params.R b/R/opt_params.R index 77118e1e..c8fd3474 100644 --- a/R/opt_params.R +++ b/R/opt_params.R @@ -84,7 +84,10 @@ OptParamsList = function(... ) valid$opt_params_list_arg$assert(r) } + + ## Composition self$init_mats = MatsList() + return_object(self, "OptParamsList") } diff --git a/R/parse_expr.R b/R/parse_expr.R index 5572eeda..2fef9383 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -346,8 +346,6 @@ empty_matrix = matrix(numeric(0L), 0L, 0L) #' themselves will be discovered and added to this list. #' @param valid_methods \code{\link{MethList}} object. #' @param valid_int_vecs \code{\link{IntVecs}} object. -#' @param offset The zero-based row index for the first row of the table. -#' This is useful when combining tables. #' #' @export parse_expr_list = function(expr_list @@ -355,8 +353,8 @@ parse_expr_list = function(expr_list , valid_literals = numeric(0L) , valid_methods = MethList() , valid_int_vecs = IntVecs() - , offset = 0L ) { + offset = 0L eval_env = nlist( valid_funcs, valid_vars, valid_literals, valid_methods, valid_int_vecs, offset @@ -388,3 +386,4 @@ parse_expr_list = function(expr_list num_p_table_rows = vapply(p_tables, nrow, integer(1L)) ) } +parse_expr_list = memoise(parse_expr_list) diff --git a/R/tmb_model.R b/R/tmb_model.R index a7b18aba..27c63afc 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -101,7 +101,7 @@ TMBModel = function( self$expr_list$data_arg(), self$params$data_arg(), self$random$data_arg("r"), - self$obj_fn$data_arg(existing_literals), + self$obj_fn$data_arg(), self$time_steps$data_arg(), self$engine_methods$meth_list$data_arg(), self$engine_methods$int_vecs$data_arg(), @@ -145,39 +145,21 @@ TMBModel = function( self$replace = TMBReplacer(self) - ## Refreshments - self$refresh_init_mats = function(init_mats) { - self$init_mats = init_mats - self$expr_list$init_mats = init_mats - self$obj_fn$init_mats = init_mats - self$params$init_mats = init_mats - self$random$init_mats = init_mats - self$engine_methods$refresh_init_mats(init_mats) - } - self$refresh_expr_list = function(expr_list) { - self$expr_list = expr_list - self$refresh_init_mats(self$init_mats) - } - self$refresh_obj_fn = function(obj_fn) { - self$obj_fn = obj_fn - self$refresh_init_mats(self$init_mats) - } - self$refresh_params = function(params) { - self$params = params - self$refresh_init_mats(self$init_mats) - } - self$refresh_random = function(random) { - self$random = random - self$refresh_init_mats(self$init_mats) - } - self$refresh_engine_methods = function(engine_methods) { - self$engine_methods = engine_methods - self$expr_list$engine_methods = engine_methods - self$obj_fn$engine_methods = engine_methods - self$refresh_init_mats(self$init_mats) - } - self$refresh_init_mats(self$init_mats) - self$refresh_engine_methods(self$engine_methods) + ## Dependency management + self$dependencies = Dependencies(self + , init_mats = "MatsList" + , expr_list = "ExprList" + , obj_fn = "ObjectiveFunction" + , engine_methods = "EngineMethods" + , params = "OptParamsList" + , random = "OptParamsList" + , time_steps = "Time" + ) + self$refresh = Refresher(self$dependencies) + self$refresh$init_mats(self$init_mats) + self$refresh$expr_list(self$expr_list) + self$refresh$engine_methods(self$engine_methods) + self$refresh$obj_fn(self$obj_fn) return_object( valid$tmb_model$assert(self), @@ -238,7 +220,7 @@ TMBSimulationUtils = function() { self$.find_problematic_expression = function(row) { expr_num_p_table_rows = self$tmb_model$data_arg()$expr_num_p_table_rows expr_num = min(which(row < cumsum(expr_num_p_table_rows))) - deparse1(self$tmb_model$expr_list$expr_list()[[expr_num]]) + deparse1(self$tmb_model$expr_list$formula_list()[[expr_num]]) } self$.runner = function(... , .phases = c("before", "during", "after") diff --git a/R/validity.R b/R/validity.R index ba106872..46c133fd 100644 --- a/R/validity.R +++ b/R/validity.R @@ -377,21 +377,29 @@ TMBAdaptorValidity <- function() { # model component compound validity self$expr_arg = AllValid( - self$n_components(7L), + self$n_components(10L), self$component_names( - "expr_output_id", "expr_sim_block", "expr_num_p_table_rows", - "eval_schedule", "p_table_x", "p_table_n", "p_table_i" + "expr_sim_block", + "expr_num_p_table_rows", "assign_num_a_table_rows", + "eval_schedule", + "p_table_x", "p_table_n", "p_table_i", + "a_table_x", "a_table_n", "a_table_i" ), self$homo_length_components("expr_", "expressions"), - self$homo_length_components("p_", "the parse table"), + self$homo_length_components("p_", "the expression parse table"), + self$homo_length_components("a_", "the assignment parse table"), self$all_int, self$component_lengths("eval_schedule", 3L, 3L), self$component_ranges("p_table_x", 0L, Inf), - self$component_ranges("p_table_n", -2L, Inf), + self$component_ranges("p_table_n", -3L, Inf), self$component_ranges("p_table_i", -1L, Inf), - self$component_ranges("expr_output_id", 0L, Inf), + self$component_ranges("a_table_x", 0L, Inf), + self$component_ranges("a_table_n", -3L, Inf), + self$component_ranges("a_table_i", -1L, Inf), + #self$component_ranges("expr_output_id", 0L, Inf), self$component_ranges("expr_sim_block", 0L, 1L), self$component_ranges("expr_num_p_table_rows", 1L, Inf), + self$component_ranges("assign_num_a_table_rows", 1L, Inf), .msg = "expression information passed to c++ is not valid" ) diff --git a/R/variables.R b/R/variables.R index b9b4855b..74fda52a 100644 --- a/R/variables.R +++ b/R/variables.R @@ -104,6 +104,7 @@ IndexUtilities = function(labels) { function() { f = self$model$flows_expanded() flow_labels = f[[flow_component]][f$type == flow_type] + #if (length(flow_labels) == 0L) return(integer(0L)) self$match(flow_labels, self$labels[[vector_name]]()) } } diff --git a/man/parse_expr_list.Rd b/man/parse_expr_list.Rd index d00beb99..b0a3f6d6 100644 --- a/man/parse_expr_list.Rd +++ b/man/parse_expr_list.Rd @@ -9,8 +9,7 @@ parse_expr_list( valid_vars, valid_literals = numeric(0L), valid_methods = MethList(), - valid_int_vecs = IntVecs(), - offset = 0L + valid_int_vecs = IntVecs() ) } \arguments{ @@ -26,9 +25,6 @@ themselves will be discovered and added to this list.} \item{valid_methods}{\code{\link{MethList}} object.} \item{valid_int_vecs}{\code{\link{IntVecs}} object.} - -\item{offset}{The zero-based row index for the first row of the table. -This is useful when combining tables.} } \description{ Parse a list of one-sided formulas representing expressions diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index d634c33f..aef4bff5 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -12,6 +12,9 @@ // https://github.com/kaskr/adcomp/wiki/Development#distributing-code #include #include +#include +#include + //////////////////////////////////////////////////////////////////////////////// // Macpan2 is redesigned architecture. The spec is @@ -387,17 +390,6 @@ void setNthIntVec( if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { valid_int_vecs.setNthIntVec(vec_number, new_vector); } - //curr_meth_int_vecs = std::vector - //if (curr_meth_id >= 0 && curr_meth_id < curr_meth_int_vecs.size()) { - // curr_meth_int_vecs = meth_int_vecs[curr_meth_id]; - //} else { - // Rf_error("curr_meth_id is out of range."); - //} - //if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { - // valid_int_vecs.setNthIntVec(vec_number, new_vector); - //} else { - // Rf_error("vec_number is out of range."); - //} } @@ -426,6 +418,59 @@ matrix getNthMat( } +template +class ArgList { +public: + using ItemType = std::variant, std::vector>; + + ArgList(int size) : items_(size), size_(size) {} + + void set(int index, const ItemType& item) { + if (index < 0 || index >= size_) { + throw std::out_of_range("Index out of range"); + } + items_[index] = item; + } + + matrix get_as_mat(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + + if (std::holds_alternative>(items_[i])) { + return std::get>(items_[i]); + } else { + throw std::runtime_error("Item at index is not a matrix"); + } + } + + std::vector get_as_int_vec(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + + if (std::holds_alternative>(items_[i])) { + return std::get>(items_[i]); + } else { + matrix m = get_as_mat(i); + std::vector v(m.rows()); + for (int i=0; i operator[](int i) { + return get_as_mat(i); + } + +private: + std::vector items_; + int size_; +}; + + template class ExprEvaluator { private: @@ -480,21 +525,20 @@ class ExprEvaluator { strcpy(error_message, message); }; - // evaluators + // evaluator matrix EvalExpr( const vector >& hist, // current simulation history int t, // current time step ListOfMatrices& valid_vars, // current list of values of each matrix int row = 0 // current expression parse table row being evaluated - ) - { + ) { // Variables to use locally in 'macpan2 function' and // 'macpan2 method' bodies -- scare quotes to clarify that // these are not real functions and methods in either the // c++ or r sense. matrix m, m1, m2, m3, m4, m5; // return values - vector v, v1, v2, v3, v4, v5; // method integer vectors + std::vector v, v1, v2, v3, v4, v5; // method integer vectors vector u; matrix Y, X, A; matrix timeIndex; // for rbind_time @@ -668,12 +712,20 @@ class ExprEvaluator { case 0: // matrices m = valid_vars.m_matrices[table_x[row]]; return m; - default: // expressions + default: // functions int n = table_n[row]; - vector > args(n); + ArgList args(n); vector index2mats(n); for (int i=0; i::Zero(rows, 1); - for (int i = 0; i < args[0].rows(); i++) { - rowIndex = CppAD::Integer(args[1].coeff(i,0)+0.1f); - m.coeffRef(rowIndex,0) += args[0].coeff(i,0); + + m = args[0]; + v1 = args.get_as_int_vec(1); + rows = args.get_as_int_vec(2)[0]; + m1 = matrix::Zero(rows, 1); + + for (int i = 0; i < m.rows(); i++) { + m1.coeffRef(v1[i], 0) += m.coeff(i, 0); } - return m; + return m1; + + // rows = CppAD::Integer(args[2].coeff(0,0)+0.1f); + // m = matrix::Zero(rows, 1); + // for (int i = 0; i < args[0].rows(); i++) { + // rowIndex = CppAD::Integer(args[1].coeff(i,0)+0.1f); + // m.coeffRef(rowIndex,0) += args[0].coeff(i,0); + // } + // return m; // #' ### Examples // #' // #' ``` @@ -1468,41 +1531,53 @@ class ExprEvaluator { std::cout << "square bracket" << std::endl << std::endl; #endif - - int nrow; int ncol; - nrow = args[1].size(); - - if(n==2){ - m1 = matrix::Zero(1,1); - ncol=1; - } - else{ - ncol = args[2].size(); - m1 = args[2]; - } - m = matrix::Zero(nrow,ncol); - err_code = CheckIndices(args[0], args[1], m1); - if (err_code) { - SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); - return m; + m = args[0]; + v1 = args.get_as_int_vec(1); + nrow = v1.size(); + if (n == 2) { + v2.push_back(0); + ncol = 1; + } else { + v2 = args.get_as_int_vec(2); + ncol = v2.size(); } - - // if we can assume contiguous sets of rows and columns - // then mat.block(...) will be faster, so should we - // have a block function on the R side when speed - // matters? - // Can we vectorize CppAD::Integer casting?? + m1 = matrix::Zero(nrow,ncol); for (int i=0; i::Zero(1,1); + // ncol=1; + // } + // else{ + // ncol = args[2].size(); + // m1 = args[2]; + // } + // m = matrix::Zero(nrow,ncol); + + // err_code = CheckIndices(args[0], args[1], m1); + // if (err_code) { + // SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); + // return m; + // } + + // for (int i=0; i::Zero(rows, cols); for (int i=0; ispi[0]] @@ -1961,13 +2036,13 @@ class ExprEvaluator { } rows = args[0].rows(); cols = args[0].cols(); - err_code1 = RecycleInPlace(args[1], rows, cols); - err_code2 = RecycleInPlace(args[2], rows, cols); - err_code = err_code1 + err_code2; - if (err_code != 0) { - SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); - return m; - } + // err_code1 = RecycleInPlace(args[1], rows, cols); + // err_code2 = RecycleInPlace(args[2], rows, cols); + // err_code = err_code1 + err_code2; + // if (err_code != 0) { + // SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + // return m; + // } m = matrix::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i @@ -2285,6 +2359,82 @@ class ExprEvaluator { char error_message[256]; }; +template +class MatAssigner { +private: + vector table_x; + vector table_n; + vector table_i; + ListOfIntVecs valid_int_vecs; + vector valid_literals; +public: + MatAssigner ( + vector& table_x_, + vector& table_n_, + vector& table_i_, + ListOfIntVecs& valid_int_vecs_, + vector& valid_literals_ + ) { + table_x = table_x_; + table_n = table_n_; + table_i = table_i_; + valid_int_vecs = valid_int_vecs_; + valid_literals = valid_literals_; + }; + + void matAssign ( + matrix assignment_value, + ListOfMatrices& valid_vars, // current list of values of each matrix + int row = 0 // current expression parse table row being evaluated + ) { + int n = table_n[row]; + int x = table_x[row]; + matrix m; + std::vector v1; + std::vector v2; + std::cout << "---- assignment ----" << std::endl; + std::cout << "n: " << n << std::endl; + std::cout << "x: " << n << std::endl; + if (n == -1) { + Rf_error("trying to assign to a literal, which is not allowed"); + } else if (n == -2) { + Rf_error("trying to assign to an engine method, which is not allowed"); + } else if (n == -3) { + Rf_error("trying to assign to an integer vector, which is not allowed"); + } else if (n == 1) { + Rf_error("assignment error -- TODO: be more specific"); + } else if (n == 0) { + valid_vars.m_matrices[x] = assignment_value; + } else { + if (x + 1 != MP2_SQUARE_BRACKET) { + Rf_error("square bracket is the only function allowed on the left-hand-side"); + } + if (n == 3) { + // table_n[table_i[row] + 2] -- need to see what kind of thing we have + v2 = valid_int_vecs[table_x[table_i[row] + 2]]; + } else if (n == 2) { + v2.push_back(0); + } else { + Rf_error("incorrect numbers of arguments"); + } + int x1 = table_x[table_i[row]]; + m = valid_vars.m_matrices[x1]; + std::cout << "matrix: " << m << std::endl; + v1 = valid_int_vecs[table_x[table_i[row] + 1]]; + printIntVector(v1); + printIntVector(v2); + std::cout << "value: " << assignment_value << std::endl; + for (int i = 0; i < v1.size(); i++) { + for (int j = 0; j < v2.size(); j++) { + m.coeffRef(v1[i], v2[j]) = assignment_value.coeff(i, j); + } + } + valid_vars.m_matrices[x1] = m; + //valid_vars.m_matrices[matIndex].coeffRef(rowIndex,colIndex) = args[3].coeff(k,0); + } + }; +}; + #define REPORT_ERROR { \ int error = exprEvaluator.GetErrorCode(); \ int expr_row = exprEvaluator.GetExprRow(); \ @@ -2387,7 +2537,11 @@ Type objective_function::operator() () DATA_INTEGER(time_steps) // Expressions and parse table - DATA_IVECTOR(expr_output_id); + //DATA_IVECTOR(expr_output_id); + DATA_IVECTOR(a_table_x); + DATA_IVECTOR(a_table_n); + DATA_IVECTOR(a_table_i); + DATA_IVECTOR(assign_num_a_table_rows); DATA_IVECTOR(expr_sim_block); DATA_IVECTOR(expr_num_p_table_rows); DATA_IVECTOR(eval_schedule) @@ -2400,7 +2554,6 @@ Type objective_function::operator() () // Methods DATA_IVECTOR(meth_type_id); - // DATA_IVECTOR(meth_id); DATA_IVECTOR(meth_n_mats); DATA_IVECTOR(meth_n_int_vecs); DATA_IVECTOR(meth_mat_id); @@ -2418,8 +2571,6 @@ Type objective_function::operator() () // Flags DATA_INTEGER(values_adreport); - // DATA_STRUCT(settings, settings_struct); - #ifdef MP_VERBOSE std::cout << "params = " << params << std::endl; @@ -2446,8 +2597,7 @@ Type objective_function::operator() () std::cout << "eval_schedule = " << eval_schedule << std::endl; - //std::cout << "expr_output_count = " << expr_output_count << std::endl; - std::cout << "expr_output_id = " << expr_output_id << std::endl; + // std::cout << "expr_output_id = " << expr_output_id << std::endl; std::cout << "expr_sim_block = " << expr_sim_block << std::endl; std::cout << "expr_num_p_table_rows = " << expr_num_p_table_rows << std::endl; @@ -2486,8 +2636,6 @@ Type objective_function::operator() () ListOfIntVecs meth_mats(meth_mat_id, meth_n_mats); ListOfIntVecs meth_int_vecs(meth_int_vec_id, meth_n_int_vecs); - ////////////////////////////////// - // Define an expression evaluator ExprEvaluator exprEvaluator( mats_save_hist, p_table_x, @@ -2510,11 +2658,18 @@ Type objective_function::operator() () const_int_vecs, literals ); - ////////////////////////////////// + MatAssigner matAssigner( + a_table_x, + a_table_n, + a_table_i, + const_int_vecs, + literals + ); // 3 Pre-simulation int expr_index = 0; int p_table_row = 0; + int a_table_row = 0; for (int i=0; i::operator() () return 0.0; } - mats.m_matrices[expr_output_id[expr_index+i]] = result; + //mats.m_matrices[expr_output_id[expr_index+i]] = result; + matAssigner.matAssign(result, mats, a_table_row); p_table_row += expr_num_p_table_rows[i]; + a_table_row += assign_num_a_table_rows[i]; } //simulation_history[0] = mats; @@ -2557,8 +2714,10 @@ Type objective_function::operator() () expr_index += eval_schedule[0]; int p_table_row2; + int a_table_row2; for (int k=0; k::operator() () REPORT_ERROR return 0.0; } - mats.m_matrices[expr_output_id[expr_index+i]] = result; + // mats.m_matrices[expr_output_id[expr_index+i]] = result; + matAssigner.matAssign(result, mats, a_table_row2); p_table_row2 += expr_num_p_table_rows[expr_index+i]; + a_table_row2 += assign_num_a_table_rows[expr_index+i]; #ifdef MP_VERBOSE int n = mats.m_matrices.size(); @@ -2604,6 +2765,7 @@ Type objective_function::operator() () ); } p_table_row = p_table_row2; + a_table_row = a_table_row2; // 5 Post-simulation expr_index += eval_schedule[1]; @@ -2631,9 +2793,11 @@ Type objective_function::operator() () return 0.0; } - mats.m_matrices[expr_output_id[expr_index+i]] = result; + //mats.m_matrices[expr_output_id[expr_index+i]] = result; + matAssigner.matAssign(result, mats, a_table_row); p_table_row += expr_num_p_table_rows[expr_index+i]; + a_table_row += assign_num_a_table_rows[expr_index+i]; } //simulation_history[time_steps+1] = mats; diff --git a/src/macpan2.cpp b/src/macpan2.cpp index caf6bb2d..4871def2 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -13,6 +13,9 @@ // https://github.com/kaskr/adcomp/wiki/Development#distributing-code #include #include +#include +#include + //////////////////////////////////////////////////////////////////////////////// // Macpan2 is redesigned architecture. The spec is @@ -388,17 +391,6 @@ void setNthIntVec( if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { valid_int_vecs.setNthIntVec(vec_number, new_vector); } - //curr_meth_int_vecs = std::vector - //if (curr_meth_id >= 0 && curr_meth_id < curr_meth_int_vecs.size()) { - // curr_meth_int_vecs = meth_int_vecs[curr_meth_id]; - //} else { - // Rf_error("curr_meth_id is out of range."); - //} - //if (vec_number >= 0 && vec_number < valid_int_vecs.size()) { - // valid_int_vecs.setNthIntVec(vec_number, new_vector); - //} else { - // Rf_error("vec_number is out of range."); - //} } @@ -427,6 +419,59 @@ matrix getNthMat( } +template +class ArgList { +public: + using ItemType = std::variant, std::vector>; + + ArgList(int size) : items_(size), size_(size) {} + + void set(int index, const ItemType& item) { + if (index < 0 || index >= size_) { + throw std::out_of_range("Index out of range"); + } + items_[index] = item; + } + + matrix get_as_mat(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + + if (std::holds_alternative>(items_[i])) { + return std::get>(items_[i]); + } else { + throw std::runtime_error("Item at index is not a matrix"); + } + } + + std::vector get_as_int_vec(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + + if (std::holds_alternative>(items_[i])) { + return std::get>(items_[i]); + } else { + matrix m = get_as_mat(i); + std::vector v(m.rows()); + for (int i=0; i operator[](int i) { + return get_as_mat(i); + } + +private: + std::vector items_; + int size_; +}; + + template class ExprEvaluator { private: @@ -481,21 +526,20 @@ class ExprEvaluator { strcpy(error_message, message); }; - // evaluators + // evaluator matrix EvalExpr( const vector >& hist, // current simulation history int t, // current time step ListOfMatrices& valid_vars, // current list of values of each matrix int row = 0 // current expression parse table row being evaluated - ) - { + ) { // Variables to use locally in 'macpan2 function' and // 'macpan2 method' bodies -- scare quotes to clarify that // these are not real functions and methods in either the // c++ or r sense. matrix m, m1, m2, m3, m4, m5; // return values - vector v, v1, v2, v3, v4, v5; // method integer vectors + std::vector v, v1, v2, v3, v4, v5; // method integer vectors vector u; matrix Y, X, A; matrix timeIndex; // for rbind_time @@ -669,12 +713,20 @@ class ExprEvaluator { case 0: // matrices m = valid_vars.m_matrices[table_x[row]]; return m; - default: // expressions + default: // functions int n = table_n[row]; - vector > args(n); + ArgList args(n); vector index2mats(n); for (int i=0; i::Zero(rows, 1); - for (int i = 0; i < args[0].rows(); i++) { - rowIndex = CppAD::Integer(args[1].coeff(i,0)+0.1f); - m.coeffRef(rowIndex,0) += args[0].coeff(i,0); + + m = args[0]; + v1 = args.get_as_int_vec(1); + rows = args.get_as_int_vec(2)[0]; + m1 = matrix::Zero(rows, 1); + + for (int i = 0; i < m.rows(); i++) { + m1.coeffRef(v1[i], 0) += m.coeff(i, 0); } - return m; + return m1; + + // rows = CppAD::Integer(args[2].coeff(0,0)+0.1f); + // m = matrix::Zero(rows, 1); + // for (int i = 0; i < args[0].rows(); i++) { + // rowIndex = CppAD::Integer(args[1].coeff(i,0)+0.1f); + // m.coeffRef(rowIndex,0) += args[0].coeff(i,0); + // } + // return m; // #' ### Examples // #' // #' ``` @@ -1469,41 +1532,53 @@ class ExprEvaluator { std::cout << "square bracket" << std::endl << std::endl; #endif - - int nrow; int ncol; - nrow = args[1].size(); - - if(n==2){ - m1 = matrix::Zero(1,1); - ncol=1; - } - else{ - ncol = args[2].size(); - m1 = args[2]; - } - m = matrix::Zero(nrow,ncol); - err_code = CheckIndices(args[0], args[1], m1); - if (err_code) { - SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); - return m; + m = args[0]; + v1 = args.get_as_int_vec(1); + nrow = v1.size(); + if (n == 2) { + v2.push_back(0); + ncol = 1; + } else { + v2 = args.get_as_int_vec(2); + ncol = v2.size(); } - - // if we can assume contiguous sets of rows and columns - // then mat.block(...) will be faster, so should we - // have a block function on the R side when speed - // matters? - // Can we vectorize CppAD::Integer casting?? + m1 = matrix::Zero(nrow,ncol); for (int i=0; i::Zero(1,1); + // ncol=1; + // } + // else{ + // ncol = args[2].size(); + // m1 = args[2]; + // } + // m = matrix::Zero(nrow,ncol); + + // err_code = CheckIndices(args[0], args[1], m1); + // if (err_code) { + // SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); + // return m; + // } + + // for (int i=0; i::Zero(rows, cols); for (int i=0; ispi[0]] @@ -1962,13 +2037,13 @@ class ExprEvaluator { } rows = args[0].rows(); cols = args[0].cols(); - err_code1 = RecycleInPlace(args[1], rows, cols); - err_code2 = RecycleInPlace(args[2], rows, cols); - err_code = err_code1 + err_code2; - if (err_code != 0) { - SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); - return m; - } + // err_code1 = RecycleInPlace(args[1], rows, cols); + // err_code2 = RecycleInPlace(args[2], rows, cols); + // err_code = err_code1 + err_code2; + // if (err_code != 0) { + // SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + // return m; + // } m = matrix::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i @@ -2286,6 +2360,81 @@ class ExprEvaluator { char error_message[256]; }; +template +class MatAssigner { +private: + vector table_x; + vector table_n; + vector table_i; + ListOfIntVecs valid_int_vecs; + vector valid_literals; +public: + MatAssigner ( + vector& table_x_, + vector& table_n_, + vector& table_i_, + ListOfIntVecs& valid_int_vecs_, + vector& valid_literals_ + ) { + table_x = table_x_; + table_n = table_n_; + table_i = table_i_; + valid_int_vecs = valid_int_vecs_; + valid_literals = valid_literals_; + }; + + void matAssign ( + matrix assignment_value, + ListOfMatrices& valid_vars, // current list of values of each matrix + int row = 0 // current expression parse table row being evaluated + ) { + int n = table_n[row]; + int x = table_x[row]; + matrix m; + std::vector v1; + std::vector v2; + std::cout << "---- assignment ----" << std::endl; + std::cout << "n: " << n << std::endl; + std::cout << "x: " << n << std::endl; + if (n == -1) { + Rf_error("trying to assign to a literal, which is not allowed"); + } else if (n == -2) { + Rf_error("trying to assign to an engine method, which is not allowed"); + } else if (n == -3) { + Rf_error("trying to assign to an integer vector, which is not allowed"); + } else if (n == 1) { + Rf_error("assignment error -- TODO: be more specific"); + } else if (n == 0) { + valid_vars.m_matrices[x] = assignment_value; + } else { + if (x + 1 != MP2_SQUARE_BRACKET) { + Rf_error("square bracket is the only function allowed on the left-hand-side"); + } + if (n == 3) { + v2 = valid_int_vecs[table_x[table_i[row] + 2]]; + } else if (n == 2) { + v2.push_back(0); + } else { + Rf_error("incorrect numbers of arguments"); + } + int x1 = table_x[table_i[row]]; + m = valid_vars.m_matrices[x1]; + std::cout << "matrix: " << m << std::endl; + v1 = valid_int_vecs[table_x[table_i[row] + 1]]; + printIntVector(v1); + printIntVector(v2); + std::cout << "value: " << assignment_value << std::endl; + for (int i = 0; i < v1.size(); i++) { + for (int j = 0; j < v2.size(); j++) { + m.coeffRef(v1[i], v2[j]) = assignment_value.coeff(i, j); + } + } + valid_vars.m_matrices[x1] = m; + //valid_vars.m_matrices[matIndex].coeffRef(rowIndex,colIndex) = args[3].coeff(k,0); + } + }; +}; + #define REPORT_ERROR { \ int error = exprEvaluator.GetErrorCode(); \ int expr_row = exprEvaluator.GetExprRow(); \ @@ -2388,7 +2537,11 @@ Type objective_function::operator() () DATA_INTEGER(time_steps) // Expressions and parse table - DATA_IVECTOR(expr_output_id); + //DATA_IVECTOR(expr_output_id); + DATA_IVECTOR(a_table_x); + DATA_IVECTOR(a_table_n); + DATA_IVECTOR(a_table_i); + DATA_IVECTOR(assign_num_a_table_rows); DATA_IVECTOR(expr_sim_block); DATA_IVECTOR(expr_num_p_table_rows); DATA_IVECTOR(eval_schedule) @@ -2401,7 +2554,6 @@ Type objective_function::operator() () // Methods DATA_IVECTOR(meth_type_id); - // DATA_IVECTOR(meth_id); DATA_IVECTOR(meth_n_mats); DATA_IVECTOR(meth_n_int_vecs); DATA_IVECTOR(meth_mat_id); @@ -2419,8 +2571,6 @@ Type objective_function::operator() () // Flags DATA_INTEGER(values_adreport); - // DATA_STRUCT(settings, settings_struct); - #ifdef MP_VERBOSE std::cout << "params = " << params << std::endl; @@ -2447,8 +2597,7 @@ Type objective_function::operator() () std::cout << "eval_schedule = " << eval_schedule << std::endl; - //std::cout << "expr_output_count = " << expr_output_count << std::endl; - std::cout << "expr_output_id = " << expr_output_id << std::endl; + // std::cout << "expr_output_id = " << expr_output_id << std::endl; std::cout << "expr_sim_block = " << expr_sim_block << std::endl; std::cout << "expr_num_p_table_rows = " << expr_num_p_table_rows << std::endl; @@ -2487,8 +2636,6 @@ Type objective_function::operator() () ListOfIntVecs meth_mats(meth_mat_id, meth_n_mats); ListOfIntVecs meth_int_vecs(meth_int_vec_id, meth_n_int_vecs); - ////////////////////////////////// - // Define an expression evaluator ExprEvaluator exprEvaluator( mats_save_hist, p_table_x, @@ -2511,11 +2658,18 @@ Type objective_function::operator() () const_int_vecs, literals ); - ////////////////////////////////// + MatAssigner matAssigner( + a_table_x, + a_table_n, + a_table_i, + const_int_vecs, + literals + ); // 3 Pre-simulation int expr_index = 0; int p_table_row = 0; + int a_table_row = 0; for (int i=0; i::operator() () return 0.0; } - mats.m_matrices[expr_output_id[expr_index+i]] = result; + //mats.m_matrices[expr_output_id[expr_index+i]] = result; + matAssigner.matAssign(result, mats, a_table_row); p_table_row += expr_num_p_table_rows[i]; + a_table_row += assign_num_a_table_rows[i]; } //simulation_history[0] = mats; @@ -2558,8 +2714,10 @@ Type objective_function::operator() () expr_index += eval_schedule[0]; int p_table_row2; + int a_table_row2; for (int k=0; k::operator() () REPORT_ERROR return 0.0; } - mats.m_matrices[expr_output_id[expr_index+i]] = result; + // mats.m_matrices[expr_output_id[expr_index+i]] = result; + matAssigner.matAssign(result, mats, a_table_row2); p_table_row2 += expr_num_p_table_rows[expr_index+i]; + a_table_row2 += assign_num_a_table_rows[expr_index+i]; #ifdef MP_VERBOSE int n = mats.m_matrices.size(); @@ -2605,6 +2765,7 @@ Type objective_function::operator() () ); } p_table_row = p_table_row2; + a_table_row = a_table_row2; // 5 Post-simulation expr_index += eval_schedule[1]; @@ -2632,9 +2793,11 @@ Type objective_function::operator() () return 0.0; } - mats.m_matrices[expr_output_id[expr_index+i]] = result; + //mats.m_matrices[expr_output_id[expr_index+i]] = result; + matAssigner.matAssign(result, mats, a_table_row); p_table_row += expr_num_p_table_rows[expr_index+i]; + a_table_row += assign_num_a_table_rows[expr_index+i]; } //simulation_history[time_steps+1] = mats; From 9c324143e5cd34d2466534dd801e77ec8ff866a5 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 09:57:02 -0400 Subject: [PATCH 024/332] old experiments --- misc/experiments/bigmatmult/bigmatmult.R | 128 +++++ .../bigmatmult/model/derivations.json | 14 + misc/experiments/bigmatmult/model/flows.csv | 4 + .../bigmatmult/model/settings.json | 6 + .../bigmatmult/model/variables.csv | 9 + misc/experiments/calibration/Rplots.pdf | Bin 0 -> 115579 bytes misc/experiments/calibration/prof | 0 .../calibration/seg_fault_tester.R | 2 + misc/experiments/convolution/convolution.R | 47 ++ misc/experiments/error_row/error_row.R | 22 + misc/experiments/forecasting/forecasting.R | 171 ++++++ misc/experiments/hackathon_model/defaults.csv | 228 ++++++++ .../hackathon_model/dimensions.csv | 1 + .../performance/profiling_for_long_sir.png | Bin 0 -> 77461 bytes .../piecewise_time_variation.R | 42 ++ misc/experiments/refactorcpp.R | 524 ++++++++++++++++++ misc/experiments/runge_kutta/runge_kutta.R | 38 ++ .../synchronization/synchronization.R | 169 ++++++ 18 files changed, 1405 insertions(+) create mode 100644 misc/experiments/bigmatmult/bigmatmult.R create mode 100644 misc/experiments/bigmatmult/model/derivations.json create mode 100644 misc/experiments/bigmatmult/model/flows.csv create mode 100644 misc/experiments/bigmatmult/model/settings.json create mode 100644 misc/experiments/bigmatmult/model/variables.csv create mode 100644 misc/experiments/calibration/Rplots.pdf create mode 100644 misc/experiments/calibration/prof create mode 100644 misc/experiments/calibration/seg_fault_tester.R create mode 100644 misc/experiments/convolution/convolution.R create mode 100644 misc/experiments/error_row/error_row.R create mode 100644 misc/experiments/forecasting/forecasting.R create mode 100644 misc/experiments/hackathon_model/defaults.csv create mode 100644 misc/experiments/hackathon_model/dimensions.csv create mode 100644 misc/experiments/performance/profiling_for_long_sir.png create mode 100644 misc/experiments/piecewise_time_variation/piecewise_time_variation.R create mode 100644 misc/experiments/refactorcpp.R create mode 100644 misc/experiments/runge_kutta/runge_kutta.R create mode 100644 misc/experiments/synchronization/synchronization.R diff --git a/misc/experiments/bigmatmult/bigmatmult.R b/misc/experiments/bigmatmult/bigmatmult.R new file mode 100644 index 00000000..77e85e76 --- /dev/null +++ b/misc/experiments/bigmatmult/bigmatmult.R @@ -0,0 +1,128 @@ +library(macpan2) +library(dplyr) +library(ggplot2) +macpan2:::dev_compile() +#dyn.load("/usr/local/lib/libprofiler.0.dylib") + +# d = 20 +# X = rbf(1000, d) +# plot(X %*% rnorm(d), X %*% rnorm(d), type = "l") + +# model_starter("sir", "misc/experiments/bigmatmult/model") + +# obs_incidence = rbf(2661, 20) %*% runif(20, max = 5) +# plot(obs_incidence, type = "l") + +d = 20 ## dimension of the radial basis +left = Sys.time() +sir = Compartmental( + "misc/experiments/bigmatmult/model" +)$simulators$tmb( + time_steps = 2661 + , state = c(S = 100000 - 500, I = 500, R = 0) + , flow = c(foi = 0, gamma = 0.2, wane = 0.01) + , beta = 1 + , N = 100000 + , X = rbf(2661, d) + , b = rnorm(d, sd = 0.01) + , incidence = empty_matrix + , eta = empty_matrix + , .mats_to_save = c("state", "incidence", "beta") + , .mats_to_return = c("state", "incidence", "beta") + , .tmb_cpp = "dev" +)$insert$expressions( + eta ~ N * gamma * exp(X %*% b) + , .phase = "before" + , .at = Inf +)$insert$expressions( + beta ~ eta[time_step(1)] / clamp(S, 0.01) + , .phase = "during" + , .at = 1 +)$insert$expressions( + incidence ~ I + , .vec_by_states = "total_inflow" + , .phase = "during" + , .at = Inf +)$replace$params( + default = rnorm(d, sd = 0.01) + , mat = rep("b", d) + , row = seq_len(d) - 1L +) + +(sir$report(rnorm(d, sd = 0.01), .phases = "during") + %>% mutate(variable = if_else(matrix == "state", row, matrix)) + %>% ggplot() + + facet_wrap(~ variable, ncol = 1, scales = 'free') + + geom_line(aes(time, value)) +) + +plot(sir$report(rnorm(d, sd = 0.01), .phases = "during")$value, type = "l") +plot(sir) +sir$current$params_frame() +sir$get$initial("flow") + +engine_eval(~clamp(2, 1)) + +sir$insert$expressions( + log_lik ~ dpois(obs_incidence, I) + , .phase = "during" + , .at = Inf + , .vec_by_states = "total_inflow" +) +sir$insert$expressions( + log_lik ~ sum(rbind_time(log_lik)) + , .phase = "after" + , .at = Inf +) +sir$replace$obj_fn(~ -log_lik) +sir$add$transformations(Logit("flow")) +sir$replace$params( + default = c(100, 100000, qlogis(0.2), rep(0.5, 10)), + mat = c("state", "state", "logit_flow", rep("b", 10)), + row = c(1, 0, 1, 0:9) +) + +sir$optimize$nlminb(control = list(iter.max = 1000, eval.max = 1000)) +sir$current$params_frame() +sir$optimize$optim() +sir$optimization_history$get() +sir$ad_fun()$par +sir$report(sir$current$params_vector()) +sir$objective(sir$current$params_vector()) +plot(filter(sir$report(.phases = "after"), matrix == "beta_values")$value, type = "l") +(filter(sir$report(.phases = "during"), matrix == "state") + %>% ggplot() + + facet_wrap(~row, scales = 'free', ncol = 1) + + geom_line(aes(time, value)) +) + +sim_incidence = filter(sir$report(.phases = "during"), matrix == "total_inflow", row == 1L)$value + +plot(sim_incidence, obs_incidence[1:2661], type = "l") + +#beta_values = filter(sir$report(.phases = "after"), matrix == "beta_values") +rr = sir$report(.phases = "during") +rr = sir$report(sir$current$params_vector(), .phases = "during") +right = Sys.time() +right - left + +(ggplot(filter(rr, matrix == "state")) + + facet_wrap(~row, ncol = 1, scales = 'free') + + geom_line(aes(time, value)) +) + +(ggplot(filter(rr, matrix == "total_inflow")) + + facet_wrap(~row, ncol = 1, scales = 'free') + + geom_line(aes(time, value)) +) + +ggplot(data.frame(time = 0:2661, value = obs_incidence)) + geom_line(aes(time, value)) + + +plot(beta_values$value, type = "l") + +## nothing big -- 0.6819479 +## big X small T -- 0.834451 +## small X big T -- 2.698912 +## big X and T -- 30.11578 +## big X and T with resetting of X before the loop -- 3.078685 diff --git a/misc/experiments/bigmatmult/model/derivations.json b/misc/experiments/bigmatmult/model/derivations.json new file mode 100644 index 00000000..81c90eb0 --- /dev/null +++ b/misc/experiments/bigmatmult/model/derivations.json @@ -0,0 +1,14 @@ +[ + { + "output_names" : ["N"], + "simulation_phase" : "before", + "arguments" : ["S", "I", "R"], + "expression" : "sum(S, I, R)" + }, + { + "output_names" : ["foi"], + "simulation_phase" : "during_pre_update", + "arguments" : ["I", "beta", "N"], + "expression" : "I * beta / N" + } +] diff --git a/misc/experiments/bigmatmult/model/flows.csv b/misc/experiments/bigmatmult/model/flows.csv new file mode 100644 index 00000000..3bb52294 --- /dev/null +++ b/misc/experiments/bigmatmult/model/flows.csv @@ -0,0 +1,4 @@ +from ,to ,flow ,type +S ,I ,foi ,per_capita +I ,R ,gamma ,per_capita +R ,S ,wane ,per_capita diff --git a/misc/experiments/bigmatmult/model/settings.json b/misc/experiments/bigmatmult/model/settings.json new file mode 100644 index 00000000..7adf6dd9 --- /dev/null +++ b/misc/experiments/bigmatmult/model/settings.json @@ -0,0 +1,6 @@ +{ + "required_partitions" : ["Epi"], + "null_partition" : "Null", + "state_variables" : ["S", "I", "R"], + "flow_variables" : ["foi", "gamma", "wane"] +} diff --git a/misc/experiments/bigmatmult/model/variables.csv b/misc/experiments/bigmatmult/model/variables.csv new file mode 100644 index 00000000..b640f900 --- /dev/null +++ b/misc/experiments/bigmatmult/model/variables.csv @@ -0,0 +1,9 @@ +Epi +S +I +R +N +beta +foi +gamma +wane diff --git a/misc/experiments/calibration/Rplots.pdf b/misc/experiments/calibration/Rplots.pdf new file mode 100644 index 0000000000000000000000000000000000000000..38406d2208a0a76cebe3d011c0b39f776ca92815 GIT binary patch literal 115579 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
  • RcX;fTE_4#$ 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

    !^JPQ9Y(VHj|m=? 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 filter(matrix %in% c("reports", "incidence")) + %>% ggplot() + + geom_line(aes(time, value, colour = matrix)) +) diff --git a/misc/experiments/error_row/error_row.R b/misc/experiments/error_row/error_row.R new file mode 100644 index 00000000..b19e8044 --- /dev/null +++ b/misc/experiments/error_row/error_row.R @@ -0,0 +1,22 @@ +library(macpan2) +engine_eval(~ x[-1], x = 0) + +m = TMBModel( + expr_list = ExprList( + during = list( + z ~ 1 + x[1] + 1, + y ~ rbind_time(z) + ) + ), + init_mats = MatsList( + x = 1:10, + y = empty_matrix, + z = empty_matrix, + .mats_to_save = "y", + .mats_to_return = "y" + ), + time_steps = Time(10) +) + +undebug(TMBSimulator) +m$simulator()$report() diff --git a/misc/experiments/forecasting/forecasting.R b/misc/experiments/forecasting/forecasting.R new file mode 100644 index 00000000..b615ca19 --- /dev/null +++ b/misc/experiments/forecasting/forecasting.R @@ -0,0 +1,171 @@ +library(macpan2) +library(dplyr) +library(TMB) +library(MASS) +#macpan2:::dev_compile() +#dyn.load("/usr/local/lib/libprofiler.0.dylib") +sir = Compartmental(file.path("inst", "starter_models", "sir")) +N = 100 +sir$labels$state() +sir$labels$flow() +sir$labels$other() +sir$flows() +# -------- +simulator = sir$simulators$tmb(time_steps = 100 + , state = c(S = N - 1, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.2) + , N = N + , beta = 0.4 + # , .mats_to_save = c( + # "state", "flow", "N", "beta", "state_length", "per_capita_from", + # "per_capita_to", "per_capita_flow", "absolute_from", "absolute_to", + # "absolute_flow", "per_capita_inflow_from", "per_capita_inflow_to", + # "per_capita_inflow_flow", "per_capita_outflow_from", + # "per_capita_outflow_flow", "absolute_inflow_to", "absolute_inflow_flow", + # "absolute_outflow_from", "absolute_outflow_flow", "per_capita", + # "absolute", "per_capita_inflow", "per_capita_outflow", "absolute_inflow", + # "absolute_outflow", "total_inflow", "total_outflow", "dummy" + # ) +) + +simulator$print$matrix_dims() +simulator$print$expressions() + +## generate simulated data so that i can fit the generating +## model to these data for a sanity check -- can we recover +## the parameters from the simulating model? +sims = simulator$report(.phases = "during") + +obs_time_steps = unique(sort(sample(1:100, 30))) +deterministic_prevalence = (sims + %>% filter(row == "I") + %>% pull(value) +)[obs_time_steps] + + + + +set.seed(1L) +observed_I = rpois(30, deterministic_prevalence) + + +## Step 1: add observed data and declare matrices storing +## the simulation history of variables to compare +## with observed data, as well as a matrix for +## storing log-likelihood values. need to make sure +## that the simulation histories are saved, and +## if you want you can return them as well as the +## log likelihood values. +simulator$add$matrices( + observed_I = observed_I + , obs_time_steps = obs_time_steps + , simulated_I = empty_matrix + , noisy_I = empty_matrix + , log_lik = empty_matrix + , .mats_to_save = c("simulated_I", "total_inflow", "noisy_I") + , .mats_to_return = c("noisy_I") + , .dimnames = list(total_inflow = list(c("S", "I", "R"), "")) +) +simulator$print$matrix_dims() + +## Step 2: collect simulated values into matrices to be +## compared with data. note here that `1` indicates +## the position of the I state. the `.at = Inf` and +## `.phase = "during"` indicates that this expression +## should come at the end of the expressions evaluated +## during each iteration of the simulation loop. +simulator$insert$expressions( + #trajectory = simulated_I ~ 0.1 * sum(total_inflow[c(20, 21)]) + trajectory = simulated_I ~ I + , poisson = noisy_I ~ rpois(simulated_I) + , .at = Inf ## place the inserted expressions at the end of the expression list + , .phase = "during" + , .simulate_exprs = "poisson" +) +simulator$print$expressions() + +## Step 3: compute any values that will be part of the +## objective function to be optimized. here we +## have the log of the Poisson density of the +## observed `I` values with mean (i.e. predicted) +## value at the simulated `I` values. the +## `rbind_time` function gathers together the +## full simulation history of the `simulated_I` +## matrix by binding together the rows at each +## iteration. +simulator$insert$expressions( + likelihood = log_lik ~ + ( + dpois( + observed_I, ## observed values + clamp(rbind_time(simulated_I, obs_time_steps)) ## simulated values + ) + # + dpois( + # observed_var_a, + # clamp(rbind_time(sim_a, ts_a)) + # ) + ) + , .at = Inf + , .phase = "after" +) +# simulator$insert$expressions( +# poisson = simulated_I ~ rpois(simulated_I) +# , .simulate_exprs = "poisson" +# , .at = Inf +# , .phase = "after" +# ) +simulator$print$expressions() + +## Step 4: specify the objective function (very often +## this will be minus the sum of the log likelihoods). +simulator$replace$obj_fn(~ -sum(log_lik)) + +## Step 5: declare (and maybe transform) parameters to be optimized, +## as well as starting values for the parameters to be optimized +simulator$add$matrices( + log_beta = log(0.6), + logit_gamma = qlogis(0.4) +) + +simulator$insert$expressions( + beta ~ exp(log_beta) + , gamma ~ 1 / (1 + exp(-logit_gamma)) + , .phase = "before" +) + +simulator$replace$params( + default = c(log(0.1), qlogis(0.1)), + mat = c("log_beta", "logit_gamma") +) + +dd = simulator$ad_fun() +dd$simulate()$values[,5] + +filter(simulator$simulate(), matrix == "noisy_I") %>% pull(value) %>% plot(las = 1) +filter(simulator$report(), matrix == "noisy_I") %>% pull(value) %>% plot(las = 1) +# simulator$replace$random( +# default = qlogis(0.2), +# mat = "logit_gamma" +# ) + +simulator$print$expressions() +t(apply(apply(mvrnorm(10, simulator$par.fixed(), simulator$cov.fixed()), 1, simulator$report_values), 1, quantile, probs = c(0.025, 0.5, 0.975))) +simulator$cov.fixed() +simulator$par.fixed() + +View(simulator$report_ensemble(.phases = "during")) +## Step 6: use the engine object +simulator$optimize$optim() +sdreport(simulator$ad_fun()) + +simulator$current$params_frame() +plot(obs_time_steps, observed_I) +lines(1:100, filter(simulator$report(.phases = "during"), matrix == "state", row == "I")$value, col = "red") + +## Forecasting + +simulator$current$params_frame() +simulator$replace$time_steps(200) +plot(obs_time_steps, observed_I) +lines(1:200, filter(simulator$report(.phases = "during"), matrix == "state", row == "I")$value, col = "red") +simulator$current$params_frame() diff --git a/misc/experiments/hackathon_model/defaults.csv b/misc/experiments/hackathon_model/defaults.csv new file mode 100644 index 00000000..ec7bcf4d --- /dev/null +++ b/misc/experiments/hackathon_model/defaults.csv @@ -0,0 +1,228 @@ +Epi ,Age ,Matrix ,Value ,Notes +S ,lb0 ,state ,1000000 +E ,lb0 ,state ,1 +R ,lb0 ,state ,0 +H ,lb0 ,state ,0 +I ,lb0 ,state ,1 +D ,lb0 ,state ,0 +progression ,lb0 ,flow , +recovery ,lb0 ,flow , +hospitalization ,lb0 ,flow , +discharge ,lb0 ,flow , +death_H ,lb0 ,flow , +death_I ,lb0 ,flow , +infection ,lb0 ,flow , +S ,lb5 ,state , +E ,lb5 ,state , +R ,lb5 ,state , +H ,lb5 ,state , +I ,lb5 ,state , +D ,lb5 ,state , +progression ,lb5 ,flow , +recovery ,lb5 ,flow , +hospitalization ,lb5 ,flow , +discharge ,lb5 ,flow , +death_H ,lb5 ,flow , +death_I ,lb5 ,flow , +infection ,lb5 ,flow , +S ,lb10 ,state , +E ,lb10 ,state , +R ,lb10 ,state , +H ,lb10 ,state , +I ,lb10 ,state , +D ,lb10 ,state , +progression ,lb10 ,flow , +recovery ,lb10 ,flow , +hospitalization ,lb10 ,flow , +discharge ,lb10 ,flow , +death_H ,lb10 ,flow , +death_I ,lb10 ,flow , +infection ,lb10 ,flow , +S ,lb15 ,state , +E ,lb15 ,state , +R ,lb15 ,state , +H ,lb15 ,state , +I ,lb15 ,state , +D ,lb15 ,state , +progression ,lb15 ,flow , +recovery ,lb15 ,flow , +hospitalization ,lb15 ,flow , +discharge ,lb15 ,flow , +death_H ,lb15 ,flow , +death_I ,lb15 ,flow , +infection ,lb15 ,flow , +S ,lb20 ,state , +E ,lb20 ,state , +R ,lb20 ,state , +H ,lb20 ,state , +I ,lb20 ,state , +D ,lb20 ,state , +progression ,lb20 ,flow , +recovery ,lb20 ,flow , +hospitalization ,lb20 ,flow , +discharge ,lb20 ,flow , +death_H ,lb20 ,flow , +death_I ,lb20 ,flow , +infection ,lb20 ,flow , +S ,lb25 ,state , +E ,lb25 ,state , +R ,lb25 ,state , +H ,lb25 ,state , +I ,lb25 ,state , +D ,lb25 ,state , +progression ,lb25 ,flow , +recovery ,lb25 ,flow , +hospitalization ,lb25 ,flow , +discharge ,lb25 ,flow , +death_H ,lb25 ,flow , +death_I ,lb25 ,flow , +infection ,lb25 ,flow , +S ,lb30 ,state , +E ,lb30 ,state , +R ,lb30 ,state , +H ,lb30 ,state , +I ,lb30 ,state , +D ,lb30 ,state , +progression ,lb30 ,flow , +recovery ,lb30 ,flow , +hospitalization ,lb30 ,flow , +discharge ,lb30 ,flow , +death_H ,lb30 ,flow , +death_I ,lb30 ,flow , +infection ,lb30 ,flow , +S ,lb35 ,state , +E ,lb35 ,state , +R ,lb35 ,state , +H ,lb35 ,state , +I ,lb35 ,state , +D ,lb35 ,state , +progression ,lb35 ,flow , +recovery ,lb35 ,flow , +hospitalization ,lb35 ,flow , +discharge ,lb35 ,flow , +death_H ,lb35 ,flow , +death_I ,lb35 ,flow , +infection ,lb35 ,flow , +S ,lb40 ,state , +E ,lb40 ,state , +R ,lb40 ,state , +H ,lb40 ,state , +I ,lb40 ,state , +D ,lb40 ,state , +progression ,lb40 ,flow , +recovery ,lb40 ,flow , +hospitalization ,lb40 ,flow , +discharge ,lb40 ,flow , +death_H ,lb40 ,flow , +death_I ,lb40 ,flow , +infection ,lb40 ,flow , +S ,lb45 ,state , +E ,lb45 ,state , +R ,lb45 ,state , +H ,lb45 ,state , +I ,lb45 ,state , +D ,lb45 ,state , +progression ,lb45 ,flow , +recovery ,lb45 ,flow , +hospitalization ,lb45 ,flow , +discharge ,lb45 ,flow , +death_H ,lb45 ,flow , +death_I ,lb45 ,flow , +infection ,lb45 ,flow , +S ,lb50 ,state , +E ,lb50 ,state , +R ,lb50 ,state , +H ,lb50 ,state , +I ,lb50 ,state , +D ,lb50 ,state , +progression ,lb50 ,flow , +recovery ,lb50 ,flow , +hospitalization ,lb50 ,flow , +discharge ,lb50 ,flow , +death_H ,lb50 ,flow , +death_I ,lb50 ,flow , +infection ,lb50 ,flow , +S ,lb55 ,state , +E ,lb55 ,state , +R ,lb55 ,state , +H ,lb55 ,state , +I ,lb55 ,state , +D ,lb55 ,state , +progression ,lb55 ,flow , +recovery ,lb55 ,flow , +hospitalization ,lb55 ,flow , +discharge ,lb55 ,flow , +death_H ,lb55 ,flow , +death_I ,lb55 ,flow , +infection ,lb55 ,flow , +S ,lb60 ,state , +E ,lb60 ,state , +R ,lb60 ,state , +H ,lb60 ,state , +I ,lb60 ,state , +D ,lb60 ,state , +progression ,lb60 ,flow , +recovery ,lb60 ,flow , +hospitalization ,lb60 ,flow , +discharge ,lb60 ,flow , +death_H ,lb60 ,flow , +death_I ,lb60 ,flow , +infection ,lb60 ,flow , +S ,lb65 ,state , +E ,lb65 ,state , +R ,lb65 ,state , +H ,lb65 ,state , +I ,lb65 ,state , +D ,lb65 ,state , +progression ,lb65 ,flow , +recovery ,lb65 ,flow , +hospitalization ,lb65 ,flow , +discharge ,lb65 ,flow , +death_H ,lb65 ,flow , +death_I ,lb65 ,flow , +infection ,lb65 ,flow , +S ,lb70 ,state , +E ,lb70 ,state , +R ,lb70 ,state , +H ,lb70 ,state , +I ,lb70 ,state , +D ,lb70 ,state , +progression ,lb70 ,flow , +recovery ,lb70 ,flow , +hospitalization ,lb70 ,flow , +discharge ,lb70 ,flow , +death_H ,lb70 ,flow , +death_I ,lb70 ,flow , +infection ,lb70 ,flow , +S ,lb75 ,state , +E ,lb75 ,state , +R ,lb75 ,state , +H ,lb75 ,state , +I ,lb75 ,state , +D ,lb75 ,state , +progression ,lb75 ,flow , +recovery ,lb75 ,flow , +hospitalization ,lb75 ,flow , +discharge ,lb75 ,flow , +death_H ,lb75 ,flow , +death_I ,lb75 ,flow , +infection ,lb75 ,flow , +S ,lb80 ,state , +E ,lb80 ,state , +R ,lb80 ,state , +H ,lb80 ,state , +I ,lb80 ,state , +D ,lb80 ,state , +progression ,lb80 ,flow , +recovery ,lb80 ,flow , +hospitalization ,lb80 ,flow , +discharge ,lb80 ,flow , +death_H ,lb80 ,flow , +death_I ,lb80 ,flow , +infection ,lb80 ,flow , +transmission , ,transmission , +scaled_infected , ,scaled_infected , +contact , ,contact , +infection , ,infection , +infected , ,infected , +dummy , ,dummy , diff --git a/misc/experiments/hackathon_model/dimensions.csv b/misc/experiments/hackathon_model/dimensions.csv new file mode 100644 index 00000000..e63d56f7 --- /dev/null +++ b/misc/experiments/hackathon_model/dimensions.csv @@ -0,0 +1 @@ +Matrix, Row, Column \ No newline at end of file diff --git a/misc/experiments/performance/profiling_for_long_sir.png b/misc/experiments/performance/profiling_for_long_sir.png new file mode 100644 index 0000000000000000000000000000000000000000..754ba8126ef07c3a51744ec3bef057f9dc546f9d GIT binary patch literal 77461 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

    u3MX7zD zhcw_Q39@_4D1V`oPc`tjdF)XXSwH_~>|;P; zEqJt?jR};4z($9sjiJ>dQ{xGak8`D!e5SB8MKa^G@s3Zwi*n|+bA0W-kJ+2qw_PMN zKX?^qIrbB|xf~Rr@9k||(P|eCC+%lCkAc_vBHuMPd7Kzswairc1(CX48%|XjO3Zt1 z`xiX{uS887*9o)0U1p8#<^JsDCV~FT;ormL8gn!$df3IrdsF4yYnw0JehtxSO_r!r z-95a8kO`IR-nrF7gzv6(;~%5RMV3JO;toM%>_(pi@ENY3i2EY(8YD1g0fuH9X^qD;3c@jomoJnaV6CD(UcITHoC`yHzmrsw;gdJ282S;(th`8s;EJ;K^idwDQ7FGZk>8~#%l>=XPV-{GKc`a{Z?ZYE{(?tuE3 zYGnQad~m2!%TasFGM*=0>uH94JJ>jekG47%9lww*>Zc|v(0iRT=r2!9e*Y?r!6Tyr ziM?0|i*H!=ns3{K_`|W$y6sJUHyg|R?RE>fTJVj%J6z5=Y0t}XZ7VOWvs~=gS^)DK zhtuE_$#7rlUYcVG6`h2m8trFs`tD+a-+ z9zYbfV~zEgE%%H6AxHUlpo?3A(<41A8POI$#MUcD!f7TG@|pq8da%HZd?qe@f86t4 zQIqJ!pb89VAa&0O`fV!CSvLwvB7wfoPX?%?*1H&VAEVrh-NlinVT6$?-{UUCmK#oI z6O$jDK{j*emfT+F7Uoj@#5k+&KM!K;+_uup`Gbkv6i2mNS?`Z^Ud~e*4X&2EiD_`D zZ8>Zoc^gIY!nEm<B~+4s%Jgli?s6-J=N+0R-c`AU^>l@6pN!GSQJ`m&drE~`QH(%Hsf%*6`yQF&!IE*T zP$o8))dc-^LgcRz7`4b`=D;G!ddIG}-Q8qkRu0Jmm;Ki~QSAk(SHr)JVN6jxR;1 zMNrSk`b)1DoFT4#NC8q_m%ElUvWX7H$wzU0FF)0jdaMMv^<41G^k52QzA{X4@1uJZ zZlEqMOeIH@-Oulf#I6qq!Bbxs9E(ns>zfPr@k6Z**>^nN?F3`@$?-;VFiITE)zvs{ zj`Rsg-AVm3HhmJ!YIe#mJm|mvfp*)k|k;L>$R>cI>+M=$c_lUaj`i!u8|v zJ3mco*q$I9M&<~g)N9c63_nBUN#wQPt)SZi$o;@LCfcp$($$E%<#d=at1qt6e4(xl zFP(#ax$yFlr(!Cn;OZJKVbcI9J8c%+;QqU9@#;lcknPbS*#dce?0Qrmdu5CxDjO^U zTGeMI;aN|_JLd9wkc^{K7)N0Z9-R;19UAPdRrQkhU?2lt zN@mhC0+F`W@m&h~XOXu*b+y8CfDqg*@nOAOjtsN--BHvt12<>4NESL}Iwg%*jTDg! zqZAH_y|abQqeaaI$d-F{R0fC&3 zJ%P&FYS1gp@y>85H`ORQ_FnQMuhyq`Xu&jPR*TJb0)7Jr?z~jEXExo)HR)^^eZi8N zm93`{sQSHG>AXtRqpAlO_fOmchyz9ot)6d3>RDqqQ*K1UTf4siIpZPkKsvYW zOO4)x`9{Zr?>QpNT1DhOD`E|mhsaOD{Sh;28)Y?*t^;o5=XCQ(CT{!SZ$V7j^`mX( zo0zfAl*zN+ur{;RG`sF%yl7-7@5nCf6Y9!zsA{&qe8DaFhTW@_egKSevoQjfyrsS_ zwp8!}6=tyXTbxQ_tebSoFZjaUjRxv*=h*e>2Kf=wH$IyP+xbFgeuejgX8vyX$(G8=;C7 zHVP$i3e)wSoOJVg7`Od3|5*o{b!$ls`EJn@9zQ54_6RL|y#Y+&-c%kb1Pn{@%0mXe zwdONbhTJYQ%G<)OX$7Cmg$cKM-AM462gc$Lk;pUdlagN zfbW8GW-o6td09Q8AZeuDlck#Ov5%Day!3u$HLRB}+cTGyY}ilEs5MI6WrZ3(uKN=c zY7;4C3Ao1ywh!gxd_I!mwq2^WoxP-Ze2r*2`jWuSFxxnIPpZjjGZTSa!|Z_7TGvNh zP{cu_EleHb(ZP7nurEv5R7Dtz9YDMt(|EBdIse{#40~|M zF{y?b0unGzJt4Ly%Q}hb^{%?)@HbaHNJ{86Pi5As?iH#>lv<=4Q*gnEePB>)j#`}i zJtf)4;TvVs2YZX@pp_T|(I0b>;&Tz_j~2ixrO z_RTU(LfhwK_VF(qPM)uruc%67>gz&?7mLCRl*EMe|H{}2m1py%3{KrUXxh$J4GUVS zWB&U+iHn1ZMe1r%Kyeh8=TC(ovkfS|i5;^!17lY!CzgoIKLBDO^NzVst_p8HMBu2E zQ|)3PoBW6~q_K+?&8;bXE$QduK2_ZAj6`_f*=k1NQ**s}9emVTuDHAq{m^2w`-oMN zmN4sh@*0!A4BbBp9M=oxx0x2hxxR%kbgpBPuRa`6rRSwPpv1D~oh~Oc{kSX_@b-`1 zSTHCu+_`ieR(xRn`-=aoVB=7r1(|Qpe}9%$0Y-t^S8bwiyDL^Ylx1&j)l9$OV<`iQnBpiV#-`{=eIYNvr_cj_o_&TIbJ0CLH>*_17Zj z0wT4qMqtemm6*zWl~Vl1Fy|%J-y?V0LNySt-rc zS{egdMYMkUfHCo|mM+ize=po&PzF28^E#fUt5v^?JKWf^5Lwev7;OX|Lf6_L*oUQFk* zHbW$JiQ}@FVUw^{UG2Z2%W_FrbQxE0Tk_nVEiTHoqHh1Y4nPct!^c9@ z0FX(Z(K636b={o+0|L15=%~D5N!CTSp2QpIMdfd!UtR*4p44QDtjGg0AwtF*%eCE; zyZbxF%GR~pZbIW1`R0)9dfRz!|Cy;u{iF6sPKe+*VP&25v|A)!Jj^_g^0!#tHKc(; zzlYmHfqOvw1xfvK6If>K18bc(%CsJLqaLEJWncdG#cL*YX-baR&gJ zsly=Up8@CqdSCed$YP6otf<&f8i#3w8VkMu5KRiG!|x&UT>X&YGMGzwRO@RrkZu10ii+Jjb>YBQnW!XL= z0OWGoYCHdXr0G+}wL&U%R ziO_REZMC1+-UXH8IGxQfQ0U=&ygxsN03Ti1!EiWI>LosWRV|f|xsN{j%Qp_|L|@sO z?+#mh@hl(Odt@C4Vo1xygHe$66Nk#=v-l^=%|^3@1a{wRBol(7iFw?P$bF8*CLBi} zSliAMY!{m+(LZ?vnV)V9*{ukk|B!g*l|?#;rA*hg>t0y6b9{hEYd|&4O2QvLKU!X1 zL~^`(e7IVU&H#WNS}jOs;%geGxmn{2G$3^Vx-&s>NPQd7r>0um?TL9Eavx4dSmTY$ z#L)@aI=O9Tux5+U|9HUz9AM~;kTi@I9AkxVEsEYeqpYSyr!pXU8S{QYpr+%Xa*3s* z+{Q-N)2vxzrqHlA%vf0qbhEi1c@ndxS=wy7gi1yM;X;i zHd5iY!6tKWUzo*rw_vL+Ib*c2+*Prx$3*Y3RU9Ld`&8YvN8mDGA0?8`#TugQ9U#+X zuIOwVGs;aV>60z)fCtE&hbx!2m$h6BRw(FNVEmb@q<$_q@-9+u%w>kkS zCbmX>MM^os?Ty|?KMO+hq^a F)X{%*8%F-0!qiv7G>e0ly|P!$5Y#(m(P@k|Oh? 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 z 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 literal 0 HcmV?d00001 diff --git a/misc/experiments/piecewise_time_variation/piecewise_time_variation.R b/misc/experiments/piecewise_time_variation/piecewise_time_variation.R new file mode 100644 index 00000000..420d06fd --- /dev/null +++ b/misc/experiments/piecewise_time_variation/piecewise_time_variation.R @@ -0,0 +1,42 @@ +library(macpan2) +library(dplyr) +library(ggplot2) +sir = Compartmental(file.path("inst", "starter_models", "sir")) +N = 100 +sir$labels$state() +sir$labels$flow() +sir$labels$other() +sir$flows() +# -------- +simulator = sir$simulators$tmb(time_steps = 50 + , state = c(S = N - 1, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.2) + , N = empty_matrix + , beta = empty_matrix +) +simulator$print$matrix_dims() +simulator$print$expressions() + +beta_changepoints = c(0, 10, 15) +beta_values = c(0.8, 0.01, 0.4) + +simulator$add$matrices( + beta_values = beta_values + , beta_changepoints = beta_changepoints + , beta_pointer = 0 +) + +simulator$insert$expressions( + beta_pointer ~ time_group(beta_pointer, beta_changepoints) + , beta ~ beta_values[beta_pointer] + , .phase = "during" +) + +s = simulator$report(.phases = "during") +(s + %>% rename(state = row) + %>% mutate(state = factor(state, sir$labels$state())) + %>% ggplot() + + geom_line(aes(time, value, colour = state)) + + geom_vline(aes(xintercept = x), linetype = "dashed", alpha = 0.5, data = data.frame(x = beta_changepoints)) +) diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R new file mode 100644 index 00000000..25748a93 --- /dev/null +++ b/misc/experiments/refactorcpp.R @@ -0,0 +1,524 @@ +library(macpan2) +library(macpan2helpers) +library(oor) +library(ggplot2) +library(dplyr) + +debug(macpan2:::get_indices) + + +dir = file.path("inst", "model_library", "sir_age") +f = macpan2:::ModelFiles(dir) +m = macpan2:::Model(f) + +m$indices$flow$per_capita$flow() +m$infection_info$connections[[1]]$frame() +m$infection_info$connections[[1]]$from_vars() + +sir = read_partition("inst", "model_library", "sir", "variables.csv") +loc = partition(Loc = c("a", "b", "c")) +cartesian_self( + cartesian(sir, loc) + , "infection" + , "infectious" + , "Type" + , "Type.Vec" +) + + + +cartesian(sir, loc) + +v = read_partition("inst", "model_library", "sir_age", "variables.csv") +cartesian_self(v, "infection", "infectious", "Type") + +if (FALSE) { +engine_eval(~1) +engine_eval(~ groupSums(x, i, 2), x = 1:6, i = c(0,0,0,1,1,1)) +oor_debug$flag(".simulation_formatter", "TMBSimulator") + +#options(macpan2_dll = "dev") +#TMB::compile("misc/dev/dev.cpp") +#dyn.load(TMB::dynlib('misc/dev/dev')) +options(oor_catch_dollar = FALSE) +epi_dir = system.file("model_library", "sir", package = "macpan2") +epi_dir = file.path("inst", "model_library", "sir") +epi_dir = system.file("starter_models", "product_example", "Epi_model", package = "macpan2") +# oor_debug$flag(".fetch") +#oor_debug$flag(".parse_table", "ObjectiveFunction") +#oor_debug$unflag("data_arg") +#oor_debug$flag(".assign_num_a_table_rows") +# oor_debug$flag(".type", "Variables") +# oor_debug$print_flagged() +# oor_debug$.objects$.objects +#oor_debug$flag(".assign_table") +#trace(macpan2:::connection_merge, quote(.counter <<- .counter + 1), print = FALSE) +#.counter = 0 +model = Compartmental(epi_dir) +#print(.counter) +indices = model$indices +model$labels$state() +#print(.counter) +model$labels$flow() +#print(.counter) + +#undebug(oor::clean_method_environment) + +iv = IntVecs( + state_length = as.integer(length(model$labels$state())) + + , per_capita_from = indices$flow$per_capita$from() + , per_capita_to = indices$flow$per_capita$to() + , per_capita_flow = indices$flow$per_capita$flow() + + , absolute_from = indices$flow$absolute$from() + , absolute_to = indices$flow$absolute$to() + , absolute_flow = indices$flow$absolute$flow() + + , per_capita_inflow_from = indices$flow$per_capita_inflow$from() + , per_capita_inflow_to = indices$flow$per_capita_inflow$to() + , per_capita_inflow_flow = indices$flow$per_capita_inflow$flow() + + , per_capita_outflow_from = indices$flow$per_capita_outflow$from() + , per_capita_outflow_flow = indices$flow$per_capita_outflow$flow() + + , absolute_inflow_to = indices$flow$absolute_inflow$to() + , absolute_inflow_flow = indices$flow$absolute_inflow$flow() + + , absolute_outflow_from = indices$flow$absolute_outflow$from() + , absolute_outflow_flow = indices$flow$absolute_outflow$flow() + + , infection = model$indices$transmission$infection_flow() + , infectious = model$indices$transmission$infectious_state() + +) +m = TMBModel( + init_mats = MatsList( + state = c(S = 1 - 1e-5, E = 1e-5, I = 0, R = 0) + , flow = c(total_foi = NA_real_, progression = 0.2, recovery = 0.01) + , trans = 0.25 + , per_capita = empty_matrix + , per_capita_inflow = empty_matrix + , per_capita_outflow = empty_matrix + , absolute = empty_matrix + , absolute_inflow = empty_matrix + , absolute_outflow = empty_matrix + , total_inflow = empty_matrix + , total_outflow = empty_matrix + , .mats_to_save = "state" + , .mats_to_return = "state" + ), + expr_list = ExprList( + during = list( + flow[infection] ~ trans * state[infectious] + , per_capita ~ state[per_capita_from] * flow[per_capita_flow] + , per_capita_inflow ~ state[per_capita_inflow_from] * flow[per_capita_inflow_flow] + , per_capita_outflow ~ state[per_capita_outflow_from] * flow[per_capita_outflow_flow] + , absolute ~ flow[absolute_flow] + , absolute_inflow ~ flow[absolute_inflow_flow] + , absolute_outflow ~ flow[absolute_outflow_flow] + , total_inflow ~ + groupSums(per_capita, per_capita_to, state_length) + + groupSums(absolute, absolute_to, state_length) + + groupSums(per_capita_inflow, per_capita_inflow_to, state_length) + + groupSums(absolute_inflow, absolute_inflow_to, state_length) + , total_outflow ~ + groupSums(per_capita, per_capita_from, state_length) + + groupSums(absolute, absolute_from, state_length) + + groupSums(per_capita_inflow, per_capita_outflow_from, state_length) + + groupSums(absolute_inflow, absolute_outflow_from, state_length) + , state ~ state + total_inflow - total_outflow + ) + ), + engine_methods = EngineMethods(int_vecs = iv), + time_steps = Time(100L), + params = OptParamsList(0.5, 0.2, 0.1 + , par_id = 0:2 + , mat = c("trans", "flow", "flow") + , row_id = 0:2 + , col_id = rep(0L, 3L) + ), + obj_fn = ObjectiveFunction(~ sum(state^2)) +) +s = m$simulator() +s$optimize$optim()$value +s$optimize$optim()$par +s$objective(s$optimize$optim()$par) +r = s$report() + +if (interactive()) { + (r + |> mutate(state = factor(row, levels = topological_sort(model))) + |> ggplot() + + geom_line(aes(time, value, colour = state)) + ) +} else { + print(head(r, 100)) + print(tail(r, 100)) +} + +#if (FALSE) { +valid_int_vecs = m$engine_methods$int_vecs +valid_vars = m$init_mats +valid_funcs = macpan2:::valid_funcs +pp = macpan2:::make_expr_parser("pp", finalizer_char) +qq = macpan2:::make_expr_parser("qq", finalizer_index) +pp(~ state[infectious]) +qq(~ flow[infection, infectious])$parse_table +ff = function(x) qq(x)$parse_table +xx = m$expr_list$expr_list() +xx = append(xx, flow[infection] ~ trans %*% state[infectious]) +yy = (xx + |> lapply(macpan2:::lhs) + |> lapply(ff) +) +(Reduce(rbind, yy)[1:3] + |> setNames(sprintf("a_table_%s", c("x", "n", "i"))) + |> as.list() +) +vapply(yy, nrow, integer(1L)) + +sir_vax = Model(ModelFiles(system.file("starter_models", "sir_vax", package = "macpan2"))) +#sir = Model(ModelFiles(system.file("starter_models", "sir", package = "macpan2"))) +#oor_debug$flag("arguments", class = "Derivation") +# oor_debug$unflag("which_in") +# oor_debug$flag("which_not_in", class = "String") +# oor_debug$flag("order_by", class = "String") +dd = Derivations(sir_vax) +Rprof() +dd$during() +summaryRprof() + + + +f1 = function(x) x$x +f2 = function(x) x[["x"]] +f3 = function(x) x[[1L]] +x = list(x = 1:1000) +microbenchmark::microbenchmark( + f1(x), f2(x), f3(x) +) +debug(`%in%`) +Rprof() +summaryRprof() + +dd$before() +dd$after() + +dd = Derivation(sir_vax, 1L) +dd$expr_list() +do.call(mm, as.list(dd$all_arguments()[[1L]])) +(dd$all_arguments()[[1]]) +dd$get() +dd$var_list() +dd$output_labels() +dd$get()$expression +dd$arguments() +dd$argument_dots() +dd$all_arguments() + + +dg_sir = DerivationGrouping(sir) +#debug(dg_sir$unique_var_lists) +dg_sir$unique_var_lists() +dg_sir$var_list(1L) +dg_sir$spec_map() + +dg = DerivationGrouping(mf) +dg$n_derivations() +dg$spec_map() +dg$unique_var_lists() +dg$specs()[[4L]] +dg$var_list(1L)$vax + +Derivation = function(derivation, model) { + self = Base() + self$derivation = derivation + self$model = model + self$groups = function() { + grps = list() + for (g in self$derivation$group_names) { + grps[[g]] = self$model$variables$all()$filter(g + , .wrt = self$derivation$group_partition + ) + } + grps + } + return_object(self, "Derivation") +} +lapply(mf$derivations(), getElement, "group_names") +lapply(mf$derivations(), getElement, "group_partition") +dd = Derivation(mf$derivations()[[3L]], m) +dd$groups() +# debug(FlowExpander) +# oor_debug$flag("filter_flow") +# oor_debug$flag(".filter_blanks_not_special") + + +mf$derivations()[[1L]] + +m = Model(mf) +f = m$flows() +v = macpan2:::Variables(m) +debug(macpan2:::Variables) + +xx = macpan2:::FilterTraits() + +xx$.filter_blanks_not_special(m$variables$all()$frame(), "Epi", c("S", "I")) + +m$variables() +m$flows()[1L, "from"] +ss = m$variables$state() + +ss$filter(m$flows()[1L, "from"], .wrt = m$flows()[1L, "from_partition"]) +ss$filter(m$flows()[1L, "to"], .wrt = m$flows()[1L, "to_partition"]) +m$flows()[1L, "from_to_partition"] + + +xx = macpan2:::CompartmentalAlt(system.file("starter_models", "sir", package = "macpan2")) +y = xx$simulators$tmb(time_steps = 25L + , state = c(S = 99, I = 1, R = 0) + , flow = c(foi = NA, gamma = 0.1) + , beta = 0.2 + , N = empty_matrix +)# |> Euler() +y$print$expressions() + +y$report() + +SimulatorConstructor(epi_dir, + time_steps = 25L, + state = c(S = 999, E = 1, I = 0, R = 0), + flow = c(total_foi = NA, progression = 0.1, recovery = 0.05), + N = empty_matrix, + transmissability = 0.75, + per_capita_transmission = empty_matrix, + .mats_to_return = c("state") +) + + + +ff = Formula(flow[infection] ~ per_capita_transmission %*% state[infectious]) +ff$meth_list() +ff$expr_list() +ff$mat_args() + +ff = Formula(foi[e] ~ beta * I / N) +ff$could_be_getter() +ff$could_be_setter() + + +ff$could_be_getter() +ff$setter_name() +ff$getter_name() + +Formula(absolute ~ flow[absolute_flow], "xx")$getter_name() + +epi_dir = system.file("starter_models", "product_example", "Epi_model", package = "macpan2") +model = Compartmental(epi_dir) +model$indices$transmission$infectious_state() + +for (type in model$indices$flow$flow_types) { + model$indices$flow[[type]] +} +indices = model$indices +model$expr_list() + +m = TMBModel(time_steps = Time(100)) +m$insert$expressions() +f = flow[infection] ~ per_capita_transmission %*% state[infectious] +xx = macpan2:::MethodTypes() +y = try(xx$make_method(f, "f"), silent = TRUE) +y +meth_types = macpan2:::MethodTypes() +meth = meth_types$make_method(f, "set_infection_flows") +macpan2:::mat_vec_nms(f) +meth$int_vec_args +meth$mat_args + +xx$meth_mat_mult_to_rows$int_vec_args(f) +xx$meth_mat_mult_to_rows$int_vec_arg_nms +xx$meth_mat_mult_to_rows$mat_args(f) + + + +MatsExprMeth = function(init_mats = list(), int_vecs = list(), before = list(), during = list(), after = list()) { + meth_types = macpan2:::MethodTypes() + mat_nms = names(init_mats) + vec_nms = names(int_vecs) + sort_expr_list = function(expr_list) { + expr_args = list() + meth_args = list() + mat_args = character(0L) + vec_args = character(0L) + expr_nms = names(expr_list) + for (i in seq_along(expr_list)) { + meth_i = try(meth_types$make_method(expr_list[[i]], expr_nms[i]), silent = TRUE) + if (inherits(meth_i, "try-error")) { + expr_args = append(expr_args, expr_list[[i]]) + mat_args = append(mat_args, macpan2:::mat_vec_nms(expr_list[[i]])) + } else { + meth_args = c(meth_args, setNames(list(expr_list[[i]]), expr_nms[[i]])) + new_expr = macpan2:::two_sided() + # if (is_setter(meth_i$formula)) { + # + # } else if (is_getter(meth_i$formula)) { + # + # } + } + } + } +} + +TMBModel( + init_mats = MatsList( + state = c(S = 1 - 1e-5, E = 1e-5, I = 0, R = 0) + , flow = c(total_foi = NA_real_, progression = 0.1, recovery = 0.1) + , per_capita_transmission = 0.2 + , per_capita = empty_matrix + , per_capita_inflow = empty_matrix + , per_capita_outflow = empty_matrix + , absolute = empty_matrix + , absolute_inflow = empty_matrix + , absolute_outflow = empty_matrix + , total_inflow = empty_matrix + , total_outflow = empty_matrix + , . = empty_matrix + , .mats_to_save = c("state", "absolute") + , .mats_to_return = c("state", "absolute") + ), + expr_list = ExprList( + during = list( + . ~ set_infection_flow + , per_capita ~ get_per_capita + , per_capita_inflow ~ get_per_capita_inflow + , per_capita_outflow ~ get_per_capita_outflow + , absolute ~ get_absolute + , absolute_inflow ~ get_absolute_inflow + , absolute_outflow ~ get_absolute_outflow + , total_inflow ~ get_per_capita_state_in + get_per_capita_state_in + get_per_capita_inflow_state_in + get_absolute_inflow_state_in + , total_outflow ~ get_per_capita_state_out + get_absolute_state_out + get_per_capita_outflow_state_out + get_absolute_outflow_state_out + , state ~ state + total_inflow - total_outflow + ) + ), + engine_methods = EngineMethods( + exprs = list( + set_infection_flow = flow[infection] ~ per_capita_transmission %*% state[infectious] + , get_per_capita = ~ state[per_capita_from] * flow[per_capita_flow] + , get_absolute = ~ flow[absolute_flow] + , get_per_capita_inflow = ~ state[per_capita_inflow_from] * flow[per_capita_inflow_flow] + , get_per_capita_outflow = ~ state[per_capita_outflow_from] * flow[per_capita_outflow_flow] + , get_absolute_inflow = ~ flow[absolute_inflow_flow] + , get_absolute_outflow = ~ flow[absolute_outflow_flow] + , get_per_capita_state_in = ~ groupSums(per_capita, per_capita_to, state_length) + , get_absolute_state_in = ~ groupSums(absolute, absolute_to, state_length) + , get_per_capita_inflow_state_in = ~ groupSums(per_capita_inflow, per_capita_inflow_to, state_length) + , get_absolute_inflow_state_in = ~ groupSums(absolute_inflow, absolute_inflow_to, state_length) + , get_per_capita_state_out = ~ groupSums(per_capita, per_capita_from, state_length) + , get_absolute_state_out = ~ groupSums(absolute, absolute_from, state_length) + , get_per_capita_outflow_state_out = ~ groupSums(per_capita_outflow, per_capita_outflow_from, state_length) + , get_absolute_outflow_state_out = ~ groupSums(absolute_outflow, absolute_outflow_from, state_length) + ), + int_vecs = IntVecs( + state_length = length(model$labels$state()) + + , per_capita_from = indices$flow$per_capita$from() + , per_capita_to = indices$flow$per_capita$to() + , per_capita_flow = indices$flow$per_capita$flow() + + , absolute_from = indices$flow$absolute$from() + , absolute_to = indices$flow$absolute$to() + , absolute_flow = indices$flow$absolute$flow() + + , per_capita_inflow_from = indices$flow$per_capita_inflow$from() + , per_capita_inflow_to = indices$flow$per_capita_inflow$to() + , per_capita_inflow_flow = indices$flow$per_capita_inflow$flow() + + , per_capita_outflow_from = indices$flow$per_capita_outflow$from() + , per_capita_outflow_flow = indices$flow$per_capita_outflow$flow() + + , absolute_inflow_to = indices$flow$absolute_inflow$to() + , absolute_inflow_flow = indices$flow$absolute_inflow$flow() + + , absolute_outflow_from = indices$flow$absolute_outflow$from() + , absolute_outflow_flow = indices$flow$absolute_outflow$flow() + + , infection = model$indices$transmission$infection_flow() + , infectious = model$indices$transmission$infectious_state() + + ) + ), + time_steps = Time(100L) +) +s = m$simulator() +r = s$report(.phases = "during") +if (interactive()) { + (r + |> filter(row == "I") + |> ggplot() + + geom_line(aes(time, value)) + ) +} else { + print(r) +} + +filter(r, matrix == "absolute") + + +if (FALSE) { +m = TMBModel( + init_mats = MatsList( + state = c(S = 1 - 1e-2, I = 1e-2, R = 0) + , flow = c(infection = 0, recovery = 0.1) + , per_capita_transmission_matrix = 0.15 + , per_capita = empty_matrix + , beta_ts = c(0.11, 0.2, 0.4) + , dummy = empty_matrix + , .mats_to_save = "state" + , .mats_to_return = "state" + ), + expr_list = ExprList( + during = list( + per_capita_transmission_matrix ~ beta + , dummy ~ update_infection_flows + , per_capita ~ from_states * flow + , state ~ state - outflow + inflow + ) + ), + time = Time(150), + engine_methods = EngineMethods( + exprs = list( + from_states = ~ state[from_indices] + , infectious_states = ~ state[infectious_indices] + , update_infection_flows = flow[infection_indices] ~ + per_capita_transmission_matrix %*% state[infectious_indices] + , inflow = ~ groupSums(per_capita, to_indices, state_length) + , outflow = ~ groupSums(per_capita, from_indices, state_length) + , beta = ~ time_var(beta_ts, beta_cp, beta_n, beta_group) + ), + int_vecs = IntVecs( + from_indices = 0:1 + , to_indices = 1:2 + , state_length = 3L + , infectious_indices = 1L + , infection_indices = 0L + , beta_cp = c(0L, 50L, 100L) + , beta_n = 1L + , beta_group = 0L + ) + ) +) +m$data_arg() +s = m$simulator() +r = s$report(.phases = "during") +if (interactive()) { + (r + |> filter(row == "I") + |> ggplot() + + geom_line(aes(time, value)) + ) +} else { + print(r) +} +} +} diff --git a/misc/experiments/runge_kutta/runge_kutta.R b/misc/experiments/runge_kutta/runge_kutta.R new file mode 100644 index 00000000..fc534ba3 --- /dev/null +++ b/misc/experiments/runge_kutta/runge_kutta.R @@ -0,0 +1,38 @@ +library(macpan2) +sir_vax = Compartmental(system.file("starter_models", "sir_vax", package = "macpan2")) +sir_vax_sim = sir_vax$simulators$tmb(time_steps = 100L + , state = c(S.unvax = 99, I.unvax = 1, R.unvax = 0, S.vax = 0, I.vax = 0, R.vax = 0) + , flow = c( + infection.unvax = 0, infection.vax = 0 + , gamma.unvax = 0.1, gamma.vax = 0.1 + , .vax_rate = 0.1 + ) + , sigma.unvax = 1 + , sigma.vax = 0.01 + , beta.unvax = 0.2 + , beta.vax = 0.2 + , foi.unvax = empty_matrix + , foi.vax = empty_matrix + , foi. = empty_matrix + , N.unvax = empty_matrix + , N.vax = empty_matrix +) + + +sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) + + +simulator = sir$simulators$tmb(time_steps = 50 + , state = c(S = 99, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.2) + , N = empty_matrix + , beta = 0.8 +) + + +dd = DerivationExtractor(sir_vax) +g = dd$extract_derivations()[[2]] +length(g$arguments) + +g$outputs + diff --git a/misc/experiments/synchronization/synchronization.R b/misc/experiments/synchronization/synchronization.R new file mode 100644 index 00000000..d0893171 --- /dev/null +++ b/misc/experiments/synchronization/synchronization.R @@ -0,0 +1,169 @@ +library(macpan2) +library(oor) + +CachedMethod = function(object, method) { + self = Base() + self$object = object + self$method = method + + self$invalidate = function() self$.valid = FALSE + self$get = function() { + if (!isTRUE(self$.valid)) { + mc = match.call() + mc[[1L]] = self$method + self$.cache = eval(mc) + self$.valid = TRUE + } + return(self$.cache) + } + + self$invalidate() + formals(self$get) = formals(method) + + return_object(self, "CachedMethod") +} + +initialize_cache = function(object, ...) { + self = Base() + self$object = object + + ## list of methods that will get a cache + self$method_names = unlist(lapply(list(...), as.character), recursive = TRUE) + + ## create a cache for each method + for (nm in self$method_names) self[[nm]] = CachedMethod(object, object[[nm]]) + + ## initialize the cache + self$object$cache = return_object(self, "MethodsCache") + + ## replace the methods in the object with ones that look in the cache instead + for (nm in self$method_names) { + + ## replace function definition + self$object[[nm]] = function(nm) { + force(nm) + function() { + mc = match.call() + mc[[1L]] = self[[nm]]$get + eval(mc) + } + }() + + ## set the argument list of the method to be identical + ## to that of the get method in the cache + formals(self$object[[nm]]) = formals(self[[nm]]$get) + } +} + +A = function(x) { + self = Base() + self$x = x + self$y = function() self$x + 1 + initialize_cache(self, "y") + return_object(self, "A") +} + +a = A(1) +a$x = 200 +a$cache$y$invalidate() +a$cache$y$get() +a$y() + + +A = function(x) { + self = Base() + self$x = x + self$y = CachedMethod(self, function() { + self$x + 1 + }) + self$update_x = function(x) { + self$x = x + self$y$invalidate() + } + return_object(self, "A") +} + +a = A(2) +a$y$get() + +a$x = 20 +a$y$get() + +a$update_x(20) +a$y$get() + + + + +library(macpan2) +library(oor) + +MethodsCache = function(...) { + self = Base() + return_object(self, "MethodsCache") +} + +cache_methods = function(self, ...) { + self$methods_cache = MethodsCache(...) + method_names = unlist(lapply(list(...), as.character), recursive = TRUE) + self$.valid = setNames(logical(length(method_names)), method_names) + for (m in method_names) { + + } + + self$invalidate = function() self$.valid = FALSE + self$get = function() { + if (!isTRUE(self$.valid)) { + mc = match.call() + mc[[1L]] = self$method + self$.cache = eval(mc) + self$.valid = TRUE + } + return(self$.cache) + } + + self$invalidate() + formals(self$get) = formals(method) + + return_object(self, "CachedMethod") +} + + + +A = function(x) { + self = Base() + self$x = x + self$y = CachedMethod(self, function() { + self$x + 1 + }) + self$update_x = function(x) { + self$x = x + self$y$invalidate() + } + return_object(self, "A") +} + +a = A(2) +a$y$get() + +a$x = 20 +a$y$get() + +a$update_x(20) +a$y$get() + + + + +cc = FALSE + +xx = 1 +yy = 2 + +ff = function() { + zz = xx + 1 + zz * 2 +} + +append(body(ff), expression(xx = 52), 2) +## modify the body of the function by placing cache checking lines at the beginning From bc97337c96913ac6a626b8af1d34e1d6256a3a90 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 09:57:42 -0400 Subject: [PATCH 025/332] old dev experiments --- misc/dev/dev-change.R | 14 + misc/dev/dev-dynamic-allocation.R | 31 +- misc/dev/dev-hello-world.R | 4 + misc/dev/dev-pre-alloc-hist.R | 128 ++ misc/dev/dev-rk4.R | 44 + misc/dev/dev-time-index-memory-leak.R | 8 + misc/dev/dev.cpp | 604 ++++--- misc/dev/dev_perf.cpp | 2302 ++++++++++++++++++++++++ misc/dev/dev_perf_testing.cpp | 2310 +++++++++++++++++++++++++ misc/dev/dev_pre_alloc_hist.cpp | 2305 ++++++++++++++++++++++++ 10 files changed, 7474 insertions(+), 276 deletions(-) create mode 100644 misc/dev/dev-change.R create mode 100644 misc/dev/dev-hello-world.R create mode 100644 misc/dev/dev-pre-alloc-hist.R create mode 100644 misc/dev/dev-rk4.R create mode 100644 misc/dev/dev-time-index-memory-leak.R create mode 100644 misc/dev/dev_perf.cpp create mode 100644 misc/dev/dev_perf_testing.cpp create mode 100644 misc/dev/dev_pre_alloc_hist.cpp diff --git a/misc/dev/dev-change.R b/misc/dev/dev-change.R new file mode 100644 index 00000000..220a3d31 --- /dev/null +++ b/misc/dev/dev-change.R @@ -0,0 +1,14 @@ +library(macpan2) + +sims = simple_sims( + iteration_exprs = list( + j ~ change(j, change_points), + y ~ x[j] + ), + time_steps = 10, + j = 0, + change_points = c(0, 4, 7), + x = c(42, pi, sqrt(2)), + y = empty_matrix +) +sims diff --git a/misc/dev/dev-dynamic-allocation.R b/misc/dev/dev-dynamic-allocation.R index 152eb912..657f3a2c 100644 --- a/misc/dev/dev-dynamic-allocation.R +++ b/misc/dev/dev-dynamic-allocation.R @@ -3,29 +3,50 @@ library(macpan2) ## choose c++ code to use: ## 1. "macpan2" to use the c++ in the installed package ## 2. "dev" to use the file for development in misc/dev/dev.cpp -tmb_cpp = "macpan2" +tmb_cpp = "dev" ## if tmb_cpp = "dev", then you will need to compile it with this command -macpan2:::dev_compile() +#print(macpan2:::dev_compile(suffix = "", ext = "cpp")) +#TMB::compile("dev.cpp") +dyn.load(TMB::dynlib("misc/dev/dev")) ## for profiling on SW's setup -#dyn.load("/usr/local/lib/libprofiler.0.dylib") +dyn.load("/usr/local/lib/libprofiler.0.dylib") +# export R_HOME=/usr/local/Cellar/r/4.2.3/lib/R +# CPUPROFILE="million.log" /usr/local/Cellar/r/4.2.3/lib/R/bin/exec/R -f misc/dev/dev-dynamic-allocation.R +# pprof --dot /usr/local/Cellar/r/4.2.3/lib/R/bin/exec/R million.log > million.dot +# dot -Tpng million.dot -o million.png sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) N = 100 -time_steps = 1000000 ## Increase this number to cause allocation problems +time_steps = 1000 ## Increase this number to cause allocation problems # -------- +left_make = Sys.time() simulator = sir$simulators$tmb(time_steps = time_steps , state = c(S = N - 1, I = 1, R = 0) , flow = c(foi = 0, gamma = 0.2) , N = N , beta = 0.4 - , .mats_to_save = c("state", "flow", "N", "beta") + #, .mats_to_save = c("state", "flow", "N", "beta") + , .mats_to_save = c("state", "total_inflow") + , .mats_to_return = c("state", "total_inflow") , .tmb_cpp = tmb_cpp + , .initialize_ad_fun = TRUE ) +right_make = Sys.time() simulator$print$matrix_dims() simulator$print$expressions() +left_ad = Sys.time() +ff = simulator$ad_fun() +right_ad = Sys.time() + +left_run = Sys.time() sims = simulator$report(.phases = "during") +right_run = Sys.time() head(sims) + +right_make - left_make +right_ad - left_ad +right_run - left_run diff --git a/misc/dev/dev-hello-world.R b/misc/dev/dev-hello-world.R new file mode 100644 index 00000000..7bc166ea --- /dev/null +++ b/misc/dev/dev-hello-world.R @@ -0,0 +1,4 @@ +library(macpan2) +library(oor) + + diff --git a/misc/dev/dev-pre-alloc-hist.R b/misc/dev/dev-pre-alloc-hist.R new file mode 100644 index 00000000..d52ab0b8 --- /dev/null +++ b/misc/dev/dev-pre-alloc-hist.R @@ -0,0 +1,128 @@ +library(macpan2) +alt_cpp = FALSE +use_dev = FALSE +if (use_dev) { + if (alt_cpp) { + #TMB::compile("misc/dev/dev_pre_alloc_hist.cpp") + dyn.load(TMB::dynlib("misc/dev/dev_pre_alloc_hist")) + } else { + #TMB::compile("misc/dev/dev.cpp") + dyn.load(TMB::dynlib("misc/dev/dev")) + } +} +#dyn.load("/usr/local/lib/libprofiler.0.dylib") +# export R_HOME=/usr/local/Cellar/r/4.2.3/lib/R +# CPUPROFILE="prealloc.log" /usr/local/Cellar/r/4.2.3/lib/R/bin/exec/R -f misc/dev/dev-pre-alloc-hist.R +# pprof --dot /usr/local/Cellar/r/4.2.3/lib/R/bin/exec/R prealloc.log > prealloc.dot +# dot -Tpng prealloc.dot -o prealloc.png + +sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) +N = 100 +time_steps = 1000 ## Increase this number to cause allocation problems + +print("-----------") +cat("\n") +print("simulator constructor") +left = Sys.time() +simulator = sir$simulators$tmb(time_steps = time_steps + , state = c(S = N - 1, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.2) + , N = N + , beta = 0.4 + , .mats_to_return = c("state", "total_inflow") + #, .mats_to_return = character() + #, .initialize_ad_fun = FALSE +) +simulator$replace$obj_fn(~sum((state / N)^2)) +simulator$replace$params(default = 0.41, mat = "beta") +right = Sys.time() +print(right - left) + +simulator$report() + +if (use_dev) { + if (alt_cpp) { + aa = simulator$tmb_model$make_ad_fun_arg(tmb_cpp = "dev_pre_alloc_hist") + mm = rep(list(empty_matrix), length(aa$data$mats)) + mm[[1L]] = aa$data$mats[[1L]] + aa$data$mats_shape = mm + } else { + aa = simulator$tmb_model$make_ad_fun_arg(tmb_cpp = "dev") + } +} else { + aa = simulator$tmb_model$make_ad_fun_arg(tmb_cpp = "macpan2") +} + +cat("\n") +print("-----------") +print("make ad fun") +left = Sys.time() +ff = do.call(TMB::MakeADFun, aa) +right = Sys.time() +print(right - left) + +# cat("\n") +# print("-----------") +# print("simulations") +# left = Sys.time() +# rr = ff$report() +# print(dim(rr$value)) +# right = Sys.time() +# print(right - left) +# +# cat("\n") +# print("-----------") +# print("objective function") +# left = Sys.time() +# ff$fn() +# right = Sys.time() +# print(right - left) +# +# cat("\n") +# print("-----------") +# print("gradient") +# left = Sys.time() +# ff$gr() +# right = Sys.time() +# print(right - left) +# +# cat("\n") +# print("-----------") +# print("hessian") +# left = Sys.time() +# ff$he() +# right = Sys.time() +# print(right - left) + +cat("\n") +print("-----------") +print("simulations new pars") +left = Sys.time() +rr = ff$report(0.45) +print(dim(rr$value)) +right = Sys.time() +print(right - left) + +# cat("\n") +# print("-----------") +# print("objective function new pars") +# left = Sys.time() +# ff$fn(0.5) +# right = Sys.time() +# print(right - left) +# +# cat("\n") +# print("-----------") +# print("gradient new pars") +# left = Sys.time() +# ff$gr(0.2) +# right = Sys.time() +# print(right - left) +# +# cat("\n") +# print("-----------") +# print("hessian new pars") +# left = Sys.time() +# ff$he(0.33) +# right = Sys.time() +# print(right - left) diff --git a/misc/dev/dev-rk4.R b/misc/dev/dev-rk4.R new file mode 100644 index 00000000..9b6bc21c --- /dev/null +++ b/misc/dev/dev-rk4.R @@ -0,0 +1,44 @@ +library(macpan2) +sir = Compartmental(file.path("inst", "starter_models", "sir")) +N = 100 +time_steps = 100 + +derivation_to_math = function(derivation) { + has_arguments = !is.null(derivation$arguments) + has_dots = !is.null(derivation$argument_dots) + if (has_arguments & has_dots) { + return(MathExpressionFromStrings(derivation$expression, + derivation$arguments, include_dots = TRUE)) + } + else if (has_arguments) { + return(MathExpressionFromStrings(derivation$expression, + derivation$arguments, include_dots = FALSE)) + } + else if (has_dots) { + return(MathExpressionFromStrings(derivation$expression, + include_dots = TRUE)) + } + else stop("Derivations file appears invalid, no arguments or argument dots.") +} +order_by_phase = function(...) { + phases = as.character(unlist(list(...), use.names = FALSE)) + function(...) { + derivation_list = unlist(list(...), recursive = FALSE, use.names = FALSE) + is_phase = function(phase) function(derivation) derivation$simulation_phase == phase + filter_phase = function(phase) Filter(is_phase(phase), derivation_list) + Reduce(c, lapply(phases, filter_phase)) + } +} +all_derivations = function(model) c(model$derivations(), StandardExpr(model)$as_derivations()) +step_math_list = (sir + |> all_derivations() + |> order_by_phase("during_pre_update", "during_update")() + |> lapply(derivation_to_math) +) + +s = sir$simulators$tmb(time_steps = 10 + , state = c(S = 99, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.1) + , N = 0, beta = 0.2 +) +s$tmb_model$expr_list$during diff --git a/misc/dev/dev-time-index-memory-leak.R b/misc/dev/dev-time-index-memory-leak.R new file mode 100644 index 00000000..a7feab84 --- /dev/null +++ b/misc/dev/dev-time-index-memory-leak.R @@ -0,0 +1,8 @@ +library(macpan2) +library(TMB) +cpp = macpan2:::dev_choose_cpp() +x = 0.1 * (1:5) +engine_eval(~x[-1] + , x = x + , .tmb_cpp = cpp +) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index aef4bff5..9cbc965a 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -151,71 +151,68 @@ void printMatrix(const matrix& mat) { // Helper function -template -int CheckIndices( - matrix& x, - matrix& rowIndices, - matrix& colIndices -) { - int rows = x.rows(); - int cols = x.cols(); - Type maxRowIndex = rowIndices.maxCoeff(); - Type maxColIndex = colIndices.maxCoeff(); - Type minRowIndex = rowIndices.minCoeff(); - Type minColIndex = colIndices.minCoeff(); - - if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { - return 0; - } - return 1; -} - -// Helper function -template -int RecycleInPlace( - matrix& mat, - int rows, - int cols -) { - #ifdef MP_VERBOSE - std::cout << "recycling ... " << std::endl; - #endif - if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. - return 0; - - matrix m(rows, cols); - if (mat.rows()==1 && mat.cols()==1) { - m = matrix::Constant(rows, cols, mat.coeff(0,0)); - } - else if (mat.rows()==rows) { - if (mat.cols()==1) { - #ifdef MP_VERBOSE - std::cout << "recycling columns ... " << std::endl; - #endif - for (int i=0; i +// int CheckIndices( +// matrix& x, +// matrix& rowIndices, +// matrix& colIndices +// ) { +// int rows = x.rows(); +// int cols = x.cols(); +// Type maxRowIndex = rowIndices.maxCoeff(); +// Type maxColIndex = colIndices.maxCoeff(); +// Type minRowIndex = rowIndices.minCoeff(); +// Type minColIndex = colIndices.minCoeff(); +// +// if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { +// return 0; +// } +// return 1; +// } + +// // Helper function +// template +// int RecycleInPlace( +// matrix& mat, +// int rows, +// int cols +// ) { +// #ifdef MP_VERBOSE +// std::cout << "recycling ... " << std::endl; +// #endif +// if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. +// return 0; +// +// matrix m(rows, cols); +// if (mat.rows()==1 && mat.cols()==1) { +// m = matrix::Constant(rows, cols, mat.coeff(0,0)); +// } +// else if (mat.rows()==rows) { +// if (mat.cols()==1) { +// #ifdef MP_VERBOSE +// std::cout << "recycling columns ... " << std::endl; +// #endif +// for (int i=0; i struct ListOfMatrices { @@ -418,10 +415,10 @@ matrix getNthMat( } -template +template class ArgList { public: - using ItemType = std::variant, std::vector>; + using ItemType = std::variant, std::vector>; ArgList(int size) : items_(size), size_(size) {} @@ -432,13 +429,13 @@ class ArgList { items_[index] = item; } - matrix get_as_mat(int i) { + matrix get_as_mat(int i) const { if (i < 0 || i >= items_.size()) { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { - return std::get>(items_[i]); + if (std::holds_alternative>(items_[i])) { + return std::get>(items_[i]); } else { throw std::runtime_error("Item at index is not a matrix"); } @@ -452,7 +449,7 @@ class ArgList { if (std::holds_alternative>(items_[i])) { return std::get>(items_[i]); } else { - matrix m = get_as_mat(i); + matrix m = get_as_mat(i); std::vector v(m.rows()); for (int i=0; i operator[](int i) { + matrix operator[](int i) { return get_as_mat(i); } + // Method to recycle elements, rows, and columns to make operands compatible for binary operations + ArgList recycle_for_bin_op() const { + ArgList result = *this; // Create a new ArgList as a copy of the current instance + + matrix mat0 = result.get_as_mat(0); + matrix mat1 = result.get_as_mat(1); + + if (mat0.rows() == mat1.rows()) { + if (mat0.cols() != mat1.cols()) { + if (mat0.cols() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat0; + mat0 = mat1; // for the shape + for (int i = 0; i < mat0.cols(); i++) { + mat0.col(i) = m.col(0); + } + } else if (mat1.cols() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat1; + //result.set(1, mat0); // Set for the shape + mat1 = mat0; + for (int i = 0; i < mat1.cols(); i++) { + mat1.col(i) = m.col(0); + } + } else { + result.set_error_code(201); // Set the error code for "The two operands do not have the same number of columns" + } + } + // else: do nothing + } else { + if (mat0.cols() == mat1.cols()) { // Only one compatible dimension + if (mat0.rows() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat0; + mat0 = mat1; + for (int i = 0; i < mat0.rows(); i++) { + mat0.row(i) = m.row(0); + } + } else if (mat1.rows() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat1; + mat1 = mat0; + for (int i = 0; i < mat0.rows(); i++) { + mat1.row(i) = m.row(0); + } + } else { + result.set_error_code(202); // Set the error code for "The two operands do not have the same number of rows" + } + } else { // No dimensions are equal + if (mat0.rows() == 1 && mat0.cols() == 1) { // Scalar vs non-scalar + Type s = mat0.coeff(0, 0); + mat0 = mat1; + mat0.setConstant(s); + } else if (mat1.rows() == 1 && mat1.cols() == 1) { // Scalar vs non-scalar + Type s = mat1.coeff(0, 0); + mat1 = mat0; + mat1.setConstant(s); + } else { + result.set_error_code(203); // Set the error code for "The two operands do not have the same number of columns or rows" + } + } + } + result.set(0, mat0); + result.set(1, mat1); + return result; + } + + + // Method to recycle elements of all arguments so that they match a given shape + ArgList recycle_to_shape(const std::vector& indices, int rows, int cols) const { + ArgList result = *this; // Create a new ArgList as a copy of the current instance + + int error_code = 0; // Initialize the error code + + for (int index : indices) { + matrix mat = result.get_as_mat(index); + + if (mat.rows() == rows && mat.cols() == cols) { + // No further action needed for this matrix + continue; + } + + matrix m(rows, cols); + + if (mat.rows() == 1 && mat.cols() == 1) { + m = matrix::Constant(rows, cols, mat.coeff(0, 0)); + } else if (mat.rows() == rows) { + if (mat.cols() == 1) { + for (int i = 0; i < cols; i++) { + m.col(i) = mat.col(0); + } + } else { + error_code = 501; + break; // Exit the loop on error + } + } else if (mat.cols() == cols) { + if (mat.rows() == 1) { + for (int i = 0; i < rows; i++) { + m.row(i) = mat.row(0); + } + } else { + error_code = 501; + break; // Exit the loop on error + } + } else { + error_code = 501; + break; // Exit the loop on error + } + + if (error_code != 0) { + result.set_error_code(error_code); + break; // Exit the loop on error + } + + // If recycling is successful, update the result ArgList object + result.set(index, m); + } + + return result; + } + + int check_indices(int mat_index, const std::vector& row_indices, const std::vector& col_indices) const { + if (mat_index < 0 || mat_index >= items_.size()) { + return 2; // Return an error code for an invalid index + } + + matrix x = get_as_mat(mat_index); + int rows = x.rows(); + int cols = x.cols(); + + for (int row_index : row_indices) { + if (row_index < 0 || row_index >= rows) { + return 1; // Indices are not valid + } + } + + for (int col_index : col_indices) { + if (col_index < 0 || col_index >= cols) { + return 1; // Indices are not valid + } + } + + return 0; // Indices are valid + } + + // Method to set an error code + void set_error_code(int error) { + error_code_ = error; + } + + // Getter for the error code + int get_error_code() const { + return error_code_; + } + + private: std::vector items_; int size_; + int error_code_ = 0; // Initialize the error code to 0 (no error) by default }; @@ -542,9 +692,9 @@ class ExprEvaluator { vector u; matrix Y, X, A; matrix timeIndex; // for rbind_time - Type sum, s, eps, var, by; // intermediate scalars + Type sum, eps, var, by; // intermediate scalars int rows, cols, lag, rowIndex, colIndex, matIndex, grpIndex, reps, cp, off, size; - int sz, start, err_code, err_code1, err_code2, curr_meth_id; + int sz, start, err_code, curr_meth_id; // size_t numMats; // size_t numIntVecs; std::vector curr_meth_mat_id_vec; @@ -559,65 +709,29 @@ class ExprEvaluator { switch (table_n[row]) { case -2: // methods (pre-processed matrices) - //std::cout << "---------------" << std::endl; - //std::cout << "IN METHODS CASE" << std::endl; - //std::cout << "---------------" << std::endl; - - // METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" - // METH_TO_ROWS = 2 // Y[i] ~ X, c("Y", "X"), "i" - // METH_ROWS_TO_ROWS = 3 // Y[i] ~ X[j], c("Y", "X"), c("i", "j") - // METH_MAT_MULT_TO_ROWS = 4 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") - // METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") - // METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") - // METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") - curr_meth_id = table_x[row]; switch(meth_type_id[curr_meth_id]) { case METH_FROM_ROWS: - //std::cout << "-----------------" << std::endl; - //std::cout << "IN METH_FROM_ROWS" << std::endl; - //std::cout << "-----------------" << std::endl; m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); - //printMatrix(m); v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); - //printIntVectorWithLabel(v, ""); m1 = matrix::Zero(v.size(), m.cols()); for (int i=0; i::Zero(v1.size(), m1.cols()); - - //std::cout << "Y index" << matIndex << std::endl; - //std::cout << "A" << m << std::endl; - //std::cout << "X" << m1 << std::endl; - //printIntVectorWithLabel(v, "i"); - //printIntVectorWithLabel(v1, "j"); - for (int i=0; i::Zero(nrow,ncol); for (int i=0; i::Zero(rows, cols); for (int i=0; ispi[0]] @@ -2036,13 +2082,17 @@ class ExprEvaluator { } rows = args[0].rows(); cols = args[0].cols(); + v1.push_back(1); + v1.push_back(2); + args = args.recycle_to_shape(v1, rows, cols); + err_code = args.get_error_code(); // err_code1 = RecycleInPlace(args[1], rows, cols); // err_code2 = RecycleInPlace(args[2], rows, cols); // err_code = err_code1 + err_code2; - // if (err_code != 0) { - // SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); - // return m; - // } + if (err_code != 0) { + SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + return m; + } m = matrix::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i m; std::vector v1; std::vector v2; - std::cout << "---- assignment ----" << std::endl; - std::cout << "n: " << n << std::endl; - std::cout << "x: " << n << std::endl; - if (n == -1) { - Rf_error("trying to assign to a literal, which is not allowed"); - } else if (n == -2) { - Rf_error("trying to assign to an engine method, which is not allowed"); - } else if (n == -3) { - Rf_error("trying to assign to an integer vector, which is not allowed"); - } else if (n == 1) { - Rf_error("assignment error -- TODO: be more specific"); - } else if (n == 0) { - valid_vars.m_matrices[x] = assignment_value; - } else { - if (x + 1 != MP2_SQUARE_BRACKET) { - Rf_error("square bracket is the only function allowed on the left-hand-side"); - } - if (n == 3) { - // table_n[table_i[row] + 2] -- need to see what kind of thing we have - v2 = valid_int_vecs[table_x[table_i[row] + 2]]; - } else if (n == 2) { + // std::cout << "---- assignment ----" << std::endl; + // std::cout << "n: " << n << std::endl; + // std::cout << "x: " << n << std::endl; + switch (n) { + case -1: + Rf_error("trying to assign to a literal, which is not allowed"); + case -2: + Rf_error("trying to assign to an engine method, which is not allowed"); + case -3: + Rf_error("trying to assign to an integer vector, which is not allowed"); + case 1: + Rf_error("assignment error -- TODO: be more specific"); + case 0: + valid_vars.m_matrices[x] = assignment_value; + return; + case 2: + if (table_n[table_i[row] + 1] != -3) { + Rf_error("indexing on the left-hand-side needs to be done using integer vectors"); + } v2.push_back(0); - } else { - Rf_error("incorrect numbers of arguments"); - } - int x1 = table_x[table_i[row]]; - m = valid_vars.m_matrices[x1]; - std::cout << "matrix: " << m << std::endl; - v1 = valid_int_vecs[table_x[table_i[row] + 1]]; - printIntVector(v1); - printIntVector(v2); - std::cout << "value: " << assignment_value << std::endl; - for (int i = 0; i < v1.size(); i++) { - for (int j = 0; j < v2.size(); j++) { - m.coeffRef(v1[i], v2[j]) = assignment_value.coeff(i, j); + case 3: + if (x + 1 != MP2_SQUARE_BRACKET) { + Rf_error("square bracket is the only function allowed on the left-hand-side"); } - } - valid_vars.m_matrices[x1] = m; - //valid_vars.m_matrices[matIndex].coeffRef(rowIndex,colIndex) = args[3].coeff(k,0); - } + x1 = table_x[table_i[row]]; + if (n == 3) v2 = valid_int_vecs[table_x[table_i[row] + 2]]; + m = valid_vars.m_matrices[x1]; + v1 = valid_int_vecs[table_x[table_i[row] + 1]]; + for (int i = 0; i < v1.size(); i++) { + for (int j = 0; j < v2.size(); j++) { + m.coeffRef(v1[i], v2[j]) = assignment_value.coeff(i, j); + } + } + valid_vars.m_matrices[x1] = m; + return; + default: + Rf_error("incorrect numbers of arguments"); + } // switch (n) }; }; diff --git a/misc/dev/dev_perf.cpp b/misc/dev/dev_perf.cpp new file mode 100644 index 00000000..acc43478 --- /dev/null +++ b/misc/dev/dev_perf.cpp @@ -0,0 +1,2302 @@ +// #define MP_VERBOSE +#define EIGEN_PERMANENTLY_DISABLE_STUPID_WARNINGS +#include +#include +#include +#include +#include +#include +#include // isnan() is defined +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// Macpan2 is redesigned architecture. The spec is +// https://canmod.net/misc/cpp_side.html +// +// Operands in a math expression and its intermediate/final results are all +// generalized as matrices, i.e., +// scalar as 1x1 matrix +// vector as mx1 or 1xn matrix +// matrix as mxn matrix +// so that we can unified the definition of function EvalExpr. The Eigen library +// (https://eigen.tuxfamily.org/dox/group__TutorialMatrixClass.html) has similar +// generalization idea. +// +// Elementwise Binary Operators (+ - * / ^) +// + if both operands have the same shape, then do the elementwise operation +// + elif one of the operands is a scalar, then do the operation of scalar vs +// elements of the other operand +// + elif one of the operands is a vector, then +// - if the length of the vector is equal to the corresponding dimension of the +// other operand, then do row- or column-wise element-vs-element operation. +// - else ERROR +// + else ERROR +// +// Matrix Binary operations (%*%) +// + if the second dimension of first operand == the first dimension of +// the second operand, then do matrix multiplication +// + else ERROR +// NOTE: 1 Inner product can be made by row_vec %*% col_vec while outer product +// can be made by col_vec %*% row_vec. +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Functions we Support +// Please follow exactly the format below when adding a new function: +// MP2_CPP_CASE_NAME = {case number} // {R function name} +// Note that the last entry does not have the comma that follows +// {case number}. +// If an entry in this enum does not have an associated case in the +// switch(table_x[row]+1) statement, then it _must_ be commented out. +// An analogous R-side list can be automatically produced using the +// package makefile. The ordering of the functions must follow their +// associated integers, and the integers must increase from 1 with no +// gaps. Therefore, you should add any new functions at the end, +// although it is possible to change the order of the functions as +// long as the integers increase from 1 without gaps as long as the +// R/enum.R file is regenerated. +enum macpan2_func { + MP2_ADD = 1 // binop,null: `+`(x, y) + , MP2_SUBTRACT = 2 // binop,null: `-`(x, y) + , MP2_MULTIPLY = 3 // binop,null: `*`(x, y) + , MP2_DIVIDE = 4 // binop,null: `/`(x, y) + , MP2_POWER = 5 // binop,null: `^`(x, y) + , MP2_EXP = 6 // fwrap,null: exp(x) + , MP2_LOG = 7 // fwrap,null: log(x) + , MP2_ROUND_BRACKET = 8 // null,null: `(`(...) + , MP2_COMBINE = 9 // null,null: c(...) + , MP2_MATRIX = 10 // fwrap,null: matrix(x, i, j) + , MP2_MATRIX_MULTIPLY = 11 // binop,null: `%*%`(x, y) + , MP2_SUM = 12 // null,null: sum(...) + , MP2_REPLICATE = 13 // fwrap,null: rep(x, times) + , MP2_ROWSUMS = 14 // fwrap,null: rowSums(x) + , MP2_COLSUMS = 15 // fwrap,null: colSums(x) + , MP2_GROUPSUMS = 16 // fwrap,null: groupSums(x, f, n) + , MP2_SQUARE_BRACKET = 17 // null,null: `[`(x, i, j) + , MP2_BLOCK = 18 // fwrap,fail: block(x, i, j, n, m) + , MP2_TRANSPOSE = 19 // fwrap,null: t(x) + , MP2_RBIND_TIME = 20 // fwrap,fail: rbind_time(x, t, t_min) + , MP2_RBIND_LAG = 21 // fwrap,fail: rbind_lag(x, lag, t_min) + , MP2_CBIND_TIME = 22 // fwrap,fail: cbind_time(x, t, t_min) + , MP2_CBIND_LAG = 23 // fwrap,fail: cbind_lag(x, lag, t_min) + , MP2_COLON = 24 // null,null: `:`(from, to) + , MP2_SEQUENCE = 25 // fwrap,fail: seq(from, length, by) + , MP2_CONVOLUTION = 26 // fwrap,fail: convolution(x, k) + , MP2_CBIND = 27 // fwrap,null: cbind(...) + , MP2_RBIND = 28 // fwrap,null: rbind(...) + , MP2_TIME_STEP = 29 // fwrap,fail: time_step(lag) + , MP2_ASSIGN = 30 // fwrap,null: assign(x, i, j, v) + , MP2_UNPACK = 31 // fwrap,fail: unpack(x, ...) + , MP2_RECYCLE = 32 // fwrap,null: recycle(x, rows, cols) + , MP2_CLAMP = 33 // fwrap,null: clamp(x, eps) + , MP2_POISSON_DENSITY = 34 // fwrap,fail: dpois(observed, simulated) + , MP2_NEGBIN_DENSITY = 35 // fwrap,fail: dnbinom(observed, simulated, over_dispersion) + , MP2_NORMAL_DENSITY = 36 // fwrap,fail: dnorm(observed, simulated, standard_deviation) + , MP2_POISSON_SIM = 37 // fwrap,fail: rpois(mean) + , MP2_NEGBIN_SIM = 38 // fwrap,fail: rnbinom(mean, over_dispersion) + , MP2_NORMAL_SIM = 39 // fwrap,fail: rnorm(mean, standard_deviation) + , MP2_KRONECKER = 40 // binop,null: `%x%`(x, y) + , MP2_TO_DIAG = 41 // fwrap,fail: to_diag(x) + , MP2_FROM_DIAG = 42 // fwrap,fail: from_diag(x) + , MP2_TIME_GROUP = 43 //fwrap,fail: time_group(i, change_points) + , MP2_COS = 44 // fwrap,null: cos(x) + //, MP2_LOGISTIC = 45 // fwrap,null: logistic(x) + //, MP2_LOGIT = 46 // fwrap,null: logit(x) +}; + +// Helper function +template +int CheckIndices( + matrix& x, + matrix& rowIndices, + matrix& colIndices +) { + int rows = x.rows(); + int cols = x.cols(); + Type maxRowIndex = rowIndices.maxCoeff(); + Type maxColIndex = colIndices.maxCoeff(); + Type minRowIndex = rowIndices.minCoeff(); + Type minColIndex = colIndices.minCoeff(); + + if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { + return 0; + } + return 1; +} + +// Helper function +template +int RecycleInPlace( + matrix& mat, + int rows, + int cols +) { + #ifdef MP_VERBOSE + std::cout << "recycling ... " << std::endl; + #endif + if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. + return 0; + + matrix m(rows, cols); + if (mat.rows()==1 && mat.cols()==1) { + m = matrix::Constant(rows, cols, mat.coeff(0,0)); + } + else if (mat.rows()==rows) { + if (mat.cols()==1) { + #ifdef MP_VERBOSE + std::cout << "recycling columns ... " << std::endl; + #endif + for (int i=0; i +struct ListOfMatrices { + // below is a vector of matrices that passed from R + vector > m_matrices; + + ListOfMatrices(SEXP ii){ // Constructor + // Get elements by their indices + int n = length(ii); + vector > vs(n); + m_matrices = vs; + + for (int i = 0; i < n; i++) { + m_matrices[i] = asMatrix(VECTOR_ELT(ii, i)); + } + } + + ListOfMatrices() { // Default Constructor + } + + // Copy constructor + ListOfMatrices(const ListOfMatrices& another) { + m_matrices = another.m_matrices; + } + + // Overload assign operator + ListOfMatrices & operator=(const ListOfMatrices& another) { + m_matrices = another.m_matrices; + return *this; + } +}; + +template +class ExprEvaluator { +public: + // constructor + ExprEvaluator() { + error_code = 0; // non-zero means error has occurred; otherwise, no error + expr_row = 0; + strcpy(error_message, "OK"); + }; + + // getters + unsigned char GetErrorCode() { return error_code; }; + const char* GetErrorMessage() { return error_message; }; + int GetExprRow() {return expr_row; }; + + // setters + void SetError(unsigned char code, const char* message, int row) + { + error_code = code; + expr_row = row; + strcpy(error_message, message); + std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + }; + + // evaluators + matrix EvalExpr( + const vector >& hist, + int t, + const vector& mats_save_hist, + const vector& table_x, + const vector& table_n, + const vector& table_i, + ListOfMatrices& valid_vars, + const vector& valid_literals, + int row = 0 + ) + { + // Variables to use locally in function bodies + matrix m, m1, m2; // return values + matrix timeIndex; // for rbind_time + Type sum, s, eps, var; // intermediate scalars + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2; + + if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. + + switch (table_n[row]) { + case -1: // literals + m = matrix::Zero(1,1); + m.coeffRef(0,0) = valid_literals[table_x[row]]; + return m; + case 0: + m = valid_vars.m_matrices[table_x[row]]; + return m; + default: + int n = table_n[row]; + vector > args(n); + vector index2mats(n); + for (int i=0; i::Zero(to-from+1,1); + for (int i=from; i<=to; i++) + m.coeffRef(i-from,0) = i; + #ifdef MP_VERBOSE + std::cout << from << ":" << to << " = " << m << std::endl << std::endl; + #endif + return m; + + case MP2_SEQUENCE: // seq + + // #' The `seq` function is a little different from the + // #' base R default, \code{\link{seq}}, in that it + // #' allows the user precise control over the length of + // #' the output through the `length` argument. The + // #' base R function gives the user this option, but not + // #' as the default. + // #' + int length, by; + from = CppAD::Integer(args[0].coeff(0,0)); + length = CppAD::Integer(args[1].coeff(0,0)); + by = CppAD::Integer(args[2].coeff(0,0)); + if (length<=0) { + SetError(MP2_SEQUENCE, "Sequence length is less than or equal to zero in seq operation", row); + return m; + } + m = matrix::Zero(length,1); + for (int i=0; i::Zero(size,1); + off = 0; + for (int i=0; i::Zero(rows, totcols); + for (int i=0; i::Zero(rows, 1); + return m; + case MP2_RBIND: + { + cols = args[0].cols(); + // std::cout << "cols: " << cols << std::endl; + // std::cout << "n: " << n << std::endl; + int rows_per_arg; + int totrows, rowmarker; + totrows = 0; + rowmarker = 0; + for (int j=0; j::Zero(totrows, cols); + for (int i=0; i::Zero(1, cols); + return m; + case MP2_MATRIX: // matrix + + // #' The `matrix` function can be used to redefine the + // #' numbers of rows and columns to use for arranging + // #' the values of a matrix. It works similarly to + // #' the base R \code{\link{matrix}} function in that it + // #' takes the same arguments. + // #' On the other hand, this function differs substantially + // #' from the base R version in that it must be filled + // #' by column and there is no `byrow` option. + // #' + m = args[0]; + + rows = CppAD::Integer(args[1].coeff(0,0)); + cols = CppAD::Integer(args[2].coeff(0,0)); + + //m.conservativeResize(rows, cols); // don't know why this doesn't work + m.resize(rows, cols); + + // m2 = m.transpose(); // m = m.transpose() doesn't work !!! + m2 = m; + + #ifdef MP_VERBOSE + std::cout << "matrix(" << args[0] << ") reshaped into [" << rows << ", " << cols << "] = " \ + << m2 << std::endl << std::endl; + #endif + + return m2; + + // #' Matrices can be transposed with the usual + // #' function, \code{\link{t}}. + // #' + case MP2_TRANSPOSE: // t or transpose + m = args[0].transpose(); + return m; + + // #' ### Examples + // #' + // #' ``` + // #' engine_eval(~ c(a, b, c), a = 1, b = 10:13, c = matrix(20:25, 3, 2)) + // #' engine_eval(~ cbind(a, 10 + a), a = 0:3) + // #' engine_eval(~ rbind(a, 10 + a), a = t(0:3)) + // #' engine_eval(~ matrix(1:12, 4, 3)) + // #' engine_eval(~ t(1:3)) + // #' ``` + // #' + + + // #' ## Matrix Diagonals + // #' + // #' ### Functions + // #' + // #' * `to_diag(x)` -- Create a diagonal matrix by setting + // #' the diagonal to a column vector, `x`. + // #' * `from_diag(x)` -- Extract the diagonal from a + // #' matrix, `x`, and return the diagonal as a column + // #' vector. + // #' + // #' ### Arguments + // #' + // #' * `x` -- Any matrix (for `from_diag`) or a + // #' column vector (for `to_diag`). It is common to assume + // #' that `x` is square for `from_diag` but this is + // #' not required. + // #' + // #' ### Return + // #' + // #' * `to_diag(x)` -- Diagonal matrix with `x` on the + // #' diagonal. + // #' * `from_diag(x)` -- Column vector containing the + // #' diagonal of `x`. A value is considered to be on + // #' the diagonal if it has a row index equal to + // #' the column index. + // #' + // #' ### Details + // #' + // #' The `to_diag` function can be used to produce a + // #' diagonal matrix by setting a column vector equal + // #' to the desired diagonal. The `from_diag` does + // #' (almost) the opposite, which is to get a column vector + // #' containing the diagonal of an existing matrix. + // #' + // #' ### Examples + // #' + // #' ``` + // #' engine_eval(~from_diag(matrix(1:9, 3, 3))) + // #' engine_eval(~to_diag(from_diag(matrix(1:9, 3, 3)))) + // #' engine_eval(~from_diag(to_diag(from_diag(matrix(1:9, 3, 3))))) + // #' ``` + // #' + case MP2_TO_DIAG: // to_diag + rows = args[0].rows(); + m = matrix::Zero(rows, rows); + for (int i=0; i::Zero(1,1); + sum = 0.0; + for (int i=0; i::Zero(1,1); + ncol=1; + } + else{ + ncol = args[2].size(); + m1 = args[2]; + } + m = matrix::Zero(nrow,ncol); + + err_code = CheckIndices(args[0], args[1], m1); + if (err_code) { + SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); + return m; + } + + // if we can assume contiguous sets of rows and columns + // then mat.block(...) will be faster, so should we + // have a block function on the R side when speed + // matters? + // Can we vectorize CppAD::Integer casting?? + for (int i=0; i::Zero(t - 1, 1); + for (int i=0; i < t - 1; i++) { + timeIndex.coeffRef(i, 0) = i + 1; + } + } else { + timeIndex = args[1]; + } + if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { + SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) initialized matrices with saved history", row); + return args[0]; + } + + int lowerTimeBound; + if (table_n[row]==3) + lowerTimeBound = CppAD::Integer(args[2].coeff(0,0)); + else + lowerTimeBound = 0; + + // Get the length of legitimate times in rbind_time. + // Check if the shape of the matrix changes. + // Error if yes or assign variables "rows" and "cols" with + // the correct values otherwise. + int rbind_length, nRows, nCols; + rbind_length = 0; // count of legitimate time steps to select + for (int i=0; i=lowerTimeBound) { + nRows = hist[rowIndex].m_matrices[matIndex].rows(); + nCols = hist[rowIndex].m_matrices[matIndex].cols(); + } + else if (rowIndex==t) { + nRows = valid_vars.m_matrices[matIndex].rows(); + nCols = valid_vars.m_matrices[matIndex].cols(); + } + else + continue; + + if (nRows==0 || nCols==0) // skip empty matrix + continue; + + if (rbind_length==0) { // first one + rows = nRows; + cols = nCols; + } + else { + if (rows!=nRows || cols!=nCols) { // Shall we allow inconsistent rows? + SetError(MP2_RBIND_TIME, "Inconsistent rows or columns in rbind_time (or rbind_lag)", row); + return args[0]; + } + } + + rbind_length++; + } + #ifdef MP_VERBOSE + std::cout << "rbind_time(" << timeIndex << ") = " << std::endl; + #endif + + if (rbind_length>0) { + //rows = hist[0].m_matrices[matIndex].rows(); + //cols = hist[0].m_matrices[matIndex].cols(); + m = matrix::Zero(rbind_length*rows, cols); + rbind_length = 0; + for (int i=0; i=lowerTimeBound) { + if (hist[rowIndex].m_matrices[matIndex].rows()!=0 && + hist[rowIndex].m_matrices[matIndex].cols()!=0) { + m.block(rbind_length*rows, 0, rows, cols) = hist[rowIndex].m_matrices[matIndex]; + rbind_length++; + } + } + else if (rowIndex==t) { + if (valid_vars.m_matrices[matIndex].rows()!=0 && + valid_vars.m_matrices[matIndex].cols()!=0) { + m.block(rbind_length*rows, 0, rows, cols) = valid_vars.m_matrices[matIndex]; + rbind_length++; + } + } + #ifdef MP_VERBOSE + std::cout << m.block((rbind_length-1)*rows, 0, rows, cols) << std::endl << std::endl; + #endif + } + } + + return m; // empty matrix (if colIndex==0) or non-empty one (otherwise) + + // #' ## Time Indexing + // #' + // #' Get the index of current or lagged time step or + // #' the index of the current time group. A time group + // #' is a contiguous set of time steps defined by two + // #' change points. + // #' + // #' ### Functions + // #' + // #' * `time_step(lag)`: Get the time-step associated + // #' with a particular lag from the current time-step. + // #' If the lagged time-step is less than zero, the + // #' function returns zero. + // #' * `time_group(index, change_points)`: Update the + // #' `index` associated with the current time group. + // #' The current group is defined by the minimum + // #' of all elements of `change_points` that are + // #' greater than the current time step. The time group + // #' `index` is the index associated with this element. + // #' Please see the examples below, they are easier + // #' to understand than this explanation. + // #' + // #' ### Arguments + // #' + // #' * `lag`: Number of time-steps to look back for + // #' the time-step to return. + // #' * `index`: Index associated with the current time + // #' group. + // #' * `change_points`: Increasing column vector of + // #' time steps giving the lower bound of each time + // #' group. + // #' + // #' ### Return + // #' + // #' A 1-by-1 matrix with the time-step `lag` steps + // #' ago, or with zero if `t+1 < lag` + // #' + // #' ### Examples + // #' + // #' ``` + // #' simple_sims( + // #' iteration_exprs = list(x ~ time_step(0)), + // #' time_steps = 10, + // #' x = empty_matrix + // #' ) + // #' sims = simple_sims( + // #' iteration_exprs = list( + // #' j ~ time_group(j, change_points), + // #' time_varying_parameter ~ time_variation_schedule[j] + // #' ), + // #' time_steps = 10, + // #' j = 0, + // #' change_points = c(0, 4, 7), + // #' time_variation_schedule = c(42, pi, sqrt(2)), + // #' time_varying_parameter = empty_matrix + // #' ) + // #' ``` + // #' + case MP2_TIME_STEP: // time_step(lag) + m = matrix::Zero(1,1); + lag = CppAD::Integer(args[0].coeff(0,0)); + if (lag < 0) { + SetError(MP2_TIME_STEP, "Time lag needs to be non-negative", row); + return m; + } + if (t > lag) { + m.coeffRef(0,0) = t - lag; + } + return m; + + case MP2_TIME_GROUP: // time_group(i, change_points) + m = args[0]; + off = CppAD::Integer(args[0].coeff(0, 0)); + cp = CppAD::Integer(args[1].coeff(off + 1, 0)); + if (cp == t) { + m.coeffRef(0,0) = off + 1; + } + return m; + + case MP2_CONVOLUTION: + + // #' ## Convolution + // #' + // #' One may take the convolution of each element in a + // #' matrix, x, over simulation time using a kernel, k. + // #' There are two arguments of this function. + // #' + // #' ### Functions + // #' + // #' * `convolution(x, k)` + // #' + // #' ### Arguments + // #' + // #' * `x` -- The matrix containing elements to be + // #' convolved. + // #' * `k` -- A column vector giving the convolution kernel. + // #' + // #' ### Return + // #' + // #' A matrix the same size as `x` but with the + // #' convolutions, \eqn{y_{ij}}, of each element, + // #' \eqn{x_{ij}}, given by the following. + // #' + // #' \deqn{y_{ij} = \sum_{\tau = 0} x_{ij}(t-\tau) k(\tau)} + // #' + // #' unless \eqn{t < \tau}, in which case, + // #' + // #' \deqn{y_{ij} = } + // #' + // #' where \eqn{y_{ij}} is the convolution, + // #' \eqn{x_{ij}(t)} is the value of \eqn{x_{ij}} at time step, \eqn{t}, + // #' \eqn{k(\tau)} is the value of the kernel at lag, \eqn{\tau}, + // #' and \eqn{\lambda} is the length of the kernel. + // #' + // #' ### Details + // #' + // #' If any empty matrices are encountered when looking + // #' back in time, they are treated as matrices with all + // #' zeros. Similarly, any matrices encounte + // #' of `x` + // #' + matIndex = index2mats[0]; // m + #ifdef MP_VERBOSE + std::cout << "matIndex: " << matIndex << std::endl << std::endl; + #endif + length = args[1].rows(); + #ifdef MP_VERBOSE + std::cout << "length: " << length << std::endl << std::endl; + #endif + if (length>0 && args[1].cols()==1) { + #ifdef MP_VERBOSE + std::cout << "kernel 1: " << args[1] << std::endl << std::endl; + #endif + if (t+1::Zero(rows, cols); + + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; ispi[0]] + m = matrix::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i=sz) { + m1 = m.block(start, 0, sz, 1); + m1.resize(args[i].rows(), args[i].cols()); + //std::cout << "MATRIX " << valid_vars.m_matrices[index2mats[i]] << std::endl << std::endl; + valid_vars.m_matrices[index2mats[i]] = m1; + // args[i] = m1; + size -= sz; + start += sz; + } + else + break; + } + return m2; // empty matrix + + case MP2_RECYCLE: + m = args[0]; + rows = CppAD::Integer(args[1].coeff(0,0)); + cols = CppAD::Integer(args[2].coeff(0,0)); + err_code = RecycleInPlace(m, rows, cols); + if (err_code != 0) { + SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + return m; + } + return m; + + default: + SetError(255, "invalid operator in arithmetic expression", row); + return m; + } + } + }; + + +private: + // Functor for computing derivatives of expressions. + // template + // struct matrix_functor{ + // // define data members + // + // // define constructor + // matrix_functor() : // initialization list + // { // the body is empty + // } + // // the function itself + // template + // vector operator()(vector input_vector_) + // { + // vector output_vector_ = EvalExpr( + // simulation_history_, + // 0, + // mats_save_hist_, + // p_table_x_, + // p_table_n_, + // p_table_i_, + // mats_, + // literals_, + // p_table_row_ + // ); + // // call exprEval in here to convert input_vector_ into output_vector_ + // return (output_vector_); + // } + // } + unsigned char error_code; + int expr_row; + char error_message[256]; +}; + +#define REPORT_ERROR { \ + int error = exprEvaluator.GetErrorCode(); \ + int expr_row = exprEvaluator.GetExprRow(); \ + REPORT(error); \ + REPORT(expr_row); \ + \ + logfile.open (LOG_FILE_NAME, std::ios_base::app); \ + logfile << "Error code = " << error << std::endl; \ + logfile << "Error message = " << exprEvaluator.GetErrorMessage() << std::endl; \ + logfile << "Expression row = " << expr_row << std::endl; \ + logfile.close(); \ +} + +template +vector > MakeSimulationHistory( + const int time_steps, + const vector& mats_save_hist, + ListOfMatrices& hist_shape_template +) { + vector > simulation_history(time_steps+2); + matrix empty_matrix; + for (int i=0; i +void UpdateSimulationHistory( + vector >& hist, + int t, + const ListOfMatrices& mats, + const vector& mats_save_hist, + ListOfMatrices& hist_shape_template +) { + // matrix emptyMat; + bool no_row_change; + bool no_col_change; + + // ListOfMatrices ms(mats); + // if the history of the matrix is not to be saved, + // just save a 1-by-1 with a zero instead to save space + for (int i=0; i +Type objective_function::operator() () +{ + #ifdef MP_VERBOSE + std::cout << "============== objective_function =============" << std::endl; + #endif + + std::ofstream logfile; + logfile.open (LOG_FILE_NAME); + logfile << "======== log file of MacPan2 ========\n"; + logfile.close(); + + std::setprecision(9); // Set the precision of std::cout + + // 1 Get all data and parameters from the R side + // Parameters themselves + int n; + + // Fixed effects + PARAMETER_VECTOR(params); + + // Random effects + PARAMETER_VECTOR(random); + + // Matrices + DATA_STRUCT(mats, ListOfMatrices); + DATA_IVECTOR(mats_save_hist); + DATA_IVECTOR(mats_return); + + // Fixed parameter replacements + DATA_IVECTOR(p_par_id); + DATA_IVECTOR(p_mat_id); + DATA_IVECTOR(p_row_id); + DATA_IVECTOR(p_col_id); + + // Random parameter replacements + DATA_IVECTOR(r_par_id); + DATA_IVECTOR(r_mat_id); + DATA_IVECTOR(r_row_id); + DATA_IVECTOR(r_col_id); + + // Trajectory simulation + DATA_INTEGER(time_steps) + + // Expressions and parse table + DATA_IVECTOR(expr_output_id); + DATA_IVECTOR(expr_sim_block); + DATA_IVECTOR(expr_num_p_table_rows); + DATA_IVECTOR(eval_schedule) + DATA_IVECTOR(p_table_x); + DATA_IVECTOR(p_table_n); + DATA_IVECTOR(p_table_i); + + // Literals + DATA_VECTOR(literals); + + // Objective function parse table + DATA_IVECTOR(o_table_n); + DATA_IVECTOR(o_table_x); + DATA_IVECTOR(o_table_i); + + // DATA_STRUCT(settings, settings_struct); + + #ifdef MP_VERBOSE + std::cout << "params = " << params << std::endl; + + std::cout << "random = " << random << std::endl; + + n = mats.m_matrices.size(); + for (int i = 0; i < n; i++) + std::cout << "mats = " << mats.m_matrices[i] << std::endl; + + std::cout << "p_par_id = " << p_par_id << std::endl; + std::cout << "p_mat_id = " << p_mat_id << std::endl; + std::cout << "p_row_id = " << p_row_id << std::endl; + std::cout << "p_col_id = " << p_col_id << std::endl; + + std::cout << "r_par_id = " << r_par_id << std::endl; + std::cout << "r_mat_id = " << r_mat_id << std::endl; + std::cout << "r_row_id = " << r_row_id << std::endl; + std::cout << "r_col_id = " << r_col_id << std::endl; + + std::cout << "time_steps = " << time_steps << std::endl; + + std::cout << "mats_save_hist = " << mats_save_hist << std::endl; + std::cout << "mats_return = " << mats_return << std::endl; + + std::cout << "eval_schedule = " << eval_schedule << std::endl; + + //std::cout << "expr_output_count = " << expr_output_count << std::endl; + std::cout << "expr_output_id = " << expr_output_id << std::endl; + std::cout << "expr_sim_block = " << expr_sim_block << std::endl; + std::cout << "expr_num_p_table_rows = " << expr_num_p_table_rows << std::endl; + + std::cout << "p_table_x = " << p_table_x << std::endl; + std::cout << "p_table_n = " << p_table_n << std::endl; + std::cout << "p_table_i = " << p_table_i << std::endl; + + std::cout << "literals = " << literals << std::endl; + + std::cout << "o_table_x = " << o_table_x << std::endl; + std::cout << "o_table_n = " << o_table_n << std::endl; + std::cout << "o_table_i = " << o_table_i << std::endl; + #endif + + // 2 Replace some of elements of some matrices with parameters + n = p_par_id.size(); + for (int i=0; i hist_shape_template(mats); + vector > simulation_history = MakeSimulationHistory( + time_steps, + mats_save_hist, + hist_shape_template + ); + + + + ////////////////////////////////// + // Define an expression evaluator + ExprEvaluator exprEvaluator; + ////////////////////////////////// + + // 3 Pre-simulation + int expr_index = 0; + int p_table_row = 0; + + for (int i=0; i result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + 0, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + 0, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row += expr_num_p_table_rows[i]; + } + + //simulation_history[0] = mats; + UpdateSimulationHistory( + simulation_history, + 0, + mats, + mats_save_hist, + hist_shape_template + ); + + // 4 During simulation + expr_index += eval_schedule[0]; + + int p_table_row2; + for (int k=0; k result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + k+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row2 + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + k+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row2 + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row2 += expr_num_p_table_rows[expr_index+i]; + + #ifdef MP_VERBOSE + int n = mats.m_matrices.size(); + for (int ii = 0; ii < n; ii++) + std::cout << "mats = " << mats.m_matrices[ii] << std::endl; + #endif + } + //simulation_history[k+1] = mats; + UpdateSimulationHistory( + simulation_history, + k+1, + mats, + mats_save_hist, + hist_shape_template + ); + } + p_table_row = p_table_row2; + + // 5 Post-simulation + expr_index += eval_schedule[1]; + + for (int i=0; i result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + time_steps+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + time_steps+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row += expr_num_p_table_rows[expr_index+i]; + } + + //simulation_history[time_steps+1] = mats; + UpdateSimulationHistory( + simulation_history, + time_steps+1, + mats, + mats_save_hist, + hist_shape_template + ); + +#ifdef MP_VERBOSE + std::cout << "Simulation history ..." << std::endl; + int m = simulation_history.size(); + for (int t=0; t +#include +#include +#include +#include +#include +#include // isnan() is defined +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// Macpan2 is redesigned architecture. The spec is +// https://canmod.net/misc/cpp_side.html +// +// Operands in a math expression and its intermediate/final results are all +// generalized as matrices, i.e., +// scalar as 1x1 matrix +// vector as mx1 or 1xn matrix +// matrix as mxn matrix +// so that we can unified the definition of function EvalExpr. The Eigen library +// (https://eigen.tuxfamily.org/dox/group__TutorialMatrixClass.html) has similar +// generalization idea. +// +// Elementwise Binary Operators (+ - * / ^) +// + if both operands have the same shape, then do the elementwise operation +// + elif one of the operands is a scalar, then do the operation of scalar vs +// elements of the other operand +// + elif one of the operands is a vector, then +// - if the length of the vector is equal to the corresponding dimension of the +// other operand, then do row- or column-wise element-vs-element operation. +// - else ERROR +// + else ERROR +// +// Matrix Binary operations (%*%) +// + if the second dimension of first operand == the first dimension of +// the second operand, then do matrix multiplication +// + else ERROR +// NOTE: 1 Inner product can be made by row_vec %*% col_vec while outer product +// can be made by col_vec %*% row_vec. +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Functions we Support +// Please follow exactly the format below when adding a new function: +// MP2_CPP_CASE_NAME = {case number} // {R function name} +// Note that the last entry does not have the comma that follows +// {case number}. +// If an entry in this enum does not have an associated case in the +// switch(table_x[row]+1) statement, then it _must_ be commented out. +// An analogous R-side list can be automatically produced using the +// package makefile. The ordering of the functions must follow their +// associated integers, and the integers must increase from 1 with no +// gaps. Therefore, you should add any new functions at the end, +// although it is possible to change the order of the functions as +// long as the integers increase from 1 without gaps as long as the +// R/enum.R file is regenerated. +enum macpan2_func { + MP2_ADD = 1 // binop,null: `+`(x, y) + , MP2_SUBTRACT = 2 // binop,null: `-`(x, y) + , MP2_MULTIPLY = 3 // binop,null: `*`(x, y) + , MP2_DIVIDE = 4 // binop,null: `/`(x, y) + , MP2_POWER = 5 // binop,null: `^`(x, y) + , MP2_EXP = 6 // fwrap,null: exp(x) + , MP2_LOG = 7 // fwrap,null: log(x) + , MP2_ROUND_BRACKET = 8 // null,null: `(`(...) + , MP2_COMBINE = 9 // null,null: c(...) + , MP2_MATRIX = 10 // fwrap,null: matrix(x, i, j) + , MP2_MATRIX_MULTIPLY = 11 // binop,null: `%*%`(x, y) + , MP2_SUM = 12 // null,null: sum(...) + , MP2_REPLICATE = 13 // fwrap,null: rep(x, times) + , MP2_ROWSUMS = 14 // fwrap,null: rowSums(x) + , MP2_COLSUMS = 15 // fwrap,null: colSums(x) + , MP2_GROUPSUMS = 16 // fwrap,null: groupSums(x, f, n) + , MP2_SQUARE_BRACKET = 17 // null,null: `[`(x, i, j) + , MP2_BLOCK = 18 // fwrap,fail: block(x, i, j, n, m) + , MP2_TRANSPOSE = 19 // fwrap,null: t(x) + , MP2_RBIND_TIME = 20 // fwrap,fail: rbind_time(x, t, t_min) + , MP2_RBIND_LAG = 21 // fwrap,fail: rbind_lag(x, lag, t_min) + , MP2_CBIND_TIME = 22 // fwrap,fail: cbind_time(x, t, t_min) + , MP2_CBIND_LAG = 23 // fwrap,fail: cbind_lag(x, lag, t_min) + , MP2_COLON = 24 // null,null: `:`(from, to) + , MP2_SEQUENCE = 25 // fwrap,fail: seq(from, length, by) + , MP2_CONVOLUTION = 26 // fwrap,fail: convolution(x, k) + , MP2_CBIND = 27 // fwrap,null: cbind(...) + , MP2_RBIND = 28 // fwrap,null: rbind(...) + , MP2_TIME_STEP = 29 // fwrap,fail: time_step(lag) + , MP2_ASSIGN = 30 // fwrap,null: assign(x, i, j, v) + , MP2_UNPACK = 31 // fwrap,fail: unpack(x, ...) + , MP2_RECYCLE = 32 // fwrap,null: recycle(x, rows, cols) + , MP2_CLAMP = 33 // fwrap,null: clamp(x, eps) + , MP2_POISSON_DENSITY = 34 // fwrap,fail: dpois(observed, simulated) + , MP2_NEGBIN_DENSITY = 35 // fwrap,fail: dnbinom(observed, simulated, over_dispersion) + , MP2_NORMAL_DENSITY = 36 // fwrap,fail: dnorm(observed, simulated, standard_deviation) + , MP2_POISSON_SIM = 37 // fwrap,fail: rpois(mean) + , MP2_NEGBIN_SIM = 38 // fwrap,fail: rnbinom(mean, over_dispersion) + , MP2_NORMAL_SIM = 39 // fwrap,fail: rnorm(mean, standard_deviation) + , MP2_KRONECKER = 40 // binop,null: `%x%`(x, y) + , MP2_TO_DIAG = 41 // fwrap,fail: to_diag(x) + , MP2_FROM_DIAG = 42 // fwrap,fail: from_diag(x) + , MP2_TIME_GROUP = 43 //fwrap,fail: time_group(i, change_points) + , MP2_COS = 44 // fwrap,null: cos(x) + //, MP2_LOGISTIC = 45 // fwrap,null: logistic(x) + //, MP2_LOGIT = 46 // fwrap,null: logit(x) +}; + +// Helper function +template +int CheckIndices( + matrix& x, + matrix& rowIndices, + matrix& colIndices +) { + int rows = x.rows(); + int cols = x.cols(); + Type maxRowIndex = rowIndices.maxCoeff(); + Type maxColIndex = colIndices.maxCoeff(); + Type minRowIndex = rowIndices.minCoeff(); + Type minColIndex = colIndices.minCoeff(); + + if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { + return 0; + } + return 1; +} + +// Helper function +template +int RecycleInPlace( + matrix& mat, + int rows, + int cols +) { + #ifdef MP_VERBOSE + std::cout << "recycling ... " << std::endl; + #endif + if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. + return 0; + + matrix m(rows, cols); + if (mat.rows()==1 && mat.cols()==1) { + m = matrix::Constant(rows, cols, mat.coeff(0,0)); + } + else if (mat.rows()==rows) { + if (mat.cols()==1) { + #ifdef MP_VERBOSE + std::cout << "recycling columns ... " << std::endl; + #endif + for (int i=0; i +struct ListOfMatrices { + // below is a vector of matrices that passed from R + vector > m_matrices; + + ListOfMatrices(SEXP RListOfMatrices){ // Constructor + // Get elements by their indices + int n = length(RListOfMatrices); + vector > vs(n); + m_matrices = vs; + + for (int i = 0; i < n; i++) { + m_matrices[i] = asMatrix(VECTOR_ELT(RListOfMatrices, i)); + } + } + + ListOfMatrices() { // Default Constructor + } + + // Copy constructor + ListOfMatrices(const ListOfMatrices& another) { + m_matrices = another.m_matrices; + } + + // Overload assign operator + ListOfMatrices & operator=(const ListOfMatrices& another) { + m_matrices = another.m_matrices; + return *this; + } +}; + +template +class ExprEvaluator { +public: + // constructor + ExprEvaluator() { + error_code = 0; // non-zero means error has occurred; otherwise, no error + expr_row = 0; + strcpy(error_message, "OK"); + }; + + // getters + unsigned char GetErrorCode() { return error_code; }; + const char* GetErrorMessage() { return error_message; }; + int GetExprRow() {return expr_row; }; + + // setters + void SetError(unsigned char code, const char* message, int row) + { + error_code = code; + expr_row = row; + strcpy(error_message, message); + std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + }; + + // evaluators + matrix EvalExpr( + const vector >& hist, + int t, + const vector& mats_save_hist, + const vector& mats_saved_indices_inverse, + const vector& table_x, + const vector& table_n, + const vector& table_i, + ListOfMatrices& valid_vars, + const vector& valid_literals, + int row = 0 + ) + { + // Variables to use locally in function bodies + matrix m, m1, m2; // return values + matrix timeIndex; // for rbind_time + Type sum, s, eps, var; // intermediate scalars + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2; + + if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. + + switch (table_n[row]) { + case -1: // literals + m = matrix::Zero(1,1); + m.coeffRef(0,0) = valid_literals[table_x[row]]; + return m; + case 0: + m = valid_vars.m_matrices[table_x[row]]; + return m; + default: + int n = table_n[row]; + vector > args(n); + vector index2mats(n); + for (int i=0; i::Zero(to-from+1,1); + for (int i=from; i<=to; i++) + m.coeffRef(i-from,0) = i; + #ifdef MP_VERBOSE + std::cout << from << ":" << to << " = " << m << std::endl << std::endl; + #endif + return m; + + case MP2_SEQUENCE: // seq + + // #' The `seq` function is a little different from the + // #' base R default, \code{\link{seq}}, in that it + // #' allows the user precise control over the length of + // #' the output through the `length` argument. The + // #' base R function gives the user this option, but not + // #' as the default. + // #' + int length, by; + from = CppAD::Integer(args[0].coeff(0,0)); + length = CppAD::Integer(args[1].coeff(0,0)); + by = CppAD::Integer(args[2].coeff(0,0)); + if (length<=0) { + SetError(MP2_SEQUENCE, "Sequence length is less than or equal to zero in seq operation", row); + return m; + } + m = matrix::Zero(length,1); + for (int i=0; i::Zero(size,1); + off = 0; + for (int i=0; i::Zero(rows, totcols); + for (int i=0; i::Zero(rows, 1); + return m; + case MP2_RBIND: + { + cols = args[0].cols(); + // std::cout << "cols: " << cols << std::endl; + // std::cout << "n: " << n << std::endl; + int rows_per_arg; + int totrows, rowmarker; + totrows = 0; + rowmarker = 0; + for (int j=0; j::Zero(totrows, cols); + for (int i=0; i::Zero(1, cols); + return m; + case MP2_MATRIX: // matrix + + // #' The `matrix` function can be used to redefine the + // #' numbers of rows and columns to use for arranging + // #' the values of a matrix. It works similarly to + // #' the base R \code{\link{matrix}} function in that it + // #' takes the same arguments. + // #' On the other hand, this function differs substantially + // #' from the base R version in that it must be filled + // #' by column and there is no `byrow` option. + // #' + m = args[0]; + + rows = CppAD::Integer(args[1].coeff(0,0)); + cols = CppAD::Integer(args[2].coeff(0,0)); + + //m.conservativeResize(rows, cols); // don't know why this doesn't work + m.resize(rows, cols); + + // m2 = m.transpose(); // m = m.transpose() doesn't work !!! + m2 = m; + + #ifdef MP_VERBOSE + std::cout << "matrix(" << args[0] << ") reshaped into [" << rows << ", " << cols << "] = " \ + << m2 << std::endl << std::endl; + #endif + + return m2; + + // #' Matrices can be transposed with the usual + // #' function, \code{\link{t}}. + // #' + case MP2_TRANSPOSE: // t or transpose + m = args[0].transpose(); + return m; + + // #' ### Examples + // #' + // #' ``` + // #' engine_eval(~ c(a, b, c), a = 1, b = 10:13, c = matrix(20:25, 3, 2)) + // #' engine_eval(~ cbind(a, 10 + a), a = 0:3) + // #' engine_eval(~ rbind(a, 10 + a), a = t(0:3)) + // #' engine_eval(~ matrix(1:12, 4, 3)) + // #' engine_eval(~ t(1:3)) + // #' ``` + // #' + + + // #' ## Matrix Diagonals + // #' + // #' ### Functions + // #' + // #' * `to_diag(x)` -- Create a diagonal matrix by setting + // #' the diagonal to a column vector, `x`. + // #' * `from_diag(x)` -- Extract the diagonal from a + // #' matrix, `x`, and return the diagonal as a column + // #' vector. + // #' + // #' ### Arguments + // #' + // #' * `x` -- Any matrix (for `from_diag`) or a + // #' column vector (for `to_diag`). It is common to assume + // #' that `x` is square for `from_diag` but this is + // #' not required. + // #' + // #' ### Return + // #' + // #' * `to_diag(x)` -- Diagonal matrix with `x` on the + // #' diagonal. + // #' * `from_diag(x)` -- Column vector containing the + // #' diagonal of `x`. A value is considered to be on + // #' the diagonal if it has a row index equal to + // #' the column index. + // #' + // #' ### Details + // #' + // #' The `to_diag` function can be used to produce a + // #' diagonal matrix by setting a column vector equal + // #' to the desired diagonal. The `from_diag` does + // #' (almost) the opposite, which is to get a column vector + // #' containing the diagonal of an existing matrix. + // #' + // #' ### Examples + // #' + // #' ``` + // #' engine_eval(~from_diag(matrix(1:9, 3, 3))) + // #' engine_eval(~to_diag(from_diag(matrix(1:9, 3, 3)))) + // #' engine_eval(~from_diag(to_diag(from_diag(matrix(1:9, 3, 3))))) + // #' ``` + // #' + case MP2_TO_DIAG: // to_diag + rows = args[0].rows(); + m = matrix::Zero(rows, rows); + for (int i=0; i::Zero(1,1); + sum = 0.0; + for (int i=0; i::Zero(1,1); + ncol=1; + } + else{ + ncol = args[2].size(); + m1 = args[2]; + } + m = matrix::Zero(nrow,ncol); + + err_code = CheckIndices(args[0], args[1], m1); + if (err_code) { + SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); + return m; + } + + // if we can assume contiguous sets of rows and columns + // then mat.block(...) will be faster, so should we + // have a block function on the R side when speed + // matters? + // Can we vectorize CppAD::Integer casting?? + for (int i=0; i::Zero(t - 1, 1); + for (int i=0; i < t - 1; i++) { + timeIndex.coeffRef(i, 0) = i + 1; + } + } else { + timeIndex = args[1]; + } + if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { + SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) initialized matrices with saved history", row); + return args[0]; + } + + int lowerTimeBound; + if (table_n[row]==3) + lowerTimeBound = CppAD::Integer(args[2].coeff(0,0)); + else + lowerTimeBound = 0; + + // Get the length of legitimate times in rbind_time. + // Check if the shape of the matrix changes. + // Error if yes or assign variables "rows" and "cols" with + // the correct values otherwise. + int rbind_length, nRows, nCols, savedMatIndex; + savedMatIndex = mats_saved_indices_inverse[matIndex]; + rbind_length = 0; // count of legitimate time steps to select + for (int i=0; i=lowerTimeBound) { + nRows = hist[rowIndex].m_matrices[savedMatIndex].rows(); + nCols = hist[rowIndex].m_matrices[savedMatIndex].cols(); + } + else if (rowIndex==t) { + nRows = valid_vars.m_matrices[matIndex].rows(); + nCols = valid_vars.m_matrices[matIndex].cols(); + } + else + continue; + + if (nRows==0 || nCols==0) // skip empty matrix + continue; + + if (rbind_length==0) { // first one + rows = nRows; + cols = nCols; + } + else { + if (rows!=nRows || cols!=nCols) { // Shall we allow inconsistent rows? + SetError(MP2_RBIND_TIME, "Inconsistent rows or columns in rbind_time (or rbind_lag)", row); + return args[0]; + } + } + + rbind_length++; + } + #ifdef MP_VERBOSE + std::cout << "rbind_time(" << timeIndex << ") = " << std::endl; + #endif + + if (rbind_length>0) { + //rows = hist[0].m_matrices[matIndex].rows(); + //cols = hist[0].m_matrices[matIndex].cols(); + m = matrix::Zero(rbind_length*rows, cols); + rbind_length = 0; + for (int i=0; i=lowerTimeBound) { + if (hist[rowIndex].m_matrices[savedMatIndex].rows()!=0 && + hist[rowIndex].m_matrices[savedMatIndex].cols()!=0) { + m.block(rbind_length*rows, 0, rows, cols) = hist[rowIndex].m_matrices[savedMatIndex]; + rbind_length++; + } + } + else if (rowIndex==t) { + if (valid_vars.m_matrices[matIndex].rows()!=0 && + valid_vars.m_matrices[matIndex].cols()!=0) { + m.block(rbind_length*rows, 0, rows, cols) = valid_vars.m_matrices[matIndex]; + rbind_length++; + } + } + #ifdef MP_VERBOSE + std::cout << m.block((rbind_length-1)*rows, 0, rows, cols) << std::endl << std::endl; + #endif + } + } + + return m; // empty matrix (if colIndex==0) or non-empty one (otherwise) + + // #' ## Time Indexing + // #' + // #' Get the index of current or lagged time step or + // #' the index of the current time group. A time group + // #' is a contiguous set of time steps defined by two + // #' change points. + // #' + // #' ### Functions + // #' + // #' * `time_step(lag)`: Get the time-step associated + // #' with a particular lag from the current time-step. + // #' If the lagged time-step is less than zero, the + // #' function returns zero. + // #' * `time_group(index, change_points)`: Update the + // #' `index` associated with the current time group. + // #' The current group is defined by the minimum + // #' of all elements of `change_points` that are + // #' greater than the current time step. The time group + // #' `index` is the index associated with this element. + // #' Please see the examples below, they are easier + // #' to understand than this explanation. + // #' + // #' ### Arguments + // #' + // #' * `lag`: Number of time-steps to look back for + // #' the time-step to return. + // #' * `index`: Index associated with the current time + // #' group. + // #' * `change_points`: Increasing column vector of + // #' time steps giving the lower bound of each time + // #' group. + // #' + // #' ### Return + // #' + // #' A 1-by-1 matrix with the time-step `lag` steps + // #' ago, or with zero if `t+1 < lag` + // #' + // #' ### Examples + // #' + // #' ``` + // #' simple_sims( + // #' iteration_exprs = list(x ~ time_step(0)), + // #' time_steps = 10, + // #' x = empty_matrix + // #' ) + // #' sims = simple_sims( + // #' iteration_exprs = list( + // #' j ~ time_group(j, change_points), + // #' time_varying_parameter ~ time_variation_schedule[j] + // #' ), + // #' time_steps = 10, + // #' j = 0, + // #' change_points = c(0, 4, 7), + // #' time_variation_schedule = c(42, pi, sqrt(2)), + // #' time_varying_parameter = empty_matrix + // #' ) + // #' ``` + // #' + case MP2_TIME_STEP: // time_step(lag) + m = matrix::Zero(1,1); + lag = CppAD::Integer(args[0].coeff(0,0)); + if (lag < 0) { + SetError(MP2_TIME_STEP, "Time lag needs to be non-negative", row); + return m; + } + if (t > lag) { + m.coeffRef(0,0) = t - lag; + } + return m; + + case MP2_TIME_GROUP: // time_group(i, change_points) + m = args[0]; + off = CppAD::Integer(args[0].coeff(0, 0)); + cp = CppAD::Integer(args[1].coeff(off + 1, 0)); + if (cp == t) { + m.coeffRef(0,0) = off + 1; + } + return m; + + case MP2_CONVOLUTION: + + // #' ## Convolution + // #' + // #' One may take the convolution of each element in a + // #' matrix, x, over simulation time using a kernel, k. + // #' There are two arguments of this function. + // #' + // #' ### Functions + // #' + // #' * `convolution(x, k)` + // #' + // #' ### Arguments + // #' + // #' * `x` -- The matrix containing elements to be + // #' convolved. + // #' * `k` -- A column vector giving the convolution kernel. + // #' + // #' ### Return + // #' + // #' A matrix the same size as `x` but with the + // #' convolutions, \eqn{y_{ij}}, of each element, + // #' \eqn{x_{ij}}, given by the following. + // #' + // #' \deqn{y_{ij} = \sum_{\tau = 0} x_{ij}(t-\tau) k(\tau)} + // #' + // #' unless \eqn{t < \tau}, in which case, + // #' + // #' \deqn{y_{ij} = } + // #' + // #' where \eqn{y_{ij}} is the convolution, + // #' \eqn{x_{ij}(t)} is the value of \eqn{x_{ij}} at time step, \eqn{t}, + // #' \eqn{k(\tau)} is the value of the kernel at lag, \eqn{\tau}, + // #' and \eqn{\lambda} is the length of the kernel. + // #' + // #' ### Details + // #' + // #' If any empty matrices are encountered when looking + // #' back in time, they are treated as matrices with all + // #' zeros. Similarly, any matrices encounte + // #' of `x` + // #' + matIndex = index2mats[0]; // m + #ifdef MP_VERBOSE + std::cout << "matIndex: " << matIndex << std::endl << std::endl; + #endif + length = args[1].rows(); + #ifdef MP_VERBOSE + std::cout << "length: " << length << std::endl << std::endl; + #endif + if (length>0 && args[1].cols()==1) { + #ifdef MP_VERBOSE + std::cout << "kernel 1: " << args[1] << std::endl << std::endl; + #endif + if (t+1::Zero(rows, cols); + + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; ispi[0]] + m = matrix::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i=sz) { + m1 = m.block(start, 0, sz, 1); + m1.resize(args[i].rows(), args[i].cols()); + //std::cout << "MATRIX " << valid_vars.m_matrices[index2mats[i]] << std::endl << std::endl; + valid_vars.m_matrices[index2mats[i]] = m1; + // args[i] = m1; + size -= sz; + start += sz; + } + else + break; + } + return m2; // empty matrix + + case MP2_RECYCLE: + m = args[0]; + rows = CppAD::Integer(args[1].coeff(0,0)); + cols = CppAD::Integer(args[2].coeff(0,0)); + err_code = RecycleInPlace(m, rows, cols); + if (err_code != 0) { + SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + } + return m; + + default: + SetError(255, "invalid operator in arithmetic expression", row); + return m; + } + } + }; // ExprEvaluator + + +private: + // Functor for computing derivatives of expressions. + // template + // struct matrix_functor{ + // // define data members + // + // // define constructor + // matrix_functor() : // initialization list + // { // the body is empty + // } + // // the function itself + // template + // vector operator()(vector input_vector_) + // { + // vector output_vector_ = EvalExpr( + // simulation_history_, + // 0, + // mats_save_hist_, + // p_table_x_, + // p_table_n_, + // p_table_i_, + // mats_, + // literals_, + // p_table_row_ + // ); + // // call exprEval in here to convert input_vector_ into output_vector_ + // return (output_vector_); + // } + // } + unsigned char error_code; + int expr_row; + char error_message[256]; +}; + +#define REPORT_ERROR { \ + int error = exprEvaluator.GetErrorCode(); \ + int expr_row = exprEvaluator.GetExprRow(); \ + REPORT(error); \ + REPORT(expr_row); \ + \ + logfile.open (LOG_FILE_NAME, std::ios_base::app); \ + logfile << "Error code = " << error << std::endl; \ + logfile << "Error message = " << exprEvaluator.GetErrorMessage() << std::endl; \ + logfile << "Expression row = " << expr_row << std::endl; \ + logfile.close(); \ +} + +// Helper function +template +void UpdateSimulationHistory( + vector >& hist, + int t, + const ListOfMatrices& mats, + const vector& mats_save_hist, + const vector& mats_saved_indices, + const vector& mats_saved_indices_inverse, + const ListOfMatrices& init_hist +) { + matrix emptyMat; + // std::cout << "IUYIUYIUYIUY" << mats[mats_saved_indices_inverse] << std::endl; + ListOfMatrices ms(init_hist); + for (int i=0; i +Type objective_function::operator() () +{ + #ifdef MP_VERBOSE + std::cout << "============== objective_function =============" << std::endl; + #endif + + std::ofstream logfile; + logfile.open (LOG_FILE_NAME); + logfile << "======== log file of MacPan2 ========\n"; + logfile.close(); + + std::setprecision(9); // Set the precision of std::cout + + // 1 Get all data and parameters from the R side + // Parameters themselves + int n; + + // Fixed effects + PARAMETER_VECTOR(params); + + // Random effects + PARAMETER_VECTOR(random); + + // Matrices + DATA_STRUCT(mats, ListOfMatrices); + DATA_IVECTOR(mats_save_hist); + DATA_IVECTOR(mats_return); + // DATA_IVECTOR(mats_save_dims); + DATA_STRUCT(init_hist, ListOfMatrices); + + // Fixed parameter replacements + DATA_IVECTOR(p_par_id); + DATA_IVECTOR(p_mat_id); + DATA_IVECTOR(p_row_id); + DATA_IVECTOR(p_col_id); + + // Random parameter replacements + DATA_IVECTOR(r_par_id); + DATA_IVECTOR(r_mat_id); + DATA_IVECTOR(r_row_id); + DATA_IVECTOR(r_col_id); + + // Trajectory simulation + DATA_INTEGER(time_steps) + + // Expressions and parse table + DATA_IVECTOR(expr_output_id); + DATA_IVECTOR(expr_sim_block); + DATA_IVECTOR(expr_num_p_table_rows); + DATA_IVECTOR(eval_schedule) + DATA_IVECTOR(p_table_x); + DATA_IVECTOR(p_table_n); + DATA_IVECTOR(p_table_i); + + // Literals + DATA_VECTOR(literals); + + // Objective function parse table + DATA_IVECTOR(o_table_n); + DATA_IVECTOR(o_table_x); + DATA_IVECTOR(o_table_i); + + #ifdef MP_VERBOSE + std::cout << "params = " << params << std::endl; + + std::cout << "random = " << random << std::endl; + + n = mats.m_matrices.size(); + for (int i = 0; i < n; i++) + std::cout << "mats = " << mats.m_matrices[i] << std::endl; + + std::cout << "p_par_id = " << p_par_id << std::endl; + std::cout << "p_mat_id = " << p_mat_id << std::endl; + std::cout << "p_row_id = " << p_row_id << std::endl; + std::cout << "p_col_id = " << p_col_id << std::endl; + + std::cout << "r_par_id = " << r_par_id << std::endl; + std::cout << "r_mat_id = " << r_mat_id << std::endl; + std::cout << "r_row_id = " << r_row_id << std::endl; + std::cout << "r_col_id = " << r_col_id << std::endl; + + std::cout << "time_steps = " << time_steps << std::endl; + + std::cout << "mats_save_hist = " << mats_save_hist << std::endl; + std::cout << "mats_return = " << mats_return << std::endl; + + std::cout << "eval_schedule = " << eval_schedule << std::endl; + + //std::cout << "expr_output_count = " << expr_output_count << std::endl; + std::cout << "expr_output_id = " << expr_output_id << std::endl; + std::cout << "expr_sim_block = " << expr_sim_block << std::endl; + std::cout << "expr_num_p_table_rows = " << expr_num_p_table_rows << std::endl; + + std::cout << "p_table_x = " << p_table_x << std::endl; + std::cout << "p_table_n = " << p_table_n << std::endl; + std::cout << "p_table_i = " << p_table_i << std::endl; + + std::cout << "literals = " << literals << std::endl; + + std::cout << "o_table_x = " << o_table_x << std::endl; + std::cout << "o_table_n = " << o_table_n << std::endl; + std::cout << "o_table_i = " << o_table_i << std::endl; + #endif + + vector mats_saved_indices(mats_save_hist.sum()); + vector mats_saved_indices_inverse(mats_save_hist.size()); + int matIndex = 0; + for (int i=0; i > simulation_history(time_steps+2); + + + + ////////////////////////////////// + // Define an expression evaluator + ExprEvaluator exprEvaluator; + ////////////////////////////////// + + // 3 Pre-simulation + int expr_index = 0; + int p_table_row = 0; + + for (int i=0; i result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + 0, + mats_save_hist, + mats_saved_indices_inverse, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + 0, + mats_save_hist, + mats_saved_indices_inverse, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row += expr_num_p_table_rows[i]; + } + + //simulation_history[0] = mats; + UpdateSimulationHistory( + simulation_history, + 0, + mats, + mats_save_hist, + mats_saved_indices, + mats_saved_indices_inverse + ); + + // 4 During simulation + expr_index += eval_schedule[0]; + + int p_table_row2; + for (int k=0; k result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + k+1, + mats_save_hist, + mats_saved_indices_inverse, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row2 + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + k+1, + mats_save_hist, + mats_saved_indices_inverse, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row2 + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row2 += expr_num_p_table_rows[expr_index+i]; + + #ifdef MP_VERBOSE + int n = mats.m_matrices.size(); + for (int ii = 0; ii < n; ii++) + std::cout << "mats = " << mats.m_matrices[ii] << std::endl; + #endif + } + //simulation_history[k+1] = mats; + UpdateSimulationHistory( + simulation_history, + k+1, + mats, + mats_save_hist, + mats_saved_indices, + mats_saved_indices_inverse + ); + } + p_table_row = p_table_row2; + + // 5 Post-simulation + expr_index += eval_schedule[1]; + + for (int i=0; i result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + time_steps+1, + mats_save_hist, + mats_saved_indices_inverse, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + time_steps+1, + mats_save_hist, + mats_saved_indices_inverse, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row += expr_num_p_table_rows[expr_index+i]; + } + + //simulation_history[time_steps+1] = mats; + UpdateSimulationHistory( + simulation_history, + time_steps+1, + mats, + mats_save_hist, + mats_saved_indices, + mats_saved_indices_inverse + ); + +#ifdef MP_VERBOSE + std::cout << "Simulation history ..." << std::endl; + int m = simulation_history.size(); + for (int t=0; t +#include +#include +#include +#include +#include +#include // isnan() is defined +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// Macpan2 is redesigned architecture. The spec is +// https://canmod.net/misc/cpp_side.html +// +// Operands in a math expression and its intermediate/final results are all +// generalized as matrices, i.e., +// scalar as 1x1 matrix +// vector as mx1 or 1xn matrix +// matrix as mxn matrix +// so that we can unified the definition of function EvalExpr. The Eigen library +// (https://eigen.tuxfamily.org/dox/group__TutorialMatrixClass.html) has similar +// generalization idea. +// +// Elementwise Binary Operators (+ - * / ^) +// + if both operands have the same shape, then do the elementwise operation +// + elif one of the operands is a scalar, then do the operation of scalar vs +// elements of the other operand +// + elif one of the operands is a vector, then +// - if the length of the vector is equal to the corresponding dimension of the +// other operand, then do row- or column-wise element-vs-element operation. +// - else ERROR +// + else ERROR +// +// Matrix Binary operations (%*%) +// + if the second dimension of first operand == the first dimension of +// the second operand, then do matrix multiplication +// + else ERROR +// NOTE: 1 Inner product can be made by row_vec %*% col_vec while outer product +// can be made by col_vec %*% row_vec. +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Functions we Support +// Please follow exactly the format below when adding a new function: +// MP2_CPP_CASE_NAME = {case number} // {R function name} +// Note that the last entry does not have the comma that follows +// {case number}. +// If an entry in this enum does not have an associated case in the +// switch(table_x[row]+1) statement, then it _must_ be commented out. +// An analogous R-side list can be automatically produced using the +// package makefile. The ordering of the functions must follow their +// associated integers, and the integers must increase from 1 with no +// gaps. Therefore, you should add any new functions at the end, +// although it is possible to change the order of the functions as +// long as the integers increase from 1 without gaps as long as the +// R/enum.R file is regenerated. +enum macpan2_func { + MP2_ADD = 1 // binop,null: `+`(x, y) + , MP2_SUBTRACT = 2 // binop,null: `-`(x, y) + , MP2_MULTIPLY = 3 // binop,null: `*`(x, y) + , MP2_DIVIDE = 4 // binop,null: `/`(x, y) + , MP2_POWER = 5 // binop,null: `^`(x, y) + , MP2_EXP = 6 // fwrap,null: exp(x) + , MP2_LOG = 7 // fwrap,null: log(x) + , MP2_ROUND_BRACKET = 8 // null,null: `(`(...) + , MP2_COMBINE = 9 // null,null: c(...) + , MP2_MATRIX = 10 // fwrap,null: matrix(x, i, j) + , MP2_MATRIX_MULTIPLY = 11 // binop,null: `%*%`(x, y) + , MP2_SUM = 12 // null,null: sum(...) + , MP2_REPLICATE = 13 // fwrap,null: rep(x, times) + , MP2_ROWSUMS = 14 // fwrap,null: rowSums(x) + , MP2_COLSUMS = 15 // fwrap,null: colSums(x) + , MP2_GROUPSUMS = 16 // fwrap,null: groupSums(x, f, n) + , MP2_SQUARE_BRACKET = 17 // null,null: `[`(x, i, j) + , MP2_BLOCK = 18 // fwrap,fail: block(x, i, j, n, m) + , MP2_TRANSPOSE = 19 // fwrap,null: t(x) + , MP2_RBIND_TIME = 20 // fwrap,fail: rbind_time(x, t, t_min) + , MP2_RBIND_LAG = 21 // fwrap,fail: rbind_lag(x, lag, t_min) + , MP2_CBIND_TIME = 22 // fwrap,fail: cbind_time(x, t, t_min) + , MP2_CBIND_LAG = 23 // fwrap,fail: cbind_lag(x, lag, t_min) + , MP2_COLON = 24 // null,null: `:`(from, to) + , MP2_SEQUENCE = 25 // fwrap,fail: seq(from, length, by) + , MP2_CONVOLUTION = 26 // fwrap,fail: convolution(x, k) + , MP2_CBIND = 27 // fwrap,null: cbind(...) + , MP2_RBIND = 28 // fwrap,null: rbind(...) + , MP2_TIME_STEP = 29 // fwrap,fail: time_step(lag) + , MP2_ASSIGN = 30 // fwrap,null: assign(x, i, j, v) + , MP2_UNPACK = 31 // fwrap,fail: unpack(x, ...) + , MP2_RECYCLE = 32 // fwrap,null: recycle(x, rows, cols) + , MP2_CLAMP = 33 // fwrap,null: clamp(x, eps) + , MP2_POISSON_DENSITY = 34 // fwrap,fail: dpois(observed, simulated) + , MP2_NEGBIN_DENSITY = 35 // fwrap,fail: dnbinom(observed, simulated, over_dispersion) + , MP2_NORMAL_DENSITY = 36 // fwrap,fail: dnorm(observed, simulated, standard_deviation) + , MP2_POISSON_SIM = 37 // fwrap,fail: rpois(mean) + , MP2_NEGBIN_SIM = 38 // fwrap,fail: rnbinom(mean, over_dispersion) + , MP2_NORMAL_SIM = 39 // fwrap,fail: rnorm(mean, standard_deviation) + , MP2_KRONECKER = 40 // binop,null: `%x%`(x, y) + , MP2_TO_DIAG = 41 // fwrap,fail: to_diag(x) + , MP2_FROM_DIAG = 42 // fwrap,fail: from_diag(x) + , MP2_TIME_GROUP = 43 //fwrap,fail: time_group(i, change_points) + , MP2_COS = 44 // fwrap,null: cos(x) + //, MP2_LOGISTIC = 45 // fwrap,null: logistic(x) + //, MP2_LOGIT = 46 // fwrap,null: logit(x) +}; + +// Helper function +template +int CheckIndices( + matrix& x, + matrix& rowIndices, + matrix& colIndices +) { + int rows = x.rows(); + int cols = x.cols(); + Type maxRowIndex = rowIndices.maxCoeff(); + Type maxColIndex = colIndices.maxCoeff(); + Type minRowIndex = rowIndices.minCoeff(); + Type minColIndex = colIndices.minCoeff(); + + if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { + return 0; + } + return 1; +} + +// Helper function +template +int RecycleInPlace( + matrix& mat, + int rows, + int cols +) { + #ifdef MP_VERBOSE + std::cout << "recycling ... " << std::endl; + #endif + if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. + return 0; + + matrix m(rows, cols); + if (mat.rows()==1 && mat.cols()==1) { + m = matrix::Constant(rows, cols, mat.coeff(0,0)); + } + else if (mat.rows()==rows) { + if (mat.cols()==1) { + #ifdef MP_VERBOSE + std::cout << "recycling columns ... " << std::endl; + #endif + for (int i=0; i +struct ListOfMatrices { + // below is a vector of matrices that passed from R + vector > m_matrices; + + ListOfMatrices(SEXP ii){ // Constructor + // Get elements by their indices + int n = length(ii); + vector > vs(n); + m_matrices = vs; + + for (int i = 0; i < n; i++) { + m_matrices[i] = asMatrix(VECTOR_ELT(ii, i)); + } + } + + ListOfMatrices() { // Default Constructor + } + + // Copy constructor + ListOfMatrices(const ListOfMatrices& another) { + m_matrices = another.m_matrices; + } + + // Overload assign operator + ListOfMatrices & operator=(const ListOfMatrices& another) { + m_matrices = another.m_matrices; + return *this; + } +}; + +template +class ExprEvaluator { +public: + // constructor + ExprEvaluator() { + error_code = 0; // non-zero means error has occurred; otherwise, no error + expr_row = 0; + strcpy(error_message, "OK"); + }; + + // getters + unsigned char GetErrorCode() { return error_code; }; + const char* GetErrorMessage() { return error_message; }; + int GetExprRow() {return expr_row; }; + + // setters + void SetError(unsigned char code, const char* message, int row) + { + error_code = code; + expr_row = row; + strcpy(error_message, message); + std::cout << "MACPAN ERROR #" << (int) code << ": " << message << std::endl; + }; + + // evaluators + matrix EvalExpr( + const vector >& hist, + int t, + const vector& mats_save_hist, + const vector& table_x, + const vector& table_n, + const vector& table_i, + ListOfMatrices& valid_vars, + const vector& valid_literals, + int row = 0 + ) + { + // Variables to use locally in function bodies + matrix m, m1, m2; // return values + matrix timeIndex; // for rbind_time + Type sum, s, eps, var; // intermediate scalars + int rows, cols, lag, rowIndex, colIndex, matIndex, reps, cp, off, size, sz, start, err_code, err_code1, err_code2; + + if (GetErrorCode()) return m; // Check if error has already happened at some point of the recursive call. + + switch (table_n[row]) { + case -1: // literals + m = matrix::Zero(1,1); + m.coeffRef(0,0) = valid_literals[table_x[row]]; + return m; + case 0: + m = valid_vars.m_matrices[table_x[row]]; + return m; + default: + int n = table_n[row]; + vector > args(n); + vector index2mats(n); + for (int i=0; i::Zero(to-from+1,1); + for (int i=from; i<=to; i++) + m.coeffRef(i-from,0) = i; + #ifdef MP_VERBOSE + std::cout << from << ":" << to << " = " << m << std::endl << std::endl; + #endif + return m; + + case MP2_SEQUENCE: // seq + + // #' The `seq` function is a little different from the + // #' base R default, \code{\link{seq}}, in that it + // #' allows the user precise control over the length of + // #' the output through the `length` argument. The + // #' base R function gives the user this option, but not + // #' as the default. + // #' + int length, by; + from = CppAD::Integer(args[0].coeff(0,0)); + length = CppAD::Integer(args[1].coeff(0,0)); + by = CppAD::Integer(args[2].coeff(0,0)); + if (length<=0) { + SetError(MP2_SEQUENCE, "Sequence length is less than or equal to zero in seq operation", row); + return m; + } + m = matrix::Zero(length,1); + for (int i=0; i::Zero(size,1); + off = 0; + for (int i=0; i::Zero(rows, totcols); + for (int i=0; i::Zero(rows, 1); + return m; + case MP2_RBIND: + { + cols = args[0].cols(); + // std::cout << "cols: " << cols << std::endl; + // std::cout << "n: " << n << std::endl; + int rows_per_arg; + int totrows, rowmarker; + totrows = 0; + rowmarker = 0; + for (int j=0; j::Zero(totrows, cols); + for (int i=0; i::Zero(1, cols); + return m; + case MP2_MATRIX: // matrix + + // #' The `matrix` function can be used to redefine the + // #' numbers of rows and columns to use for arranging + // #' the values of a matrix. It works similarly to + // #' the base R \code{\link{matrix}} function in that it + // #' takes the same arguments. + // #' On the other hand, this function differs substantially + // #' from the base R version in that it must be filled + // #' by column and there is no `byrow` option. + // #' + m = args[0]; + + rows = CppAD::Integer(args[1].coeff(0,0)); + cols = CppAD::Integer(args[2].coeff(0,0)); + + //m.conservativeResize(rows, cols); // don't know why this doesn't work + m.resize(rows, cols); + + // m2 = m.transpose(); // m = m.transpose() doesn't work !!! + m2 = m; + + #ifdef MP_VERBOSE + std::cout << "matrix(" << args[0] << ") reshaped into [" << rows << ", " << cols << "] = " \ + << m2 << std::endl << std::endl; + #endif + + return m2; + + // #' Matrices can be transposed with the usual + // #' function, \code{\link{t}}. + // #' + case MP2_TRANSPOSE: // t or transpose + m = args[0].transpose(); + return m; + + // #' ### Examples + // #' + // #' ``` + // #' engine_eval(~ c(a, b, c), a = 1, b = 10:13, c = matrix(20:25, 3, 2)) + // #' engine_eval(~ cbind(a, 10 + a), a = 0:3) + // #' engine_eval(~ rbind(a, 10 + a), a = t(0:3)) + // #' engine_eval(~ matrix(1:12, 4, 3)) + // #' engine_eval(~ t(1:3)) + // #' ``` + // #' + + + // #' ## Matrix Diagonals + // #' + // #' ### Functions + // #' + // #' * `to_diag(x)` -- Create a diagonal matrix by setting + // #' the diagonal to a column vector, `x`. + // #' * `from_diag(x)` -- Extract the diagonal from a + // #' matrix, `x`, and return the diagonal as a column + // #' vector. + // #' + // #' ### Arguments + // #' + // #' * `x` -- Any matrix (for `from_diag`) or a + // #' column vector (for `to_diag`). It is common to assume + // #' that `x` is square for `from_diag` but this is + // #' not required. + // #' + // #' ### Return + // #' + // #' * `to_diag(x)` -- Diagonal matrix with `x` on the + // #' diagonal. + // #' * `from_diag(x)` -- Column vector containing the + // #' diagonal of `x`. A value is considered to be on + // #' the diagonal if it has a row index equal to + // #' the column index. + // #' + // #' ### Details + // #' + // #' The `to_diag` function can be used to produce a + // #' diagonal matrix by setting a column vector equal + // #' to the desired diagonal. The `from_diag` does + // #' (almost) the opposite, which is to get a column vector + // #' containing the diagonal of an existing matrix. + // #' + // #' ### Examples + // #' + // #' ``` + // #' engine_eval(~from_diag(matrix(1:9, 3, 3))) + // #' engine_eval(~to_diag(from_diag(matrix(1:9, 3, 3)))) + // #' engine_eval(~from_diag(to_diag(from_diag(matrix(1:9, 3, 3))))) + // #' ``` + // #' + case MP2_TO_DIAG: // to_diag + rows = args[0].rows(); + m = matrix::Zero(rows, rows); + for (int i=0; i::Zero(1,1); + sum = 0.0; + for (int i=0; i::Zero(1,1); + ncol=1; + } + else{ + ncol = args[2].size(); + m1 = args[2]; + } + m = matrix::Zero(nrow,ncol); + + err_code = CheckIndices(args[0], args[1], m1); + if (err_code) { + SetError(MP2_SQUARE_BRACKET, "Illegal index to square bracket", row); + return m; + } + + // if we can assume contiguous sets of rows and columns + // then mat.block(...) will be faster, so should we + // have a block function on the R side when speed + // matters? + // Can we vectorize CppAD::Integer casting?? + for (int i=0; i::Zero(t - 1, 1); + for (int i=0; i < t - 1; i++) { + timeIndex.coeffRef(i, 0) = i + 1; + } + } else { + timeIndex = args[1]; + } + if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { + SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) initialized matrices with saved history", row); + return args[0]; + } + + int lowerTimeBound; + if (table_n[row]==3) + lowerTimeBound = CppAD::Integer(args[2].coeff(0,0)); + else + lowerTimeBound = 0; + + // Get the length of legitimate times in rbind_time. + // Check if the shape of the matrix changes. + // Error if yes or assign variables "rows" and "cols" with + // the correct values otherwise. + int rbind_length, nRows, nCols; + rbind_length = 0; // count of legitimate time steps to select + for (int i=0; i=lowerTimeBound) { + nRows = hist[rowIndex].m_matrices[matIndex].rows(); + nCols = hist[rowIndex].m_matrices[matIndex].cols(); + } + else if (rowIndex==t) { + nRows = valid_vars.m_matrices[matIndex].rows(); + nCols = valid_vars.m_matrices[matIndex].cols(); + } + else + continue; + + if (nRows==0 || nCols==0) // skip empty matrix + continue; + + if (rbind_length==0) { // first one + rows = nRows; + cols = nCols; + } + else { + if (rows!=nRows || cols!=nCols) { // Shall we allow inconsistent rows? + SetError(MP2_RBIND_TIME, "Inconsistent rows or columns in rbind_time (or rbind_lag)", row); + return args[0]; + } + } + + rbind_length++; + } + #ifdef MP_VERBOSE + std::cout << "rbind_time(" << timeIndex << ") = " << std::endl; + #endif + + if (rbind_length>0) { + //rows = hist[0].m_matrices[matIndex].rows(); + //cols = hist[0].m_matrices[matIndex].cols(); + m = matrix::Zero(rbind_length*rows, cols); + rbind_length = 0; + for (int i=0; i=lowerTimeBound) { + if (hist[rowIndex].m_matrices[matIndex].rows()!=0 && + hist[rowIndex].m_matrices[matIndex].cols()!=0) { + m.block(rbind_length*rows, 0, rows, cols) = hist[rowIndex].m_matrices[matIndex]; + rbind_length++; + } + } + else if (rowIndex==t) { + if (valid_vars.m_matrices[matIndex].rows()!=0 && + valid_vars.m_matrices[matIndex].cols()!=0) { + m.block(rbind_length*rows, 0, rows, cols) = valid_vars.m_matrices[matIndex]; + rbind_length++; + } + } + #ifdef MP_VERBOSE + std::cout << m.block((rbind_length-1)*rows, 0, rows, cols) << std::endl << std::endl; + #endif + } + } + + return m; // empty matrix (if colIndex==0) or non-empty one (otherwise) + + // #' ## Time Indexing + // #' + // #' Get the index of current or lagged time step or + // #' the index of the current time group. A time group + // #' is a contiguous set of time steps defined by two + // #' change points. + // #' + // #' ### Functions + // #' + // #' * `time_step(lag)`: Get the time-step associated + // #' with a particular lag from the current time-step. + // #' If the lagged time-step is less than zero, the + // #' function returns zero. + // #' * `time_group(index, change_points)`: Update the + // #' `index` associated with the current time group. + // #' The current group is defined by the minimum + // #' of all elements of `change_points` that are + // #' greater than the current time step. The time group + // #' `index` is the index associated with this element. + // #' Please see the examples below, they are easier + // #' to understand than this explanation. + // #' + // #' ### Arguments + // #' + // #' * `lag`: Number of time-steps to look back for + // #' the time-step to return. + // #' * `index`: Index associated with the current time + // #' group. + // #' * `change_points`: Increasing column vector of + // #' time steps giving the lower bound of each time + // #' group. + // #' + // #' ### Return + // #' + // #' A 1-by-1 matrix with the time-step `lag` steps + // #' ago, or with zero if `t+1 < lag` + // #' + // #' ### Examples + // #' + // #' ``` + // #' simple_sims( + // #' iteration_exprs = list(x ~ time_step(0)), + // #' time_steps = 10, + // #' x = empty_matrix + // #' ) + // #' sims = simple_sims( + // #' iteration_exprs = list( + // #' j ~ time_group(j, change_points), + // #' time_varying_parameter ~ time_variation_schedule[j] + // #' ), + // #' time_steps = 10, + // #' j = 0, + // #' change_points = c(0, 4, 7), + // #' time_variation_schedule = c(42, pi, sqrt(2)), + // #' time_varying_parameter = empty_matrix + // #' ) + // #' ``` + // #' + case MP2_TIME_STEP: // time_step(lag) + m = matrix::Zero(1,1); + lag = CppAD::Integer(args[0].coeff(0,0)); + if (lag < 0) { + SetError(MP2_TIME_STEP, "Time lag needs to be non-negative", row); + return m; + } + if (t > lag) { + m.coeffRef(0,0) = t - lag; + } + return m; + + case MP2_TIME_GROUP: // time_group(i, change_points) + m = args[0]; + off = CppAD::Integer(args[0].coeff(0, 0)); + cp = CppAD::Integer(args[1].coeff(off + 1, 0)); + if (cp == t) { + m.coeffRef(0,0) = off + 1; + } + return m; + + case MP2_CONVOLUTION: + + // #' ## Convolution + // #' + // #' One may take the convolution of each element in a + // #' matrix, x, over simulation time using a kernel, k. + // #' There are two arguments of this function. + // #' + // #' ### Functions + // #' + // #' * `convolution(x, k)` + // #' + // #' ### Arguments + // #' + // #' * `x` -- The matrix containing elements to be + // #' convolved. + // #' * `k` -- A column vector giving the convolution kernel. + // #' + // #' ### Return + // #' + // #' A matrix the same size as `x` but with the + // #' convolutions, \eqn{y_{ij}}, of each element, + // #' \eqn{x_{ij}}, given by the following. + // #' + // #' \deqn{y_{ij} = \sum_{\tau = 0} x_{ij}(t-\tau) k(\tau)} + // #' + // #' unless \eqn{t < \tau}, in which case, + // #' + // #' \deqn{y_{ij} = } + // #' + // #' where \eqn{y_{ij}} is the convolution, + // #' \eqn{x_{ij}(t)} is the value of \eqn{x_{ij}} at time step, \eqn{t}, + // #' \eqn{k(\tau)} is the value of the kernel at lag, \eqn{\tau}, + // #' and \eqn{\lambda} is the length of the kernel. + // #' + // #' ### Details + // #' + // #' If any empty matrices are encountered when looking + // #' back in time, they are treated as matrices with all + // #' zeros. Similarly, any matrices encounte + // #' of `x` + // #' + matIndex = index2mats[0]; // m + #ifdef MP_VERBOSE + std::cout << "matIndex: " << matIndex << std::endl << std::endl; + #endif + length = args[1].rows(); + #ifdef MP_VERBOSE + std::cout << "length: " << length << std::endl << std::endl; + #endif + if (length>0 && args[1].cols()==1) { + #ifdef MP_VERBOSE + std::cout << "kernel 1: " << args[1] << std::endl << std::endl; + #endif + if (t+1::Zero(rows, cols); + + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; ispi[0]] + m = matrix::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i::Zero(rows, cols); + for (int i=0; i=sz) { + m1 = m.block(start, 0, sz, 1); + m1.resize(args[i].rows(), args[i].cols()); + //std::cout << "MATRIX " << valid_vars.m_matrices[index2mats[i]] << std::endl << std::endl; + valid_vars.m_matrices[index2mats[i]] = m1; + // args[i] = m1; + size -= sz; + start += sz; + } + else + break; + } + return m2; // empty matrix + + case MP2_RECYCLE: + m = args[0]; + rows = CppAD::Integer(args[1].coeff(0,0)); + cols = CppAD::Integer(args[2].coeff(0,0)); + err_code = RecycleInPlace(m, rows, cols); + if (err_code != 0) { + SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + return m; + } + return m; + + default: + SetError(255, "invalid operator in arithmetic expression", row); + return m; + } + } + }; + + +private: + // Functor for computing derivatives of expressions. + // template + // struct matrix_functor{ + // // define data members + // + // // define constructor + // matrix_functor() : // initialization list + // { // the body is empty + // } + // // the function itself + // template + // vector operator()(vector input_vector_) + // { + // vector output_vector_ = EvalExpr( + // simulation_history_, + // 0, + // mats_save_hist_, + // p_table_x_, + // p_table_n_, + // p_table_i_, + // mats_, + // literals_, + // p_table_row_ + // ); + // // call exprEval in here to convert input_vector_ into output_vector_ + // return (output_vector_); + // } + // } + unsigned char error_code; + int expr_row; + char error_message[256]; +}; + +#define REPORT_ERROR { \ + int error = exprEvaluator.GetErrorCode(); \ + int expr_row = exprEvaluator.GetExprRow(); \ + REPORT(error); \ + REPORT(expr_row); \ + \ + logfile.open (LOG_FILE_NAME, std::ios_base::app); \ + logfile << "Error code = " << error << std::endl; \ + logfile << "Error message = " << exprEvaluator.GetErrorMessage() << std::endl; \ + logfile << "Expression row = " << expr_row << std::endl; \ + logfile.close(); \ +} + +template +vector > MakeSimulationHistory( + const int time_steps, + const vector& mats_save_hist, + ListOfMatrices& hist_shape_template +) { + vector > simulation_history(time_steps+2);//, hist_shape_template); + for (int tt=0; tt >::iterator iter; + //std::fill(iter.begin(), iter_end(), hist_shape_template); + // matrix empty_matrix; + // for (int i=0; i +void UpdateSimulationHistory( + vector >& hist, + int t, + const ListOfMatrices& mats, + const vector& mats_save_hist, + ListOfMatrices& hist_shape_template +) { + // matrix emptyMat; + // ListOfMatrices ms(mats); + // if the history of the matrix is not to be saved, + // just save a 1-by-1 with a zero instead to save space + for (int i=0; i +Type objective_function::operator() () +{ + #ifdef MP_VERBOSE + std::cout << "============== objective_function =============" << std::endl; + #endif + + std::ofstream logfile; + logfile.open (LOG_FILE_NAME); + logfile << "======== log file of MacPan2 ========\n"; + logfile.close(); + + std::setprecision(9); // Set the precision of std::cout + + // 1 Get all data and parameters from the R side + // Parameters themselves + int n; + + // Fixed effects + PARAMETER_VECTOR(params); + + // Random effects + PARAMETER_VECTOR(random); + + // Matrices + DATA_STRUCT(mats, ListOfMatrices); + DATA_STRUCT(mats_shape, ListOfMatrices); + DATA_IVECTOR(mats_save_hist); + DATA_IVECTOR(mats_return); + + // Fixed parameter replacements + DATA_IVECTOR(p_par_id); + DATA_IVECTOR(p_mat_id); + DATA_IVECTOR(p_row_id); + DATA_IVECTOR(p_col_id); + + // Random parameter replacements + DATA_IVECTOR(r_par_id); + DATA_IVECTOR(r_mat_id); + DATA_IVECTOR(r_row_id); + DATA_IVECTOR(r_col_id); + + // Trajectory simulation + DATA_INTEGER(time_steps) + + // Expressions and parse table + DATA_IVECTOR(expr_output_id); + DATA_IVECTOR(expr_sim_block); + DATA_IVECTOR(expr_num_p_table_rows); + DATA_IVECTOR(eval_schedule) + DATA_IVECTOR(p_table_x); + DATA_IVECTOR(p_table_n); + DATA_IVECTOR(p_table_i); + + // Literals + DATA_VECTOR(literals); + + // Objective function parse table + DATA_IVECTOR(o_table_n); + DATA_IVECTOR(o_table_x); + DATA_IVECTOR(o_table_i); + + + // DATA_STRUCT(settings, settings_struct); + + #ifdef MP_VERBOSE + std::cout << "params = " << params << std::endl; + + std::cout << "random = " << random << std::endl; + + n = mats.m_matrices.size(); + for (int i = 0; i < n; i++) + std::cout << "mats = " << mats.m_matrices[i] << std::endl; + + std::cout << "p_par_id = " << p_par_id << std::endl; + std::cout << "p_mat_id = " << p_mat_id << std::endl; + std::cout << "p_row_id = " << p_row_id << std::endl; + std::cout << "p_col_id = " << p_col_id << std::endl; + + std::cout << "r_par_id = " << r_par_id << std::endl; + std::cout << "r_mat_id = " << r_mat_id << std::endl; + std::cout << "r_row_id = " << r_row_id << std::endl; + std::cout << "r_col_id = " << r_col_id << std::endl; + + std::cout << "time_steps = " << time_steps << std::endl; + + std::cout << "mats_save_hist = " << mats_save_hist << std::endl; + std::cout << "mats_return = " << mats_return << std::endl; + + std::cout << "eval_schedule = " << eval_schedule << std::endl; + + //std::cout << "expr_output_count = " << expr_output_count << std::endl; + std::cout << "expr_output_id = " << expr_output_id << std::endl; + std::cout << "expr_sim_block = " << expr_sim_block << std::endl; + std::cout << "expr_num_p_table_rows = " << expr_num_p_table_rows << std::endl; + + std::cout << "p_table_x = " << p_table_x << std::endl; + std::cout << "p_table_n = " << p_table_n << std::endl; + std::cout << "p_table_i = " << p_table_i << std::endl; + + std::cout << "literals = " << literals << std::endl; + + std::cout << "o_table_x = " << o_table_x << std::endl; + std::cout << "o_table_n = " << o_table_n << std::endl; + std::cout << "o_table_i = " << o_table_i << std::endl; + #endif + + // 2 Replace some of elements of some matrices with parameters + n = p_par_id.size(); + for (int i=0; i hist_shape_template(mats); + vector > simulation_history = MakeSimulationHistory( + time_steps, + mats_save_hist, + mats_shape //hist_shape_template + ); + + + + ////////////////////////////////// + // Define an expression evaluator + ExprEvaluator exprEvaluator; + ////////////////////////////////// + + // 3 Pre-simulation + int expr_index = 0; + int p_table_row = 0; + + for (int i=0; i result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + 0, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + 0, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row += expr_num_p_table_rows[i]; + } + + //simulation_history[0] = mats; + UpdateSimulationHistory( + simulation_history, + 0, + mats, + mats_save_hist, + mats_shape //hist_shape_template + ); + + // 4 During simulation + expr_index += eval_schedule[0]; + + int p_table_row2; + for (int k=0; k result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + k+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row2 + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + k+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row2 + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row2 += expr_num_p_table_rows[expr_index+i]; + + #ifdef MP_VERBOSE + int n = mats.m_matrices.size(); + for (int ii = 0; ii < n; ii++) + std::cout << "mats = " << mats.m_matrices[ii] << std::endl; + #endif + } + //simulation_history[k+1] = mats; + UpdateSimulationHistory( + simulation_history, + k+1, + mats, + mats_save_hist, + mats_shape //hist_shape_template + ); + } + p_table_row = p_table_row2; + + // 5 Post-simulation + expr_index += eval_schedule[1]; + + for (int i=0; i result; + if (expr_sim_block[i]==1) { + SIMULATE { + result = exprEvaluator.EvalExpr( + simulation_history, + time_steps+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + } + } + else + result = exprEvaluator.EvalExpr( + simulation_history, + time_steps+1, + mats_save_hist, + p_table_x, + p_table_n, + p_table_i, + mats, + literals, + p_table_row + ); + + if (exprEvaluator.GetErrorCode()) { + REPORT_ERROR + return 0.0; + } + + mats.m_matrices[expr_output_id[expr_index+i]] = result; + + p_table_row += expr_num_p_table_rows[expr_index+i]; + } + + //simulation_history[time_steps+1] = mats; + UpdateSimulationHistory( + simulation_history, + time_steps+1, + mats, + mats_save_hist, + mats_shape //hist_shape_template + ); + +#ifdef MP_VERBOSE + std::cout << "Simulation history ..." << std::endl; + int m = simulation_history.size(); + for (int t=0; t Date: Wed, 18 Oct 2023 09:58:14 -0400 Subject: [PATCH 026/332] test partitions --- tests/testthat/test-partitions.R | 78 ++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/testthat/test-partitions.R diff --git a/tests/testthat/test-partitions.R b/tests/testthat/test-partitions.R new file mode 100644 index 00000000..25cf33ac --- /dev/null +++ b/tests/testthat/test-partitions.R @@ -0,0 +1,78 @@ +library(oor) +library(macpan2) +r = CSVReader(system.file("starter_models", "sir_vax", "variables.csv", package = "macpan2")) +x = Partition(r$read()) +x$name() + +process_partition_names = function(...) { + (list(...) + |> lapply(as.character) + |> unlist(recursive = TRUE, use.names = FALSE) + |> paste0(collapse = ".") + |> strsplit(".", fixed = TRUE) + |> unlist(recursive = TRUE, use.names = FALSE) + ) +} +process_labels = function(...) { + (list(...) + |> lapply(as.character) + |> unlist(recursive = TRUE, use.names = FALSE) + ) +} +rm_blank_rows = function(partition) { + partition[apply(partition$frame, 1L, all_equal, ""), , drop = FALSE] +} +valid_rows = function(partition) { + partition$frame |> unique() |> rm_blank_rows() |> PartitionAlt() +} +filter_join <- function(x, y, by = NULL, type = c("anti", "semi")) { + type <- match.arg(type, choices = c("anti", "semi"), several.ok = FALSE) + if (is.null(by)) { + by <- intersect(names(x), names(y)) + } + rows <- interaction(x[, by]) %in% interaction(y[, by]) + if (type == "anti") rows <- !rows + res <- x[rows, , drop = FALSE] + rownames(res) <- NULL + res +} + +PartitionAlt = function(frame) { + self = Base() + rownames(frame) = NULL + self$frame = frame + self$names = function() names(self$frame) + self$name = function() paste(self$names(), collapse = ".") + self$labels = function() do.call(paste, c(self$frame, sep = ".")) + self$dotted = function() as.data.frame(setNames(list(self$labels()), self$name())) + self$filter = function(..., .wrt, .comparison_function = all_equal) { + labels = process_labels(...) + f = self$select(.wrt) + f$frame[f$labels() %in% labels, , drop = FALSE] + } + self$select = function(...) { + cols_to_keep = process_partition_names(...) + f = self$frame[cols_to_keep] + l = do.call(paste, c(f, sep = ".")) + i = !duplicated(l) & (l != "") + PartitionAlt(f[i, , drop = FALSE]) + } + self$select_out = function(...) { + cols_to_remove = process_partition_names(...) + self$select(setdiff(self$names(), cols_to_remove)) + } + return_object(self, "PartitionAlt") +} +print.PartitionAlt = function(x, ...) print(x$frame) +xx = PartitionAlt(r$read()) +x = Partition(r$read()) +identical(xx$names(), x$names()) +identical(xx$name(), x$name()) +identical(xx$labels(), x$labels()) +identical(xx$dotted(), xx$dotted()) +identical(xx$select("Vax", "Epi")$frame, x$select("Vax", "Epi")$frame()) +identical(xx$select("Vax")$frame, x$select("Vax")$frame()) +identical(xx$select_out("Epi")$frame, x$select_out("Epi")$frame()) +xx +xx$select("Vax") +x$select("Vax") From aa4d58d0e1abd09c954c3a28a2d438b26ef46af4 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:01:57 -0400 Subject: [PATCH 027/332] model product diagram --- misc/diagrams/product_math.drawio | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/misc/diagrams/product_math.drawio b/misc/diagrams/product_math.drawio index 35fbaad7..cb2ee541 100644 --- a/misc/diagrams/product_math.drawio +++ b/misc/diagrams/product_math.drawio @@ -1,49 +1,49 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 00b9674633381450f0e4fc9e1b910269244a45ff Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:02:39 -0400 Subject: [PATCH 028/332] remove old svg --- misc/diagrams/product_math.svg | 1396 -------------------------------- 1 file changed, 1396 deletions(-) delete mode 100644 misc/diagrams/product_math.svg diff --git a/misc/diagrams/product_math.svg b/misc/diagrams/product_math.svg deleted file mode 100644 index 2691e9ec..00000000 --- a/misc/diagrams/product_math.svg +++ /dev/null @@ -1,1396 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 329641dba2d84c50c734efb71642de053ef55a52 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:04:04 -0400 Subject: [PATCH 029/332] model matrix diagram --- misc/diagrams/model_matrices.drawio | 133 ++++++++++++++++++++++++++++ misc/diagrams/model_matrices.png | Bin 0 -> 45119 bytes misc/diagrams/model_matrices.svg | 1 + 3 files changed, 134 insertions(+) create mode 100644 misc/diagrams/model_matrices.drawio create mode 100644 misc/diagrams/model_matrices.png create mode 100644 misc/diagrams/model_matrices.svg diff --git a/misc/diagrams/model_matrices.drawio b/misc/diagrams/model_matrices.drawio new file mode 100644 index 00000000..c6821c0d --- /dev/null +++ b/misc/diagrams/model_matrices.drawio @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/diagrams/model_matrices.png b/misc/diagrams/model_matrices.png new file mode 100644 index 0000000000000000000000000000000000000000..eae69a630ad7345dda61c2cf89d2d660eeaa5aca GIT binary patch literal 45119 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

    λm
    λs
    λs
    v
    v
    S
    S
    γm
    γm
    Im
    Im
    R
    R
    γs
    γs
    Is
    Is
    w
    w
    $\lambda_v$
    $\lambda_v$
    V
    V
    $\lambda_v$
    $\la...
    Text is not SVG - cannot display \ No newline at end of file From 7e2a9bb3cfa1f5f872c2e712f19a2047040d10ee Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:04:37 -0400 Subject: [PATCH 030/332] drawio integration experiments --- .../drawio-templates/data_frame.drawio | 52 + misc/diagrams/drawio-templates/data_frame.pdf | Bin 0 -> 13441 bytes .../drawio-templates/sir_vax_flows.drawio | 268 +++++ .../drawio-templates/sir_vax_vars.drawio | 286 +++++ misc/diagrams/drawio-templates/test.drawio | 29 + misc/diagrams/drawio-templates/xx.drawio | 440 +++++++ misc/diagrams/flows/flows.csv.drawio | 1064 +++++++++++++++++ misc/diagrams/product.png | Bin 0 -> 29718 bytes misc/diagrams/seir.png | Bin 0 -> 13978 bytes 9 files changed, 2139 insertions(+) create mode 100644 misc/diagrams/drawio-templates/data_frame.drawio create mode 100644 misc/diagrams/drawio-templates/data_frame.pdf create mode 100644 misc/diagrams/drawio-templates/sir_vax_flows.drawio create mode 100644 misc/diagrams/drawio-templates/sir_vax_vars.drawio create mode 100644 misc/diagrams/drawio-templates/test.drawio create mode 100644 misc/diagrams/drawio-templates/xx.drawio create mode 100644 misc/diagrams/flows/flows.csv.drawio create mode 100644 misc/diagrams/product.png create mode 100644 misc/diagrams/seir.png diff --git a/misc/diagrams/drawio-templates/data_frame.drawio b/misc/diagrams/drawio-templates/data_frame.drawio new file mode 100644 index 00000000..ad588b3c --- /dev/null +++ b/misc/diagrams/drawio-templates/data_frame.drawio @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/diagrams/drawio-templates/data_frame.pdf b/misc/diagrams/drawio-templates/data_frame.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7a828ddace6bde61caa5aaaa2538b444ffeb48a6 GIT binary patch literal 13441 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 literal 0 HcmV?d00001 diff --git a/misc/diagrams/drawio-templates/sir_vax_flows.drawio b/misc/diagrams/drawio-templates/sir_vax_flows.drawio new file mode 100644 index 00000000..c650f457 --- /dev/null +++ b/misc/diagrams/drawio-templates/sir_vax_flows.drawio @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/misc/diagrams/drawio-templates/sir_vax_vars.drawio b/misc/diagrams/drawio-templates/sir_vax_vars.drawio new file mode 100644 index 00000000..d1ad11f2 --- /dev/null +++ b/misc/diagrams/drawio-templates/sir_vax_vars.drawio @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/diagrams/drawio-templates/test.drawio b/misc/diagrams/drawio-templates/test.drawio new file mode 100644 index 00000000..fc2fee41 --- /dev/null +++ b/misc/diagrams/drawio-templates/test.drawio @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/diagrams/drawio-templates/xx.drawio b/misc/diagrams/drawio-templates/xx.drawio new file mode 100644 index 00000000..87e89223 --- /dev/null +++ b/misc/diagrams/drawio-templates/xx.drawio @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/diagrams/flows/flows.csv.drawio b/misc/diagrams/flows/flows.csv.drawio new file mode 100644 index 00000000..51df80f8 --- /dev/null +++ b/misc/diagrams/flows/flows.csv.drawio @@ -0,0 +1,1064 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/diagrams/product.png b/misc/diagrams/product.png new file mode 100644 index 0000000000000000000000000000000000000000..78651c81e37d509222652d2b832f2f2197bdc35d GIT binary patch literal 29718 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 OvXY9|pI|l63I>vl3J4NJf(j@( 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 J#YKzT{{`&b;C=uA literal 0 HcmV?d00001 From a75f9245a65f2dbfe1679c43ad4f1978e34736e6 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:05:30 -0400 Subject: [PATCH 031/332] method roxygen tools --- misc/build/build_from_enum_methods.R | 29 ++++++ misc/build/method_head.R | 133 +++++++++++++++++++++++++++ misc/build/method_tail.R | 0 3 files changed, 162 insertions(+) create mode 100644 misc/build/build_from_enum_methods.R create mode 100644 misc/build/method_head.R create mode 100644 misc/build/method_tail.R diff --git a/misc/build/build_from_enum_methods.R b/misc/build/build_from_enum_methods.R new file mode 100644 index 00000000..b5101d51 --- /dev/null +++ b/misc/build/build_from_enum_methods.R @@ -0,0 +1,29 @@ +dev = readLines("misc/dev/dev.cpp") +meth_head = readLines("misc/build/method_head.R") +meth_tail = readLines("misc/build/method_tail.R") +repl = function(x, pat, repl) sub(pat, repl, x, perl = TRUE) +re = "^[, ]*[ ]*(METH_[A-Z_]*)[ ]*=[ ]*[0-9][0-9]*(\\,)*[ ]*//[ ]*(.*)" +meth_lines = (re + |> grep(dev, value = TRUE) +) +meth_names = (meth_lines + |> repl(re, "\\1") + |> tolower() +) +meth_defs = (meth_lines + |> repl(re, "MethodPrototype(\\3)") +) +output_lines = c("## Auto-generated - do not edit by hand" + , "" + , meth_head + , "" + , "MethodTypes = function() {" + , " self = MethodTypeUtils()" + , sprintf(" self$method_ordering = c(%s)", paste0( + sprintf('"%s"', meth_names), + collapse = ", ")) + , sprintf(" self$%s = %s", meth_names, meth_defs) + , " return_object(self, \"MethodTypes\")" + , "}" +) +writeLines(output_lines, "R/enum_methods.R") diff --git a/misc/build/method_head.R b/misc/build/method_head.R new file mode 100644 index 00000000..5213fba0 --- /dev/null +++ b/misc/build/method_head.R @@ -0,0 +1,133 @@ +#' Method Prototype +#' +#' Define a method type using a prototype. These prototypes can be compared +#' with methods defined in R to see if they are consistent with a method +#' type that has been defined in C++. All +#' arguments are automatically derived through comments in the C++ code where +#' the method types are defined. +#' +#' @param formula Formula for defining a method type using a prototype. +#' @param mat_arg_nms Character vector naming the matrix-valued arguments. +#' @param int_vec_arg_nms Character vector naming the integer-vector-valued +#' arguments. +#' +#' @nord +MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { + self = Base() + self$formula = formula + self$mat_arg_nms = mat_arg_nms + self$int_vec_arg_nms = int_vec_arg_nms + self$mat_args = function(other_formula) { + stopifnot(self$consistent(other_formula)) + that = concat_parse_table(other_formula) + this = concat_parse_table(self$formula) + which_mats = this$x %in% self$mat_arg_nms + that$x[which_mats] + } + self$int_vec_args = function(other_formula) { + stopifnot(self$consistent(other_formula)) + that = concat_parse_table(other_formula) + this = concat_parse_table(self$formula) + which_int_vecs = this$x %in% self$int_vec_arg_nms + that$x[which_int_vecs] + } + self$as_character = function() deparse(self$formula) + self$is_setter = function(other_formula) { + two_sided(self$formula) & two_sided(other_formula) + } + self$is_getter = function(other_formula) { + one_sided(self$formula) & one_sided(other_formula) + } + self$parse_table = function() { + method_parser(self$formula) + } + self$consistent = function(other_formula) { + this = concat_parse_table(self$formula) + that = concat_parse_table(other_formula) + this_funs = this$x[this$n > 0L] + that_funs = that$x[that$n > 0L] + good_n_sig = identical(this$n, that$n) + good_fun_names = identical(this_funs, that_funs) + good_n_sig & good_fun_names + } + + return_object(self, "MethodPrototype") +} + +#' Make Method Class +#' +#' Place a method object in the package namespace for a given method type +#' defined in the C++ code. +#' +#' @param cls_nm Character string giving the name of the class. +#' @param meth_type_id Integer giving the associated ID of the method type. +#' +#' @nord +mk_meth_cls = function(cls_nm, meth_type_id) { + pf = parent.frame() + force(pf) + force(cls_nm) + force(meth_type_id) + f = function(name, mat_args, const_args, init_mats = MatsList(), int_vecs = IntVecs()) { + self = Method(name, mat_args, const_args, init_mats, int_vecs) + self$meth_type_id = meth_type_id + return_object(self, cls_nm) + } + assign(cls_nm, f, envir = pf) +} + +#' Method Type Utilities +#' +#' This class is here so that `MethodTypes`, which is automatically generated +#' from the C++ code, can inherit from it. +#' +#' @nord +MethodTypeUtils = function() { + self = Base() + + # return all prototypes as character strings + self$all_prototype_formulas = function() { + l = list() + for (m in self$method_ordering) { + l[[m]] = self[[m]]$as_character() + } + unlist(l, use.names = FALSE) + } + + # construct an object of class `Method` + # + # @param formula -- a user-supplied formula for defining a method. this + # formula gets compared with the prototypes to decide what method type to + # use for this user-defined method. + # @param meth_nm -- character string giving the method name. + self$make_method = function(formula, meth_nm) { + for (meth_type_nm in self$method_ordering) { + if (self[[meth_type_nm]]$consistent(formula)) { + method_cls = mk_meth_cls( + cls_nm = var_case_to_cls_case(meth_type_nm), + meth_type_id = match(meth_type_nm, self$method_ordering) + ) + method = method_cls(meth_nm + , self[[meth_type_nm]]$mat_args(formula) + , self[[meth_type_nm]]$int_vec_args(formula) + ) + return(method) + } + } + stop( + "\nThe following engine method formula ...\n\n", deparse(formula), "\n\n" + , "... is inconsistent with all of the available prototypes:\n\n" + , paste0(self$all_prototype_formulas(), collapse = "\n") + ) + } + + self$could_make_method = function(formula) { + v = setNames(logical(length(self$method_ordering)), self$method_ordering) + for (meth_type_nm in self$method_ordering) { + v[meth_type_nm] = self[[meth_type_nm]]$consistent(formula) + } + any(v) + } + + return_object(self, "MethodTypesUtil") +} diff --git a/misc/build/method_tail.R b/misc/build/method_tail.R new file mode 100644 index 00000000..e69de29b From f4fa3ed4a873328845c3b40eabfeae9cfb72cb72 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:06:10 -0400 Subject: [PATCH 032/332] broken state on experimental branch --- .gitignore | 1 + NAMESPACE | 5 +- R/case_changes.R | 9 + R/connection.R | 30 +- R/dev_tools.R | 17 +- R/engine_eval.R | 21 +- R/engine_methods.R | 47 +- R/enum_methods.R | 10 + R/labelled_partitions.R | 18 + R/model_data_structure.R | 4 +- R/model_files.R | 4 +- R/opt_params.R | 1 + R/parse_expr.R | 41 +- R/products.R | 33 +- R/readers.R | 22 +- R/settings.R | 40 +- R/tmb_model.R | 18 +- R/variables.R | 4 +- .../product_example/Epi_model/variables.csv | 1 - inst/starter_models/sir/flows.csv | 6 +- inst/starter_models/sir_vax/flows.csv | 14 +- inst/starter_models/sir_vax/settings.json | 2 +- inst/starter_models/sir_vax/variables.csv | 4 +- man/cartesian.Rd | 4 +- man/cartesian_self.Rd | 23 + man/read_partition.Rd | 15 + src/macpan2.cpp | 603 ++++++++++-------- tests/testthat/test-labelled-partitions.R | 2 +- tests/testthat/test-no-derivations.R | 30 +- 29 files changed, 640 insertions(+), 389 deletions(-) create mode 100644 man/cartesian_self.Rd create mode 100644 man/read_partition.Rd diff --git a/.gitignore b/.gitignore index 6b68ff15..d26d6b55 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .RData .Ruserdata inst/doc +hoard *.o *.so *.DS_Store diff --git a/NAMESPACE b/NAMESPACE index a5d18b06..d4e3b2bc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -40,6 +40,7 @@ export(Files) export(FlowExpander) export(Flows) export(Formula) +export(Infection) export(IntVecs) export(JSONReader) export(Log) @@ -71,13 +72,13 @@ export(TMBModel) export(TMBSimulator) export(TXTReader) export(Time) -export(Trans) export(Transform) export(UserExpr) export(all_consistent) export(all_equal) export(all_not_equal) export(cartesian) +export(cartesian_self) export(empty_matrix) export(engine_eval) export(finalizer_char) @@ -89,7 +90,9 @@ export(model_starter) export(nlist) export(not_all_equal) export(parse_expr_list) +export(partition) export(rbf) +export(read_partition) export(reader_spec) export(show_models) export(simple_sims) diff --git a/R/case_changes.R b/R/case_changes.R index bddc837d..a430953d 100644 --- a/R/case_changes.R +++ b/R/case_changes.R @@ -15,3 +15,12 @@ var_case_to_cls_case = function(...) { , SIMPLIFY = FALSE ) |> vapply(paste, character(1L), collapse = "") } + +cls_case_to_var_case = function(...) { + x = unlist(list(...), recursive = TRUE, use.names = FALSE) + y = strsplit(x, "(?<=[a-z0-9])(?=[A-Z])", perl = TRUE) + words = unlist(lapply(y, function(z) { + tolower(z) + })) + paste(words, collapse = "_") +} diff --git a/R/connection.R b/R/connection.R index d65f4a2b..f80aa5fe 100644 --- a/R/connection.R +++ b/R/connection.R @@ -16,9 +16,9 @@ Connection = function(row, variables self$join_ref = join_ref self$type_ref = type_ref - self$from_universe = variables[[self$from_set]] - self$to_universe = variables[[self$to_set]] - self$conn_universe = variables[[self$conn_set]] + self$from_universe = connection_universe(variables, self$from_set) + self$to_universe = connection_universe(variables, self$to_set) + self$conn_universe = connection_universe(variables, self$conn_set) self$from = self$row[[self$from_ref]] self$to = self$row[[self$to_ref]] @@ -131,7 +131,7 @@ Connection = function(row, variables } unique(rbind(from_conn, to_conn)) } - initialize_cache(self, "frame", "from_to_merge", "from_conn_merge", "to_conn_merge") + #initialize_cache(self, "frame", "from_to_merge", "from_conn_merge", "to_conn_merge") return_object(self, "Connection") } @@ -141,6 +141,10 @@ connection_merge = function(x, y, by, output_cols) { } #connection_merge = memoise(connection_merge) +connection_universe = function(v, s) { + if (!isTRUE(s %in% v$.all_types())) return(NULL) + v[[s]] +} #' @export @@ -167,29 +171,29 @@ Flows = function(flows, variables) { } #' @export -Trans = function(trans, variables) { +Infection = function(infection, variables) { self = Base() - self$trans = enforce_schema(trans, "state", "flow", "pop", "type") + self$infection = enforce_schema(infection, "state", "flow") self$variables = variables self$connections = list() - for (i in seq_row(self$trans)) { + for (i in seq_row(self$infection)) { self$connections[[i]] = Connection( - row = self$trans[i, , drop = FALSE] + row = self$infection[i, , drop = FALSE] , variables = self$variables , from_ref = "state" , to_ref = "flow" - , conn_ref = "pop" - , type_ref = "type" - , from_set = "state", to_set = "flow", conn_set = "all" + , conn_ref = "" + , type_ref = "" + , from_set = "state", to_set = "flow", conn_set = "" , filter_ref = "partition" , join_ref = "partition" ) } self$frame = function() { - if (ncol(self$trans) < 4) return(self$trans) + if (ncol(self$infection) < 3) return(self$infection) do.call(rbind, method_apply(self$connections, "frame")) } - return_object(self, "Trans") + return_object(self, "Infection") } labelled_frame = function(partition, label_name = "label") { diff --git a/R/dev_tools.R b/R/dev_tools.R index 364b28c2..e3282911 100644 --- a/R/dev_tools.R +++ b/R/dev_tools.R @@ -14,14 +14,15 @@ dev_file = function(suffix = "", ext = "cpp") { if (dev_in_root()) return(cpp("misc/dev")) if (dev_in_dev()) return(cpp("")) if (dev_in_test()) return(cpp("../../misc/dev")) - stop( - "\n------", - "\nYou are developing here:\n", getwd(), ",", - "\nwhich is not where you should be developing.", - "\nThe current options are in the root of macpan2,", - "\nor in misc/dev or tests within a macpan2 project.", - "\n------" - ) + + msg_break( + msg_colon("You are developing here", getwd()), + msg( + "which is not where you should be developing.", + "The current options are in the root of macpan2,", + "or in misc/dev or tests within a macpan2 project." + ) + ) |> stop() } dev_obj = function(suffix = "", ext = "cpp") { diff --git a/R/engine_eval.R b/R/engine_eval.R index f88a1b36..9624f00b 100644 --- a/R/engine_eval.R +++ b/R/engine_eval.R @@ -43,10 +43,10 @@ engine_eval = function(e, ..., .matrix_to_return, .tmb_cpp = getOption("macpan2_ left_hand_side = paste0(c("output", names(dot_mats)), collapse = "_") e = as.formula(paste0(c(left_hand_side, as.character(e)), collapse = " ")) } else { - stop( - "\nThe expression must be given as the", - "\nright-hand-side of a one-sided formula." - ) + msg( + "The expression must be given as the", + "right-hand-side of a one-sided formula." + ) |> stop() } if (missing(.matrix_to_return)) .matrix_to_return = left_hand_side @@ -74,7 +74,18 @@ engine_eval = function(e, ..., .matrix_to_return, .tmb_cpp = getOption("macpan2_ obj_fn = ObjectiveFunction(~0) ) - TMBSimulator(m, tmb_cpp = .tmb_cpp)$matrix(NA, matrix_name = .matrix_to_return, time_step = 1L) + TMBSimulator(m, tmb_cpp = .tmb_cpp)$matrix(NA + , matrix_name = .matrix_to_return + + # because the number of time steps, T = 0, + # we have T + 1 = 1 here because we pick up the answers + # after the simulation loop + , time_step = 1L + + # even though we evaluate "before", we pick up the + # answers after because we do not save results + , .phases = "after" + ) } diff --git a/R/engine_methods.R b/R/engine_methods.R index 5b3414ae..5caeb335 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -127,30 +127,41 @@ Method = function(name, mat_args, int_vec_args, init_mats = MatsList(), int_vecs if (length(missing_mats) != 0L) { mixup = missing_mats[missing_mats %in% names(self$int_vecs)] if (length(mixup) != 0L) { - stop( - "\nThe ", self$name, " method had the following integer vectors passed" - , "\nto arguments that require matrices:\n" - , paste(mixup, collapse = "\n") - ) + msg_colon( + msg( + "The", self$name, + "method had the following integer vectors passed", + "to arguments that require matrices" + ), + msg_indent(mixup) + ) |> stop() } - stop( - "\nThe ", self$name, " method could not find the following matrices:\n" - , paste(missing_mats, collapse = "\n") - ) + msg_colon( + msg( + "The", self$name, + "method could not find the following matrices" + ), + msg_indent(missing_mats) + ) |> stop() } if (length(missing_int_vecs) != 0L) { mixup = missing_int_vecs[missing_int_vecs %in% names(self$init_mats)] if (length(mixup) != 0L) { - stop( - "\nThe ", self$name, " method had the following matrices passed" - , "\nto arguments that require integer vectors:\n" - , paste(mixup, collapse = "\n") - ) + msg_colon( + msg( + "The", self$name, "method had the following matrices passed", + "to arguments that require integer vectors" + ), + msg_indent(mixup) + ) |> stop() } - stop( - "\nThe ", self$name, " method coult not find the following integer vectors:\n" - , paste(missing_int_vecs, collapse = "\n") - ) + msg_colon( + msg( + "The", self$name, + "method could not find the following integer vectors" + ), + msg_indent(missing_int_vecs) + ) |> stop() } } diff --git a/R/enum_methods.R b/R/enum_methods.R index 0cbd3497..a237d572 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -116,6 +116,16 @@ MethodTypeUtils = function() { return(method) } } + msg_break( + msg_colon( + msg("The following engine method formula"), + msg_indent(deparse(formula)) + ), + msg_colon( + msg("is inconsistent with all of the available prototypes"), + msg_break(self$all_prototype_formulas()) + ) + ) stop( "\nThe following engine method formula ...\n\n", deparse(formula), "\n\n" , "... is inconsistent with all of the available prototypes:\n\n" diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 402c4a4e..0af744c9 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -44,6 +44,11 @@ Partition = function(frame) { self$names = function() names(self$frame()) self$name = function() names(self$dotted()) self$labels = function() self$dotted()[[1L]] + self$prefix = function(prefix) { + f = self$frame() + names(f) = sprintf("%s%s", prefix, self$names()) + Partition(f) + } self$partial_labels = function(...) { new_names = list_to_names(...) self$.partition$change_coordinates(new_names)$dot()$frame()[[1L]] @@ -103,6 +108,19 @@ Partition = function(frame) { } Partition = memoise(Partition) +#' Read Partition +#' +#' Read a CSV file in as a \code{\link{Partition}}. +#' +#' @param ... File path components to pass to \code{\link{CSVReader}}, and +#' subsequently to \code{\link{file.path}}. +#' +#' @export +read_partition = function(...) CSVReader(...)$read() |> Partition() + +#' @export +partition = function(...) data.frame(...) |> Partition() + NullPartition = function(...) { self = Base() self$.names = list_to_names(...) diff --git a/R/model_data_structure.R b/R/model_data_structure.R index 897faf21..2fc5cbda 100644 --- a/R/model_data_structure.R +++ b/R/model_data_structure.R @@ -18,7 +18,7 @@ Model = function(definition) { self$labels = VariableLabels(self$variables) self$indices = VariableIndices(self$labels) self$flows_info = Flows(self$def$flows(), self$variables) - self$trans_info = Trans(self$def$trans(), self$variables) + self$infection_info = Infection(self$def$infection(), self$variables) # Standard Methods self$flows = function() self$def$flows() @@ -63,7 +63,7 @@ Model = function(definition) { # Composition self$simulators = Simulators(self) - self$expander = FlowExpander(self$def) + #self$expander = FlowExpander(self$def) # Set the cache in the underlying ModelFiles object # so that when the model definition files change diff --git a/R/model_files.R b/R/model_files.R index b75e2cac..2152f327 100644 --- a/R/model_files.R +++ b/R/model_files.R @@ -72,7 +72,7 @@ ModelFiles = function(model_directory , reader_spec("derivations.json", json_reader) , reader_spec("flows.csv", csv_reader) , reader_spec("settings.json", json_reader) - , reader_spec("trans.csv", csv_reader, optional = TRUE) + , reader_spec("infection.csv", csv_reader, optional = TRUE) , reader_spec("transmission_matrices.csv", csv_reader, optional = TRUE) , reader_spec("transmission_dimensions.csv", csv_reader, optional = TRUE) ) @@ -84,7 +84,7 @@ ModelFiles = function(model_directory self$variables = function() self$get("variables") self$derivations = function() self$get("derivations") self$flows = function() self$get("flows") - self$trans = function() self$get("trans", optional = TRUE) + self$infection = function() self$get("infection", optional = TRUE) self$settings = function() self$get("settings") self$transmission_matrices = function() { self$get("transmission_matrices", optional = TRUE) diff --git a/R/opt_params.R b/R/opt_params.R index c8fd3474..4fb0fa13 100644 --- a/R/opt_params.R +++ b/R/opt_params.R @@ -129,6 +129,7 @@ OptParamsFile = function(file_path ) self$.col_map = c( Matrix = "mat" + , matrix = "mat" , mat = "mat" , Mat = "mat" , Row = "row" diff --git a/R/parse_expr.R b/R/parse_expr.R index 2fef9383..ebee7f08 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -261,30 +261,35 @@ get_indices = function(x, vec, vec_type, expr_as_string, zero_based = FALSE) { if (vec_type == "functions") { pointers = "\nPlease see ?engine_functions for more information on the available functions." } else if (vec_type == "variables") { - pointers = paste( - "\nPlease ensure that all variables are being initialized", - "\nEven variables that are derived and not dependencies of", - "\nother expressions must at least be initialized as an ?empty_matrix.", - "\n", - sep = "" + pointers = msg( + "Please ensure that all variables are being initialized", + "Even variables that are derived and not dependencies of", + "other expressions must at least be initialized as an", + "?empty_matrix.\n" ) } else if (vec_type == "methods") { - pointers = "\nHelp for ?engine_methods is under construction." + pointers = msg("Help for ?engine_methods is under construction.") } else if (vec_type == "int_vecs") { - pointers = paste( - "\nPlease ensure that engine methods refer to the right integer vectors", - sep = "" + pointers = msg( + "Please ensure that engine methods refer", + "to the right integer vectors" ) } - stop( - "\nthe expression given by:\n", - expr_as_string, "\n\n", - "contained the following ", vec_type, ":\n", - paste0(unique(missing_items), collapse = " "), "\n\n", - " that were not found in the list of available ", vec_type, ":\n", - paste0(vec, collapse = " "), # TODO: smarter pasting when this list gets big + msg_break( + msg_colon( + "The expression given by", + msg_indent(expr_as_string) + ), + msg_colon( + msg("contained the following", vec_type), + msg_indent(missing_items) + ), + msg_colon( + msg("that were not found in the list of available", vec_type), + msg_indent(vec) + ), pointers - ) + ) |> stop() } one_based = apply(outer(as.character(x), vec, "=="), 1, which) if (zero_based) return(one_based - 1L) diff --git a/R/products.R b/R/products.R index dc8d58c2..bb11c881 100644 --- a/R/products.R +++ b/R/products.R @@ -28,10 +28,35 @@ Products = function(x) { #' Cartesian Product #' -#' @param x Object with a `$frame` method that returns a data frame. -#' @param y Object with a `$frame` method that returns a data frame. +#' @param x A \code{\link{Partition}} object. +#' @param y A \code{\link{Partition}} object. +#' +#' @export +cartesian = function(x, y) x$products$cartesian(y) + +#' Self-Cartesian Product +#' +#' @param x A \code{\link{Partition}} object. +#' @param left_filter Character string giving the name of the left filter. +#' @param right_filter Character string giving the name of the right filter. +#' @param .wrt Character string giving the name of a column in `x`. +#' @param .rm Character string giving the dot-concatenated names of columns +#' to remove from `x`. #' #' @export -cartesian = function(x, y) { - x$products$cartesian(y) +cartesian_self = function(x, left_filter, right_filter, .wrt, .rm = .wrt) { + left_prefix = var_case_to_cls_case(left_filter) + right_prefix = var_case_to_cls_case(right_filter) + l = cartesian( + x$filter(left_filter, .wrt = .wrt)$select_out(to_names(.rm))$prefix(left_prefix), + x$filter(right_filter, .wrt = .wrt)$select_out(to_names(.rm))$prefix(right_prefix) + ) + nms = l$names() + nms_left = head(nms, length(nms) / 2L) + nms_right = tail(nms, length(nms) / 2L) + list( + l$partial_labels(nms_left), + l$partial_labels(nms_right) + ) |> setNames(c(left_filter, right_filter)) } + diff --git a/R/readers.R b/R/readers.R index 3e977a85..09a6b05d 100644 --- a/R/readers.R +++ b/R/readers.R @@ -9,7 +9,7 @@ Reader = function(...) { self = Base() self$file = normalizePath(file.path(...), mustWork = TRUE) - self$read = function() { + self$read_base = function() { suggestion = switch(file_ext(self$file) , csv = "CSVReader" , json = "JSONReader" @@ -21,6 +21,15 @@ Reader = function(...) { suggestion, "." ) } + ## wrapper for handling errors in the reading functions + self$read = function() { + x = try(self$read_base()) + if (inherits(x, "try-error")) { + stop("\nCouldn't read this file:\n", self$file) + } + x + } + return_object(self, "Reader") } @@ -32,7 +41,7 @@ CSVReader = function(...) { self$.empty = function(row) { isTRUE(all((row == "") | startsWith(row, " "))) } - self$read = function() { + self$read_base = function() { data_frame = read.table( self$file, sep = ",", quote = "", na.strings = character(0L), colClasses = "character", header = TRUE, @@ -50,11 +59,12 @@ CSVReader = function(...) { #' @export JSONReader = function(...) { self = Reader(...) - self$read = function() { - jsonlite::fromJSON(self$file + self$read_base = function() { + l = jsonlite::fromJSON(self$file , simplifyDataFrame = FALSE , simplifyMatrix = FALSE ) + } return_object(self, "JSONReader") } @@ -63,14 +73,14 @@ JSONReader = function(...) { #' @export TXTReader = function(...) { self = Reader(...) - self$read = function() readLines(self$file) + self$read_base = function() readLines(self$file) return_object(self, "TXTReader") } #' @describeIn Reader Placeholder reader that always returns \code{NULL}. #' @export NULLReader = function(...) { - self = self = Base() + self = Base() self$file = "" self$read = function() NULL return_object(self, "NULLReader") diff --git a/R/settings.R b/R/settings.R index 5f35a8e0..ab69a58e 100644 --- a/R/settings.R +++ b/R/settings.R @@ -2,10 +2,44 @@ Settings = function(model) { self = Base() self$model = model self$.settings = model$def$settings - self$name = function() to_name(self$.settings()$required_partition) - self$names = function() to_names(self$.settings()$required_partition) + self$.synonyms = list() + self$.synonyms$lab_part = c( + "required_partitions", "required", "req_part" + , "labelling_partitions", "labelling_partition", "labelling_part", "lab_part" + ) + self$.synonyms$var_part = c( + "vector_partition", "vector_partitions", "vec_partition", "vec_partition" + , "var_partitions", "variable_partitions" + ) + self$.resolve_synonyms = function(type) { + nms = names(self$.settings()) + i = nms %in% self$.synonyms[[type]] + if (!any(i)) { + ( + msg( + "At least one of the following fields must be in the settings.json", + "file:\n%s\n", + "The first one encountered will be used to determine the", + "columns in the variables.csv file used to generate labels for", + "the quantities in that file." + ) + |> sprintf(nms) + |> stop() + ) + } + } + self$lab_part = function() { + if (!any(i)) { + stop( + + ) + } + nms[min(which(i))] + } + self$name = function() to_name(self$.settings()[[self$lab_part()]]) + self$names = function() to_names(self$.settings()[[self$lab_part()]]) self$null = function() self$.settings()$null_partition - self$var_partitions = function() self$.settings()$var_partitions + self$var_part = function() self$.settings()$var_partitions self$variable = function(type) { type_nm = sprintf("%s_variables", type) var_nms = self$.settings()[[type_nm]] diff --git a/R/tmb_model.R b/R/tmb_model.R index 27c63afc..405daf9a 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -210,10 +210,10 @@ TMBSimulationUtils = function() { r = r[r$time != 0L,,drop = FALSE] } if (!"during" %in% .phases) { - r = r[(r$time < 1L) | (r$time > num_t),,drop = FALSE] + r = r[(r$time == 0L) | (r$time == num_t + 1L),,drop = FALSE] } if (!"after" %in% .phases) { - r = r[r$time < num_t + 1,,drop = FALSE] + r = r[r$time != num_t + 1L,,drop = FALSE] } r } @@ -223,7 +223,7 @@ TMBSimulationUtils = function() { deparse1(self$tmb_model$expr_list$formula_list()[[expr_num]]) } self$.runner = function(... - , .phases = c("before", "during", "after") + , .phases = "during" , .method = c("report", "simulate") ) { .method = match.arg(.method) @@ -306,14 +306,14 @@ TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initializ self$sdreport = function() TMB::sdreport(self$ad_fun()) self$cov.fixed = function() self$sdreport()$cov.fixed self$par.fixed = function() self$sdreport()$par.fixed - self$report = function(..., .phases = c("before", "during", "after")) { + self$report = function(..., .phases = "during") { self$.runner(..., .phases = .phases, .method = "report") } - self$report_values = function(..., .phases = c("before", "during", "after")) { + self$report_values = function(..., .phases = "during") { self$report(..., .phases = .phases)$value } self$report_ensemble = function(... - , .phases = c("before", "during", "after") + , .phases = "during" , .n = 100 , .probs = c(0.025, 0.5, 0.975) ) { @@ -325,11 +325,11 @@ TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initializ ) cbind(r, rr) } - self$simulate = function(..., .phases = c("before", "during", "after")) { + self$simulate = function(..., .phases = "during") { self$.runner(..., .phases = .phases, .method = "simulate") } - self$matrix = function(..., matrix_name, time_step) { - r = self$report(...) + self$matrix = function(..., matrix_name, time_step, .phases = "during") { + r = self$report(..., .phases = .phases) i = (r$matrix == as.character(matrix_name)) & (r$time == as.integer(time_step)) rr = r[i, c("row", "col", "value")] if (!any(is.na(as.integer(rr$row)))) { diff --git a/R/variables.R b/R/variables.R index 74fda52a..fbffbd0d 100644 --- a/R/variables.R +++ b/R/variables.R @@ -5,7 +5,7 @@ Variables = function(model) { self$all = function() Partition(self$model$def$variables()) self$.type = function(type) { labels_this_type = self$model$settings$variable(type) - var_part = self$model$settings$var_partitions() + var_part = self$model$settings$var_part() wrt = self$model$settings$name() if (is.null(var_part)) { vars = self$all()$filter_ordered(labels_this_type, .wrt = wrt) @@ -32,7 +32,6 @@ Variables = function(model) { # TODO: A better way to handle the NULL case would make it possible # to return a null Partition object. This is currently not possible # for 'technical' reasons. - all_types = c("flow", "state", "infectious_state", "infected_state", "infection_flow") if (length(self$model$labels$other()) == 0L) { warning( "\nThere are no other variables in this model", @@ -46,6 +45,7 @@ Variables = function(model) { .wrt = s$required_partitions ) } + self$.all_types = function() setdiff(ls(self), c("cache", "model")) initialize_cache(self, "all") return_object(self, "Variables") } diff --git a/inst/starter_models/product_example/Epi_model/variables.csv b/inst/starter_models/product_example/Epi_model/variables.csv index cc6d5178..8f0a6a6d 100644 --- a/inst/starter_models/product_example/Epi_model/variables.csv +++ b/inst/starter_models/product_example/Epi_model/variables.csv @@ -6,6 +6,5 @@ R N progression recovery -per_capita_transmission total_foi transmissability diff --git a/inst/starter_models/sir/flows.csv b/inst/starter_models/sir/flows.csv index bdb6a20d..09da3cb5 100644 --- a/inst/starter_models/sir/flows.csv +++ b/inst/starter_models/sir/flows.csv @@ -1,3 +1,3 @@ -from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition -S ,I ,foi ,per_capita,Epi,Epi,Epi,,,Null -I ,R ,gamma ,per_capita,Epi,Epi,Epi,,,Null +from ,to ,flow ,type +S ,I ,foi ,per_capita +I ,R ,gamma ,per_capita diff --git a/inst/starter_models/sir_vax/flows.csv b/inst/starter_models/sir_vax/flows.csv index aa5d566d..7ac6399d 100644 --- a/inst/starter_models/sir_vax/flows.csv +++ b/inst/starter_models/sir_vax/flows.csv @@ -1,4 +1,10 @@ -from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition -S ,I ,infection ,per_capita ,Epi ,Epi ,Epi ,Vax ,Vax ,Null -I ,R ,gamma ,per_capita ,Epi ,Epi ,Epi ,Vax ,Vax ,Null -S.unvax ,S.vax ,vax_rate ,per_capita ,Epi.Vax ,Epi.Vax ,Vax , , ,Null +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S ,I ,infection ,per_capita ,Epi ,Epi ,Epi ,Vax ,Vax ,Null +I ,R ,gamma ,per_capita ,Epi ,Epi ,Epi ,Vax ,Vax ,Null +S.unvax ,S.vax ,vaccination ,per_capita ,Epi.Vax ,Epi.Vax ,Vax , , ,Null +S ,S.unvax ,birth ,per_capita_inflow ,Epi ,Epi.Vax ,Epi , , ,Null +I ,S.unvax ,birth ,per_capita_inflow ,Epi ,Epi.Vax ,Epi , , ,Null +R ,S.unvax ,birth ,per_capita_inflow ,Epi ,Epi.Vax ,Epi , , ,Null +S , ,death ,per_capita_outflow ,Epi ,Null ,Epi ,Null , ,Null +I , ,death ,per_capita_outflow ,Epi ,Null ,Epi ,Null , ,Null +R , ,death ,per_capita_outflow ,Epi ,Null ,Epi ,Null , ,Null diff --git a/inst/starter_models/sir_vax/settings.json b/inst/starter_models/sir_vax/settings.json index 9bf85148..d9b210fd 100644 --- a/inst/starter_models/sir_vax/settings.json +++ b/inst/starter_models/sir_vax/settings.json @@ -2,5 +2,5 @@ "required_partitions" : ["Epi", "Vax"], "null_partition" : "Null", "state_variables" : ["S.unvax", "I.unvax", "R.unvax", "S.vax", "I.vax", "R.vax"], - "flow_variables" : ["infection.unvax", "infection.vax", "gamma.unvax", "gamma.vax", ".vax_rate"] + "flow_variables" : ["infection.unvax", "infection.vax", "gamma.unvax", "gamma.vax", ".vaccination", "birth.", "death."] } diff --git a/inst/starter_models/sir_vax/variables.csv b/inst/starter_models/sir_vax/variables.csv index fb571743..359aec17 100644 --- a/inst/starter_models/sir_vax/variables.csv +++ b/inst/starter_models/sir_vax/variables.csv @@ -18,4 +18,6 @@ foi ,vax infection ,vax gamma ,vax foi , - ,vax_rate + ,vaccination +birth , +death , \ No newline at end of file diff --git a/man/cartesian.Rd b/man/cartesian.Rd index 9bbb25b8..657affe9 100644 --- a/man/cartesian.Rd +++ b/man/cartesian.Rd @@ -7,9 +7,9 @@ cartesian(x, y) } \arguments{ -\item{x}{Object with a \verb{$frame} method that returns a data frame.} +\item{x}{A \code{\link{Partition}} object.} -\item{y}{Object with a \verb{$frame} method that returns a data frame.} +\item{y}{A \code{\link{Partition}} object.} } \description{ Cartesian Product diff --git a/man/cartesian_self.Rd b/man/cartesian_self.Rd new file mode 100644 index 00000000..ec504d11 --- /dev/null +++ b/man/cartesian_self.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/products.R +\name{cartesian_self} +\alias{cartesian_self} +\title{Self-Cartesian Product} +\usage{ +cartesian_self(x, left_filter, right_filter, .wrt, .rm = .wrt) +} +\arguments{ +\item{x}{A \code{\link{Partition}} object.} + +\item{left_filter}{Character string giving the name of the left filter.} + +\item{right_filter}{Character string giving the name of the right filter.} + +\item{.wrt}{Character string giving the name of a column in \code{x}.} + +\item{.rm}{Character string giving the dot-concatenated names of columns +to remove from \code{x}.} +} +\description{ +Self-Cartesian Product +} diff --git a/man/read_partition.Rd b/man/read_partition.Rd new file mode 100644 index 00000000..7b39a8d8 --- /dev/null +++ b/man/read_partition.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/labelled_partitions.R +\name{read_partition} +\alias{read_partition} +\title{Read Partition} +\usage{ +read_partition(...) +} +\arguments{ +\item{...}{File path components to pass to \code{\link{CSVReader}}, and +subsequently to \code{\link{file.path}}.} +} +\description{ +Read a CSV file in as a \code{\link{Partition}}. +} diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 4871def2..205982e6 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -152,71 +152,68 @@ void printMatrix(const matrix& mat) { // Helper function -template -int CheckIndices( - matrix& x, - matrix& rowIndices, - matrix& colIndices -) { - int rows = x.rows(); - int cols = x.cols(); - Type maxRowIndex = rowIndices.maxCoeff(); - Type maxColIndex = colIndices.maxCoeff(); - Type minRowIndex = rowIndices.minCoeff(); - Type minColIndex = colIndices.minCoeff(); - - if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { - return 0; - } - return 1; -} - -// Helper function -template -int RecycleInPlace( - matrix& mat, - int rows, - int cols -) { - #ifdef MP_VERBOSE - std::cout << "recycling ... " << std::endl; - #endif - if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. - return 0; - - matrix m(rows, cols); - if (mat.rows()==1 && mat.cols()==1) { - m = matrix::Constant(rows, cols, mat.coeff(0,0)); - } - else if (mat.rows()==rows) { - if (mat.cols()==1) { - #ifdef MP_VERBOSE - std::cout << "recycling columns ... " << std::endl; - #endif - for (int i=0; i +// int CheckIndices( +// matrix& x, +// matrix& rowIndices, +// matrix& colIndices +// ) { +// int rows = x.rows(); +// int cols = x.cols(); +// Type maxRowIndex = rowIndices.maxCoeff(); +// Type maxColIndex = colIndices.maxCoeff(); +// Type minRowIndex = rowIndices.minCoeff(); +// Type minColIndex = colIndices.minCoeff(); +// +// if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { +// return 0; +// } +// return 1; +// } + +// // Helper function +// template +// int RecycleInPlace( +// matrix& mat, +// int rows, +// int cols +// ) { +// #ifdef MP_VERBOSE +// std::cout << "recycling ... " << std::endl; +// #endif +// if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. +// return 0; +// +// matrix m(rows, cols); +// if (mat.rows()==1 && mat.cols()==1) { +// m = matrix::Constant(rows, cols, mat.coeff(0,0)); +// } +// else if (mat.rows()==rows) { +// if (mat.cols()==1) { +// #ifdef MP_VERBOSE +// std::cout << "recycling columns ... " << std::endl; +// #endif +// for (int i=0; i struct ListOfMatrices { @@ -419,10 +416,10 @@ matrix getNthMat( } -template +template class ArgList { public: - using ItemType = std::variant, std::vector>; + using ItemType = std::variant, std::vector>; ArgList(int size) : items_(size), size_(size) {} @@ -433,13 +430,13 @@ class ArgList { items_[index] = item; } - matrix get_as_mat(int i) { + matrix get_as_mat(int i) const { if (i < 0 || i >= items_.size()) { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { - return std::get>(items_[i]); + if (std::holds_alternative>(items_[i])) { + return std::get>(items_[i]); } else { throw std::runtime_error("Item at index is not a matrix"); } @@ -453,7 +450,7 @@ class ArgList { if (std::holds_alternative>(items_[i])) { return std::get>(items_[i]); } else { - matrix m = get_as_mat(i); + matrix m = get_as_mat(i); std::vector v(m.rows()); for (int i=0; i operator[](int i) { + matrix operator[](int i) { return get_as_mat(i); } + // Method to recycle elements, rows, and columns to make operands compatible for binary operations + ArgList recycle_for_bin_op() const { + ArgList result = *this; // Create a new ArgList as a copy of the current instance + + matrix mat0 = result.get_as_mat(0); + matrix mat1 = result.get_as_mat(1); + + if (mat0.rows() == mat1.rows()) { + if (mat0.cols() != mat1.cols()) { + if (mat0.cols() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat0; + mat0 = mat1; // for the shape + for (int i = 0; i < mat0.cols(); i++) { + mat0.col(i) = m.col(0); + } + } else if (mat1.cols() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat1; + //result.set(1, mat0); // Set for the shape + mat1 = mat0; + for (int i = 0; i < mat1.cols(); i++) { + mat1.col(i) = m.col(0); + } + } else { + result.set_error_code(201); // Set the error code for "The two operands do not have the same number of columns" + } + } + // else: do nothing + } else { + if (mat0.cols() == mat1.cols()) { // Only one compatible dimension + if (mat0.rows() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat0; + mat0 = mat1; + for (int i = 0; i < mat0.rows(); i++) { + mat0.row(i) = m.row(0); + } + } else if (mat1.rows() == 1) { // Vector vs matrix or scalar vs vector + matrix m = mat1; + mat1 = mat0; + for (int i = 0; i < mat0.rows(); i++) { + mat1.row(i) = m.row(0); + } + } else { + result.set_error_code(202); // Set the error code for "The two operands do not have the same number of rows" + } + } else { // No dimensions are equal + if (mat0.rows() == 1 && mat0.cols() == 1) { // Scalar vs non-scalar + Type s = mat0.coeff(0, 0); + mat0 = mat1; + mat0.setConstant(s); + } else if (mat1.rows() == 1 && mat1.cols() == 1) { // Scalar vs non-scalar + Type s = mat1.coeff(0, 0); + mat1 = mat0; + mat1.setConstant(s); + } else { + result.set_error_code(203); // Set the error code for "The two operands do not have the same number of columns or rows" + } + } + } + result.set(0, mat0); + result.set(1, mat1); + return result; + } + + + // Method to recycle elements of all arguments so that they match a given shape + ArgList recycle_to_shape(const std::vector& indices, int rows, int cols) const { + ArgList result = *this; // Create a new ArgList as a copy of the current instance + + int error_code = 0; // Initialize the error code + + for (int index : indices) { + matrix mat = result.get_as_mat(index); + + if (mat.rows() == rows && mat.cols() == cols) { + // No further action needed for this matrix + continue; + } + + matrix m(rows, cols); + + if (mat.rows() == 1 && mat.cols() == 1) { + m = matrix::Constant(rows, cols, mat.coeff(0, 0)); + } else if (mat.rows() == rows) { + if (mat.cols() == 1) { + for (int i = 0; i < cols; i++) { + m.col(i) = mat.col(0); + } + } else { + error_code = 501; + break; // Exit the loop on error + } + } else if (mat.cols() == cols) { + if (mat.rows() == 1) { + for (int i = 0; i < rows; i++) { + m.row(i) = mat.row(0); + } + } else { + error_code = 501; + break; // Exit the loop on error + } + } else { + error_code = 501; + break; // Exit the loop on error + } + + if (error_code != 0) { + result.set_error_code(error_code); + break; // Exit the loop on error + } + + // If recycling is successful, update the result ArgList object + result.set(index, m); + } + + return result; + } + + int check_indices(int mat_index, const std::vector& row_indices, const std::vector& col_indices) const { + if (mat_index < 0 || mat_index >= items_.size()) { + return 2; // Return an error code for an invalid index + } + + matrix x = get_as_mat(mat_index); + int rows = x.rows(); + int cols = x.cols(); + + for (int row_index : row_indices) { + if (row_index < 0 || row_index >= rows) { + return 1; // Indices are not valid + } + } + + for (int col_index : col_indices) { + if (col_index < 0 || col_index >= cols) { + return 1; // Indices are not valid + } + } + + return 0; // Indices are valid + } + + // Method to set an error code + void set_error_code(int error) { + error_code_ = error; + } + + // Getter for the error code + int get_error_code() const { + return error_code_; + } + + private: std::vector items_; int size_; + int error_code_ = 0; // Initialize the error code to 0 (no error) by default }; @@ -543,9 +693,9 @@ class ExprEvaluator { vector u; matrix Y, X, A; matrix timeIndex; // for rbind_time - Type sum, s, eps, var, by; // intermediate scalars + Type sum, eps, var, by; // intermediate scalars int rows, cols, lag, rowIndex, colIndex, matIndex, grpIndex, reps, cp, off, size; - int sz, start, err_code, err_code1, err_code2, curr_meth_id; + int sz, start, err_code, curr_meth_id; // size_t numMats; // size_t numIntVecs; std::vector curr_meth_mat_id_vec; @@ -560,65 +710,29 @@ class ExprEvaluator { switch (table_n[row]) { case -2: // methods (pre-processed matrices) - //std::cout << "---------------" << std::endl; - //std::cout << "IN METHODS CASE" << std::endl; - //std::cout << "---------------" << std::endl; - - // METH_FROM_ROWS = 1 // ~ Y[i], "Y", "i" - // METH_TO_ROWS = 2 // Y[i] ~ X, c("Y", "X"), "i" - // METH_ROWS_TO_ROWS = 3 // Y[i] ~ X[j], c("Y", "X"), c("i", "j") - // METH_MAT_MULT_TO_ROWS = 4 // Y[i] ~ A %*% X[j], c("Y", "A", "X"), c("i", "j") - // METH_TV_MAT_MULT_TO_ROWS = 5 // Y[i] ~ time_var(A, change_points, block_size, change_pointer) %*% X[j], c("Y", "A", "X"), c("i", "j", "change_points", "block_size", "change_pointer") - // METH_GROUP_SUMS = 6 // ~ groupSums(Y, i, n), "Y", c("i", "n") - // METH_TV_MAT = 7 // ~ time_var(Y, change_points, block_size, change_pointer), "Y", c("change_points", "block_size", "change_pointer") - curr_meth_id = table_x[row]; switch(meth_type_id[curr_meth_id]) { case METH_FROM_ROWS: - //std::cout << "-----------------" << std::endl; - //std::cout << "IN METH_FROM_ROWS" << std::endl; - //std::cout << "-----------------" << std::endl; m = getNthMat(0, curr_meth_id, valid_vars, meth_mats); - //printMatrix(m); v = getNthIntVec(0, curr_meth_id, valid_int_vecs, meth_int_vecs); - //printIntVectorWithLabel(v, ""); m1 = matrix::Zero(v.size(), m.cols()); for (int i=0; i::Zero(v1.size(), m1.cols()); - - //std::cout << "Y index" << matIndex << std::endl; - //std::cout << "A" << m << std::endl; - //std::cout << "X" << m1 << std::endl; - //printIntVectorWithLabel(v, "i"); - //printIntVectorWithLabel(v1, "j"); - for (int i=0; i::Zero(nrow,ncol); for (int i=0; i::Zero(rows, cols); for (int i=0; ispi[0]] @@ -2037,13 +2083,17 @@ class ExprEvaluator { } rows = args[0].rows(); cols = args[0].cols(); + v1.push_back(1); + v1.push_back(2); + args = args.recycle_to_shape(v1, rows, cols); + err_code = args.get_error_code(); // err_code1 = RecycleInPlace(args[1], rows, cols); // err_code2 = RecycleInPlace(args[2], rows, cols); // err_code = err_code1 + err_code2; - // if (err_code != 0) { - // SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); - // return m; - // } + if (err_code != 0) { + SetError(err_code, "cannot recycle rows and/or columns because the input is inconsistent with the recycling request", row); + return m; + } m = matrix::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i::Zero(rows, cols); for (int i=0; i m; std::vector v1; std::vector v2; - std::cout << "---- assignment ----" << std::endl; - std::cout << "n: " << n << std::endl; - std::cout << "x: " << n << std::endl; - if (n == -1) { - Rf_error("trying to assign to a literal, which is not allowed"); - } else if (n == -2) { - Rf_error("trying to assign to an engine method, which is not allowed"); - } else if (n == -3) { - Rf_error("trying to assign to an integer vector, which is not allowed"); - } else if (n == 1) { - Rf_error("assignment error -- TODO: be more specific"); - } else if (n == 0) { - valid_vars.m_matrices[x] = assignment_value; - } else { - if (x + 1 != MP2_SQUARE_BRACKET) { - Rf_error("square bracket is the only function allowed on the left-hand-side"); - } - if (n == 3) { - v2 = valid_int_vecs[table_x[table_i[row] + 2]]; - } else if (n == 2) { + // std::cout << "---- assignment ----" << std::endl; + // std::cout << "n: " << n << std::endl; + // std::cout << "x: " << n << std::endl; + switch (n) { + case -1: + Rf_error("trying to assign to a literal, which is not allowed"); + case -2: + Rf_error("trying to assign to an engine method, which is not allowed"); + case -3: + Rf_error("trying to assign to an integer vector, which is not allowed"); + case 1: + Rf_error("assignment error -- TODO: be more specific"); + case 0: + valid_vars.m_matrices[x] = assignment_value; + return; + case 2: + if (table_n[table_i[row] + 1] != -3) { + Rf_error("indexing on the left-hand-side needs to be done using integer vectors"); + } v2.push_back(0); - } else { - Rf_error("incorrect numbers of arguments"); - } - int x1 = table_x[table_i[row]]; - m = valid_vars.m_matrices[x1]; - std::cout << "matrix: " << m << std::endl; - v1 = valid_int_vecs[table_x[table_i[row] + 1]]; - printIntVector(v1); - printIntVector(v2); - std::cout << "value: " << assignment_value << std::endl; - for (int i = 0; i < v1.size(); i++) { - for (int j = 0; j < v2.size(); j++) { - m.coeffRef(v1[i], v2[j]) = assignment_value.coeff(i, j); + case 3: + if (x + 1 != MP2_SQUARE_BRACKET) { + Rf_error("square bracket is the only function allowed on the left-hand-side"); } - } - valid_vars.m_matrices[x1] = m; - //valid_vars.m_matrices[matIndex].coeffRef(rowIndex,colIndex) = args[3].coeff(k,0); - } + x1 = table_x[table_i[row]]; + if (n == 3) v2 = valid_int_vecs[table_x[table_i[row] + 2]]; + m = valid_vars.m_matrices[x1]; + v1 = valid_int_vecs[table_x[table_i[row] + 1]]; + for (int i = 0; i < v1.size(); i++) { + for (int j = 0; j < v2.size(); j++) { + m.coeffRef(v1[i], v2[j]) = assignment_value.coeff(i, j); + } + } + valid_vars.m_matrices[x1] = m; + return; + default: + Rf_error("incorrect numbers of arguments"); + } // switch (n) }; }; diff --git a/tests/testthat/test-labelled-partitions.R b/tests/testthat/test-labelled-partitions.R index 16fbe4c0..20d195e7 100644 --- a/tests/testthat/test-labelled-partitions.R +++ b/tests/testthat/test-labelled-partitions.R @@ -37,7 +37,7 @@ test_that("model files can be read in and used", { expect_identical(nrow(m$flows()), 4L) expect_identical(nrow(m$flows_expanded()), 17L) m$variables$all()$labels() - m$derivations() + m$def$derivations() }) test_that("labels, name, and names conversion is correct", { diff --git a/tests/testthat/test-no-derivations.R b/tests/testthat/test-no-derivations.R index 62dabb13..22cd15f8 100644 --- a/tests/testthat/test-no-derivations.R +++ b/tests/testthat/test-no-derivations.R @@ -3,18 +3,18 @@ library(dplyr) library(ggplot2) library(oor) age = Compartmental(system.file("starter_models", "age", package = "macpan2")) -s = age$simulators$tmb(time_steps = 100L - , state = c(age_0_19 = 100, age_20_39 = 0, age_40_59 = 0, age_60_79 = 0, age_80_99 = 0, age_100_plus = 0) - , flow = c( - birth_rate_0_19 = 0, birth_rate_20_39 = 0.09, birth_rate_40_59 = 0.02, - birth_rate_60_79 = 0, birth_rate_80_99 = 0, birth_rate_100_plus = 0, - death_rate_0_19 = 0.01, death_rate_20_39 = 0.03, death_rate_40_59 = 0.05, - death_rate_60_79 = 0.1, death_rate_80_99 = 0.2, death_rate_100_plus = 0.5, - age_rate = 1 / 20 - ) -) -(s$report() - |> filter(matrix == "state", row != "dead") - |> ggplot() - + geom_line(aes(time, value, colour = row)) -) +# s = age$simulators$tmb(time_steps = 100L +# , state = c(age_0_19 = 100, age_20_39 = 0, age_40_59 = 0, age_60_79 = 0, age_80_99 = 0, age_100_plus = 0) +# , flow = c( +# birth_rate_0_19 = 0, birth_rate_20_39 = 0.09, birth_rate_40_59 = 0.02, +# birth_rate_60_79 = 0, birth_rate_80_99 = 0, birth_rate_100_plus = 0, +# death_rate_0_19 = 0.01, death_rate_20_39 = 0.03, death_rate_40_59 = 0.05, +# death_rate_60_79 = 0.1, death_rate_80_99 = 0.2, death_rate_100_plus = 0.5, +# age_rate = 1 / 20 +# ) +# ) +# (s$report() +# |> filter(matrix == "state", row != "dead") +# |> ggplot() +# + geom_line(aes(time, value, colour = row)) +# ) From 95e0dffb02800a52f5ac29487ab92672927f9368 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:06:35 -0400 Subject: [PATCH 033/332] standardizing error messages --- R/msg_utils.R | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 R/msg_utils.R diff --git a/R/msg_utils.R b/R/msg_utils.R new file mode 100644 index 00000000..895dbdb3 --- /dev/null +++ b/R/msg_utils.R @@ -0,0 +1,99 @@ +msg <- function(..., .max_char_limit = 70, .sep = "\n") { + input_string = (list(...) + |> lapply(as.character) + |> unlist(recursive = TRUE, use.names = FALSE) + |> trimws(whitespace = "[ \t]") + |> paste0(collapse = " ") + ) + + # Split the input string into words + words <- unlist(strsplit(input_string, " ")) + + # Initialize variables + result <- character(0) + current_word <- "" + + for (word in words) { + # If adding the current word would exceed the character limit, start a new element + if (nchar(paste(current_word, word, sep = " ")) > .max_char_limit) { + result <- c(result, current_word) + current_word <- "" + } + + # Add the word to the current element + current_word <- paste(current_word, word, sep = " ") + } + + # Add the last element + if (nchar(current_word) > 0) { + result <- c(result, current_word) + } + + break_start = function(x) sprintf("%s%s", .sep, x) + (result + |> trimws(whitespace = "[ \t]") + |> break_start() + |> paste0(collapse = "") + ) +} + +msg_colon = function(x, y) sprintf("%s:\n%s", x, y) +msg_break = function(...) paste(paste(..., sep = "\n"), collapse = "\n") +msg_indent = function(..., .max_char_limit = 66) { + (msg(..., .max_char_limit = .max_char_limit, .sep = "\n ") + |> trimws(which = "left", whitespace = "[\n]") + ) +} +msg_indent_break = function(...) { + (list(...) + |> lapply(sprintf, fmt = " %s") + |> unlist(use.names = FALSE, recursive = TRUE) + |> msg_break() + ) +} +msg_paste = function(...) paste(..., sep = "") +msg_space = function(...) paste(..., sep = " ") + +if (FALSE) { + +word_vector <- c("In", "the", "midst", "of", "a", "bustling", "city", "where", "the", "cacophony", "of", "car", "horns", "the", "hustle", "of", "pedestrians", "rushing", "to", "their", "destinations", "and", "the", "towering", "skyscrapers", "that", "seemed", "to", "touch", "the", "heavens", "created", "a", "dynamic", "and", "overwhelming", "urban", "landscape", "a", "solitary", "tree", "stood", "as", "a", "silent", "sentinel", "its", "branches", "swaying", "gently", "in", "the", "breeze", "offering", "a", "tranquil", "oasis", "amidst", "the", "chaos", "a", "place", "where", "one", "could", "find", "solace", "and", "a", "moment", "of", "reflection", "in", "the", "heart", "of", "the", "metropolis") +msg_indent(word_vector) |> cat() + +msg_colon( + msg( + "In the midst of a bustling city, where the cacophony of,", + "the hustle of pedestrians rushing to their destinations, and the ", + "towering skyscrapers that seemed to touch the heavens created a dynamic", + "and overwhelming urban landscape", + "a solitary tree stood as a silent sentinel", + "its branches swaying gently in the breeze", + "offering a tranquil oasis amidst the chaos", + "a place where one could find solace and a moment of reflection in the", + "heart of the metropolis" + ), + msg_indent(word_vector) +) |> cat() + + +expr_as_string = "y ~ x[i]" +missing_items = c("z", "a") +vec_type = "THINGS" +pointers = "this is nother thing" +vec = c(letters, letters, letters) +msg_break( + msg_colon( + "The expression given by", + msg_indent(expr_as_string) + ), + msg_colon( + msg("contained the following", vec_type), + msg_indent(missing_items) + ), + msg_colon( + msg("that were not found in the list of available", vec_type), + msg_indent(vec) + ), + msg(pointers) +)|>cat() + +} From b19dd1365eca6a5e0a24c3d32fc1303f6861eb14 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:06:48 -0400 Subject: [PATCH 034/332] code archive --- misc/old-r-source/tmb_expressions.R | 81 +++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 misc/old-r-source/tmb_expressions.R diff --git a/misc/old-r-source/tmb_expressions.R b/misc/old-r-source/tmb_expressions.R new file mode 100644 index 00000000..272ef50b --- /dev/null +++ b/misc/old-r-source/tmb_expressions.R @@ -0,0 +1,81 @@ + +TMBObjectiveFunction = function(parse_table, parsed_literals, existing_literals) { + self = Base() + parse_table$i = parse_table$i - 1L + parse_table$x[parse_table$n == -1L] = parse_table$x[parse_table$n == -1L] + length(existing_literals) + parse_table$x[parse_table$n > 0L] = parse_table$x[parse_table$n > 0L] - 1L + self$parse_table = as.list(parse_table) + self$literals = c(existing_literals, parsed_literals) + return_object(self, "TMBObjectiveFunction") +} + + +TMBExpressions = function(parse_tables, valid_vars, literals_list) { + self = Base() + self$parse_tables = lapply(parse_tables, getElement, "parse_table") + self$valid_vars = valid_vars + self$literals_list = literals_list + self$literal_offsets = c(0L, cumsum(vapply(self$literals_list, length, integer(1L)))[-length(self$literals_list)]) + self$parse_table_offsets = c( + 0L, + cumsum( + vapply( + self$parse_tables[-length(self$parse_tables)], + nrow, + integer(1L) + ) + ) + ) + unnamed_parse_table = as.list(do.call(rbind, self$parse_tables)) + if (length(unnamed_parse_table) == 0L) { + self$parse_table = list( + p_table_x = integer(0L), + p_table_n = integer(0L), + p_table_i = integer(0L) + ) + } else { + self$parse_table = setNames( + unnamed_parse_table, + c("p_table_x", "p_table_n", "p_table_i") + ) + } + self$parse_table$p_table_i = unlist(mapply( + function(ii, y) { + ii[ii != -1L] = ii[ii != -1L] + y + ii + }, + lapply(lapply(self$parse_tables, getElement, "i"), function(x) x - 1L), + self$parse_table_offsets, + SIMPLIFY = FALSE + ), use.names = FALSE) + self$parse_table$p_table_x = unlist(mapply( + function(xx, nn, y) { + xx[nn == -1L] = xx[nn == -1L] + y + xx[nn == 0L] = xx[nn == 0L] + xx[nn > 0L] = xx[nn > 0L] - 1L + xx + }, + lapply(self$parse_tables, getElement, "x"), + lapply(self$parse_tables, getElement, "n"), + self$literal_offsets, + SIMPLIFY = FALSE + ), use.names = FALSE) +## hack to switch p_table_x to zero-based indexing. +## todo: fix properly in package r code +#parse_table$p_table_x[parse_table$p_table_n > 0L] = parse_table$p_table_x[parse_table$p_table_n > 0L] - 1L + + self$expr_output_count = rep(1L, length(self$parse_tables)) + self$expr_output_id = apply( + outer(names(self$parse_tables), names(self$valid_vars), "=="), + 1L, + which + ) - 1L + self$expr_sim_block = rep(0L, length(self$parse_tables)) + self$expr_num_p_table_rows = vapply( + self$parse_tables, + nrow, + integer(1L), + USE.NAMES = FALSE + ) + return_object(self, "TMBExpressions") +} From f916f8b8d13f7ddd01b1edc5ce856628c4a275df Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:07:28 -0400 Subject: [PATCH 035/332] old experiments --- inst/tmb_examples/sir.R | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 inst/tmb_examples/sir.R diff --git a/inst/tmb_examples/sir.R b/inst/tmb_examples/sir.R new file mode 100644 index 00000000..444988d5 --- /dev/null +++ b/inst/tmb_examples/sir.R @@ -0,0 +1,104 @@ +library(macpan2) +library(ggplot2) +library(dplyr) +library(tidyr) +N = 1e6 +state = c(S = N - 1, I = 1, R = 0) +sir_for_fake_data = TMBModel( + init_mats = MatsList( + + ## state vectors + state_vector = state # exact + , noisy_state = empty_matrix # with observation error + , incidence = empty_matrix # number of new individuals flowing in + , outflow = empty_matrix # number of individuals flowing out + + ## flow vectors + , flow_vector = c(foi = 0, gamma = 0.15) # per-capita flow rate + # one element for each flow + , flow_pars = c(beta = 0.25) # parameters for modifying flow vector + , flows = empty_matrix # + + ## state summaries + , N = N + , n_states = length(state) + + ## state indices + , from_indices = c(0, 1) + , to_indices = c(1, 2) + , I_index = 1 + + ## flow indices + , foi_index = 0 + , beta_index = 0 + + , dummy = empty_matrix + , .mats_to_save = "noisy_state" + , .mats_to_return = "noisy_state" + , .dimnames = list(noisy_state = list(names(state), "")) + ), + expr_list = ExprList( + before = list( + N ~ sum(state_vector) + ), + during = list( + dummy ~ assign(flow_vector + , foi_index, 0 + , flow_pars[beta_index] * state_vector[I_index] / N + ) + , flows ~ flow_vector * state_vector[from_indices] + , incidence ~ groupSums(flows, to_indices, n_states) + , outflow ~ groupSums(flows, from_indices, n_states) + , state_vector ~ state_vector + incidence - outflow + , noisy_state ~ rnbinom(state_vector, 10) + ) + ), + time_steps = Time(300L) +) +fake_data_simulator = sir_for_fake_data$simulator() +fake_data = fake_data_simulator$report() + +(fake_data + %>% filter(matrix == "noisy_state") + %>% mutate(state = factor(row, levels = names(state))) + %>% ggplot() + + facet_wrap(~state, ncol = 1, scale = 'free_y') + + geom_line(aes(time, value, colour = state)) +) + + + + + + + + +sir_for_fake_data$print_exprs() + + + + + +simulated_reports = filter(fake_data, matrix == "simulated_reports")$value +expected_reports = filter(fake_data, matrix == "expected_reports")$value + +plot(simulated_reports) +lines(expected_reports) + +length(simulated_reports) +length(expected_reports) + +sir_for_fitting = sir_for_fake_data$insert_exprs( + condensed_vector ~ c(state_vector[1], incidence[1]), + .at = 1, .phase = "after" +)$add_mats( + observed_reports = simulated_reports, + neg_log_lik = empty_matrix +) +# $insert_exprs( +# neg_log_lik ~ dnbinom(observed_reports, simulated_reports, 10), +# .at = 3, .phase = "after" +# ) +sir_for_fitting$simulator() +xx$print_exprs("testingtesting.txt") +xx$simulator()$report() %>% filter(matrix == "condensed_vector") From 5190cb0417a9e05cd4dfd72f92786f55af037053 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 18 Oct 2023 10:08:10 -0400 Subject: [PATCH 036/332] vignette work for birs --- vignettes/debugging.Rmd | 28 ++ vignettes/general_compartmental_model.Rmd | 404 ++++++++++++++++++++ vignettes/model_inputs.Rmd | 32 ++ vignettes/sir_radial_basis_transmission.Rmd | 125 ++++++ vignettes/tmb_model.Rmd | 80 ++++ vignettes/updating_models.Rmd | 29 ++ 6 files changed, 698 insertions(+) create mode 100644 vignettes/debugging.Rmd create mode 100644 vignettes/general_compartmental_model.Rmd create mode 100644 vignettes/model_inputs.Rmd create mode 100644 vignettes/sir_radial_basis_transmission.Rmd create mode 100644 vignettes/tmb_model.Rmd create mode 100644 vignettes/updating_models.Rmd diff --git a/vignettes/debugging.Rmd b/vignettes/debugging.Rmd new file mode 100644 index 00000000..ce8ad87d --- /dev/null +++ b/vignettes/debugging.Rmd @@ -0,0 +1,28 @@ +--- +title: "Debugging" +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{Debugging} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +library(macpan2) +library(oor) +``` + +[![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) + +## Choosing a Debugging Technique + +1. `traceback(m = 1)` +2. `options(error = recover)` +3. `debug({package-function})` +4. `oor_debug$flag({object-method})` diff --git a/vignettes/general_compartmental_model.Rmd b/vignettes/general_compartmental_model.Rmd new file mode 100644 index 00000000..23215bb7 --- /dev/null +++ b/vignettes/general_compartmental_model.Rmd @@ -0,0 +1,404 @@ +--- +title: "General Compartmental Model" +header-includes: + - \usepackage{amsmath} +output: + rmarkdown::html_vignette: + mathjax: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" + toc: true +vignette: > + %\VignetteIndexEntry{General Compartmental Model} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + chunk_output_type: console +--- + + +TODO: USE MACROS +TODO: good letter for state vector -- $X$ +TODO: maybe use underline and overline for `from` and `to` +TODO: use sans-serif for matrices +TODO: consider defining special matrix/vector symbols for subsetted matrices and vectors (e.g. $I$ just means $X_\text{infectious}$) +TODO: more common to use capital lambda for force of infection +TODO: perhaps call 'Normalization Variables' 'Aggregated Variables' + + + +$\newcommand{\Coll}[1]{\boldsymbol{\mathrm{#1}}}$ +$\newcommand{\Item}[2]{{#1}_{#2}}$ +$\newcommand{\Def}[2]{\Coll{#1} = [\Item{#1}{#2}]}$ +$\newcommand{\Range}[2]{{#1} = 1, ..., {#2}}$ +$\newcommand{\SetFont}[1]{\mathtt{#1}}$ +$\newcommand{\IndexSet}[2]{\SetFont{#1}\left({#2}\right)}$ +$\newcommand{\InIndexSet}[3]{{#1} \in \IndexSet{#2}{#3}}$ +$\newcommand{\InvIndexSet}[2]{\SetFont{#1}^{-1}\left({#2}\right)}$ +$\newcommand{\GetItem}[3]{\Item{#1}{\InvIndexSet{#2}{#3}}}$ +$\newcommand{\GoesTo}[1]{\xrightarrow{#1}}$ + +$\newcommand{\State}{Z}$ +$\newcommand{\NormState}{\widetilde{Z}}$ +$\newcommand{\Flow}{\phi}$ +$\newcommand{\Trans}{\beta}$ +$\newcommand{\Norm}{N}$ +$\newcommand{\From}{from}$ +$\newcommand{\To}{to}$ +$\newcommand{\Edge}{flow}$ +$\newcommand{\Infectious}{infectious}$ +$\newcommand{\Infection}{infection}$ +$\newcommand{\InfectiousTrans}{infectious\_trans}$ +$\newcommand{\InfectionTrans}{infection\_trans}$ +$\newcommand{\Normalization}{normalization}$ +$\newcommand{\FlowFunc}[3]{f_{#1}\left({#2},{#3}\right)}$ + +$\newcommand{\StateLen}{n}$ +$\newcommand{\FlowLen}{m}$ +$\newcommand{\FlowPaths}{p}$ +$\newcommand{\InfPaths}{q}$ +$\newcommand{\NormLen}{r}$ +$\newcommand{\InfFlowLen}{s}$ + +## Preparing to Generalize the SIR Model + +Compartmental models in `macpan2` are generalizations of the the SIR model. Like the SIR model, most compartmental models in `macpan2` have the following four kinds of quantities that map onto the SIR model in a familiar way. + +* Variables + * State + * $S$ -- number susceptible individuals + * $I$ -- number of infectious individuals + * $R$ -- number of recovered individuals + * Normalization + * $N = S + I + R$ -- total population size +* Rates + * Transmission + * $\beta$ -- transmission rate + * Flow + * $\lambda = \beta I / N$ -- force of infection + * $\gamma$ -- recovery rate + +We also assume that mathematical epidemiologists will be familiar with how these variables are involved in the following state transitions that occur in the SIR model. + +$$ +\begin{align} +S \GoesTo{\lambda S} I \\ +I \GoesTo{\gamma I} R \\ +\end{align} +$$ + +We can convert this state-transition notation into a differential equation. + +$$ +\begin{align*} +\frac{dS}{dt} &= -\lambda S \\ +\frac{dI}{dt} &= \lambda S - \gamma I \\ +\frac{dR}{dt} &= \gamma I \\ +\end{align*} +$$ + +We could also convert state-transitions into a difference equation. + +$$ +\begin{align*} +S(t+1) - S(t) &= -\lambda S(t) \\ +I(t+1) - I(t) &= \lambda S(t) - \gamma I(t) \\ +R(t+1) - R(t) &= \gamma I(t) \\ +\end{align*} +$$ + +## Generalized Compartmental Model + +The last section clarified our typology of variables involved in a compartmental diagram using the example of the SIR model. Now we will develop a notation for generalizing this model. + +* State variables : $\Def{\State}{i}$, $\Range{i}{\StateLen}$ +* Flow variables : $\Def{\Flow}{i}$, $\Range{i}{\FlowLen}$ +* Transmission variables : $\Def{\Trans}{i}$, $\Range{i}{\InfPaths}$ +* Normalization variables : $\Def{\Norm}{i}$, $\Range{i}{\NormLen}$ + +## Index Sets + +* $\IndexSet{\From}{i}$ : The set of all flow path indices that have $\Item{\State}{i}$ as the compartment from which individuals are flowing. +* $\IndexSet{\To}{i}$ : The set of all flow path indices that have $\Item{\State}{i}$ as the compartment to which individuals are flowing. +* $\IndexSet{\Edge}{i}$ : The set of all flow path indices that have $\Item{\Flow}{i}$ as the variable describing the magnitude of the flow. + +The inverse of these sets pick out a single variable from either the $\Coll{\State}$ or $\Coll{\Flow}$ vector. For example, we can describe the $i$th flow path in the following way. + + +$$ +\Item{\State}{j} +\GoesTo{\FlowFunc{i}{\Item{\State}{j}}{\Item{\Flow}{l}}} +\Item{\State}{k} +$$ + +Where, + +* $j = \InvIndexSet{\From}{i}$ +* $k = \InvIndexSet{\To}{i}$ +* $l = \InvIndexSet{\Edge}{i}$ + +and the flow function can take on one of the following two options depending on what the user chooses for flow path $i$. + +$$ +\FlowFunc{i}{\Item{\State}{j}}{\Item{\Flow}{l}} = +\begin{cases} +\Item{\Flow}{l}\Item{\State}{j}, & \text{per-capita} \\ +\Item{\Flow}{l}, & \text{absolute} \\ +\end{cases} +$$ + +We can also have one-sided flows where nothing flows out of $\Item{\State}{j}$ or nothing flows in to $\Item{\State}{k}$. + +$$ +\GoesTo{\FlowFunc{i}{\Item{\State}{j}}{\Item{\Flow}{l}}} +\Item{\State}{k} +$$ + +$$ +\Item{\State}{j} +\GoesTo{\FlowFunc{i}{\Item{\State}{j}}{\Item{\Flow}{l}}} +$$ + + + +This is abstract, I know. But it only means things like this. + +$$ +S \GoesTo{\lambda S} I +$$ +Or this. + +$$ +I \GoesTo{\gamma I} R +$$ + +### State Normalization + +We often need to work state variables that have been normalized by the total number of individuals in their stratum. + +$$ +\Item{\NormState}{i} = \frac{\Item{\State}{i}}{\Item{\Norm}{j[i]}} +$$ + +Where , + +* $j[i] = \InvIndexSet{\Normalization}{i}$ is the index into the normalization + +### Infection Flow + +The $i$th force of infection is given as follows. + +$$ +\Item{\Trans}{i} \Item{\State}{j[i]} +$$ + +Where , + +* $j[i] = \InvIndexSet{\InfectiousTrans}{i}$ + +The $i$th infection flow is computed as follows. + +$$ +\Flow_j = \sum_i \Item{\Trans}{i} \Item{\State}{k} +$$ + +Where, + +* $j = \InvIndexSet{\InfectionTrans}{i}$ +* $k = \InvIndexSet{\InfectiousTrans}{i}$ +* $l = \InvIndexSet{\Normalization}{i}$ + + +## General Data Structure + +All compartmental models in `macpan2` contain a state vector, $s = [s_i]$, a flow +vector, $f = [f_i]$, and population vector, $n = [n_i]$. + +* $s_i$ : Number of individuals in compartment $i$. +* $f_i$ : Magnitude of the $i$th flow of individuals between the compartments. +* $n_i$ : Number of individuals in the $i$th sub-population of compartments. + +We identify other vectors that are composed entirely of elements taken from $s$ +or $f$. Some of these vectors are just subsets of either $s$ or $f$, including +the infectious states, $s_\text{infectious}$, infected states, +$s_\text{infected}$, and infection flows, $f_\text{infection}$. Other vectors +are not necessarily strict subsets because the elements from the original vector +can be repeated. The $s_\text{from}$ and $s_\text{to}$ contain elements from $s$ +giving the from and to compartments associated with each flow in $f$. In +particular, $s_\text{from}$, $s_\text{to}$, and $f$ all have the same length and +the $i$th element of each gives the number in the from compartment, to +compartment, and magnitude of the flow respectively. + +The elements of the flow vector get transformed into a rate of flow per unit time into a compartment, $\Phi_i\left(f_i, s_{\text{from}[i]}\right)$, and out of a compartment, $\Psi_i\left(f_i, s_{\text{from}[i]}\right)$. + +$$ +F(f, s, t) = +\underbrace{\sum_i\Phi_i\left(f_i, s_{\text{from}[i]}, t\right)}_\text{inflow} - +\underbrace{\sum_i\Psi_i\left(f_i, s_{\text{from}[i]}, t\right)}_\text{outflow} +$$ + +This $F$ function can get used in many ways. For example, it could be used in a discrete time model. + +$$ +s(t+1) = s(t) + F(f,s, t) +$$ + +In a continuous time model we would have the following. + +$$ +\frac{ds}{dt} = F(f, s, t) +$$ + +For each flow, $i$, a type must be chosen for the inflow function, $\Phi_i$, and outflow function, $\Psi_i$, can be one of the following types. + +$$ + \Phi_i\left(f_i, s_{\text{from}[i]}, t\right) +- \Psi_i\left(f_i, s_{\text{from}[i]}, t\right) += F_i(f, s, t) += \begin{cases} +f_is_{\text{from}[i]} & \text{per-capita} \\ +f_i & \text{absolute} \\ +0 & \text{outflow only} +\end{cases} +$$ +$$ + = +\begin{cases} +f_is_{\text{from}[i]} & \text{per-capita} \\ +f_i & \text{absolute} \\ +0 & \text{inflow only} +\end{cases} +$$ + + +The per-capita transmission matrix, $\mathbf{B}$, relates the infectious states +to the infection flows. + +$$ +f_\text{infection} = B s_\text{infectious} +$$ + +The elements in $f$ that correspond to elements in $f_\text{infection}$ are also +updated when this expression is evaluated. + +We know that the units of the elements of $f_\text{infection}$ are those of a +force of infection, which has the same units as a transmission rate. + +$$ +\frac{\text{number of infections}}{\text{number of people} \times \text{duration of exposure}} +$$ +By applying these units to the left-hand side of the equation that relates $f\_\text{infection}$ to $s\_\text{infectious}$ through $\mathbf{B}$, we find that the units of the elements of $\mathbf{B}$ +are. + +$$ +\frac{\text{number of infections}}{\text{number of people}^2 \times \text{duration of exposure}} +$$ + +The appearance of a another `number of people` in the denominator relative to +the transmission rate case is why we refer to $\mathbf{B}$ as a per-capita +transmission rate. + +There are different types of flow variables that differ in how they measure the +magnitude of the flow. The user may choose from a number of flow types for each flow. A common convenient choice is a per-capita flow rate, which has units of ... + +## Example + +We present an example model that is just complex enough to illustrate the +general model. + +![](../misc/diagrams/model_matrices.png) + +The state vector is given as follows. + +$$ +s = \begin{bmatrix} +S \\ V \\ I_m \\ I_s \\ R +\end{bmatrix}, +f = \begin{bmatrix} +v \\ w \\ \lambda_m \\ \lambda_s \\ \lambda_v \\ \gamma_m \\ \gamma_s \\ \mu \\ b +\end{bmatrix} +$$ + +$$ +\text{from} = \begin{bmatrix} +0 \\ 1 \\ 0 \\ 0 \\ 1 \\ 2 \\ 3 \\ 0 \\ 1 \\ 2 \\ 3 \\ 4 \\ -1 \\ +\end{bmatrix}, +\text{to} = \begin{bmatrix} +1 \\ 0 \\ 2 \\ 3 \\ 2 \\ 4 \\ 4 \\ -1 \\ -1 \\ -1 \\ -1 \\ -1 \\ 0 +\end{bmatrix}, +\text{flow} = \begin{bmatrix} +0 \\ 1 \\ 2 \\ 3 \\ 4 \\ 5 \\ 6 \\ 7 \\ 7 \\ 7 \\ 7 \\ 7 \\ 8 +\end{bmatrix} +$$ + + + +The flows are defined by the following three vectors that line up with one row +per flow. + +$$ +s_\text{from} = \begin{bmatrix} +S \\ V \\ S \\ S \\ V \\ I_m \\ I_s +\end{bmatrix} +, +s_\text{to} = \begin{bmatrix} +V \\ S \\ I_m \\ I_s \\ I_m \\ R \\ R +\end{bmatrix} +, +f_\text{flow} = \begin{bmatrix} +v \\ w \\ \lambda_m \\ \lambda_s \\ \lambda_v \\ \gamma_m \\ \gamma_s +\end{bmatrix} +$$ + +The infection flow vector is given by the following. + +$$ +f_\text{infection} = +\begin{bmatrix} +\lambda_m \\ \lambda_s \\ \lambda_v +\end{bmatrix} +$$ + +The infectious state vector is given by the following. + +$$ +s_\text{infectious} = +\begin{bmatrix} +I_m \\ I_s +\end{bmatrix} +$$ + +$$ +f_\text{infection} = Bs_\text{infectious} +$$ + +We also define a vector of reference population sizes that lines up with the +infectious state vector. + +$$ +N_\text{infectious} = +\begin{bmatrix} +N \\ N +\end{bmatrix}, +s_\text{infectious} = +\begin{bmatrix} +I_m \\ I_s +\end{bmatrix} +$$ + +The per-capita transmission matrix is given by the following. + +$$ +B = +\frac{1}{N}\begin{bmatrix} +\beta_{mm} & \beta_{ms} \\ +\beta_{sm} & \beta_{ss} \\ +\beta_{vm} & \beta_{vs} \\ +\end{bmatrix} +$$ +Here $N$ is the total population size and each of the $\beta$s +give rates of transmission of each infection flow contributed by each infectious +state. + +The key point is that the infection flow vector is the matrix product of the +transmission matrix and the infectious state vector. + +## Alternative Formulation diff --git a/vignettes/model_inputs.Rmd b/vignettes/model_inputs.Rmd new file mode 100644 index 00000000..7847e4a5 --- /dev/null +++ b/vignettes/model_inputs.Rmd @@ -0,0 +1,32 @@ +--- +title: "Variable Types and Dimensions" +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{Variable Types} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +[![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) + +```{r, include = FALSE} +library(macpan2) +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## State Variables + +## Flow Variables + +## Flow Parameters + +## Observed Variables + +## Fixed Effect Parameters + +## Random Effect Parameters diff --git a/vignettes/sir_radial_basis_transmission.Rmd b/vignettes/sir_radial_basis_transmission.Rmd new file mode 100644 index 00000000..89a836ef --- /dev/null +++ b/vignettes/sir_radial_basis_transmission.Rmd @@ -0,0 +1,125 @@ +--- +title: "Radial Basis Functions for Time Varying Transmission Rates" +header-includes: + - \usepackage{amsmath} +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{Radial Basis Functions for Time Varying Transmission Rates} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + + + +```{r pkgs, include = FALSE} +library(macpan2) +library(macpan2helpers) +library(dplyr) +library(ggplot2) +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +cat_file = function(...) cat(readLines(file.path(...)), sep = "\n") +``` + + +## Getting and the Base Model + +Before we can add the fancy radial basis for the transmission rate, we need a base model. We use an SIR model that has been modified to include waning. + +```{r} +sir = Compartmental(system.file("starter_models", "sir_waning", package = "macpan2")) +sir$flows() +``` + + +## Radial Basis Functions + +The `macpan2::rbf` function can be used to produce a matrix giving the values of each basis function (each column) at each time step (each row). Using this matrix, $X$, and a weights vector, $b$, we can get a flexible output vector, $y$, with a shape that can be modified into a wide variety of shapes by changing the weights vector. + +$$ +y = Xb +$$ + +The following code illustrates this approach. + +```{r, fig.height=8, fig.width=6} +set.seed(1L) +d = 20 +n = 2500 +X = rbf(n, d) +b = rnorm(d, sd = 0.01) +par(mfrow = c(3, 1)) +plot(c(1, n), range(X), type = "n", + xlab = "time", ylab = "basis functions") +for (i in 1:d) lines(X[,i]) +barplot(b, xlab = "", ylab = "weights") +plot(X %*% b, type = "l", xlab = "time", ylab = "output") +``` + +Here `d` is the dimension of the basis, or number of functions, and `n` is the number of time steps. By multiplying the uniform basis matrix (top panel) to a set of weights (middle panel), one can obtain a non-uniform curve (bottom panel). Note how the peaks in the output are associated with large positive weights. + +## Radial Transmission Rate Basis + +To transform the output of the matrix multiplication of the radial basis function matrix and the weights vector into a time-series for the transmission rate, $\beta$. Althought we could just use the output vector as the $\beta$ time series, it is more convenient to transform it a little so that the $\beta$ values yield more interesting dynamics in an SIR model. In particular, our model for $\beta_t$ as a function of time, $t$, is as follows. + +$$ +\log(\beta_t) = \log(\gamma_t) + \log(N) - \log(S_t) + x_tb +$$ + +Here we have the recovery rate, $\gamma_t$, total population, $N_t$, and number of susceptibles, $S_t$, at time, $t$, and the $t$th row of $X$, $x_t$. To better understand the rationale for this equation note that if every element of $b$ is set to zero, we have the following condition. + +$$ +\frac{\beta_t S_t}{N} = \gamma_t +$$ + +This condition assures that the number of infected individuals remains constant at time, $t$. This means that positive values of $b$ will tend to generate outbreaks and negative values will tend to reduce transmission. + +## Simulations + +```{r} +set.seed(1L) +simulator = sir$simulators$tmb( + time_steps = n + , state = c(S = 100000 - 500, I = 500, R = 0) + , flow = c(foi = 0, gamma = 0.2, wane = 0.01) + , beta = 1 + , N = 100000 + , X = rbf(n, d) + , b = rnorm(d, sd = 0.01) + , incidence = empty_matrix + , eta = empty_matrix + , .mats_to_save = c("state", "incidence", "beta") + , .mats_to_return = c("state", "incidence", "beta") +)$insert$expressions( + eta ~ gamma * exp(X %*% b) + , .phase = "before" + , .at = Inf +)$insert$expressions( + beta ~ eta[time_step(1)] / clamp(S/N, 1/100) + , .phase = "during" + , .at = 1 +)$insert$expressions( + incidence ~ I + , .vec_by_states = "total_inflow" + , .phase = "during" + , .at = Inf +)$replace$params( + default = rnorm(d, sd = 0.01) + , mat = rep("b", d) + , row = seq_len(d) - 1L +) +``` + +```{r, fig.height=8, fig.width=6} +set.seed(5L) +(simulator$report(rnorm(d, sd = 0.01), .phases = "during") + %>% mutate(variable = if_else(matrix == "state", row, matrix)) + %>% ggplot() + + facet_wrap(~ variable, ncol = 1, scales = 'free') + + geom_line(aes(time, value)) +) +``` diff --git a/vignettes/tmb_model.Rmd b/vignettes/tmb_model.Rmd new file mode 100644 index 00000000..13da37da --- /dev/null +++ b/vignettes/tmb_model.Rmd @@ -0,0 +1,80 @@ +--- +title: "TMB Model" +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{TMB Model} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +[![status](https://img.shields.io/badge/status-stub-red)](https://img.shields.io/badge/status-stub-red) + + +```{r, include = FALSE} +library(macpan2) +library(dplyr) +library(ggplot2) +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r} +state_vector = c(S = 9999, I = 1, R = 0) +flow_vector = c(foi = 0, gamma = 0.1) +from_states = c("S", "I") +to_states = c("I", "R") +flow_params = c(beta = 0.3, N = sum(state_vector)) + +sir = TMBModel( + init_mats = MatsList( + state = state_vector + , rates = flow_vector + , params = flow_params + + , I = match("I", names(state_vector)) - 1 + , foi = match("foi", names(flow_vector)) - 1 + , beta = match("beta", names(flow_params)) - 1 + , N = match("N", names(flow_params)) - 1 + + , from = match(from_states, names(state_vector)) - 1 + , to = match(to_states, names(state_vector)) - 1 + + , n_states = length(state_vector) + + , flow = empty_matrix + , incidence = empty_matrix + , outflow = empty_matrix + , noisy_state = empty_matrix + , dummy = empty_matrix + + , .mats_to_save = "noisy_state" + , .mats_to_return = "noisy_state" + , .dimnames = list(noisy_state = list(names(state_vector), "")) + ), + expr_list = ExprList( + during = list( + dummy ~ assign(rates, foi, 0, params[beta] * state[I] / params[N]) + , flow ~ state[from] * rates + , incidence ~ groupSums(flow, to, n_states) + , outflow ~ groupSums(flow, from, n_states) + , state ~ state + incidence - outflow + , noisy_state ~ rnbinom(state, 10) + ) + ), + time_steps = Time(100) +) +sir +``` + +```{r} +(sir$simulator()$report() + %>% mutate(state = factor(row, levels = names(state_vector))) + %>% ggplot() + + facet_wrap(~state, ncol = 1, scales = "free_y") + + geom_line(aes(time, value)) +) +``` diff --git a/vignettes/updating_models.Rmd b/vignettes/updating_models.Rmd new file mode 100644 index 00000000..70b2432c --- /dev/null +++ b/vignettes/updating_models.Rmd @@ -0,0 +1,29 @@ +--- +title: "Updating Models" +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{Updating Models} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + chunk_output_type: console +--- + +[![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) + +```{r pkgs, include = FALSE} +library(macpan2) +library(macpan2helpers) +library(dplyr) +library(ggplot2) +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Inserting, Adding, and Replacing + +After using `?Compartmental` to read in a set of `vignette("model_definitions", "macpan2")`. From 3de5b9c0ecb522994c926369f2503f2597ea1843 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 25 Oct 2023 08:42:35 -0400 Subject: [PATCH 037/332] clean the repo --- NAMESPACE | 24 + R/connection.R | 5 +- R/formula_utils.R | 6 + R/labelled_partitions.R | 474 +++++++++++++++++- R/mapping.R | 10 + R/model_data_structure.R | 6 +- R/msg_utils.R | 3 + R/name_utils.R | 24 +- R/names_and_labels.R | 46 +- R/quantities.R | 78 +++ R/settings.R | 12 +- inst/model_library/.vscode/settings.json | 2 + inst/model_library/sir/derivations.json | 1 + inst/model_library/sir/flows.csv | 3 + inst/model_library/sir/infection.csv | 2 + inst/model_library/sir/settings.json | 5 + inst/model_library/sir/variables.csv | 6 + inst/model_library/sir/vectors.csv | 8 + inst/model_library/sir_1d_space/variables.csv | 0 inst/model_library/sir_age/README.md | 20 + inst/model_library/sir_age/derivations.json | 1 + inst/model_library/sir_age/flows.csv | 4 + inst/model_library/sir_age/indices.csv | 0 inst/model_library/sir_age/infection.csv | 2 + inst/model_library/sir_age/mappings.csv | 9 + inst/model_library/sir_age/model.drawio | 49 ++ inst/model_library/sir_age/normalization.csv | 0 inst/model_library/sir_age/settings.json | 7 + inst/model_library/sir_age/variables.csv | 12 + inst/model_library/sir_age/vectors.csv | 7 + .../param_examples/epi-symp-vax_variables.csv | 7 + .../hello_location/derivations.json | 1 + .../SI_products/hello_location/flows.csv | 3 + .../SI_products/hello_location/settings.json | 6 + .../SI_products/hello_location/variables.csv | 5 + inst/starter_models/cross_immunity/flows.csv | 2 + .../cross_immunity/variables.csv | 8 + .../minimal_all_index_cases/derivations.json | 1 + .../minimal_all_index_cases/flows.csv | 9 + .../minimal_all_index_cases/settings.json | 8 + .../minimal_all_index_cases/trans.csv | 2 + .../minimal_all_index_cases/variables.csv | 17 + .../product_example/Epi_model/params.csv | 0 .../quebec_windsor/derivations.json | 11 + inst/starter_models/quebec_windsor/flows.csv | 2 + .../quebec_windsor/settings.json | 8 + .../quebec_windsor/variables.csv | 45 ++ inst/starter_models/sir/settings.json | 5 +- .../sir/transmission_dimensions.csv | 1 + .../sir/transmission_matrices.csv | 2 + inst/starter_models/sir/variables.csv | 18 +- inst/starter_models/sir_age/derivations.json | 5 + inst/starter_models/sir_age/flows.csv | 3 + inst/starter_models/sir_age/settings.json | 8 + .../sir_age/transmission_dimensions.csv | 1 + .../sir_age/transmission_matrices.csv | 2 + inst/starter_models/sir_age/variables.csv | 15 + inst/starter_models/sir_geog/flows.csv | 5 + inst/starter_models/sir_geog/trans.csv | 5 + inst/starter_models/sir_geog/variables.csv | 29 ++ inst/starter_models/sir_symp/derivations.json | 1 + inst/starter_models/sir_symp/flows.csv | 3 + inst/starter_models/sir_symp/settings.json | 6 + inst/starter_models/sir_symp/trans.csv | 3 + inst/starter_models/sir_symp/variables.csv | 10 + inst/starter_models/sir_vax/trans.csv | 2 + .../sir_vax/transmission_dimensions.csv | 1 + .../sir_vax/transmission_matrices.csv | 2 + man/Select.Rd | 15 + man/to_labels.Rd | 2 +- misc/diagrams/drawio-templates/test.drawio | 177 ++++++- misc/diagrams/toy_model.drawio | 3 + misc/experiments/refactorcpp.R | 374 +++++++++++++- 73 files changed, 1597 insertions(+), 62 deletions(-) create mode 100644 R/mapping.R create mode 100644 R/quantities.R create mode 100644 inst/model_library/.vscode/settings.json create mode 100644 inst/model_library/sir/derivations.json create mode 100644 inst/model_library/sir/flows.csv create mode 100644 inst/model_library/sir/infection.csv create mode 100644 inst/model_library/sir/settings.json create mode 100644 inst/model_library/sir/variables.csv create mode 100644 inst/model_library/sir/vectors.csv create mode 100644 inst/model_library/sir_1d_space/variables.csv create mode 100644 inst/model_library/sir_age/README.md create mode 100644 inst/model_library/sir_age/derivations.json create mode 100644 inst/model_library/sir_age/flows.csv create mode 100644 inst/model_library/sir_age/indices.csv create mode 100644 inst/model_library/sir_age/infection.csv create mode 100644 inst/model_library/sir_age/mappings.csv create mode 100644 inst/model_library/sir_age/model.drawio create mode 100644 inst/model_library/sir_age/normalization.csv create mode 100644 inst/model_library/sir_age/settings.json create mode 100644 inst/model_library/sir_age/variables.csv create mode 100644 inst/model_library/sir_age/vectors.csv create mode 100644 inst/param_examples/epi-symp-vax_variables.csv create mode 100644 inst/starter_models/SI_products/hello_location/derivations.json create mode 100644 inst/starter_models/SI_products/hello_location/flows.csv create mode 100644 inst/starter_models/SI_products/hello_location/settings.json create mode 100644 inst/starter_models/SI_products/hello_location/variables.csv create mode 100644 inst/starter_models/cross_immunity/flows.csv create mode 100644 inst/starter_models/cross_immunity/variables.csv create mode 100644 inst/starter_models/minimal_all_index_cases/derivations.json create mode 100644 inst/starter_models/minimal_all_index_cases/flows.csv create mode 100644 inst/starter_models/minimal_all_index_cases/settings.json create mode 100644 inst/starter_models/minimal_all_index_cases/trans.csv create mode 100644 inst/starter_models/minimal_all_index_cases/variables.csv create mode 100644 inst/starter_models/product_example/Epi_model/params.csv create mode 100644 inst/starter_models/quebec_windsor/derivations.json create mode 100644 inst/starter_models/quebec_windsor/flows.csv create mode 100644 inst/starter_models/quebec_windsor/settings.json create mode 100644 inst/starter_models/quebec_windsor/variables.csv create mode 100644 inst/starter_models/sir/transmission_dimensions.csv create mode 100644 inst/starter_models/sir/transmission_matrices.csv create mode 100644 inst/starter_models/sir_age/derivations.json create mode 100644 inst/starter_models/sir_age/flows.csv create mode 100644 inst/starter_models/sir_age/settings.json create mode 100644 inst/starter_models/sir_age/transmission_dimensions.csv create mode 100644 inst/starter_models/sir_age/transmission_matrices.csv create mode 100644 inst/starter_models/sir_age/variables.csv create mode 100644 inst/starter_models/sir_geog/flows.csv create mode 100644 inst/starter_models/sir_geog/trans.csv create mode 100644 inst/starter_models/sir_geog/variables.csv create mode 100644 inst/starter_models/sir_symp/derivations.json create mode 100644 inst/starter_models/sir_symp/flows.csv create mode 100644 inst/starter_models/sir_symp/settings.json create mode 100644 inst/starter_models/sir_symp/trans.csv create mode 100644 inst/starter_models/sir_symp/variables.csv create mode 100644 inst/starter_models/sir_vax/trans.csv create mode 100644 inst/starter_models/sir_vax/transmission_dimensions.csv create mode 100644 inst/starter_models/sir_vax/transmission_matrices.csv create mode 100644 man/Select.Rd diff --git a/NAMESPACE b/NAMESPACE index d4e3b2bc..51aa45f0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,21 +5,28 @@ S3method(c,StringData) S3method(names,IntVecs) S3method(names,MatsList) S3method(names,MethList) +S3method(print,Core) S3method(print,MathExpression) +S3method(print,Merged) S3method(print,Partition) +S3method(print,Quantities) S3method(print,String) S3method(print,StringData) +S3method(split_by,character) +S3method(split_by,formula) S3method(to_labels,Labels) S3method(to_labels,Partition) S3method(to_labels,Scalar) S3method(to_labels,StringData) S3method(to_labels,Vector) S3method(to_labels,character) +S3method(to_labels,data.frame) S3method(to_name,Names) S3method(to_name,Partition) S3method(to_name,Scalar) S3method(to_name,StringData) S3method(to_name,character) +S3method(to_names,"NULL") S3method(to_names,Names) S3method(to_names,Partition) S3method(to_names,Scalar) @@ -30,6 +37,7 @@ export(CSVReader) export(Collection) export(Compartmental) export(CompartmentalSimulator) +export(Core) export(DerivationExtractor) export(Derivations) export(Derivations2ExprList) @@ -49,6 +57,7 @@ export(Logit) export(MathExpressionFromFunc) export(MathExpressionFromStrings) export(MatsList) +export(Merged) export(MethList) export(Method) export(Model) @@ -60,8 +69,10 @@ export(ObjectiveFunction) export(OptParamsList) export(Partition) export(Products) +export(Quantities) export(Reader) export(Scalar2Vector) +export(Select) export(SimulatorConstructor) export(Simulators) export(StandardExpr) @@ -77,21 +88,34 @@ export(UserExpr) export(all_consistent) export(all_equal) export(all_not_equal) +export(atomic_partition) export(cartesian) export(cartesian_self) +export(core) export(empty_matrix) export(engine_eval) export(finalizer_char) export(finalizer_index) +export(init_merge) +export(initial_column_map) export(initial_valid_vars) +export(join_partitions) +export(labelled_frame) export(labelled_zero_vector) export(make_expr_parser) export(model_starter) +export(mp_cartesian) +export(mp_group) +export(mp_join) +export(mp_select) +export(mp_subset) +export(mp_union) export(nlist) export(not_all_equal) export(parse_expr_list) export(partition) export(rbf) +export(read_frame) export(read_partition) export(reader_spec) export(show_models) diff --git a/R/connection.R b/R/connection.R index f80aa5fe..80c736b4 100644 --- a/R/connection.R +++ b/R/connection.R @@ -196,9 +196,10 @@ Infection = function(infection, variables) { return_object(self, "Infection") } -labelled_frame = function(partition, label_name = "label") { +#' @export +labelled_frame = function(partition, label_by, label_name = "label") { f = partition$frame() - f[[label_name]] = partition$labels() + f[[label_name]] = partition$partial_labels(label_by) f } labelled_frame = memoise(labelled_frame) diff --git a/R/formula_utils.R b/R/formula_utils.R index 0b7e8cb8..5c6ae1ab 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -103,6 +103,12 @@ lhs_char = function(formula) { "" } +rhs_char = function(formula) { + i = 2L + if (is_two_sided(formula)) i = 3L + deparse(formula[[i]], 500) +} + # formula parsing in macpan2 works one side at a time. but sometimes # it is helpful to parse two-sided formulas. this function does so # by parsing each side at a time and rbinding the results. diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 0af744c9..1e673907 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -42,13 +42,18 @@ Partition = function(frame) { self$frame = function() self$.partition$frame() self$dotted = function() self$.partition$dot()$frame() self$names = function() names(self$frame()) - self$name = function() names(self$dotted()) + self$name = function() names(self$dotted()) ## inefficient without memoisation self$labels = function() self$dotted()[[1L]] self$prefix = function(prefix) { f = self$frame() names(f) = sprintf("%s%s", prefix, self$names()) Partition(f) } + self$constant = function(name, value) { + f = self$frame() + f[[name]] = value + Partition(f) + } self$partial_labels = function(...) { new_names = list_to_names(...) self$.partition$change_coordinates(new_names)$dot()$frame()[[1L]] @@ -68,6 +73,15 @@ Partition = function(frame) { } return(p) } + self$blank_on = function(.wrt) { + f = self$frame() + cols = to_names(.wrt) + z = rep(TRUE, nrow(f)) + for (col in cols) { + z = z & (f[[col]] == "") + } + Partition(f[z, , drop = FALSE]) + } self$filter = function(..., .wrt, .comparison_function = all_equal) { self$.filter(... , .wrt = .wrt @@ -108,6 +122,367 @@ Partition = function(frame) { } Partition = memoise(Partition) +ColumnGetter = function(labelled_partition, dimension_name, column_name) { + self = Base() + self$labelled_partition = labelled_partition + self$dimension_name = dimension_name + self$column_name = column_name + self$get = function() { + lp = self$labelled_partition + i = lp$column_map[[self$dimension_name]][[self$column_name]] + lp$frame[[i]] + } + self = return_object(self, "ColumnGetter") + self$get +} + +FrameGetter = function(labelled_partition, dimension_name) { + self = Base() + self$labelled_partition = labelled_partition + self$dimension_name = dimension_name + self$get_frame = function() { + i = unlist( + self$labelled_partition$column_map[[self$dimension_name]], + use.names = FALSE + ) + setNames( + self$labelled_partition$frame[, i, drop = FALSE], + names(self$labelled_partition$column_map[[self$dimension_name]]) + ) + } + self$get_partition = function() self$get_frame() |> Partition() + self$get_labels = function() { + i = self$labelled_partition$labelling_names + f = self$get_frame() + i = i[i %in% names(f)] + f[, i, drop = FALSE] |> as.list() + do.call(paste, c(f, sep = ".")) + } + return_object(self, "FrameGetter") +} + +#' @export +initial_column_map = function(column_names, dimension_name) { + setNames( + list(setNames(as.list(column_names), column_names)), + dimension_name + ) +} + +## take two Merge objects and merge their frames +## and update the prevenance-preserving column maps +merge_util = function(x, y, by.x, by.y) { + + ## ---- + ## resolve non-unique column names in the output, with + ## names of the original tables used to this point + ## ---- + suffixes = c( + paste0(sprintf(":%s", names(x$column_map)), collapse = ""), + paste0(sprintf(":%s", names(y$column_map)), collapse = "") + ) + + ## ---- + ## the merge itself + ## ---- + z = merge( + x$frame, y$frame, + by.x = by.x, by.y = by.y, + suffixes = suffixes + ) + + ## ---- + ## update the column name map to preserve provenance + ## ---- + + # column names in inputs and output + x_cnms = names(x$frame) + y_cnms = names(y$frame) + z_cnms = names(z) + + # numbers of types of output columns + n_common = length(by.x) + n_x_only = length(x_cnms) - n_common + n_y_only = length(y_cnms) - n_common + + # column names in the output categorized + # by contributing input table(s) + z_common = z_cnms[seq_len(n_common)] + z_x_only = z_cnms[n_common + seq_len(n_x_only)] + z_y_only = z_cnms[n_common + n_x_only + seq_len(n_y_only)] + + # column names in the input tables, ordered + # as they are in the output table + x_z_order = c(by.x, x_cnms[!x_cnms %in% by.x]) + y_z_order = c(by.y, y_cnms[!y_cnms %in% by.y]) + + # these can be used to update the column map, + # by looping through the existing map and + # translating to the new column names with these + # x|y_map vectors. of course you will also need + # to concatenate the column maps first + x_map = setNames(c(z_common, z_x_only), x_z_order) + y_map = setNames(c(z_common, z_y_only), y_z_order) + + x_cmap = x$column_map + y_cmap = y$column_map + for (tnm in names(x_cmap)) { + for (cnm in names(x_cmap[[tnm]])) { + old_cnm = x_cmap[[tnm]][[cnm]] + x_cmap[[tnm]][[cnm]] = x_map[[old_cnm]] + } + } + for (tnm in names(y_cmap)) { + for (cnm in names(y_cmap[[tnm]])) { + old_cnm = y_cmap[[tnm]][[cnm]] + y_cmap[[tnm]][[cnm]] = y_map[[old_cnm]] + } + } + + z_column_map = c(x_cmap, y_cmap) + + ## ---- + ## wrap up the result with provenance-preserving column map + ## ---- + Merged( + z, + union(x$labelling_names, y$labelling_names), + z_column_map + ) +} + +#' @export +init_merge = function(frame, dimension_name, labelling_names = names(frame)) { + Merged(frame + , labelling_names + , initial_column_map(names(frame), dimension_name) + ) +} + + +#' @export +core = function(..., labelling_names) { + f = data.frame(...) + if (missing(labelling_names)) labelling_names = names(f) + Core(f, to_names(labelling_names)) +} + +#' @export +Core = function(frame, labelling_names = names(frame)) { + self = Base() + self$labelling_names = to_names(labelling_names) + self$partition = Partition(frame) + self$labels = function() self$partition$select(self$labelling_names)$labels() + return_object(self, "Core") +} + +#' @export +print.Core = function(x, ...) print(x$partition) + +#' @export +mp_cartesian = function(x, y) { + labelling_names = union(x$labelling_names, y$labelling_names) + f = join_partitions(x$partition$frame(), y$partition$frame()) + Core(f, labelling_names = labelling_names) +} + +#' @export +mp_union = function(...) { + l = list(...) + partitions = lapply(l, getElement, "partition") + labelling_names = (l + |> lapply(getElement, "labelling_names") + |> unlist(recursive = FALSE, use.names = FALSE) + |> unique() + ) + Core(do.call(union_vars, partitions)$frame(), labelling_names) +} + +#' @export +mp_subset = function(x, subset_name, ...) { + l = list(...) + p = x$partition + for (cc in names(l)) { + vals = l[[cc]] + is_blank = nchar(vals) == 0L + vals = setdiff(vals, "") + if (any(is_blank)) { + p = union_vars(p$blank_on(cc), p$filter(vals, .wrt = cc)) + } else { + p = p$filter(vals, .wrt = cc) + } + } + init_merge(p$frame(), subset_name, x$labelling_names) +} + +#' @export +mp_join = function(...) { + args = list(...) + is_core = vapply(args, inherits, logical(1L), "Core") + is_merged = vapply(args, inherits, logical(1L), "Merged") + table_list = args[is_core | is_merged] + by_list = args[!is_core & !is_merged] + z = table_list[[1L]] + for (i in 2:length(table_list)) { + args = c( + list(x = z, y = table_list[[i]]), + by_list + ) + z = do.call(merge_generic_by_util, args) + } + z +} + +#' @export +mp_group = function(group, grouping_name, subset) { + map = subset$column_map + map[[grouping_name]] = list(group$labelling_names) |> setNames(group$labelling_names) + Merged(subset$frame, subset$labelling_names, map) +} + +#' @export +mp_select = function(core, group_name, grouping_dimension) { + frame = core$partition$select(grouping_dimension)$frame() + init_merge( + frame, + group_name, + labelling_names = names(frame) + ) +} + + +split_by = function(by, table_origins) UseMethod("split_by") + +#' @export +split_by.character = function(by, table_origins) { + by = to_names(by) + orig = to_names(table_origins) + list(by, by) |> setNames(orig) +} + +#' @export +split_by.formula = function(by, table_origins) { + orig = to_names(table_origins) + list( + to_names(lhs(by)[[2L]]), + to_names(rhs(by)[[2L]]) + ) |> setNames(orig) +} + +## change to Mergable + +#' @export +Merged = function(frame, labelling_names, column_map) { + self = Base() + self$frame = frame + self$labelling_names = to_names(labelling_names) + self$column_map = column_map + self$labels_by_dim = list() + self$labels_frame = function() { + l = list() + for (d in names(self$column_map)) l[[d]] = self$labels_by_dim[[d]]() + l |> as.data.frame() + } + self$frame_by_dim = list() + self$labels_by_dim = list() + self$partition_by_dim = list() + for (d in names(self$column_map)) { + getter = FrameGetter(self, d) + self$frame_by_dim[[d]] = getter$get_frame + self$labels_by_dim[[d]] = getter$get_labels + self$partition_by_dim[[d]] = getter$get_partition + } + self$column_by_dim = list() + for (d in names(self$column_map)) { + self$column_by_dim[[d]] = list() + for (c in names(self$column_map[[d]])) { + self$column_by_dim[[d]][[c]] = ColumnGetter(self, d, c) + } + } + self$filter = function(condition) { + condition = substitute(condition) + i = eval(condition, envir = c(self$column_by_dim, self$frame)) + Merged(self$frame[i,,drop = FALSE] + , labelling_names = self$labelling_names + , column_map = self$column_map + ) + } + self$partition = self$partition_by_dim[[1L]]() ## hack! should probably have a method and then change the partition field in Core to a method as well + return_object(self, "Merged") +} + +apply_col_map = function(map, orig_table_nm, by) { + map[[orig_table_nm]][by] |> unlist(use.names = FALSE) +} + +filter_by_list = function(x_orig, y_orig, by_list) { + l = list() + nms = names(by_list) + for (i in seq_along(by_list)) { + nm_i = nms[[i]] + nms_i = to_names(nm_i) + if ((nms_i[[1L]] %in% x_orig) & (nms_i[[2L]] %in% y_orig)) { + l[[nm_i]] = by_list[[nm_i]] + } + } + l +} + + +merge_generic_by_util = function(x, y, ...) { + by = filter_by_list( + names(x$column_map), + names(y$column_map), + list(...) + ) + by = mapply(split_by + , by + , names(by) + , SIMPLIFY = FALSE + , USE.NAMES = FALSE + ) |> setNames(names(by)) + by.x = character() + by.y = character() + for (nm in names(by)) { + orig = names(by[[nm]]) + by.x = append( + by.x, + apply_col_map(x$column_map, orig[[1L]], by[[nm]][[1L]]) + ) + by.y = append( + by.y, + apply_col_map(y$column_map, orig[[2L]], by[[nm]][[2L]]) + ) + } + merge_util(x, y, by.x, by.y) +} + +#' @export +print.Merged = function(x, ...) print(cbind(x$labels_frame(), x$frame)) + +CompartmentalPartition = function(frame + , special_partitions = c(vec = "Vec", type = "Type") + ) { + special_partitions = c(vec, type) + if (!all(special_partitions %in% names(frame))) { + msg_break( + msg_colon( + msg_csv( + "The vec or type arguments", + special_partitions, + "are not in the names of the frame"), + msg_indent(names(frame)) + ) + ) + } + self = Base() + self$partition = Partition(frame) + self$vec = "Vec" + self$state = function() vec(self, "state", self$vec) + self$flow = function() vec(self, "flow", self$vec) + return_object(self, "CompartmentalPartition") +} + #' Read Partition #' #' Read a CSV file in as a \code{\link{Partition}}. @@ -116,11 +491,35 @@ Partition = memoise(Partition) #' subsequently to \code{\link{file.path}}. #' #' @export -read_partition = function(...) CSVReader(...)$read() |> Partition() +read_partition = function(...) read_frame(...) |> Partition() + +#' @export +read_frame = function(...) CSVReader(...)$read() #' @export partition = function(...) data.frame(...) |> Partition() +#' @export +atomic_partition = function(state_variables, flow_rates, partition_name) { + l = list( + c(state_variables, flow_rates), + c( + rep("state", length(state_variables)), + rep("flow", length(flow_rates)) + ) + ) + (l + |> setNames(c(partition_name, "Vec")) + |> as.data.frame() + |> CompartmentalPartition() + ) +} + +vec = function(x, vec_name, vec_partition = "Vec") { + x$filter(vec_name, .wrt = vec_partition)$select_out(vec_partition) +} + + NullPartition = function(...) { self = Base() self$.names = list_to_names(...) @@ -151,6 +550,10 @@ NullPartition = function(...) { new_name = name_set_op(self$name(), other$name(), union) other$expand(new_name) } + self$new = function(frame) { + if (nrow(frame) == 0L) return(NullPartition(names(frame))) + Partition(frame) + } self = return_object(self, "Partition") return_object(self, "NullPartition") } @@ -193,6 +596,7 @@ enforce_schema = function(frame, ...) { union_vars = function(...) { not_null = function(x) !is.null(x) l = Filter(not_null, list(...)) + #vec_parts = vec_part_names(l) y = l[[1L]] if (length(l) > 1L) { for (i in 2:length(l)) { @@ -202,6 +606,19 @@ union_vars = function(...) { y } +renew_part = function(x, vec_parts) { + if (length(vec_parts) == 1L) x = CompartmentalPartition(x, vec_parts) + x +} + +vec_part_names = function(...) { + (list(...) + |> lapply(getElement, "vec") + |> unlist(recursive = FALSE, use.names = FALSE) + |> unique() + ) +} + NumericPartition = function(frame, numeric_vector) { if (nrow(frame) != length(numeric_vector)) stop("Inconsitent numeric partition.") self = Base() @@ -258,3 +675,56 @@ NumericPartition = function(frame, numeric_vector) { } return_object(self, "NumericPartition") } + +#' @export +join_partitions = function(x, y, by = "") { + by = by_(by) + merge(x, y + , by.x = by$x, by.y = by$y + , suffixes = suffixes_(x, y) + , sort = FALSE + ) +} + +by_ = function(by) UseMethod("by_") +by_.character = function(by) { + if (identical(nchar(by), 0L) | isTRUE(is.na(by))) return(list()) + by = process_by_char(by) + list(x = by, y = by) +} +by_.formula = function(by) { + if (is_one_sided(by)) return(by_(rhs_char(by))) + list( + x = process_by_char(lhs_char(by)), + y = process_by_char(rhs_char(by)) + ) +} +by_.NULL = function(by) list() +suffixes_ = function(x, y) { + s = c(names(x)[ncol(x)], names(y)[ncol(y)]) + sprintf(":%s", s) +} +process_by_char = function(by) { + (by + |> undot_anything() + #|> wrap_colon_terms() + ) +} + + # if (length(bad_names) != 0L) { + # macpan2:::msg_break( + # macpan2:::msg_colon( + # macpan2:::msg( + # "These partition names where asked for (via the include argument)", + # "but were not present in the output" + # ), + # macpan2:::msg_indent(bad_names) + # ), + # macpan2:::msg_colon( + # "Only these were available", + # macpan2:::msg_indent(names(z)) + # ) + # ) |> stop() + # } + #z[, include, drop = FALSE] |> Partition() +#} diff --git a/R/mapping.R b/R/mapping.R new file mode 100644 index 00000000..5990727e --- /dev/null +++ b/R/mapping.R @@ -0,0 +1,10 @@ +Mapping = function(x_name, y_name) { + self = Base() + self.x_name = x_name + self$y_name = y_name + self$map = function(x, y) { + + } + return_object(self, "Mapping") +} + diff --git a/R/model_data_structure.R b/R/model_data_structure.R index 2fc5cbda..8153467e 100644 --- a/R/model_data_structure.R +++ b/R/model_data_structure.R @@ -98,9 +98,9 @@ assert_variables = function(model) { ValidityMessager(make_pipeline("required_partitions", v$names()), "Required partitions in Settings.json are not names of columns in Variables.csv"), #ValidityMessager(make_pipeline("state_variables", v$labels()), "State variables in Settings.json are not dot-separated concatentations of the required partitions in the Variables.csv"), #ValidityMessager(make_pipeline("flow_variables", v$labels()), "Flow variables in Settings.json are not expanded labels for variables in Flows.csv"), - ValidityMessager(make_pipeline("infectious_state_variables", v$labels()), "blah"), - ValidityMessager(make_pipeline("infected_state_variables", v$labels()), "blah"), - ValidityMessager(make_pipeline("infection_flow_variables", v$labels()), "blah"), + ValidityMessager(make_pipeline("infectious_state_variables", v$labels()), "infectious state variables problem"), + ValidityMessager(make_pipeline("infected_state_variables", v$labels()), "infected state variables problem"), + ValidityMessager(make_pipeline("infection_flow_variables", v$labels()), "infection flow problem"), .msg = "the settings.json file is not consistent with the variables.csv file" )$assert(model) } diff --git a/R/msg_utils.R b/R/msg_utils.R index 895dbdb3..2b5c2ad8 100644 --- a/R/msg_utils.R +++ b/R/msg_utils.R @@ -39,6 +39,9 @@ msg <- function(..., .max_char_limit = 70, .sep = "\n") { msg_colon = function(x, y) sprintf("%s:\n%s", x, y) msg_break = function(...) paste(paste(..., sep = "\n"), collapse = "\n") +msg_csv = function(..., .max_char_limit = 68) { + paste +} msg_indent = function(..., .max_char_limit = 66) { (msg(..., .max_char_limit = .max_char_limit, .sep = "\n ") |> trimws(which = "left", whitespace = "[\n]") diff --git a/R/name_utils.R b/R/name_utils.R index 137477d7..ddb5cedd 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -1,6 +1,6 @@ #' To Labels #' -#' Convert objects to labels, which are vectors that can be dotted. +#' Convert objects to labels, which are vectors that might be dotted. #' #' @param x Object to convert to labels. #' @return Character vector that can be used as labels. @@ -14,6 +14,9 @@ to_labels.character = function(x) valid_dotted$assert(x) #' @export to_labels.Partition = function(x) x$labels() +#' @export +to_labels.data.frame = function(x) StringDataFromFrame(x)$dot()$labels()$value() + #' @export to_labels.StringData = function(x) x$dot()$labels()$value() @@ -38,6 +41,9 @@ to_labels.Labels = function(x) x$dot()$value() #' @export to_names = function(x) UseMethod("to_names") +#' @export +to_names.NULL = function(x) character(0L) + #' @export to_names.character = function(x) { if (length(x) == 1L) { @@ -96,6 +102,7 @@ to_name.Scalar = function(x) x$dot()$value() #' @export to_name.Names = function(x) x$dot()$value() + list_to_labels = function(...) unlist(lapply(list(...), to_labels), use.names = FALSE) list_to_names = function(...) unlist(lapply(list(...), to_names), use.names = FALSE) @@ -112,6 +119,7 @@ frame_to_part = function(frame) { y } #frame_to_part = memoise(frame_to_part) +# to_matrix_with_rownames = function(x, nms) { x = as.matrix(x) @@ -137,3 +145,17 @@ labelled_zero_vector = function(labels) { |> dput() ) } + +undot_anything = function(x) { + (x + |> as.character() + |> strsplit("\\.") + |> unlist(use.names = FALSE) + ) +} + +wrap_colon_terms = function(x) { + i = which(grepl(":", x)) + x[i] = sprintf("(%s)", x[i]) + x +} diff --git a/R/names_and_labels.R b/R/names_and_labels.R index 4a7db276..9a3d5fdb 100644 --- a/R/names_and_labels.R +++ b/R/names_and_labels.R @@ -15,7 +15,7 @@ valid_undotted = ValidityMessager( is.character, TestPipeline(Summarizer(valid_undotted_chars, all), TestTrue()) ), - "vector contained strings with dots" + "vector contained invalid strings" ) valid_dotted = ValidityMessager( All( @@ -101,6 +101,15 @@ character_comparison = function(x, y, comparison_function) { z } +character_mat_scal_comparison = function(x, y, comparison_function) { + z = logical(nrow(x)) + for (i in seq_row(x)) { + z[i] = comparison_function(x[i, , drop = TRUE], y) + if (z[i]) break + } + z +} + ## these functions are bottlenecks that ## get repeatedly called for the same inputs. ## so we use memoisation to solve this performance issue @@ -151,12 +160,16 @@ StringDottedScalar = function(...) { StringDottedVector(do.call(c, value_list)) } self$undot = function() { - d = read.table(text = self$value() - , sep = "." - , na.strings = character() - , colClasses = "character" - ) - StringUndottedVector(unlist(d, use.names = FALSE)) + v = self$value() + if (!identical(v, "")) { + d = read.table(text = v + , sep = "." + , na.strings = character() + , colClasses = "character" + ) + v = unlist(d, use.names = FALSE) + } + StringUndottedVector(v) } self$tuple_length = function() { length(self$undot()$value()) @@ -179,13 +192,18 @@ StringDottedVector = function(...) { self = StringDotted(valid_vector$assert(c(...))) self$regenerate = function(value) StringDottedVector(value) self$undot = function() { - d = read.table(text = self$value() - , sep = "." - , na.strings = character() - , colClasses = "character" - ) - m = as.matrix(d) - rownames(m) = colnames(m) = NULL + v = self$value() + if (!identical(v, "")) { + d = read.table(text = v + , sep = "." + , na.strings = character() + , colClasses = "character" + ) + m = as.matrix(d) + rownames(m) = colnames(m) = NULL + } else { + m = matrix(v) + } StringUndottedMatrix(m) } self$which_in = function(other, comparison_function) { diff --git a/R/quantities.R b/R/quantities.R new file mode 100644 index 00000000..2325c138 --- /dev/null +++ b/R/quantities.R @@ -0,0 +1,78 @@ +#' @export +Quantities = function(state_variables, flow_rates, labelling_descriptors) { + self = Base() + self$names = union(state_variables$names(), flow_rates$names()) + self$name = to_name(self$names) + self$state_variables = state_variables$expand(self$name) + self$flow_rates = flow_rates$expand(self$name) + self$labelling_descriptors = labelling_descriptors + self$labelled_subset = function(vector, name, ...) { + + } + self$from = function(..., filter_by) { + ## cases: + if (missing(filter_by) & length(list(...)) == 0L) s = self$state_variables + if (!missing(filter_by) & length(list(...)) == 0L) + if (!missing(filter_by)) s = s$filter(..., .wrt = filter_by) + labelled_frame(s + , label_by = self$labelling_descriptors + , label_name = "from" + ) + } + self$to = function(..., filter_by) { + s = self$state_variables + if (!missing(filter_by)) s = s$filter(..., .wrt = filter_by) + labelled_frame(s + , label_by = self$labelling_descriptors + , label_name = "to" + ) + } + self$infectious = function(..., filter_by) { + + } + self$flow = function(..., filter_by) { + f = self$flow_rates + if (!missing(filter_by)) f = f$filter(..., .wrt = filter_by) + labelled_frame(f + , label_by = self$labelling_descriptors + , label_name = "flow" + ) + } + self$join = join_partitions + self$join3 = function(x, y, z, by1 = "", by2 = "") { + self$join(x, y, by1) |> self$join(z, by2) + } + self$flow_mechanism = function(x, y, z, by1 = "", by2 = "", type = "per_capita") { + z = self$join3(x, y, z, by1, by2)[, c("from", "to", "flow"), drop = FALSE] + z$type = type + z + } + return_object(self, "Quantities") +} + +#' @export +print.Quantities = function(x, ...) { + print(union_vars(x$state_variables, x$flow_rates)) +} + +quantities = function(..., .labelling_descriptors) { + l = list(...) + if (missing(.labelling_descriptors)) .labelling_descriptors = names(l) + data.frame(...) |> Quantities(.labelling_descriptors) +} + +# A = function(x) { +# self = Base() +# self$x = x +# self$f = function(y) self$x * y +# return_object(self, "A") +# } +# B = function(x, y_default) { +# self = A(x) +# formals(self$f)$y = y_default +# return_object(self, "B") +# } +# a = A(100) +# b = B(100, 300) +# a$f(300) +# b$f() diff --git a/R/settings.R b/R/settings.R index ab69a58e..374c007d 100644 --- a/R/settings.R +++ b/R/settings.R @@ -27,19 +27,13 @@ Settings = function(model) { |> stop() ) } + nms[which.min(i)] # take the first synonym found } - self$lab_part = function() { - if (!any(i)) { - stop( - - ) - } - nms[min(which(i))] - } + self$lab_part = function() self$.resolve_synonyms("lab_part") self$name = function() to_name(self$.settings()[[self$lab_part()]]) self$names = function() to_names(self$.settings()[[self$lab_part()]]) self$null = function() self$.settings()$null_partition - self$var_part = function() self$.settings()$var_partitions + self$var_part = function() self$.resolve_synonyms("var_part") self$variable = function(type) { type_nm = sprintf("%s_variables", type) var_nms = self$.settings()[[type_nm]] diff --git a/inst/model_library/.vscode/settings.json b/inst/model_library/.vscode/settings.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/inst/model_library/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/inst/model_library/sir/derivations.json b/inst/model_library/sir/derivations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/inst/model_library/sir/derivations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/inst/model_library/sir/flows.csv b/inst/model_library/sir/flows.csv new file mode 100644 index 00000000..aa5c2e0d --- /dev/null +++ b/inst/model_library/sir/flows.csv @@ -0,0 +1,3 @@ +from ,to ,flow ,type +S ,I ,lambda ,per_capita +I ,R ,gamma ,per_capita diff --git a/inst/model_library/sir/infection.csv b/inst/model_library/sir/infection.csv new file mode 100644 index 00000000..e514ad6d --- /dev/null +++ b/inst/model_library/sir/infection.csv @@ -0,0 +1,2 @@ +flow ,state ,norm ,type ,flow_partition ,state_partition ,norm_partition ,flow_state_partition ,flow_norm_partition ,state_norm_partition +lambda ,I ,N ,state_normalized ,Epi ,Epi ,Epi , , ,Null diff --git a/inst/model_library/sir/settings.json b/inst/model_library/sir/settings.json new file mode 100644 index 00000000..f91b0908 --- /dev/null +++ b/inst/model_library/sir/settings.json @@ -0,0 +1,5 @@ +{ + "labelling_partition" : ["Epi"], + "null_partition" : "Null", + +} diff --git a/inst/model_library/sir/variables.csv b/inst/model_library/sir/variables.csv new file mode 100644 index 00000000..f49c77e1 --- /dev/null +++ b/inst/model_library/sir/variables.csv @@ -0,0 +1,6 @@ +Epi ,Type ,Vec +S ,susceptible ,state +I ,infectious ,state +R ,immune ,state +lambda ,infection ,flow +gamma ,recovery ,flow diff --git a/inst/model_library/sir/vectors.csv b/inst/model_library/sir/vectors.csv new file mode 100644 index 00000000..3138d4d6 --- /dev/null +++ b/inst/model_library/sir/vectors.csv @@ -0,0 +1,8 @@ +vector ,subset ,dimension ,partition +state , ,state ,Vec +state ,infectious_state ,infectious ,Infect +flow , ,flow ,Vec +flow ,infection_flows ,infection ,Infect +trans , ,infectious ,Infect +trans , ,infected ,Infect +N , ,norm ,Vec diff --git a/inst/model_library/sir_1d_space/variables.csv b/inst/model_library/sir_1d_space/variables.csv new file mode 100644 index 00000000..e69de29b diff --git a/inst/model_library/sir_age/README.md b/inst/model_library/sir_age/README.md new file mode 100644 index 00000000..237bd318 --- /dev/null +++ b/inst/model_library/sir_age/README.md @@ -0,0 +1,20 @@ +# Model Specification Format Challenges + +The `variables.csv` file contains two types variables, state and flow, as well as others that do not fit as nicely. For example, what do we do about transmission variables that need to map states to flows? + +* So we are tempted to place these items into another kind of file, say `mappings.csv`. But now when resolving `flows.csv` we need to look in both `variables.csv` and `mappings.csv`. +* An alternative is to have lots of blank cells in `variables.csv`. But this how do we name the mappings without lots of dots in the basic state and flow variables? +* An alternative is to break the rule that all labels have the same numbers of dots, but this breaks the long-existing labelling standard. +* An alternative is to add mapping variables to `variables.csv` as vectors (or matrices?), but this causes problems for lining up the elements of these matrices through mechanisms like `flows.csv`. + +All this to say that from the user's perspective what we want is really the first suggestion, and bite the bullet as a developer for resolving `flows.csv` in multiple locations. + +Or actually, maybe the better thing is to have smarter labelling standards that check to see if labels can be truncated without destroying invertibility? + +Well ... maybe we can save the labelling standards by letting the mapping component names get split into the atomic labels first to produce valid labels that can be inverted. + +Well ... this doesn't need to happen, because in the mappings file we might be able to keep them split up. + +``` +flow[infection] ~ groupSums(beta * state[trans_infectious], trans_infection, length(infection)) +``` diff --git a/inst/model_library/sir_age/derivations.json b/inst/model_library/sir_age/derivations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/inst/model_library/sir_age/derivations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/inst/model_library/sir_age/flows.csv b/inst/model_library/sir_age/flows.csv new file mode 100644 index 00000000..36b8dacd --- /dev/null +++ b/inst/model_library/sir_age/flows.csv @@ -0,0 +1,4 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S ,I ,lambda ,per_capita ,Epi ,Epi ,Epi , , ,Null +I ,R ,gamma ,per_capita ,Epi ,Epi ,Epi , , ,Null +young ,old ,aging ,per_capita ,Age ,Age ,Age , , ,Null diff --git a/inst/model_library/sir_age/indices.csv b/inst/model_library/sir_age/indices.csv new file mode 100644 index 00000000..e69de29b diff --git a/inst/model_library/sir_age/infection.csv b/inst/model_library/sir_age/infection.csv new file mode 100644 index 00000000..8a1f46fb --- /dev/null +++ b/inst/model_library/sir_age/infection.csv @@ -0,0 +1,2 @@ +flow ,state ,flow_partition ,state_partition ,flow_state_partition +lambda ,I ,Epi ,Epi , diff --git a/inst/model_library/sir_age/mappings.csv b/inst/model_library/sir_age/mappings.csv new file mode 100644 index 00000000..ee60b6ab --- /dev/null +++ b/inst/model_library/sir_age/mappings.csv @@ -0,0 +1,9 @@ +mapping ,state ,flow +trans ,I.young ,lambda.young +trans ,I.old ,lambda.young +trans ,I.young ,lambda.old +trans ,I.old ,lambda.old +contact ,I.young ,lambda.young +contact ,I.old ,lambda.young +contact ,I.young ,lambda.old +contact ,I.old ,lambda.old diff --git a/inst/model_library/sir_age/model.drawio b/inst/model_library/sir_age/model.drawio new file mode 100644 index 00000000..bd589800 --- /dev/null +++ b/inst/model_library/sir_age/model.drawio @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/inst/model_library/sir_age/normalization.csv b/inst/model_library/sir_age/normalization.csv new file mode 100644 index 00000000..e69de29b diff --git a/inst/model_library/sir_age/settings.json b/inst/model_library/sir_age/settings.json new file mode 100644 index 00000000..610e3708 --- /dev/null +++ b/inst/model_library/sir_age/settings.json @@ -0,0 +1,7 @@ +{ + "labelling_partition" : ["Epi", "Age"], + "null_partition" : "Null", + "vec_partition" : "Vec", + "state" : ["state"], + "flow" : ["flow_rates"] +} diff --git a/inst/model_library/sir_age/variables.csv b/inst/model_library/sir_age/variables.csv new file mode 100644 index 00000000..8d29fcea --- /dev/null +++ b/inst/model_library/sir_age/variables.csv @@ -0,0 +1,12 @@ +Epi ,Age ,Type ,Vec +S ,young ,susceptible ,state +I ,young ,infectious ,state +R ,young ,immune ,state +S ,old ,susceptible ,state +I ,old ,infectious ,state +R ,old ,immune ,state +lambda ,young ,infection ,flow_rates +gamma ,young ,recovery ,flow_rates +lambda ,old ,infection ,flow_rates +gamma ,old ,recovery ,flow_rates + ,aging ,aging ,flow_rates diff --git a/inst/model_library/sir_age/vectors.csv b/inst/model_library/sir_age/vectors.csv new file mode 100644 index 00000000..0c6e5606 --- /dev/null +++ b/inst/model_library/sir_age/vectors.csv @@ -0,0 +1,7 @@ +vector ,subset ,components ,partition +state , ,state ,Vec +state ,infectious ,infectious ,Infect +flow , ,flow ,Vec +flow ,infection ,infection ,Infect +trans , ,infectious ,Infect +norm , ,norm ,Vec diff --git a/inst/param_examples/epi-symp-vax_variables.csv b/inst/param_examples/epi-symp-vax_variables.csv new file mode 100644 index 00000000..a44bdb96 --- /dev/null +++ b/inst/param_examples/epi-symp-vax_variables.csv @@ -0,0 +1,7 @@ +epi,symp,vax +S,, +E,, +I,mild, +I,severe, +R,, +,, diff --git a/inst/starter_models/SI_products/hello_location/derivations.json b/inst/starter_models/SI_products/hello_location/derivations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/inst/starter_models/SI_products/hello_location/derivations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/inst/starter_models/SI_products/hello_location/flows.csv b/inst/starter_models/SI_products/hello_location/flows.csv new file mode 100644 index 00000000..23eb76f5 --- /dev/null +++ b/inst/starter_models/SI_products/hello_location/flows.csv @@ -0,0 +1,3 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +toronto ,hamilton ,west ,per_capita ,Location ,Location ,Location , , ,Null +hamilton ,toronto ,east ,per_capita ,Location ,Location ,Location , , ,Null diff --git a/inst/starter_models/SI_products/hello_location/settings.json b/inst/starter_models/SI_products/hello_location/settings.json new file mode 100644 index 00000000..1f6d17f0 --- /dev/null +++ b/inst/starter_models/SI_products/hello_location/settings.json @@ -0,0 +1,6 @@ +{ + "required_partitions" : ["Location"], + "null_partition" : ["Null"], + "state_variables" : ["toronto", "hamilton"], + "flow_variables" : ["west", "east"] +} diff --git a/inst/starter_models/SI_products/hello_location/variables.csv b/inst/starter_models/SI_products/hello_location/variables.csv new file mode 100644 index 00000000..15479292 --- /dev/null +++ b/inst/starter_models/SI_products/hello_location/variables.csv @@ -0,0 +1,5 @@ +Location +hamilton +toronto +east +west diff --git a/inst/starter_models/cross_immunity/flows.csv b/inst/starter_models/cross_immunity/flows.csv new file mode 100644 index 00000000..51681f56 --- /dev/null +++ b/inst/starter_models/cross_immunity/flows.csv @@ -0,0 +1,2 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S.a ,E.a ,infection.a ,per_capita ,Epi.Strain ,Epi.Strain ,Epi.Strain , , ,Null diff --git a/inst/starter_models/cross_immunity/variables.csv b/inst/starter_models/cross_immunity/variables.csv new file mode 100644 index 00000000..dbc3c914 --- /dev/null +++ b/inst/starter_models/cross_immunity/variables.csv @@ -0,0 +1,8 @@ +Epi, Strain +S, a +E, a +I, a +R, a +E, b +I, b +R, b diff --git a/inst/starter_models/minimal_all_index_cases/derivations.json b/inst/starter_models/minimal_all_index_cases/derivations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/inst/starter_models/minimal_all_index_cases/derivations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/inst/starter_models/minimal_all_index_cases/flows.csv b/inst/starter_models/minimal_all_index_cases/flows.csv new file mode 100644 index 00000000..4683ba62 --- /dev/null +++ b/inst/starter_models/minimal_all_index_cases/flows.csv @@ -0,0 +1,9 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S.unvax ,I ,infection.unvax ,per_capita ,Epi.Vax ,Epi ,Epi.Vax , ,Symp ,Null +S.vax ,I.mild ,infection.vax ,per_capita ,Epi.Vax ,Epi.Symp ,Epi.Vax , , ,Null +S.unvax ,S.vax ,vaccination ,per_capita ,Epi.Vax ,Epi.Vax ,Epi , , ,Null +S.vax ,S.unvax ,waning ,per_capita ,Epi.Vax ,Epi.Vax ,Epi , , ,Null +I ,R ,recovery ,per_capita ,Epi ,Epi ,Epi , , ,Null +alive ,S ,birth ,per_capita_inflow ,Vital ,Epi ,Epi , , ,Null +alive , ,death ,per_capita_outflow ,Vital ,Null ,Epi , , ,Null + ,S ,importation ,absolute_inflow ,Null ,Epi ,Epi , , ,Null \ No newline at end of file diff --git a/inst/starter_models/minimal_all_index_cases/settings.json b/inst/starter_models/minimal_all_index_cases/settings.json new file mode 100644 index 00000000..9d68cfec --- /dev/null +++ b/inst/starter_models/minimal_all_index_cases/settings.json @@ -0,0 +1,8 @@ +{ + "required_partitions" : ["Epi", "Symp", "Vax"], + "null_partition" : "Null", + "var_partitions" : ["Var"], + "state_variables" : ["state"], + "flow_variables" : ["flow"], + "pop_variables" : ["pop"] +} diff --git a/inst/starter_models/minimal_all_index_cases/trans.csv b/inst/starter_models/minimal_all_index_cases/trans.csv new file mode 100644 index 00000000..27400d8e --- /dev/null +++ b/inst/starter_models/minimal_all_index_cases/trans.csv @@ -0,0 +1,2 @@ +state ,flow ,pop ,type ,state_partition ,flow_partition ,pop_partition ,state_flow_partition ,state_pop_partition ,flow_pop_partition +I ,infection ,N ,normalize ,Epi ,Epi ,Epi , , ,Null diff --git a/inst/starter_models/minimal_all_index_cases/variables.csv b/inst/starter_models/minimal_all_index_cases/variables.csv new file mode 100644 index 00000000..b9090d98 --- /dev/null +++ b/inst/starter_models/minimal_all_index_cases/variables.csv @@ -0,0 +1,17 @@ +Epi ,Symp ,Vax ,Vital ,Var +S , ,unvax ,alive ,state +S , ,vax ,alive ,state +I ,mild , ,alive ,state +I ,severe , ,alive ,state +R , , ,alive ,state +infection ,mild ,unvax , ,flow +infection ,severe ,unvax , ,flow +infection ,mild ,vax , ,flow +vaccination , , , ,flow +waning , , , ,flow +recovery ,mild , , ,flow +recovery ,severe , , ,flow +birth , , , ,flow +death , , , ,flow +importation , , , ,flow +N , , , ,pop diff --git a/inst/starter_models/product_example/Epi_model/params.csv b/inst/starter_models/product_example/Epi_model/params.csv new file mode 100644 index 00000000..e69de29b diff --git a/inst/starter_models/quebec_windsor/derivations.json b/inst/starter_models/quebec_windsor/derivations.json new file mode 100644 index 00000000..a04d6a9f --- /dev/null +++ b/inst/starter_models/quebec_windsor/derivations.json @@ -0,0 +1,11 @@ +[ + { + "filter_partition" : "GeogType", + "filter_names" : ["connection"], + "output_partition" : "GeogEntity.GeogType", + "output_names" : ["travel_matrix.collection"], + "input_partition" : "From.To", + "dots" : [], + "expression" : "" + } +] diff --git a/inst/starter_models/quebec_windsor/flows.csv b/inst/starter_models/quebec_windsor/flows.csv new file mode 100644 index 00000000..2f2cc88e --- /dev/null +++ b/inst/starter_models/quebec_windsor/flows.csv @@ -0,0 +1,2 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +city ,city ,connection ,flow ,GeogType ,GeogType ,GeogType , ,From ,To diff --git a/inst/starter_models/quebec_windsor/settings.json b/inst/starter_models/quebec_windsor/settings.json new file mode 100644 index 00000000..f957cc20 --- /dev/null +++ b/inst/starter_models/quebec_windsor/settings.json @@ -0,0 +1,8 @@ +{ + "required_partitions" : ["GeogEntity"], + "null_partition" : "Null", + "state_partition_name" : "GeogType", + "state_variable_labels" : ["city"], + "flow_partition_name" : "GeogType", + "flow_variable_labels" : ["connection"] +} diff --git a/inst/starter_models/quebec_windsor/variables.csv b/inst/starter_models/quebec_windsor/variables.csv new file mode 100644 index 00000000..2d821a6c --- /dev/null +++ b/inst/starter_models/quebec_windsor/variables.csv @@ -0,0 +1,45 @@ +GeogEntity ,GeogType ,From ,To +quebec ,city ,quebec ,quebec +trois_rivieres ,city ,trois_rivieres ,trois_rivieres +drummondville ,city ,drummondville ,drummondville +montreal ,city ,montreal ,montreal +cornwall ,city ,cornwall ,cornwall +brockville ,city ,brockville ,brockville +kingston ,city ,kingston ,kingston +belleville ,city ,belleville ,belleville +oshawa ,city ,oshawa ,oshawa +toronto ,city ,toronto ,toronto +mississauga ,city ,mississauga ,mississauga +hamilton ,city ,hamilton ,hamilton +brantford ,city ,brantford ,brantford +london ,city ,london ,london +windsor ,city ,windsor ,windsor +c01 ,connection ,quebec ,trois_rivieres +c02 ,connection ,trois_rivieres ,quebec +c03 ,connection ,trois_rivieres ,drummondville +c04 ,connection ,drummondville ,trois_rivieres +c05 ,connection ,drummondville ,montreal +c06 ,connection ,montreal ,drummondville +c07 ,connection ,montreal ,cornwall +c08 ,connection ,cornwall ,montreal +c09 ,connection ,cornwall ,brockville +c10 ,connection ,brockville ,cornwall +c11 ,connection ,brockville ,kingston +c12 ,connection ,kingston ,brockville +c13 ,connection ,kingston ,belleville +c14 ,connection ,belleville ,kingston +c15 ,connection ,belleville ,oshawa +c16 ,connection ,oshawa ,belleville +c17 ,connection ,oshawa ,toronto +c18 ,connection ,toronto ,oshawa +c19 ,connection ,toronto ,mississauga +c20 ,connection ,mississauga ,toronto +c21 ,connection ,mississauga ,hamilton +c22 ,connection ,hamilton ,mississauga +c23 ,connection ,hamilton ,brantford +c24 ,connection ,brantford ,hamilton +c25 ,connection ,brantford ,london +c26 ,connection ,london ,brantford +c27 ,connection ,london ,windsor +c28 ,connection ,windsor ,london +travel_matrix ,collection , , diff --git a/inst/starter_models/sir/settings.json b/inst/starter_models/sir/settings.json index 1c7e1e77..94152e70 100644 --- a/inst/starter_models/sir/settings.json +++ b/inst/starter_models/sir/settings.json @@ -1,8 +1,9 @@ { "required_partitions" : ["Epi"], "null_partition" : "Null", - "state_variables" : ["S", "I", "R"], - "flow_variables" : ["foi", "gamma"], + "vector_partition" : ["Vec"], + "state" : ["state"], + "flow_rates" : ["flow_rates"], "infection_flow_variables" : ["foi"], "infectious_state_variables" : ["I"] } diff --git a/inst/starter_models/sir/transmission_dimensions.csv b/inst/starter_models/sir/transmission_dimensions.csv new file mode 100644 index 00000000..66afd4cd --- /dev/null +++ b/inst/starter_models/sir/transmission_dimensions.csv @@ -0,0 +1 @@ +matrix ,matrix_partition ,infection_partition ,infectious_partition diff --git a/inst/starter_models/sir/transmission_matrices.csv b/inst/starter_models/sir/transmission_matrices.csv new file mode 100644 index 00000000..bb320c55 --- /dev/null +++ b/inst/starter_models/sir/transmission_matrices.csv @@ -0,0 +1,2 @@ +matrix ,infection_flow ,infectious_state +transmission ,infection ,I diff --git a/inst/starter_models/sir/variables.csv b/inst/starter_models/sir/variables.csv index 3895a36a..16fa0419 100644 --- a/inst/starter_models/sir/variables.csv +++ b/inst/starter_models/sir/variables.csv @@ -1,9 +1,9 @@ -Epi -S -I -R -N -beta -foi -gamma -per_capita_transmission +Epi ,Vec +S ,state +I ,state +R ,state +N , +beta , +foi ,flow_rates +gamma ,flow_rates +per_capita_transmission , diff --git a/inst/starter_models/sir_age/derivations.json b/inst/starter_models/sir_age/derivations.json new file mode 100644 index 00000000..c58eb1a2 --- /dev/null +++ b/inst/starter_models/sir_age/derivations.json @@ -0,0 +1,5 @@ +[ + { + + } +] diff --git a/inst/starter_models/sir_age/flows.csv b/inst/starter_models/sir_age/flows.csv new file mode 100644 index 00000000..c938a62e --- /dev/null +++ b/inst/starter_models/sir_age/flows.csv @@ -0,0 +1,3 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S ,I ,infection ,per_capita ,Epi ,Epi ,Epi , , ,Null +I ,R ,recovery ,per_capita ,Epi ,Epi ,Epi ,Age ,Age ,Null diff --git a/inst/starter_models/sir_age/settings.json b/inst/starter_models/sir_age/settings.json new file mode 100644 index 00000000..134307b3 --- /dev/null +++ b/inst/starter_models/sir_age/settings.json @@ -0,0 +1,8 @@ +{ + "required_partitions" : ["Epi", "Age"], + "null_partition" : "Null", + "state_variables" : ["S.young", "I.young", "R.young", "S.old", "I.old", "R.old"], + "flow_variables" : ["infection.young", "infection.old", "gamma.young", "gamma.old", ".old_rate"], + "infection_flow_variables" : ["infection.young", "infection.old"], + "infectious_state_variables" : ["I.young", "I.old"] +} diff --git a/inst/starter_models/sir_age/transmission_dimensions.csv b/inst/starter_models/sir_age/transmission_dimensions.csv new file mode 100644 index 00000000..66afd4cd --- /dev/null +++ b/inst/starter_models/sir_age/transmission_dimensions.csv @@ -0,0 +1 @@ +matrix ,matrix_partition ,infection_partition ,infectious_partition diff --git a/inst/starter_models/sir_age/transmission_matrices.csv b/inst/starter_models/sir_age/transmission_matrices.csv new file mode 100644 index 00000000..bb320c55 --- /dev/null +++ b/inst/starter_models/sir_age/transmission_matrices.csv @@ -0,0 +1,2 @@ +matrix ,infection_flow ,infectious_state +transmission ,infection ,I diff --git a/inst/starter_models/sir_age/variables.csv b/inst/starter_models/sir_age/variables.csv new file mode 100644 index 00000000..f397f9fe --- /dev/null +++ b/inst/starter_models/sir_age/variables.csv @@ -0,0 +1,15 @@ +Epi ,Age +S ,young +I ,young +R ,young +infection ,young +recovery ,young +S ,old +I ,old +R ,old +infection ,old +recovery ,old +N , +contact , +sigma , +transmissivity , diff --git a/inst/starter_models/sir_geog/flows.csv b/inst/starter_models/sir_geog/flows.csv new file mode 100644 index 00000000..384c3464 --- /dev/null +++ b/inst/starter_models/sir_geog/flows.csv @@ -0,0 +1,5 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S ,I ,infection ,per-capita ,Epi ,Epi ,Epi ,Geog ,Geog ,Null +I ,R ,recovery ,per-capita ,Epi ,Epi ,Epi ,Geog ,Geog ,Null +tor ,mon ,move ,per-capita ,Geog ,Geog ,Geog ,Epi ,Epi ,Null +mon ,tor ,move ,per-capita ,Geog ,Geog ,Geog ,Epi ,Epi ,Null diff --git a/inst/starter_models/sir_geog/trans.csv b/inst/starter_models/sir_geog/trans.csv new file mode 100644 index 00000000..10b35cf1 --- /dev/null +++ b/inst/starter_models/sir_geog/trans.csv @@ -0,0 +1,5 @@ +state ,flow ,pop ,type ,state_partition ,flow_partition ,pop_partition ,state_flow_partition ,state_pop_partition ,flow_pop_partition +I ,infection ,N ,mat-norm-state ,Epi ,Epi ,Epi , , ,Null +S ,move , , ,Geog +I ,move , , ,Geog +R ,move , , ,Geog \ No newline at end of file diff --git a/inst/starter_models/sir_geog/variables.csv b/inst/starter_models/sir_geog/variables.csv new file mode 100644 index 00000000..6bb8872d --- /dev/null +++ b/inst/starter_models/sir_geog/variables.csv @@ -0,0 +1,29 @@ +Epi ,Geog +S ,tor +I ,tor +R ,tor +infection ,tor +recovery ,tor +S ,mon +I ,mon +R ,mon +infection ,mon +recovery ,mon +S ,ham +I ,ham +R ,ham +infection ,ham +recovery ,ham +S ,cal +I ,cal +R ,cal +infection ,cal +recovery ,cal +S ,van +I ,van +R ,van +infection ,van +recovery ,van +S ,move +I ,move +R ,move diff --git a/inst/starter_models/sir_symp/derivations.json b/inst/starter_models/sir_symp/derivations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/inst/starter_models/sir_symp/derivations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/inst/starter_models/sir_symp/flows.csv b/inst/starter_models/sir_symp/flows.csv new file mode 100644 index 00000000..3cf70c21 --- /dev/null +++ b/inst/starter_models/sir_symp/flows.csv @@ -0,0 +1,3 @@ +from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition +S ,I ,infection ,per_capita ,Epi ,Epi ,Epi , ,Null ,Symp +I ,R ,recovery ,per_capita ,Epi ,Epi ,Epi , ,Symp ,Null diff --git a/inst/starter_models/sir_symp/settings.json b/inst/starter_models/sir_symp/settings.json new file mode 100644 index 00000000..fb0ce18d --- /dev/null +++ b/inst/starter_models/sir_symp/settings.json @@ -0,0 +1,6 @@ +{ + "required_partitions" : ["Epi", "Symp"], + "null_partition" : ["Null"], + "state_variables" : ["S.", "I.mild", "I.severe", "R."], + "flow_variables" : ["infection.mild", "infection.severe", "recovery.mild", "recovery.severe"] +} diff --git a/inst/starter_models/sir_symp/trans.csv b/inst/starter_models/sir_symp/trans.csv new file mode 100644 index 00000000..ad431c70 --- /dev/null +++ b/inst/starter_models/sir_symp/trans.csv @@ -0,0 +1,3 @@ +state ,flow ,pop ,type ,state_partition ,flow_partition ,pop_partition ,state_flow_partition ,state_pop_partition ,flow_pop_partition +I ,infection ,N ,norm-state ,Epi ,Epi ,Epi , , ,Null + , , ,vec-norm-state \ No newline at end of file diff --git a/inst/starter_models/sir_symp/variables.csv b/inst/starter_models/sir_symp/variables.csv new file mode 100644 index 00000000..784f6221 --- /dev/null +++ b/inst/starter_models/sir_symp/variables.csv @@ -0,0 +1,10 @@ +Epi ,Symp +S , +I ,mild +I ,severe +R , +infection ,mild +infection ,severe +recovery ,mild +recovery ,severe +N , diff --git a/inst/starter_models/sir_vax/trans.csv b/inst/starter_models/sir_vax/trans.csv new file mode 100644 index 00000000..c72ae6c6 --- /dev/null +++ b/inst/starter_models/sir_vax/trans.csv @@ -0,0 +1,2 @@ +state ,flow ,trans ,type ,state_partition ,flow_partition ,trans_partition ,state_flow_partition ,state_trans_partition ,flow_trans_partition +I ,infection ,beta ,normalize ,Epi ,Epi ,Epi , , ,Null diff --git a/inst/starter_models/sir_vax/transmission_dimensions.csv b/inst/starter_models/sir_vax/transmission_dimensions.csv new file mode 100644 index 00000000..66afd4cd --- /dev/null +++ b/inst/starter_models/sir_vax/transmission_dimensions.csv @@ -0,0 +1 @@ +matrix ,matrix_partition ,infection_partition ,infectious_partition diff --git a/inst/starter_models/sir_vax/transmission_matrices.csv b/inst/starter_models/sir_vax/transmission_matrices.csv new file mode 100644 index 00000000..bb320c55 --- /dev/null +++ b/inst/starter_models/sir_vax/transmission_matrices.csv @@ -0,0 +1,2 @@ +matrix ,infection_flow ,infectious_state +transmission ,infection ,I diff --git a/man/Select.Rd b/man/Select.Rd new file mode 100644 index 00000000..46d02a0d --- /dev/null +++ b/man/Select.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/labelled_partitions.R +\name{Select} +\alias{Select} +\title{mp_select = function(core, dimension_name, labelling_names) { +Select(core, dimension_name, labelling_names) +}} +\usage{ +Select(subset, dimension_name, labelling_names) +} +\description{ +mp_select = function(core, dimension_name, labelling_names) { +Select(core, dimension_name, labelling_names) +} +} diff --git a/man/to_labels.Rd b/man/to_labels.Rd index b7ca32fa..db70d69a 100644 --- a/man/to_labels.Rd +++ b/man/to_labels.Rd @@ -13,5 +13,5 @@ to_labels(x) Character vector that can be used as labels. } \description{ -Convert objects to labels, which are vectors that can be dotted. +Convert objects to labels, which are vectors that might be dotted. } diff --git a/misc/diagrams/drawio-templates/test.drawio b/misc/diagrams/drawio-templates/test.drawio index fc2fee41..2ecc4643 100644 --- a/misc/diagrams/drawio-templates/test.drawio +++ b/misc/diagrams/drawio-templates/test.drawio @@ -1,29 +1,170 @@ - - - + + - - + + + - - + + + - - - - - - + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - \ No newline at end of file diff --git a/misc/diagrams/toy_model.drawio b/misc/diagrams/toy_model.drawio index 4484c4cc..414cc0d0 100644 --- a/misc/diagrams/toy_model.drawio +++ b/misc/diagrams/toy_model.drawio @@ -4,6 +4,9 @@ + + + diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 25748a93..a3940858 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -3,8 +3,380 @@ library(macpan2helpers) library(oor) library(ggplot2) library(dplyr) +# m = Compartmental(file.path("inst", "starter_models", "sir")) +# m$simulators$tmb(100 +# , state = c(S = 99, I = 1) +# ) + + +flow_file = CSVReader("inst/model_library/sir_age/flows.csv")$read() +vars_file = CSVReader("inst/model_library/sir_age/variables.csv")$read() +settings_file = JSONReader("inst/model_library/sir_age/settings.json")$read() + +make_core = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { + vec_labs = to_labels(vars_table[settings_list[[vec_part_field]]]) + core( + vars_table[vec_labs == vec_name, , drop = FALSE], + labelling_names = to_name(settings_list[[lab_part_field]]) + ) +} +state = make_core("state", vars_file, settings_file, "vec_partition", "labelling_partition") +flow_rates = make_core("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") + +state$labels() +flow_rates$labels() +tag = function(key, val) list(val) |> setNames(key) +flow_file +i = 1L +flow = flow_file[i,,drop=FALSE] +mp_join( + mp_subset(state, "from", flow$from_partition, + mp_subset(state, "to", Epi = flow$to), + mp_subset(flow_rates, "flow", Epi = flow$flow), + from.to = "Epi" +) + +sir_state = core(Epi = c("S", "I", "R")) +sir_flow_rates = core(Epi = c("lambda", "gamma")) +loc_state = core( + Loc = c("ham", "tor", "mon"), + Move = "" +) +loc_flow_rates = core( + Loc = c("ham", "tor", "tor", "mon"), + Move = c("tor", "ham", "mon", "tor") +) + + +state = mp_cartesian(sir_state, loc_state) +flow_rates = mp_union(mp_cartesian(sir_flow_rates, loc_state), loc_flow_rates) + +N = mp_select(state, "N", "Loc") +active = mp_subset(state, "active", Epi = c("S", "I")) +infectious = mp_subset(state, "infectious", Epi = "I") +mp_join(N, infectious, N.infectious = "Loc") + +mp_join(xx, yy, N.norm_inf = "Loc")$labels_frame() +mp_join(N, mp_subset(state, "infectious", Epi = "I")) + + +match(N$labels_frame()$active, state$labels()) - 1L +match(N$labels_frame()$N, unique(N$labels_frame()$N)) - 1L +groupSums(state[active], strata, length(active)) + +xx = mp_join( + mp_subset(state, "state"), + mp_select(state, "N", "Loc"), + state.N = "Loc" +) +xx$labels_by_dim$state() +xx$labels_by_dim$N() + +mp_init_vec = function(components, ...) { + values = unlist(list(...)) + labs = components$labels() + vec = setNames(numeric(length(labs)), labs) + vec[names(values)] = values + vec +} +mp_init_vec(state, S.tor = 99) +mp_join( + mp_subset(state, "infectious", Epi = "I"), + mp_subset(flow_rates, "infection", Epi = "lambda"), + infectious.infection = "Loc" +)$labels_frame() + +mp_union( + mp_join( + mp_subset(state, "from") + , mp_subset(state, "to") + , mp_subset(flow_rates, "flow", Epi = "") + , from.to = "Epi" + , from.flow = "Loc" + , to.flow = "Loc" ~ "Move" + ), + mp_join( + mp_subset(state, "from", Epi = "S") + , mp_subset(state, "to", Epi = "I") + , mp_subset(flow_rates, "flow", Epi = "lambda") + , from.to = "Loc" + , from.flow = "Loc" + ) +) + +match(ff$from, state$labels()) - 1L +match(ff$to, state$labels()) - 1L +match(ff$flow, flow_rates$labels()) - 1L + +merge_generic_by_util( + from, + to, + from.to = "Epi" +) |> merge_generic_by_util( + flow, + from.flow = "Loc", + to.flow = "Loc" ~ "Move" +) + +#mp_join = function(table_list, by_list) { + z = table_list[[1L]] + for (i in 2:length(table_list)) { + args = c( + list(x = z, y = table_list[[i]]), + by_list + ) + z = do.call(merge_generic_by_util, args) + } + z +} + +is_orig_col_nm = function(by) { + nn = (by + |> strsplit(":") + |> vapply(length, integer(1L)) + ) + nn == 1L +} + +z = merge_util( + from, + to, + by.x = "Epi", + by.y = "Epi" +) + +fix_column_provenence = function(nms, table_nms) { + split_nms = nms |> strsplit(":") + orig_nms = split_nms |> vapply(getElement, character(1L), 1L) + inv_map = lapply(split_nms, setdiff, orig_nms) + for (nm in unique(orig_nms)) { + v = unique(unlist(inv_map[orig_nms == nm], use.names = FALSE)) + } +} + +column_map_from_names = function(nms) { + split_nms = nms |> strsplit(":") + orig_nms = split_nms |> vapply(getElement, character(1L), 1L) + inv_map = lapply(split_nms, setdiff, orig_nms) + table_nms = inv_map |> unlist(use.names = FALSE) |> unique() + l = list() + for (nm in table_nms) { + l[[nm]] = list() + for (i in seq_along(inv_map)) { + if (nm %in% inv_map[[i]]) l[[nm]][[orig_nms[[i]]]] = nms[[i]] + } + } + l +} +split_nms |> vapply(function(x) "from" %in% x, logical(1L)) +split_nms |> vapply(function(x) "to" %in% x, logical(1L)) +names(z) |> strsplit(":") |> lapply(setdiff, original_colnames) + +hh = merge_util( + from, + to, + by.x = "Epi", + by.y = "Epi" +) + +ii = merge_util( + hh, + flow, + by.x = c("Loc:from", "Loc:to"), + by.y = c("Loc", "Move") +) + +ii$column_map + + +prod_state = cartesian( + sir_state$partition_by_dim$component() + , loc_state$partition_by_dim$component() +) +prod_flow_rates = union_vars( + cartesian(sir$flow_rates, loc$state_variables), + loc_flow_rates +) + + + +state_components = sir_state$partition_by_dim$component()$filter("S", .wrt = "Epi") +state_components$filter("S", .wrt = "Epi") +state_components$.filter +filter_lp = function(partition, x) { + eval(macpan2:::rhs(x)[[2L]], envir = partition$column_by_dim) +} +filter_lp(loc_flow_rates, ~ flow_rates$Move() == "tor") + + +x = prod$join3( + prod$from(), + prod$to(), + prod$flow(), + "Loc", + Loc:to ~ Move:flow +) +x = x[setdiff(names(x), c("from", "to"))] + +l = list( + from = list( + Loc = "Loc", + Epi = "Epi:from" + ), + to = list( + Loc = "Loc", + Epi = "Epi:to" + ) +) + +tt = LabelledPartition(x, l, "Epi.Loc") +tt$labels() +tt$labels_by_dim$from() +tt$labels_by_dim$from() +tt$frame_by_dim$to() +tt$column_by_dim$from$Epi() + + +sir = Quantities(sir_state, sir_flow_rates, "Epi") +loc = Quantities(loc_state, loc_flow_rates, "Loc.Move") + + +method_parser = macpan2:::method_parser +method_parser(Move[flow]~Loc) + +loc$join(loc$from(), loc$from()) |> loc$join(loc$from()) |> loc$join(loc$from()) +loc$join3(loc$flow(), loc$from(), loc$to(), Loc~Loc, Move[flow]~Loc) + +sir$join3( + sir$from("S", filter_by = "Epi"), + sir$to("I", filter_by = "Epi"), + sir$flow("lambda", filter_by = "Epi") +) + + + + +prod = Quantities( + cartesian(sir$state_variables, loc$state_variables), + union_vars( + cartesian(sir$flow_rates, loc$state_variables), + loc_flow_rates + ), + labelling_descriptors = "Epi.Loc.Move" +) + +## learned lots about joining and a bit about filtering: +## +## 1. inner joins are commutative so we can just use from-to-flow order +## (maybe later we can optimize) +## 2. need to be able to map across partitions when matching (for example: +## the Move column in a flow rate to the Loc column in a to variable) +## 3. we can have partition matching for all 3 tables (the Loc model is +## a good example from #2 above). this all just means that the first +## criterion (e.g. from_to) is used in the first join and both of the +## two remaining criteria (e.g. from_flow, to_flow) are used in the +## second join. +## 4. multiple ways to represent partition matching: +## a) in flows.csv we have a formula for each from_to, from_flow, to_flow. +## these formulas are things like Epi.Loc ~ Epi.Move. +## b) in the ... arguments of a 'mechanism' function we need things like +## Loc[to] ~ Move[flow], Epi[from] ~ Epi[to], ... +## c) in the by arguments of a non-user-facing wrapper for merge, we need +## things like Loc:from.Loc:to ~ Loc.Move. this version is not +## readible but connects with merge well. +## d) however, maybe we do not need (c) and instead just need to supply +## things like by.x = c("Loc:from", "Loc:to"), by.y = c("Loc", "Move"). +## yes i think this is better +## so we will need to have a generic representation of partition matching +## that can accept and convert among all of these forms +## 5. when filtering there are a few cases: +## a) no-op : just return all of the, either, state variables or flow rates +## b) ... but no filter_by : +## b) only filter_by : +## probably need to think more about filtering cases. + +list( + prod$from() + , prod$to() + , prod$flow() + , from_to = Epi ~ Epi + , from_flow = Loc ~ Loc + , to_flow = Loc ~ Move +) +from_to = macpan2:::join_partitions(prod$from(), prod$to(), ~Epi) +macpan2:::join_partitions(from_to, prod$flow(), Loc:from.Loc:to ~ Loc.Move)[c("from", "to", "flow")] + + + +prod$flow_mechanism( + prod$from("S", filter_by = "Epi"), + prod$to("I", filter_by = "Epi"), + prod$flow("lambda", filter_by = "Epi"), + Loc ~ Loc, + Loc ~ Loc +) + + + +prod$join( + x = prod$state_variables$frame(), + y = prod$flow_rates$frame(), + by = "Loc", + main = "y" +) + + +join_partitions( + from("S", dimensions = prod_state, filter_by = "Epi", label_by = "Epi.Loc"), + to("I", dimensions = prod_state, filter_by = "Epi", label_by = "Epi.Loc"), + by = "Loc" +) + + +x = labelled_frame(prod_state$filter("S", .wrt = "Epi"), "from") +y = labelled_frame(prod_state$filter("I", .wrt = "Epi"), "to") +z = merge(x, y, by = "Loc") +setdiff(intersect(names(x), names(y)), "Loc") + + +macpan2:::labelled_frame(sir_state, "from") + +merge( + macpan2:::labelled_frame(sir_state$filter("I"), "from"), + macpan2:::labelled_frame(sir_flow_rates$filter("lambda"), "to"), + by = character(0L) +)[,c("from", "to"),drop = FALSE] + +macpan2:::Connection( + data.frame( + from = "S", to = "I", flow = "lambda", type = "per_capita", + from_filter = "Epi", to_filter = "Epi", flow_filter = "Epi", + from_to_join = "", from_flow_join = "", to_flow_join = "Null" + ), + union_vars(sir_state, sir_flow_rates), + conn_ref = "flow" +) + + +map_partitions = function(x, y, x_name, y_name) + +cartesian(sir_state$filter("I"), sir_flow$filter("lambda")) +join_partitions( + sir_state$filter("I"), + sir_flow_rates$filter("lambda"), + by = NULL, + x_suffix = "State", + y_suffix = "Flow", + include = "EpiState.EpiFlow" +) + + +sir_loc_state = cartesian(sir_state, loc_state) +sir_loc_flow = union_vars( + cartesian(sir_state, loc_flow), + cartesian(sir_flow, loc_state) +) -debug(macpan2:::get_indices) dir = file.path("inst", "model_library", "sir_age") From 21cd36ab4424e4e30f6686af3f958b07553d0a5c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 25 Oct 2023 09:50:40 -0400 Subject: [PATCH 038/332] remove unused c++ library --- NAMESPACE | 1 - R/enum_methods.R | 10 ---------- man/Select.Rd | 15 --------------- misc/dev/dev.cpp | 1 - src/macpan2.cpp | 1 - 5 files changed, 28 deletions(-) delete mode 100644 man/Select.Rd diff --git a/NAMESPACE b/NAMESPACE index 51aa45f0..033892f9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -72,7 +72,6 @@ export(Products) export(Quantities) export(Reader) export(Scalar2Vector) -export(Select) export(SimulatorConstructor) export(Simulators) export(StandardExpr) diff --git a/R/enum_methods.R b/R/enum_methods.R index a237d572..0cbd3497 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -116,16 +116,6 @@ MethodTypeUtils = function() { return(method) } } - msg_break( - msg_colon( - msg("The following engine method formula"), - msg_indent(deparse(formula)) - ), - msg_colon( - msg("is inconsistent with all of the available prototypes"), - msg_break(self$all_prototype_formulas()) - ) - ) stop( "\nThe following engine method formula ...\n\n", deparse(formula), "\n\n" , "... is inconsistent with all of the available prototypes:\n\n" diff --git a/man/Select.Rd b/man/Select.Rd deleted file mode 100644 index 46d02a0d..00000000 --- a/man/Select.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/labelled_partitions.R -\name{Select} -\alias{Select} -\title{mp_select = function(core, dimension_name, labelling_names) { -Select(core, dimension_name, labelling_names) -}} -\usage{ -Select(subset, dimension_name, labelling_names) -} -\description{ -mp_select = function(core, dimension_name, labelling_names) { -Select(core, dimension_name, labelling_names) -} -} diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 9cbc965a..b3e3e725 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -12,7 +12,6 @@ // https://github.com/kaskr/adcomp/wiki/Development#distributing-code #include #include -#include #include diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 205982e6..3a0f88ff 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -13,7 +13,6 @@ // https://github.com/kaskr/adcomp/wiki/Development#distributing-code #include #include -#include #include From 7407787586b8635b3958ece294cd164beb6aac92 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 25 Oct 2023 10:15:27 -0400 Subject: [PATCH 039/332] changing branches --- misc/experiments/refactorcpp.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index a3940858..73f19a1d 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -23,6 +23,8 @@ make_core = function(vec_name, vars_table, settings_list, vec_part_field, lab_pa state = make_core("state", vars_file, settings_file, "vec_partition", "labelling_partition") flow_rates = make_core("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") + + state$labels() flow_rates$labels() tag = function(key, val) list(val) |> setNames(key) From 2eaae43163c26ff9a0ffba6dcef050fd51a3a7cd Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 25 Oct 2023 10:51:20 -0400 Subject: [PATCH 040/332] bit of organization --- R/labelled_partitions.R | 18 ------------------ R/model_shape.R | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 R/model_shape.R diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 1e673907..4aa3fb33 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -260,24 +260,6 @@ init_merge = function(frame, dimension_name, labelling_names = names(frame)) { } -#' @export -core = function(..., labelling_names) { - f = data.frame(...) - if (missing(labelling_names)) labelling_names = names(f) - Core(f, to_names(labelling_names)) -} - -#' @export -Core = function(frame, labelling_names = names(frame)) { - self = Base() - self$labelling_names = to_names(labelling_names) - self$partition = Partition(frame) - self$labels = function() self$partition$select(self$labelling_names)$labels() - return_object(self, "Core") -} - -#' @export -print.Core = function(x, ...) print(x$partition) #' @export mp_cartesian = function(x, y) { diff --git a/R/model_shape.R b/R/model_shape.R new file mode 100644 index 00000000..07767a7d --- /dev/null +++ b/R/model_shape.R @@ -0,0 +1,18 @@ +#' @export +core = function(..., labelling_names) { + f = data.frame(...) + if (missing(labelling_names)) labelling_names = names(f) + Core(f, to_names(labelling_names)) +} + +#' @export +Core = function(frame, labelling_names = names(frame)) { + self = Base() + self$labelling_names = to_names(labelling_names) + self$partition = Partition(frame) + self$labels = function() self$partition$select(self$labelling_names)$labels() + return_object(self, "Core") +} + +#' @export +print.Core = function(x, ...) print(x$partition) From 325c4358dff58f77e4f898b1cd7f3ab56637c97e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 25 Oct 2023 10:52:56 -0400 Subject: [PATCH 041/332] use windows when testing --- .github/workflows/R-CMD-check.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 054279b7..bf5284cf 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -2,7 +2,7 @@ # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help on: push: - branches: [main, master] + branches: [main, master, refactorcpp] pull_request: branches: [main, master] @@ -18,9 +18,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + # - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} # - {os: macos-latest, r: 'release'} - # - {os: windows-latest, r: 'release'} + - {os: windows-latest, r: 'release'} # - {os: ubuntu-latest, r: 'release'} # - {os: ubuntu-latest, r: 'oldrel-1'} From 951cffce0574332e9816cb46168b9c4298ce6514 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 25 Oct 2023 10:59:53 -0400 Subject: [PATCH 042/332] sp err --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index b54c9b8f..13cf6c2e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,7 +43,7 @@ LinkingTo: VignetteBuilder: knitr Remotes: - canmod/oor@valitidy + canmod/oor@validity Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true From bdd139b02a5805b61b034c4c9ee363baa789da36 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sat, 28 Oct 2023 04:22:20 -0400 Subject: [PATCH 043/332] more model products --- DESCRIPTION | 2 +- Makefile | 2 +- NAMESPACE | 34 +++- R/labelled_partitions.R | 298 ++++++++++++++++++++++++++++---- R/model_shape.R | 31 +++- R/msg_utils.R | 14 +- R/name_utils.R | 12 +- R/vector.R | 49 ++++++ man/to_names.Rd | 2 +- misc/experiments/refactorcpp.R | 213 +++++++++++++++++++---- vignettes/model_definitions.Rmd | 14 +- 11 files changed, 572 insertions(+), 99 deletions(-) create mode 100644 R/vector.R diff --git a/DESCRIPTION b/DESCRIPTION index 13cf6c2e..5703f1fa 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,7 +43,7 @@ LinkingTo: VignetteBuilder: knitr Remotes: - canmod/oor@validity + canmod/oor Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true diff --git a/Makefile b/Makefile index e4d84897..686a47ef 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ doc-update: R/*.R misc/dev/dev.cpp pkg-build:: ../macpan2_$(VERSION).tar.gz ../macpan2_$(VERSION).tar.gz: DESCRIPTION man/*.Rd R/*.R src/*.cpp tests/testthat/test-*.R tests/testthat.R inst/starter_models/**/*.csv inst/starter_models/**/*.json - R CMD build . + R CMD build --no-build-vignettes . pkg-check: ../macpan2_$(VERSION).tar.gz diff --git a/NAMESPACE b/NAMESPACE index 033892f9..3809b06d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,19 +1,34 @@ # Generated by roxygen2: do not edit by hand +S3method(Basis,Basis) +S3method(Basis,Partition) +S3method(Basis,data.frame) S3method(c,String) S3method(c,StringData) +S3method(head,Merged) +S3method(mp_labels,Basis) +S3method(mp_labels,Merged) +S3method(names,Basis) S3method(names,IntVecs) S3method(names,MatsList) +S3method(names,Merged) S3method(names,MethList) -S3method(print,Core) +S3method(names,Partition) +S3method(print,Basis) S3method(print,MathExpression) S3method(print,Merged) S3method(print,Partition) S3method(print,Quantities) S3method(print,String) S3method(print,StringData) +S3method(print,Vector) +S3method(print,summary.Merged) S3method(split_by,character) S3method(split_by,formula) +S3method(str,Merged) +S3method(summary,Merged) +S3method(tail,Merged) +S3method(to_labels,Basis) S3method(to_labels,Labels) S3method(to_labels,Partition) S3method(to_labels,Scalar) @@ -21,23 +36,25 @@ S3method(to_labels,StringData) S3method(to_labels,Vector) S3method(to_labels,character) S3method(to_labels,data.frame) +S3method(to_name,Basis) S3method(to_name,Names) S3method(to_name,Partition) S3method(to_name,Scalar) S3method(to_name,StringData) S3method(to_name,character) S3method(to_names,"NULL") +S3method(to_names,Basis) S3method(to_names,Names) S3method(to_names,Partition) S3method(to_names,Scalar) S3method(to_names,StringData) S3method(to_names,character) +export(Basis) export(BinaryOperator) export(CSVReader) export(Collection) export(Compartmental) export(CompartmentalSimulator) -export(Core) export(DerivationExtractor) export(Derivations) export(Derivations2ExprList) @@ -84,13 +101,14 @@ export(TXTReader) export(Time) export(Transform) export(UserExpr) +export(Vector) export(all_consistent) export(all_equal) export(all_not_equal) export(atomic_partition) +export(basis) export(cartesian) export(cartesian_self) -export(core) export(empty_matrix) export(engine_eval) export(finalizer_char) @@ -104,10 +122,20 @@ export(labelled_zero_vector) export(make_expr_parser) export(model_starter) export(mp_cartesian) +export(mp_filter) +export(mp_filter_out) export(mp_group) +export(mp_indicator) +export(mp_indices) export(mp_join) +export(mp_labels) +export(mp_linear) +export(mp_map_to_selection) export(mp_select) +export(mp_square) export(mp_subset) +export(mp_symmetric) +export(mp_triangle) export(mp_union) export(nlist) export(not_all_equal) diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 4aa3fb33..2b7c0d32 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -122,6 +122,9 @@ Partition = function(frame) { } Partition = memoise(Partition) +#' @export +names.Partition = function(x) x$names() + ColumnGetter = function(labelled_partition, dimension_name, column_name) { self = Base() self$labelled_partition = labelled_partition @@ -152,7 +155,7 @@ FrameGetter = function(labelled_partition, dimension_name) { } self$get_partition = function() self$get_frame() |> Partition() self$get_labels = function() { - i = self$labelled_partition$labelling_names + i = self$labelled_partition$labelling_names() f = self$get_frame() i = i[i %in% names(f)] f[, i, drop = FALSE] |> as.list() @@ -170,7 +173,7 @@ initial_column_map = function(column_names, dimension_name) { } ## take two Merge objects and merge their frames -## and update the prevenance-preserving column maps +## and update the provenance-preserving column maps merge_util = function(x, y, by.x, by.y) { ## ---- @@ -246,26 +249,123 @@ merge_util = function(x, y, by.x, by.y) { ## ---- Merged( z, - union(x$labelling_names, y$labelling_names), z_column_map ) } #' @export -init_merge = function(frame, dimension_name, labelling_names = names(frame)) { +init_merge = function(frame, dimension_name) { Merged(frame - , labelling_names , initial_column_map(names(frame), dimension_name) ) } +mp = function(mp_func) { + f = ("mp_%s" + |> sprintf(mp_func) + |> getFromNamespace("macpan2") + ) + prototype = function(...) {l = list(...)} + target_e = body(f)[[2L]] + proto_e = body(prototype)[[2L]] + if (!identical(target_e, proto_e)) stop("developer error: invalid mp function") + body(f)[[2L]][[3L]] = (~unlist(list(...), recursive = FALSE))[[2L]] + f +} +#' @export +mp_map_to_selection = function(x, filter_cond, select_cond) { + filter = mp("filter")(x, "filter", filter_cond, select_cond) + replacement = mp_select(filter, "select", names(select_cond)) + mp_join(filter, replacement, filter.select = names(select_cond)) +} #' @export mp_cartesian = function(x, y) { labelling_names = union(x$labelling_names, y$labelling_names) f = join_partitions(x$partition$frame(), y$partition$frame()) - Core(f, labelling_names = labelling_names) + Basis(f, labelling_names = labelling_names) +} + +#' @export +mp_square = function(x, y_labelling_names) { + nms = to_names(y_labelling_names) + y = (x$partition$frame() + |> setNames(nms) + |> Basis(nms) + ) + mp_cartesian(x, y) +} + +#' @export +mp_triangle = function(x, y_labelling_names, exclude_diag = TRUE, lower_tri = FALSE) { + f = x$partition$frame() + g = setNames(f, y_labelling_names) + n = nrow(f) + if (exclude_diag) { + k = 2:n + i = sequence(k - 1) + j = rep(k, k - 1) + } else if (!exclude_diag) { + k = seq_len(n) + i = sequence(k) + j = rep(k, k) + } + if (lower_tri) { + ii = i + i = j + j = ii + } + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Basis(f, names(f)) +} + +#' @export +mp_symmetric = function(x, y_labelling_names, exclude_diag = TRUE) { + f = x$partition$frame() + g = setNames(f, y_labelling_names) + n = nrow(f) + k = seq_len(n) + + if (exclude_diag) { + i = rep( k , times = n - 1L) + j = rep(rev(k), each = n - 1L) + } else { + i = rep(k, times = n) + j = rep(k, each = n) + } + + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Basis(f, names(f)) +} + +#' @export +mp_linear = function(x, y_labelling_names) { + f = x$partition$frame() + g = setNames(f, y_labelling_names) + n = nrow(f) + + k = c(1L, rep(2L, n - 2L), 1L) + i = rep(seq_len(n), k) + j = sequence(k, c(2, seq_len(n - 2L), n - 1L), by = 2) + + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Basis(f, names(f)) +} + +#' @export +mp_subset = function(x, ...) { + partition = mp_filter(x, "pick", ...)$partition + Basis(partition, x$labelling_names) } #' @export @@ -277,11 +377,11 @@ mp_union = function(...) { |> unlist(recursive = FALSE, use.names = FALSE) |> unique() ) - Core(do.call(union_vars, partitions)$frame(), labelling_names) + Basis(do.call(union_vars, partitions)$frame(), labelling_names) } #' @export -mp_subset = function(x, subset_name, ...) { +mp_filter = function(x, subset_name, ...) { l = list(...) p = x$partition for (cc in names(l)) { @@ -294,16 +394,27 @@ mp_subset = function(x, subset_name, ...) { p = p$filter(vals, .wrt = cc) } } - init_merge(p$frame(), subset_name, x$labelling_names) + init_merge(p$frame(), subset_name) +} + +#' @export +mp_filter_out = function(x, subset_name, ...) { + l = list(...) + p = x$partition + for (cc in names(l)) { + vals = l[[cc]] + p = p$filter_out(vals, .wrt = cc) + } + init_merge(p$frame(), subset_name) } #' @export mp_join = function(...) { args = list(...) - is_core = vapply(args, inherits, logical(1L), "Core") + is_basis = vapply(args, inherits, logical(1L), "Basis") is_merged = vapply(args, inherits, logical(1L), "Merged") - table_list = args[is_core | is_merged] - by_list = args[!is_core & !is_merged] + table_list = args[is_basis | is_merged] + by_list = args[!is_basis & !is_merged] z = table_list[[1L]] for (i in 2:length(table_list)) { args = c( @@ -316,22 +427,47 @@ mp_join = function(...) { } #' @export -mp_group = function(group, grouping_name, subset) { - map = subset$column_map - map[[grouping_name]] = list(group$labelling_names) |> setNames(group$labelling_names) - Merged(subset$frame, subset$labelling_names, map) +mp_group = function(group, group_nm, group_labelling_names, ...) { + filter = list(...) + } #' @export -mp_select = function(core, group_name, grouping_dimension) { - frame = core$partition$select(grouping_dimension)$frame() - init_merge( - frame, - group_name, - labelling_names = names(frame) - ) +mp_select = function(basis, grouping_dimension) { + frame = basis$partition$select(to_names(grouping_dimension))$frame() + nms = names(frame)[names(frame) %in% basis$labelling_names] + frame |> Basis(labelling_names = nms) +} + +#' @export +mp_indicator = function(x, ...) { + l = list(...) + for (nm in names(l)) { + l[[nm]] = x$partition$partial_labels(nm) %in% l[[nm]] + } + Reduce(`&`, l) +} + +#' @export +mp_indices = function(x, table) { + match(x, table) - 1L +} + +#' @export +mp_labels = function(x, labelling_names) { + UseMethod("mp_labels") } +#' @export +mp_labels.Basis = function(x, labelling_names) { + if (missing(labelling_names)) return(x$labels()) + x$partial_labels(labelling_names) +} + +#' @export +mp_labels.Merged = function(x, labelling_names) { + x$labels_for[[labelling_names]]() +} split_by = function(by, table_origins) UseMethod("split_by") @@ -354,24 +490,30 @@ split_by.formula = function(by, table_origins) { ## change to Mergable #' @export -Merged = function(frame, labelling_names, column_map) { +Merged = function(frame, column_map) { self = Base() self$frame = frame - self$labelling_names = to_names(labelling_names) self$column_map = column_map - self$labels_by_dim = list() + self$labelling_names = function() { + l = self$partition_list() + for (nm in names(l)) { + l[[nm]] = names(l[[nm]]) + } + unlist(l, use.names = FALSE) |> unique() + } + self$labels_for = list() self$labels_frame = function() { l = list() - for (d in names(self$column_map)) l[[d]] = self$labels_by_dim[[d]]() + for (d in names(self$column_map)) l[[d]] = self$labels_for[[d]]() l |> as.data.frame() } self$frame_by_dim = list() - self$labels_by_dim = list() + self$labels_for = list() self$partition_by_dim = list() for (d in names(self$column_map)) { getter = FrameGetter(self, d) self$frame_by_dim[[d]] = getter$get_frame - self$labels_by_dim[[d]] = getter$get_labels + self$labels_for[[d]] = getter$get_labels self$partition_by_dim[[d]] = getter$get_partition } self$column_by_dim = list() @@ -381,15 +523,22 @@ Merged = function(frame, labelling_names, column_map) { self$column_by_dim[[d]][[c]] = ColumnGetter(self, d, c) } } + self$partition_list = function() { + l = list() + for (dn in names(self$partition_by_dim)) { + l[[dn]] = self$partition_by_dim[[dn]]() + } + l + } + self$frame_list = function() method_apply(self$partition_list(), "frame") self$filter = function(condition) { condition = substitute(condition) i = eval(condition, envir = c(self$column_by_dim, self$frame)) Merged(self$frame[i,,drop = FALSE] - , labelling_names = self$labelling_names , column_map = self$column_map ) } - self$partition = self$partition_by_dim[[1L]]() ## hack! should probably have a method and then change the partition field in Core to a method as well + self$partition = self$partition_by_dim[[1L]]() ## hack! should probably have a method and then change the partition field in Basis to a method as well return_object(self, "Merged") } @@ -436,11 +585,92 @@ merge_generic_by_util = function(x, y, ...) { apply_col_map(y$column_map, orig[[2L]], by[[nm]][[2L]]) ) } - merge_util(x, y, by.x, by.y) + by = data.frame(x = by.x, y = by.y) |> unique() + merge_util(x, y, by$x, by$y) +} + +#' @export +summary.Merged = function(object, ...) { + formats = c("name", "combined") + structure( + sapply(formats, merged_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), + class = "summary.Merged" + ) +} + +#' @export +print.summary.Merged = function(x, ...) { + msg_hline() |> message() + msg( + "Merged object from macpan2 describing", + "an aspect of model shape" + ) |> message() + msg_hline() |> message() + print(x$name) + print(x$combined, row.names = FALSE) +} + +#' @export +names.Merged = function(x) to_names(x$labelling_names()) + +merged_format_picker = function(x + , format = c("labels", "merged", "combined", "separate", "name", "names") + ) { + switch (match.arg(format) + , labels = x$labels_frame() + , merged = x$frame + , combined = cbind(x$labels_frame(), x$frame) + , separate = x$frame_list() + , name = to_name(x$labelling_names()) + , names = to_names(x$labelling_names()) + ) +} + +#' @export +print.Merged = function(x + , format = c("labels", "merged", "combined", "separate", "name", "names") + , ... + ) { + x = merged_format_picker(x, format) + print(x, row.names = FALSE, ...) +} + +#' @export +head.Merged = function(x + , n = 6L + , format = c("labels", "merged", "combined", "separate", "name", "names") + , ... + ) { + x = merged_format_picker(x, format) + if (format == "separate") { + return(lapply(x, head, n = n, ...)) + } else { + return(head(x, n = n, ...)) + } +} + +#' @export +tail.Merged = function(x + , n = 6L + , format = c("labels", "merged", "combined", "separate", "name", "names") + , ... + ) { + x = merged_format_picker(x, format) + if (format == "separate") { + return(lapply(x, tail, n = n, ...)) + } else { + return(tail(x, n = n, ...)) + } } #' @export -print.Merged = function(x, ...) print(cbind(x$labels_frame(), x$frame)) +str.Merged = function(x + , format = c("labels", "merged", "combined", "separate", "name", "names") + , ... +) { + x = merged_format_picker(x, format) + str(x, ...) +} CompartmentalPartition = function(frame , special_partitions = c(vec = "Vec", type = "Type") @@ -541,7 +771,7 @@ NullPartition = function(...) { } #' @export -print.Partition = function(x, ...) print(x$frame()) +print.Partition = function(x, ...) print(x$frame(), row.names = FALSE) empty_frame = function(...) { colnames = unlist( diff --git a/R/model_shape.R b/R/model_shape.R index 07767a7d..19c4227d 100644 --- a/R/model_shape.R +++ b/R/model_shape.R @@ -1,18 +1,37 @@ #' @export -core = function(..., labelling_names) { +basis = function(..., labelling_names) { f = data.frame(...) if (missing(labelling_names)) labelling_names = names(f) - Core(f, to_names(labelling_names)) + Basis(f, to_names(labelling_names)) } #' @export -Core = function(frame, labelling_names = names(frame)) { +Basis = function(partition, labelling_names = names(frame)) { + UseMethod("Basis") +} + +#' @export +Basis.Partition = function(partition, labelling_names = names(frame)) { self = Base() + self$partition = partition self$labelling_names = to_names(labelling_names) - self$partition = Partition(frame) self$labels = function() self$partition$select(self$labelling_names)$labels() - return_object(self, "Core") + self$partial_labels = function(...) self$partition$partial_labels(...) + return_object(self, "Basis") +} + +#' @export +Basis.data.frame = function(partition, labelling_names = names(frame)) { + partition |> Partition() |> Basis(labelling_names) } #' @export -print.Core = function(x, ...) print(x$partition) +Basis.Basis = function(partition, labelling_names = names(frame)) { + partition$partition |> Basis(labelling_names) +} + +#' @export +print.Basis = function(x, ...) print(x$partition) + +#' @export +names.Basis = function(x) x$partition$names() diff --git a/R/msg_utils.R b/R/msg_utils.R index 2b5c2ad8..028de6b0 100644 --- a/R/msg_utils.R +++ b/R/msg_utils.R @@ -1,4 +1,4 @@ -msg <- function(..., .max_char_limit = 70, .sep = "\n") { +msg <- function(..., .sep = "", .max_char_limit = getOption("width")) { input_string = (list(...) |> lapply(as.character) |> unlist(recursive = TRUE, use.names = FALSE) @@ -37,12 +37,15 @@ msg <- function(..., .max_char_limit = 70, .sep = "\n") { ) } +msg_hline = function(.max_char_limit = getOption("width")) { + ("-" + |> rep(.max_char_limit) + |> paste0(collapse = "") + ) +} msg_colon = function(x, y) sprintf("%s:\n%s", x, y) msg_break = function(...) paste(paste(..., sep = "\n"), collapse = "\n") -msg_csv = function(..., .max_char_limit = 68) { - paste -} -msg_indent = function(..., .max_char_limit = 66) { +msg_indent = function(..., .max_char_limit = getOption("width") - 4L) { (msg(..., .max_char_limit = .max_char_limit, .sep = "\n ") |> trimws(which = "left", whitespace = "[\n]") ) @@ -56,6 +59,7 @@ msg_indent_break = function(...) { } msg_paste = function(...) paste(..., sep = "") msg_space = function(...) paste(..., sep = " ") +msg_csv = function(...) paste(..., sep = ", ") if (FALSE) { diff --git a/R/name_utils.R b/R/name_utils.R index ddb5cedd..f4053f4f 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -14,6 +14,9 @@ to_labels.character = function(x) valid_dotted$assert(x) #' @export to_labels.Partition = function(x) x$labels() +#' @export +to_labels.Basis = function(x) x$labels() + #' @export to_labels.data.frame = function(x) StringDataFromFrame(x)$dot()$labels()$value() @@ -33,7 +36,7 @@ to_labels.Labels = function(x) x$dot()$value() #' #' Convert objects to names, which are character vectors with the following #' restrictions: (1) they cannot have dots, (2) all values must start with -#' a letter, (3) all characters must be letters, numbers, or underscores. +#' a letter, (3) all characters must be letters, numbers, or underscore. #' #' @param x Object to convert to names. #' @return Character vector that can be used as names. @@ -59,6 +62,9 @@ to_names.character = function(x) { #' @export to_names.Partition = function(x) x$names() +#' @export +to_names.Basis = function(x) x$partition$names() + #' @export to_names.StringData = function(x) x$undot()$names()$value() @@ -68,6 +74,7 @@ to_names.Scalar = function(x) x$undot()$value() #' @export to_names.Names = function(x) x$undot()$value() + #' To Name #' #' Convert objects to a name, which is a scalar string that can be dotted. @@ -93,6 +100,9 @@ to_name.character = function(x) { #' @export to_name.Partition = function(x) x$name() +#' @export +to_name.Basis = function(x) x$partition$name() + #' @export to_name.StringData = function(x) x$dot()$names()$value() diff --git a/R/vector.R b/R/vector.R new file mode 100644 index 00000000..2a1d00b6 --- /dev/null +++ b/R/vector.R @@ -0,0 +1,49 @@ +#' @export +Vector = function(labeller) { + self = Base() + self$labeller = labeller + self$.numbers = zero_vector(self$labeller$labels()) + self$numbers = function(...) { + l = list(...) + if (length(l) == 0L) return(self$.numbers) + i = mp_subset(self$labeller, ...)$labels() + self$.numbers[i] + } + self$set_numbers = function(...) { + ## generate list with: filter_cond, select_cond, select_nm + l = process_grouping_dots(...) + + args = c(list(self$labeller), l$filter_cond, l$select_cond) + filter = do.call(mp_subset, args) + replacement_values = setNames( + value[filter$partition$partial_labels(l$select_nm)], + filter$labels() + ) + + self$.numbers[names(replacement_values)] = replacement_values + self + } + self$frame = function() { + data.frame( + labels = self$labeller$labels(), + values = unname(self$numbers()) + ) + } + return_object(self, "Vector") +} + +process_grouping_dots = function(...) { + l = list(...) + replacement = Filter(is.numeric, l) + stopifnot(length(replacement) == 1L) + filter_cond = Filter(is.character, l) + value = replacement[[1L]] + select_cond = lapply(replacement, names) + select_nm = names(select_cond) + nlist(filter_cond, select_cond, select_nm) +} + +#' @export +print.Vector = function(x, ...) print(x$numbers()) + +zero_vector = function(labels) setNames(rep(0, length(labels)), labels) diff --git a/man/to_names.Rd b/man/to_names.Rd index 062106d7..52963841 100644 --- a/man/to_names.Rd +++ b/man/to_names.Rd @@ -15,5 +15,5 @@ Character vector that can be used as names. \description{ Convert objects to names, which are character vectors with the following restrictions: (1) they cannot have dots, (2) all values must start with -a letter, (3) all characters must be letters, numbers, or underscores. +a letter, (3) all characters must be letters, numbers, or underscore. } diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 73f19a1d..58e03f81 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -8,20 +8,173 @@ library(dplyr) # , state = c(S = 99, I = 1) # ) +## factor model bases --------------- + +state_sir = basis( + Epi = c("S", "I", "R", "D"), + Vital = c("alive", "alive", "alive", "dead"), + labelling_names = "Epi" +) +flow_rates_sir = basis(Epi = c("lambda", "gamma", "mu")) +trans_rates_sir = basis(Epi = "beta") +cities = basis(Loc = c("cal", "ham", "que")) +movement = mp_linear(cities, "Move") +age = basis(Age = c("young", "old")) +contact = mp_square(age, "Contact") + +## product model bases ------------- + +state = (state_sir + |> mp_cartesian(cities) + |> mp_cartesian(age) +) +flow_rates = mp_union( + (flow_rates_sir + |> mp_subset(Epi = "lambda") + |> mp_cartesian(age) + |> mp_cartesian(cities) + ), + (flow_rates_sir + |> mp_subset(Epi = "mu") + |> mp_cartesian(age) + ), + mp_subset(flow_rates_sir, Epi = "gamma"), + movement +) +trans_rates = (trans_rates_sir + |> mp_subset(Epi = "beta") + |> mp_cartesian(cities) + |> mp_cartesian(contact) +) + +strata = mp_select(state, "Loc.Age") +alive = mp_subset(state, Vital = "alive") +mp_indices(alive$partial_labels("Loc.Age"), strata$labels()) +mp_labels(alive, "Loc.Age") +mp_labels(strata) +merge(alive$partition$frame(), strata$partition$frame(), c("Loc", "Age")) + + +movement_flows = mp_join( + mp_filter(state, "from", Vital = "alive"), + mp_filter(state, "to", Vital = "alive"), + mp_filter(flow_rates, "rate", Epi = ""), + from.to = "Epi.Age", + from.rate = "Loc", + to.rate = "Loc" ~ "Move" +) +infection_flows = mp_join( + mp_filter(state, "from", Epi = "S"), + mp_filter(state, "to", Epi = "I"), + mp_filter(flow_rates, "rate", Epi = "lambda"), + from.to = "Loc.Age", + from.rate = "Loc.Age" +) +recovery_flows = mp_join( + mp_filter(state, "from", Epi = "I"), + mp_filter(state, "to", Epi = "R"), + mp_filter(flow_rates, "rate", Epi = "gamma"), + from.to = "Loc.Age" +) +death_flows = mp_join( + mp_filter(state, "from", Vital = "alive"), + mp_filter(state, "to", Vital = "dead"), + mp_filter(flow_rates, "rate", Epi = "mu"), + from.to = "Loc.Age", + from.rate = "Age" +) + + +flows = rbind( + movement_flows$labels_frame() + , infection_flows$labels_frame() + , recovery_flows$labels_frame() + , death_flows$labels_frame() +) +influences = mp_join( + mp_filter(state, "infectious", Epi = "I"), + mp_filter(flow_rates, "infection", Epi = "lambda"), + mp_filter(trans_rates, "rate", Epi = "beta"), + infectious.infection = "Loc", + infectious.rate = "Age.Loc", + infection.rate = "Loc.Age" ~ "Loc.Contact" +)$labels_frame() + +trans_rate_vector = Vector(trans_rates) +trans_rate_vector$set_numbers( + Age.Contact = c(young.young = 0.5, old = 0.25) + , Loc = "ham" +) +trans_rate_vector$frame() + + + + +mp_flow_join = function(state, flow_rates, from = list(), to = "", rate = "", from.to = "", from.rate = "", to.rate = "") { + macpan2:::mp("filter")(state, "from", from) +} +mp_flow_join(state, flow_rates) + + +vec = list() +vec$state = Vector(state) +vec$state$set_numbers( + Epi = c(I = 1), + Loc = c("que", "cal", "ham") +) +vec$state$set_numbers( + Loc = c(que = 549459, cal = 1306784, ham = 569353), + Epi = "S" +) + +N = mp_select(state, "N", "Loc") +alive = mp_filter(state, "alive", Epi = c("S", "I", "R")) + +VectorMerged = function(labeller) { + self = Base() + self$labeller = labeller + + self$.numbers = zero_vector(self$labeller$labels()) + self$numbers = function(...) { + + } + return_object(self, "VectorMerged") +} + +vec$N = Vector(N) + +N_groups = mp_join(alive, N, alive.N = "Loc") + +mp_indices(N_groups$labels_for$N(), N$labels_for$N()) +mp_indices(alive$labels_for$alive(), state$labels()) + + + +I = mp_filter(state, "I", Epi = "I") + + + +N_groups$labels_for$alive() +state$labels() + + + +#sub_pop$labels_for$alive(), state$labels() + flow_file = CSVReader("inst/model_library/sir_age/flows.csv")$read() vars_file = CSVReader("inst/model_library/sir_age/variables.csv")$read() settings_file = JSONReader("inst/model_library/sir_age/settings.json")$read() -make_core = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { +make_basis = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { vec_labs = to_labels(vars_table[settings_list[[vec_part_field]]]) - core( + basis( vars_table[vec_labs == vec_name, , drop = FALSE], labelling_names = to_name(settings_list[[lab_part_field]]) ) } -state = make_core("state", vars_file, settings_file, "vec_partition", "labelling_partition") -flow_rates = make_core("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") +state = make_basis("state", vars_file, settings_file, "vec_partition", "labelling_partition") +flow_rates = make_basis("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") @@ -32,34 +185,20 @@ flow_file i = 1L flow = flow_file[i,,drop=FALSE] mp_join( - mp_subset(state, "from", flow$from_partition, - mp_subset(state, "to", Epi = flow$to), - mp_subset(flow_rates, "flow", Epi = flow$flow), + mp_filter(state, "from", flow$from_partition), + mp_filter(state, "to", Epi = flow$to), + mp_filter(flow_rates, "flow", Epi = flow$flow), from.to = "Epi" ) -sir_state = core(Epi = c("S", "I", "R")) -sir_flow_rates = core(Epi = c("lambda", "gamma")) -loc_state = core( - Loc = c("ham", "tor", "mon"), - Move = "" -) -loc_flow_rates = core( - Loc = c("ham", "tor", "tor", "mon"), - Move = c("tor", "ham", "mon", "tor") -) - - -state = mp_cartesian(sir_state, loc_state) -flow_rates = mp_union(mp_cartesian(sir_flow_rates, loc_state), loc_flow_rates) N = mp_select(state, "N", "Loc") -active = mp_subset(state, "active", Epi = c("S", "I")) -infectious = mp_subset(state, "infectious", Epi = "I") +active = mp_filter(state, "active", Epi = c("S", "I")) +infectious = mp_filter(state, "infectious", Epi = "I") mp_join(N, infectious, N.infectious = "Loc") mp_join(xx, yy, N.norm_inf = "Loc")$labels_frame() -mp_join(N, mp_subset(state, "infectious", Epi = "I")) +mp_join(N, mp_filter(state, "infectious", Epi = "I")) match(N$labels_frame()$active, state$labels()) - 1L @@ -67,12 +206,12 @@ match(N$labels_frame()$N, unique(N$labels_frame()$N)) - 1L groupSums(state[active], strata, length(active)) xx = mp_join( - mp_subset(state, "state"), + mp_filter(state, "state"), mp_select(state, "N", "Loc"), state.N = "Loc" ) -xx$labels_by_dim$state() -xx$labels_by_dim$N() +xx$labels_for$state() +xx$labels_for$N() mp_init_vec = function(components, ...) { values = unlist(list(...)) @@ -83,24 +222,24 @@ mp_init_vec = function(components, ...) { } mp_init_vec(state, S.tor = 99) mp_join( - mp_subset(state, "infectious", Epi = "I"), - mp_subset(flow_rates, "infection", Epi = "lambda"), + mp_filter(state, "infectious", Epi = "I"), + mp_filter(flow_rates, "infection", Epi = "lambda"), infectious.infection = "Loc" )$labels_frame() mp_union( mp_join( - mp_subset(state, "from") - , mp_subset(state, "to") - , mp_subset(flow_rates, "flow", Epi = "") + mp_filter(state, "from") + , mp_filter(state, "to") + , mp_filter(flow_rates, "flow", Epi = "") , from.to = "Epi" , from.flow = "Loc" , to.flow = "Loc" ~ "Move" ), mp_join( - mp_subset(state, "from", Epi = "S") - , mp_subset(state, "to", Epi = "I") - , mp_subset(flow_rates, "flow", Epi = "lambda") + mp_filter(state, "from", Epi = "S") + , mp_filter(state, "to", Epi = "I") + , mp_filter(flow_rates, "flow", Epi = "lambda") , from.to = "Loc" , from.flow = "Loc" ) @@ -233,8 +372,8 @@ l = list( tt = LabelledPartition(x, l, "Epi.Loc") tt$labels() -tt$labels_by_dim$from() -tt$labels_by_dim$from() +tt$labels_for$from() +tt$labels_for$from() tt$frame_by_dim$to() tt$column_by_dim$from$Epi() diff --git a/vignettes/model_definitions.Rmd b/vignettes/model_definitions.Rmd index 2081258f..c910fc72 100644 --- a/vignettes/model_definitions.Rmd +++ b/vignettes/model_definitions.Rmd @@ -46,9 +46,7 @@ models = setNames( ## Prerequisites -This document assumes knowledge of compartmental epidemic models. We will not be -defining terms and symbols that we consider to be widely understood by -mathematical epidemiologists. +This document assumes knowledge of compartmental epidemic models. We will not be defining terms and symbols that we consider to be widely understood by mathematical epidemiologists. ## Document Goals @@ -82,14 +80,10 @@ These and other were developed by working with public-health modellers during the COVID-19 pandemic in Canada. -## Labelled Partitions (in progress) +## Naming and Labelling (in progress) + +A goal of `macpan2` is to provide a mechanism for representing structured compartmental models. An example of such a model is to have each compartment in an SEIR model split into a set of spatial locations and into a set of age groups. It is crucial but difficult to assign meaningful and consistent names to the compartments, flow rates, transmission rates, contact rates, sub-population sizes, and other parameters determining these quantities. Such names should convey how the different quantities relate to one another. For example, the names should make clear that the rate of flow between two compartments is specific to, say, the age group and location of those compartments. The naming system should facilitate identifying model quantities and sets of quantities. For example, in a spatially structured model we might want to refer to all states in a particular location (e.g. Toronto) and a specific state within that location (e.g. susceptible individuals in Toronto). -Before describing a data structure for representing [Compartmental Models], we -need to define the concept of labelled partitions. This concept allows for a -consistent, flexible, compact, and readable mechanism for identifying model -variables and sets of variables. For example, in a spatially structured model we -might want to refer to all states in a particular location (e.g. Toronto) and a -specific state within that location (e.g. susceptible individuals in Toronto). Labelled partitions can be stored as a data frame. The rows of the data frame represent the things being represented. Our primary example of a 'thing' is a variable in a compartmental model (e.g. number of susceptible individuals in From 08038a4182de19385097495ea0d82ab1690d29bc Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 29 Oct 2023 13:07:32 -0400 Subject: [PATCH 044/332] working towards model shape/product stuff --- NAMESPACE | 48 +-- R/labelled_partitions.R | 554 +-------------------------------- R/model_shape.R | 24 +- R/mp.R | 267 ++++++++++++++++ R/name_utils.R | 6 +- R/relationship.R | 340 ++++++++++++++++++++ R/vector.R | 11 +- man/Relationship.Rd | 11 + misc/experiments/refactorcpp.R | 129 +++++--- 9 files changed, 754 insertions(+), 636 deletions(-) create mode 100644 R/mp.R create mode 100644 R/relationship.R create mode 100644 man/Relationship.Rd diff --git a/NAMESPACE b/NAMESPACE index 3809b06d..cd57735a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,34 +1,34 @@ # Generated by roxygen2: do not edit by hand -S3method(Basis,Basis) -S3method(Basis,Partition) -S3method(Basis,data.frame) +S3method(Descriptors,Descriptors) +S3method(Descriptors,Partition) +S3method(Descriptors,data.frame) S3method(c,String) S3method(c,StringData) -S3method(head,Merged) -S3method(mp_labels,Basis) -S3method(mp_labels,Merged) -S3method(names,Basis) +S3method(head,Relationship) +S3method(length,Vector) +S3method(mp_labels,Descriptors) +S3method(mp_labels,Relationship) +S3method(names,Descriptors) S3method(names,IntVecs) S3method(names,MatsList) -S3method(names,Merged) S3method(names,MethList) S3method(names,Partition) -S3method(print,Basis) +S3method(print,Descriptors) S3method(print,MathExpression) -S3method(print,Merged) S3method(print,Partition) S3method(print,Quantities) +S3method(print,Relationship) S3method(print,String) S3method(print,StringData) S3method(print,Vector) -S3method(print,summary.Merged) +S3method(print,summary.Relationship) S3method(split_by,character) S3method(split_by,formula) -S3method(str,Merged) -S3method(summary,Merged) -S3method(tail,Merged) -S3method(to_labels,Basis) +S3method(str,Relationship) +S3method(summary,Relationship) +S3method(tail,Relationship) +S3method(to_labels,Descriptors) S3method(to_labels,Labels) S3method(to_labels,Partition) S3method(to_labels,Scalar) @@ -36,20 +36,19 @@ S3method(to_labels,StringData) S3method(to_labels,Vector) S3method(to_labels,character) S3method(to_labels,data.frame) -S3method(to_name,Basis) +S3method(to_name,Descriptors) S3method(to_name,Names) S3method(to_name,Partition) S3method(to_name,Scalar) S3method(to_name,StringData) S3method(to_name,character) S3method(to_names,"NULL") -S3method(to_names,Basis) +S3method(to_names,Descriptors) S3method(to_names,Names) S3method(to_names,Partition) S3method(to_names,Scalar) S3method(to_names,StringData) S3method(to_names,character) -export(Basis) export(BinaryOperator) export(CSVReader) export(Collection) @@ -58,6 +57,7 @@ export(CompartmentalSimulator) export(DerivationExtractor) export(Derivations) export(Derivations2ExprList) +export(Descriptors) export(EngineMethods) export(Euler) export(ExprList) @@ -74,7 +74,6 @@ export(Logit) export(MathExpressionFromFunc) export(MathExpressionFromStrings) export(MatsList) -export(Merged) export(MethList) export(Method) export(Model) @@ -88,6 +87,7 @@ export(Partition) export(Products) export(Quantities) export(Reader) +export(Relationship) export(Scalar2Vector) export(SimulatorConstructor) export(Simulators) @@ -106,15 +106,14 @@ export(all_consistent) export(all_equal) export(all_not_equal) export(atomic_partition) -export(basis) export(cartesian) export(cartesian_self) +export(descriptors) export(empty_matrix) export(engine_eval) export(finalizer_char) export(finalizer_index) export(init_merge) -export(initial_column_map) export(initial_valid_vars) export(join_partitions) export(labelled_frame) @@ -122,8 +121,10 @@ export(labelled_zero_vector) export(make_expr_parser) export(model_starter) export(mp_cartesian) -export(mp_filter) -export(mp_filter_out) +export(mp_choose) +export(mp_choose_out) +export(mp_expr_binop) +export(mp_expr_group_sum) export(mp_group) export(mp_indicator) export(mp_indices) @@ -137,6 +138,7 @@ export(mp_subset) export(mp_symmetric) export(mp_triangle) export(mp_union) +export(mp_zero_vector) export(nlist) export(not_all_equal) export(parse_expr_list) diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 2b7c0d32..e3a15e18 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -3,7 +3,12 @@ #' Create object for manipulating partitions, which are sets of #' labels for representing and naming model entities. #' -#' @param frame Data frame representing the partition. +#' @param frame Data frame representing the partition. The column names must +#' consist only of letters, numbers, and start with a letter. The columns of +#' the data frame must be character vectors such that each value is composed +#' entirely of letters, numbers, underscore, and must start with a letter +#' unless it is a blank string. Missing values are not allowed, but blank +#' strings are. Each row must be unique. #' #' @return Object of class \code{Partition} with the following methods. #' @@ -125,553 +130,6 @@ Partition = memoise(Partition) #' @export names.Partition = function(x) x$names() -ColumnGetter = function(labelled_partition, dimension_name, column_name) { - self = Base() - self$labelled_partition = labelled_partition - self$dimension_name = dimension_name - self$column_name = column_name - self$get = function() { - lp = self$labelled_partition - i = lp$column_map[[self$dimension_name]][[self$column_name]] - lp$frame[[i]] - } - self = return_object(self, "ColumnGetter") - self$get -} - -FrameGetter = function(labelled_partition, dimension_name) { - self = Base() - self$labelled_partition = labelled_partition - self$dimension_name = dimension_name - self$get_frame = function() { - i = unlist( - self$labelled_partition$column_map[[self$dimension_name]], - use.names = FALSE - ) - setNames( - self$labelled_partition$frame[, i, drop = FALSE], - names(self$labelled_partition$column_map[[self$dimension_name]]) - ) - } - self$get_partition = function() self$get_frame() |> Partition() - self$get_labels = function() { - i = self$labelled_partition$labelling_names() - f = self$get_frame() - i = i[i %in% names(f)] - f[, i, drop = FALSE] |> as.list() - do.call(paste, c(f, sep = ".")) - } - return_object(self, "FrameGetter") -} - -#' @export -initial_column_map = function(column_names, dimension_name) { - setNames( - list(setNames(as.list(column_names), column_names)), - dimension_name - ) -} - -## take two Merge objects and merge their frames -## and update the provenance-preserving column maps -merge_util = function(x, y, by.x, by.y) { - - ## ---- - ## resolve non-unique column names in the output, with - ## names of the original tables used to this point - ## ---- - suffixes = c( - paste0(sprintf(":%s", names(x$column_map)), collapse = ""), - paste0(sprintf(":%s", names(y$column_map)), collapse = "") - ) - - ## ---- - ## the merge itself - ## ---- - z = merge( - x$frame, y$frame, - by.x = by.x, by.y = by.y, - suffixes = suffixes - ) - - ## ---- - ## update the column name map to preserve provenance - ## ---- - - # column names in inputs and output - x_cnms = names(x$frame) - y_cnms = names(y$frame) - z_cnms = names(z) - - # numbers of types of output columns - n_common = length(by.x) - n_x_only = length(x_cnms) - n_common - n_y_only = length(y_cnms) - n_common - - # column names in the output categorized - # by contributing input table(s) - z_common = z_cnms[seq_len(n_common)] - z_x_only = z_cnms[n_common + seq_len(n_x_only)] - z_y_only = z_cnms[n_common + n_x_only + seq_len(n_y_only)] - - # column names in the input tables, ordered - # as they are in the output table - x_z_order = c(by.x, x_cnms[!x_cnms %in% by.x]) - y_z_order = c(by.y, y_cnms[!y_cnms %in% by.y]) - - # these can be used to update the column map, - # by looping through the existing map and - # translating to the new column names with these - # x|y_map vectors. of course you will also need - # to concatenate the column maps first - x_map = setNames(c(z_common, z_x_only), x_z_order) - y_map = setNames(c(z_common, z_y_only), y_z_order) - - x_cmap = x$column_map - y_cmap = y$column_map - for (tnm in names(x_cmap)) { - for (cnm in names(x_cmap[[tnm]])) { - old_cnm = x_cmap[[tnm]][[cnm]] - x_cmap[[tnm]][[cnm]] = x_map[[old_cnm]] - } - } - for (tnm in names(y_cmap)) { - for (cnm in names(y_cmap[[tnm]])) { - old_cnm = y_cmap[[tnm]][[cnm]] - y_cmap[[tnm]][[cnm]] = y_map[[old_cnm]] - } - } - - z_column_map = c(x_cmap, y_cmap) - - ## ---- - ## wrap up the result with provenance-preserving column map - ## ---- - Merged( - z, - z_column_map - ) -} - -#' @export -init_merge = function(frame, dimension_name) { - Merged(frame - , initial_column_map(names(frame), dimension_name) - ) -} - -mp = function(mp_func) { - f = ("mp_%s" - |> sprintf(mp_func) - |> getFromNamespace("macpan2") - ) - prototype = function(...) {l = list(...)} - target_e = body(f)[[2L]] - proto_e = body(prototype)[[2L]] - if (!identical(target_e, proto_e)) stop("developer error: invalid mp function") - body(f)[[2L]][[3L]] = (~unlist(list(...), recursive = FALSE))[[2L]] - f -} - -#' @export -mp_map_to_selection = function(x, filter_cond, select_cond) { - filter = mp("filter")(x, "filter", filter_cond, select_cond) - replacement = mp_select(filter, "select", names(select_cond)) - mp_join(filter, replacement, filter.select = names(select_cond)) -} - -#' @export -mp_cartesian = function(x, y) { - labelling_names = union(x$labelling_names, y$labelling_names) - f = join_partitions(x$partition$frame(), y$partition$frame()) - Basis(f, labelling_names = labelling_names) -} - -#' @export -mp_square = function(x, y_labelling_names) { - nms = to_names(y_labelling_names) - y = (x$partition$frame() - |> setNames(nms) - |> Basis(nms) - ) - mp_cartesian(x, y) -} - -#' @export -mp_triangle = function(x, y_labelling_names, exclude_diag = TRUE, lower_tri = FALSE) { - f = x$partition$frame() - g = setNames(f, y_labelling_names) - n = nrow(f) - if (exclude_diag) { - k = 2:n - i = sequence(k - 1) - j = rep(k, k - 1) - } else if (!exclude_diag) { - k = seq_len(n) - i = sequence(k) - j = rep(k, k) - } - if (lower_tri) { - ii = i - i = j - j = ii - } - f = cbind( - f[i, , drop = FALSE], - g[j, , drop = FALSE] - ) - Basis(f, names(f)) -} - -#' @export -mp_symmetric = function(x, y_labelling_names, exclude_diag = TRUE) { - f = x$partition$frame() - g = setNames(f, y_labelling_names) - n = nrow(f) - k = seq_len(n) - - if (exclude_diag) { - i = rep( k , times = n - 1L) - j = rep(rev(k), each = n - 1L) - } else { - i = rep(k, times = n) - j = rep(k, each = n) - } - - f = cbind( - f[i, , drop = FALSE], - g[j, , drop = FALSE] - ) - Basis(f, names(f)) -} - -#' @export -mp_linear = function(x, y_labelling_names) { - f = x$partition$frame() - g = setNames(f, y_labelling_names) - n = nrow(f) - - k = c(1L, rep(2L, n - 2L), 1L) - i = rep(seq_len(n), k) - j = sequence(k, c(2, seq_len(n - 2L), n - 1L), by = 2) - - f = cbind( - f[i, , drop = FALSE], - g[j, , drop = FALSE] - ) - Basis(f, names(f)) -} - -#' @export -mp_subset = function(x, ...) { - partition = mp_filter(x, "pick", ...)$partition - Basis(partition, x$labelling_names) -} - -#' @export -mp_union = function(...) { - l = list(...) - partitions = lapply(l, getElement, "partition") - labelling_names = (l - |> lapply(getElement, "labelling_names") - |> unlist(recursive = FALSE, use.names = FALSE) - |> unique() - ) - Basis(do.call(union_vars, partitions)$frame(), labelling_names) -} - -#' @export -mp_filter = function(x, subset_name, ...) { - l = list(...) - p = x$partition - for (cc in names(l)) { - vals = l[[cc]] - is_blank = nchar(vals) == 0L - vals = setdiff(vals, "") - if (any(is_blank)) { - p = union_vars(p$blank_on(cc), p$filter(vals, .wrt = cc)) - } else { - p = p$filter(vals, .wrt = cc) - } - } - init_merge(p$frame(), subset_name) -} - -#' @export -mp_filter_out = function(x, subset_name, ...) { - l = list(...) - p = x$partition - for (cc in names(l)) { - vals = l[[cc]] - p = p$filter_out(vals, .wrt = cc) - } - init_merge(p$frame(), subset_name) -} - -#' @export -mp_join = function(...) { - args = list(...) - is_basis = vapply(args, inherits, logical(1L), "Basis") - is_merged = vapply(args, inherits, logical(1L), "Merged") - table_list = args[is_basis | is_merged] - by_list = args[!is_basis & !is_merged] - z = table_list[[1L]] - for (i in 2:length(table_list)) { - args = c( - list(x = z, y = table_list[[i]]), - by_list - ) - z = do.call(merge_generic_by_util, args) - } - z -} - -#' @export -mp_group = function(group, group_nm, group_labelling_names, ...) { - filter = list(...) - -} - -#' @export -mp_select = function(basis, grouping_dimension) { - frame = basis$partition$select(to_names(grouping_dimension))$frame() - nms = names(frame)[names(frame) %in% basis$labelling_names] - frame |> Basis(labelling_names = nms) -} - -#' @export -mp_indicator = function(x, ...) { - l = list(...) - for (nm in names(l)) { - l[[nm]] = x$partition$partial_labels(nm) %in% l[[nm]] - } - Reduce(`&`, l) -} - -#' @export -mp_indices = function(x, table) { - match(x, table) - 1L -} - -#' @export -mp_labels = function(x, labelling_names) { - UseMethod("mp_labels") -} - -#' @export -mp_labels.Basis = function(x, labelling_names) { - if (missing(labelling_names)) return(x$labels()) - x$partial_labels(labelling_names) -} - -#' @export -mp_labels.Merged = function(x, labelling_names) { - x$labels_for[[labelling_names]]() -} - -split_by = function(by, table_origins) UseMethod("split_by") - -#' @export -split_by.character = function(by, table_origins) { - by = to_names(by) - orig = to_names(table_origins) - list(by, by) |> setNames(orig) -} - -#' @export -split_by.formula = function(by, table_origins) { - orig = to_names(table_origins) - list( - to_names(lhs(by)[[2L]]), - to_names(rhs(by)[[2L]]) - ) |> setNames(orig) -} - -## change to Mergable - -#' @export -Merged = function(frame, column_map) { - self = Base() - self$frame = frame - self$column_map = column_map - self$labelling_names = function() { - l = self$partition_list() - for (nm in names(l)) { - l[[nm]] = names(l[[nm]]) - } - unlist(l, use.names = FALSE) |> unique() - } - self$labels_for = list() - self$labels_frame = function() { - l = list() - for (d in names(self$column_map)) l[[d]] = self$labels_for[[d]]() - l |> as.data.frame() - } - self$frame_by_dim = list() - self$labels_for = list() - self$partition_by_dim = list() - for (d in names(self$column_map)) { - getter = FrameGetter(self, d) - self$frame_by_dim[[d]] = getter$get_frame - self$labels_for[[d]] = getter$get_labels - self$partition_by_dim[[d]] = getter$get_partition - } - self$column_by_dim = list() - for (d in names(self$column_map)) { - self$column_by_dim[[d]] = list() - for (c in names(self$column_map[[d]])) { - self$column_by_dim[[d]][[c]] = ColumnGetter(self, d, c) - } - } - self$partition_list = function() { - l = list() - for (dn in names(self$partition_by_dim)) { - l[[dn]] = self$partition_by_dim[[dn]]() - } - l - } - self$frame_list = function() method_apply(self$partition_list(), "frame") - self$filter = function(condition) { - condition = substitute(condition) - i = eval(condition, envir = c(self$column_by_dim, self$frame)) - Merged(self$frame[i,,drop = FALSE] - , column_map = self$column_map - ) - } - self$partition = self$partition_by_dim[[1L]]() ## hack! should probably have a method and then change the partition field in Basis to a method as well - return_object(self, "Merged") -} - -apply_col_map = function(map, orig_table_nm, by) { - map[[orig_table_nm]][by] |> unlist(use.names = FALSE) -} - -filter_by_list = function(x_orig, y_orig, by_list) { - l = list() - nms = names(by_list) - for (i in seq_along(by_list)) { - nm_i = nms[[i]] - nms_i = to_names(nm_i) - if ((nms_i[[1L]] %in% x_orig) & (nms_i[[2L]] %in% y_orig)) { - l[[nm_i]] = by_list[[nm_i]] - } - } - l -} - - -merge_generic_by_util = function(x, y, ...) { - by = filter_by_list( - names(x$column_map), - names(y$column_map), - list(...) - ) - by = mapply(split_by - , by - , names(by) - , SIMPLIFY = FALSE - , USE.NAMES = FALSE - ) |> setNames(names(by)) - by.x = character() - by.y = character() - for (nm in names(by)) { - orig = names(by[[nm]]) - by.x = append( - by.x, - apply_col_map(x$column_map, orig[[1L]], by[[nm]][[1L]]) - ) - by.y = append( - by.y, - apply_col_map(y$column_map, orig[[2L]], by[[nm]][[2L]]) - ) - } - by = data.frame(x = by.x, y = by.y) |> unique() - merge_util(x, y, by$x, by$y) -} - -#' @export -summary.Merged = function(object, ...) { - formats = c("name", "combined") - structure( - sapply(formats, merged_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), - class = "summary.Merged" - ) -} - -#' @export -print.summary.Merged = function(x, ...) { - msg_hline() |> message() - msg( - "Merged object from macpan2 describing", - "an aspect of model shape" - ) |> message() - msg_hline() |> message() - print(x$name) - print(x$combined, row.names = FALSE) -} - -#' @export -names.Merged = function(x) to_names(x$labelling_names()) - -merged_format_picker = function(x - , format = c("labels", "merged", "combined", "separate", "name", "names") - ) { - switch (match.arg(format) - , labels = x$labels_frame() - , merged = x$frame - , combined = cbind(x$labels_frame(), x$frame) - , separate = x$frame_list() - , name = to_name(x$labelling_names()) - , names = to_names(x$labelling_names()) - ) -} - -#' @export -print.Merged = function(x - , format = c("labels", "merged", "combined", "separate", "name", "names") - , ... - ) { - x = merged_format_picker(x, format) - print(x, row.names = FALSE, ...) -} - -#' @export -head.Merged = function(x - , n = 6L - , format = c("labels", "merged", "combined", "separate", "name", "names") - , ... - ) { - x = merged_format_picker(x, format) - if (format == "separate") { - return(lapply(x, head, n = n, ...)) - } else { - return(head(x, n = n, ...)) - } -} - -#' @export -tail.Merged = function(x - , n = 6L - , format = c("labels", "merged", "combined", "separate", "name", "names") - , ... - ) { - x = merged_format_picker(x, format) - if (format == "separate") { - return(lapply(x, tail, n = n, ...)) - } else { - return(tail(x, n = n, ...)) - } -} - -#' @export -str.Merged = function(x - , format = c("labels", "merged", "combined", "separate", "name", "names") - , ... -) { - x = merged_format_picker(x, format) - str(x, ...) -} - CompartmentalPartition = function(frame , special_partitions = c(vec = "Vec", type = "Type") ) { diff --git a/R/model_shape.R b/R/model_shape.R index 19c4227d..fae3f841 100644 --- a/R/model_shape.R +++ b/R/model_shape.R @@ -1,37 +1,37 @@ #' @export -basis = function(..., labelling_names) { +descriptors = function(..., labelling_names) { f = data.frame(...) if (missing(labelling_names)) labelling_names = names(f) - Basis(f, to_names(labelling_names)) + Descriptors(f, to_names(labelling_names)) } #' @export -Basis = function(partition, labelling_names = names(frame)) { - UseMethod("Basis") +Descriptors = function(partition, labelling_names = names(frame)) { + UseMethod("Descriptors") } #' @export -Basis.Partition = function(partition, labelling_names = names(frame)) { +Descriptors.Partition = function(partition, labelling_names = names(frame)) { self = Base() self$partition = partition self$labelling_names = to_names(labelling_names) self$labels = function() self$partition$select(self$labelling_names)$labels() self$partial_labels = function(...) self$partition$partial_labels(...) - return_object(self, "Basis") + return_object(self, "Descriptors") } #' @export -Basis.data.frame = function(partition, labelling_names = names(frame)) { - partition |> Partition() |> Basis(labelling_names) +Descriptors.data.frame = function(partition, labelling_names = names(frame)) { + partition |> Partition() |> Descriptors(labelling_names) } #' @export -Basis.Basis = function(partition, labelling_names = names(frame)) { - partition$partition |> Basis(labelling_names) +Descriptors.Descriptors = function(partition, labelling_names = names(frame)) { + partition$partition |> Descriptors(labelling_names) } #' @export -print.Basis = function(x, ...) print(x$partition) +print.Descriptors = function(x, ...) print(x$partition) #' @export -names.Basis = function(x) x$partition$names() +names.Descriptors = function(x) x$partition$names() diff --git a/R/mp.R b/R/mp.R new file mode 100644 index 00000000..c0208e4b --- /dev/null +++ b/R/mp.R @@ -0,0 +1,267 @@ +mp = function(mp_func) { + f = ("mp_%s" + |> sprintf(mp_func) + |> getFromNamespace("macpan2") + ) + prototype = function(...) {l = list(...)} + target_e = body(f)[[2L]] + proto_e = body(prototype)[[2L]] + if (!identical(target_e, proto_e)) stop("developer error: invalid mp function") + body(f)[[2L]][[3L]] = (~unlist(list(...), recursive = FALSE))[[2L]] + f +} + +#' @export +mp_map_to_selection = function(x, filter_cond, select_cond) { + filter = mp("filter")(x, "filter", filter_cond, select_cond) + replacement = mp_select(filter, "select", names(select_cond)) + mp_join(filter, replacement, filter.select = names(select_cond)) +} + +#' @export +mp_cartesian = function(x, y) { + labelling_names = union(x$labelling_names, y$labelling_names) + f = join_partitions(x$partition$frame(), y$partition$frame()) + Descriptors(f, labelling_names = labelling_names) +} + +#' @export +mp_square = function(x, y_labelling_names) { + nms = to_names(y_labelling_names) + y = (x$partition$frame() + |> setNames(nms) + |> Descriptors(nms) + ) + mp_cartesian(x, y) +} + +#' @export +mp_triangle = function(x, y_labelling_names, exclude_diag = TRUE, lower_tri = FALSE) { + f = x$partition$frame() + g = setNames(f, y_labelling_names) + n = nrow(f) + if (exclude_diag) { + k = 2:n + i = sequence(k - 1) + j = rep(k, k - 1) + } else if (!exclude_diag) { + k = seq_len(n) + i = sequence(k) + j = rep(k, k) + } + if (lower_tri) { + ii = i + i = j + j = ii + } + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Descriptors(f, names(f)) +} + +#' @export +mp_symmetric = function(x, y_labelling_names, exclude_diag = TRUE) { + f = x$partition$frame() + g = setNames(f, y_labelling_names) + n = nrow(f) + k = seq_len(n) + + if (exclude_diag) { + i = rep( k , times = n - 1L) + j = rep(rev(k), each = n - 1L) + } else { + i = rep(k, times = n) + j = rep(k, each = n) + } + + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Descriptors(f, names(f)) +} + +#' @export +mp_linear = function(x, y_labelling_names) { + f = x$partition$frame() + g = setNames(f, y_labelling_names) + n = nrow(f) + + k = c(1L, rep(2L, n - 2L), 1L) + i = rep(seq_len(n), k) + j = sequence(k, c(2, seq_len(n - 2L), n - 1L), by = 2) + + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Descriptors(f, names(f)) +} + +#' @export +mp_subset = function(x, ...) { + partition = mp_choose(x, "pick", ...)$partition + Descriptors(partition, x$labelling_names) +} + +#' @export +mp_union = function(...) { + l = list(...) + partitions = lapply(l, getElement, "partition") + labelling_names = (l + |> lapply(getElement, "labelling_names") + |> unlist(recursive = FALSE, use.names = FALSE) + |> unique() + ) + Descriptors(do.call(union_vars, partitions)$frame(), labelling_names) +} + +#' @export +mp_choose = function(x, subset_name, ...) { + l = list(...) + p = x$partition + for (cc in names(l)) { + vals = l[[cc]] + is_blank = nchar(vals) == 0L + vals = setdiff(vals, "") + if (any(is_blank)) { + p = union_vars(p$blank_on(cc), p$filter(vals, .wrt = cc)) + } else { + p = p$filter(vals, .wrt = cc) + } + } + init_merge(p$frame(), subset_name, x$labelling_names) +} + +#' @export +mp_choose_out = function(x, subset_name, ...) { + l = list(...) + p = x$partition + for (cc in names(l)) { + vals = l[[cc]] + p = p$filter_out(vals, .wrt = cc) + } + init_merge(p$frame(), subset_name, x$labelling_names) +} + +#' @export +mp_join = function(...) { + args = list(...) + is_basis = vapply(args, inherits, logical(1L), "Descriptors") + is_relationship = vapply(args, inherits, logical(1L), "Relationship") + table_list = args[is_basis | is_relationship] + by_list = args[!is_basis & !is_relationship] + z = table_list[[1L]] + for (i in 2:length(table_list)) { + args = c( + list(x = z, y = table_list[[i]]), + by_list + ) + z = do.call(merge_generic_by_util, args) + } + z +} + +#' @export +mp_group = function(group, group_nm, group_labelling_names, ...) { + filter = list(...) + +} + +#' @export +mp_select = function(basis, grouping_dimension) { + frame = basis$partition$select(to_names(grouping_dimension))$frame() + nms = names(frame)[names(frame) %in% basis$labelling_names] + frame |> Descriptors(labelling_names = nms) +} + +#' @export +mp_indicator = function(x, ...) { + l = list(...) + for (nm in names(l)) { + l[[nm]] = x$partition$partial_labels(nm) %in% l[[nm]] + } + Reduce(`&`, l) +} + +#' @export +mp_indices = function(x, table) { + match(x, table) - 1L +} + +#' @export +mp_labels = function(x, labelling_names) { + UseMethod("mp_labels") +} + +#' @export +mp_labels.Descriptors = function(x, labelling_names) { + if (missing(labelling_names)) return(x$labels()) + x$partial_labels(labelling_names) +} + +#' @export +mp_zero_vector = function(x, labelling_names, ...) { + (x + |> mp_subset(...) + |> mp_labels(labelling_names) + |> zero_vector() + ) +} + +#' @export +mp_labels.Relationship = function(x, labelling_names) { + x$labels_for[[labelling_names]]() +} + +#' @export +mp_expr_group_sum = function(x + , stratify_by + , output_name + , vector_name + , subset_name + , grouping_name + , length_name + , ... +) { + strata = mp_select(x, stratify_by) + subset = mp_subset(x, ...) + grouping_indices = mp_indices( + mp_labels(subset, stratify_by), + mp_labels(strata) + ) + subset_indices = mp_indices( + mp_labels(subset), + mp_labels(x) + ) + length_int = strata$labels() |> length() + e_lhs = output_name + e_rhs = sprintf("groupSums(%s[%s], %s, %s)" + , vector_name + , subset_name + , grouping_name + , length_name + ) + list( + formula = two_sided(e_lhs, e_rhs), + int_vecs = setNames( + list(grouping_indices, subset_indices, length_int), + c(grouping_name, subset_name, length_name) + ), + strata = strata, + subset = subset + ) +} + +#' @export +mp_expr_binop = function(x, y + , stratify_by + , output_name + , vector_name + , subset_name + , grouping_name + , length_name + , ... +) {} diff --git a/R/name_utils.R b/R/name_utils.R index f4053f4f..5c87286f 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -15,7 +15,7 @@ to_labels.character = function(x) valid_dotted$assert(x) to_labels.Partition = function(x) x$labels() #' @export -to_labels.Basis = function(x) x$labels() +to_labels.Descriptors = function(x) x$labels() #' @export to_labels.data.frame = function(x) StringDataFromFrame(x)$dot()$labels()$value() @@ -63,7 +63,7 @@ to_names.character = function(x) { to_names.Partition = function(x) x$names() #' @export -to_names.Basis = function(x) x$partition$names() +to_names.Descriptors = function(x) x$labelling_names #' @export to_names.StringData = function(x) x$undot()$names()$value() @@ -101,7 +101,7 @@ to_name.character = function(x) { to_name.Partition = function(x) x$name() #' @export -to_name.Basis = function(x) x$partition$name() +to_name.Descriptors = function(x) to_names(x) |> to_name() #' @export to_name.StringData = function(x) x$dot()$names()$value() diff --git a/R/relationship.R b/R/relationship.R new file mode 100644 index 00000000..d8e7071e --- /dev/null +++ b/R/relationship.R @@ -0,0 +1,340 @@ +#' Relationship +#' +#' @export +Relationship = function(frame, column_map, labelling_names_list) { + self = Base() + self$frame = frame + self$column_map = column_map + self$labelling_names_list = labelling_names_list + self$labels_for = list() + self$labels_frame = function() { + l = list() + for (d in names(self$column_map)) l[[d]] = self$labels_for[[d]]() + l |> as.data.frame() + } + self$frame_by_dim = list() + self$labels_for = list() + self$partition_by_dim = list() + for (d in names(self$column_map)) { + getter = FrameGetter(self, d) + self$frame_by_dim[[d]] = getter$get_frame + self$labels_for[[d]] = getter$get_labels + self$partition_by_dim[[d]] = getter$get_partition + } + self$column_by_dim = list() + for (d in names(self$column_map)) { + self$column_by_dim[[d]] = list() + for (c in names(self$column_map[[d]])) { + self$column_by_dim[[d]][[c]] = ColumnGetter(self, d, c) + } + } + self$partition_list = function() { + l = list() + for (dn in names(self$partition_by_dim)) { + l[[dn]] = self$partition_by_dim[[dn]]() + } + l + } + self$frame_list = function() method_apply(self$partition_list(), "frame") + self$filter = function(condition) { + condition = substitute(condition) + i = eval(condition, envir = c(self$column_by_dim, self$frame)) + Relationship(self$frame[i, , drop = FALSE] + , column_map = self$column_map + , labelling_names_list = self$labelling_names_list + ) + } + self$partition = self$partition_by_dim[[1L]]() ## hack! should probably have a method and then change the partition field in Descriptors to a method as well + return_object(self, "Relationship") +} + +ColumnGetter = function(relationship, dimension_name, column_name) { + self = Base() + self$relationship = relationship + self$dimension_name = dimension_name + self$column_name = column_name + self$get = function() { + lp = self$relationship + i = lp$column_map[[self$dimension_name]][[self$column_name]] + lp$frame[[i]] + } + self = return_object(self, "ColumnGetter") + self$get +} + +FrameGetter = function(relationship, dimension_name) { + self = Base() + self$relationship = relationship + self$dimension_name = dimension_name + self$get_frame = function() { + i = unlist( + self$relationship$column_map[[self$dimension_name]], + use.names = FALSE + ) + setNames( + self$relationship$frame[, i, drop = FALSE], + names(self$relationship$column_map[[self$dimension_name]]) + ) + } + self$get_partition = function() self$get_frame() |> Partition() + self$get_labels = function() { + i = self$relationship$labelling_names_list[[self$dimension_name]] + f = self$get_frame()[, i, drop = FALSE] + l = as.list(f) + paste_args = c(l, sep = ".") + do.call(paste, paste_args) + } + return_object(self, "FrameGetter") +} + +initial_column_map = function(column_names, dimension_name) { + setNames( + list(setNames(as.list(column_names), column_names)), + dimension_name + ) +} + +initial_labelling_names_list = function(labelling_names, dimension_name) { + setNames( + list(labelling_names), + dimension_name + ) +} + +## take two Merge objects and merge their frames +## and update the provenance-preserving column maps +merge_util = function(x, y, by.x, by.y) { + + ## ---- + ## resolve non-unique column names in the output, with + ## names of the original tables used to this point + ## ---- + suffixes = c( + paste0(sprintf(":%s", names(x$column_map)), collapse = ""), + paste0(sprintf(":%s", names(y$column_map)), collapse = "") + ) + + ## ---- + ## the merge itself + ## ---- + z = merge( + x$frame, y$frame, + by.x = by.x, by.y = by.y, + suffixes = suffixes + ) + + ## ---- + ## update the column name map to preserve provenance + ## ---- + + # column names in inputs and output + x_cnms = names(x$frame) + y_cnms = names(y$frame) + z_cnms = names(z) + + # numbers of types of output columns + n_common = length(by.x) + n_x_only = length(x_cnms) - n_common + n_y_only = length(y_cnms) - n_common + + # column names in the output categorized + # by contributing input table(s) + z_common = z_cnms[seq_len(n_common)] + z_x_only = z_cnms[n_common + seq_len(n_x_only)] + z_y_only = z_cnms[n_common + n_x_only + seq_len(n_y_only)] + + # column names in the input tables, ordered + # as they are in the output table + x_z_order = c(by.x, x_cnms[!x_cnms %in% by.x]) + y_z_order = c(by.y, y_cnms[!y_cnms %in% by.y]) + + # these can be used to update the column map, + # by looping through the existing map and + # translating to the new column names with these + # x|y_map vectors. of course you will also need + # to concatenate the column maps first + x_map = setNames(c(z_common, z_x_only), x_z_order) + y_map = setNames(c(z_common, z_y_only), y_z_order) + + x_cmap = x$column_map + y_cmap = y$column_map + for (tnm in names(x_cmap)) { + for (cnm in names(x_cmap[[tnm]])) { + old_cnm = x_cmap[[tnm]][[cnm]] + x_cmap[[tnm]][[cnm]] = x_map[[old_cnm]] + } + } + for (tnm in names(y_cmap)) { + for (cnm in names(y_cmap[[tnm]])) { + old_cnm = y_cmap[[tnm]][[cnm]] + y_cmap[[tnm]][[cnm]] = y_map[[old_cnm]] + } + } + + z_column_map = c(x_cmap, y_cmap) + z_lab_names_list = c(x$labelling_names_list, y$labelling_names_list) + + ## ---- + ## wrap up the result with provenance-preserving column map + ## ---- + Relationship( + z, + z_column_map, + z_lab_names_list + ) +} + +filter_by_list = function(x_orig, y_orig, by_list) { + l = list() + nms = names(by_list) + for (i in seq_along(by_list)) { + nm_i = nms[[i]] + nms_i = to_names(nm_i) + if ((nms_i[[1L]] %in% x_orig) & (nms_i[[2L]] %in% y_orig)) { + l[[nm_i]] = by_list[[nm_i]] + } + } + l +} + +#' @export +init_merge = function(frame, dimension_name, labelling_names) { + Relationship(frame + , initial_column_map(names(frame), dimension_name) + , initial_labelling_names_list(labelling_names, dimension_name) + ) +} + +split_by = function(by, table_origins) UseMethod("split_by") + +#' @export +split_by.character = function(by, table_origins) { + by = to_names(by) + orig = to_names(table_origins) + list(by, by) |> setNames(orig) +} + +#' @export +split_by.formula = function(by, table_origins) { + orig = to_names(table_origins) + list( + to_names(lhs(by)[[2L]]), + to_names(rhs(by)[[2L]]) + ) |> setNames(orig) +} + + + + +apply_col_map = function(map, orig_table_nm, by) { + map[[orig_table_nm]][by] |> unlist(use.names = FALSE) +} + +merge_generic_by_util = function(x, y, ...) { + by = filter_by_list( + names(x$column_map), + names(y$column_map), + list(...) + ) + by = mapply(split_by + , by + , names(by) + , SIMPLIFY = FALSE + , USE.NAMES = FALSE + ) |> setNames(names(by)) + by.x = character() + by.y = character() + for (nm in names(by)) { + orig = names(by[[nm]]) + by.x = append( + by.x, + apply_col_map(x$column_map, orig[[1L]], by[[nm]][[1L]]) + ) + by.y = append( + by.y, + apply_col_map(y$column_map, orig[[2L]], by[[nm]][[2L]]) + ) + } + by = data.frame(x = by.x, y = by.y) |> unique() + merge_util(x, y, by$x, by$y) +} + +#' @export +summary.Relationship = function(object, ...) { + formats = c("name", "combined") + structure( + sapply(formats, relationship_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), + class = "summary.Relationship" + ) +} + +#' @export +print.summary.Relationship = function(x, ...) { + msg_hline() |> message() + msg( + "Relationship object from macpan2 describing", + "an aspect of model shape" + ) |> message() + msg_hline() |> message() + print(x$name) + print(x$combined, row.names = FALSE) +} + +#names.Relationship = function(x) to_names(x$labelling_names()) + +relationship_format_picker = function(x + , format = c("labels", "relationship", "combined", "separate") + ) { + switch (match.arg(format) + , labels = x$labels_frame() + , relationship = x$frame + , combined = cbind(x$labels_frame(), x$frame) + , separate = x$frame_list() + ) +} + +#' @export +print.Relationship = function(x + , format = c("labels", "relationship", "combined", "separate") + , ... + ) { + x = relationship_format_picker(x, format) + print(x, row.names = FALSE, ...) +} + +#' @export +head.Relationship = function(x + , n = 6L + , format = c("labels", "relationship", "combined", "separate") + , ... + ) { + x = relationship_format_picker(x, format) + if (format == "separate") { + return(lapply(x, head, n = n, ...)) + } else { + return(head(x, n = n, ...)) + } +} + +#' @export +tail.Relationship = function(x + , n = 6L + , format = c("labels", "relationship", "combined", "separate") + , ... + ) { + x = relationship_format_picker(x, format) + if (format == "separate") { + return(lapply(x, tail, n = n, ...)) + } else { + return(tail(x, n = n, ...)) + } +} + +#' @export +str.Relationship = function(x + , format = c("labels", "relationship", "combined", "separate") + , ... +) { + x = relationship_format_picker(x, format) + str(x, ...) +} diff --git a/R/vector.R b/R/vector.R index 2a1d00b6..b27f6991 100644 --- a/R/vector.R +++ b/R/vector.R @@ -10,13 +10,13 @@ Vector = function(labeller) { self$.numbers[i] } self$set_numbers = function(...) { - ## generate list with: filter_cond, select_cond, select_nm + ## generate list with: value, filter_cond, select_cond, select_nm l = process_grouping_dots(...) args = c(list(self$labeller), l$filter_cond, l$select_cond) filter = do.call(mp_subset, args) replacement_values = setNames( - value[filter$partition$partial_labels(l$select_nm)], + l$value[filter$partition$partial_labels(l$select_nm)], filter$labels() ) @@ -29,9 +29,11 @@ Vector = function(labeller) { values = unname(self$numbers()) ) } + self$length = function() length(self$.numbers) return_object(self, "Vector") } + process_grouping_dots = function(...) { l = list(...) replacement = Filter(is.numeric, l) @@ -40,9 +42,12 @@ process_grouping_dots = function(...) { value = replacement[[1L]] select_cond = lapply(replacement, names) select_nm = names(select_cond) - nlist(filter_cond, select_cond, select_nm) + nlist(value, filter_cond, select_cond, select_nm) } +#' @export +length.Vector = function(x) x$length() + #' @export print.Vector = function(x, ...) print(x$numbers()) diff --git a/man/Relationship.Rd b/man/Relationship.Rd new file mode 100644 index 00000000..15395af4 --- /dev/null +++ b/man/Relationship.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/relationship.R +\name{Relationship} +\alias{Relationship} +\title{Relationship} +\usage{ +Relationship(frame, column_map, labelling_names_list) +} +\description{ +Relationship +} diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 58e03f81..0731116e 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -10,16 +10,16 @@ library(dplyr) ## factor model bases --------------- -state_sir = basis( +state_sir = descriptors( Epi = c("S", "I", "R", "D"), Vital = c("alive", "alive", "alive", "dead"), labelling_names = "Epi" ) -flow_rates_sir = basis(Epi = c("lambda", "gamma", "mu")) -trans_rates_sir = basis(Epi = "beta") -cities = basis(Loc = c("cal", "ham", "que")) +flow_rates_sir = descriptors(Epi = c("lambda", "gamma", "mu")) +trans_rates_sir = descriptors(Epi = "beta") +cities = descriptors(Loc = c("cal", "ham", "que")) movement = mp_linear(cities, "Move") -age = basis(Age = c("young", "old")) +age = descriptors(Age = c("young", "old")) contact = mp_square(age, "Contact") ## product model bases ------------- @@ -47,39 +47,74 @@ trans_rates = (trans_rates_sir |> mp_cartesian(contact) ) -strata = mp_select(state, "Loc.Age") -alive = mp_subset(state, Vital = "alive") -mp_indices(alive$partial_labels("Loc.Age"), strata$labels()) -mp_labels(alive, "Loc.Age") -mp_labels(strata) -merge(alive$partition$frame(), strata$partition$frame(), c("Loc", "Age")) +N_expr = mp_expr_group_sum(state + , "Loc.Age" + , "N", "state", "alive", "alive_groups", "length_N" + , Vital = "alive" +) +N_expr$strata$labels() + +xx = mp_join( + mp_choose(N_expr$strata, "N"), + mp_choose(state, "infectious", Epi = "I"), + N.infectious = N_expr$strata$labelling_names +) +xx$labels_for$infectious() +xx$frame + +state_strata = mp_select(state, "Loc.Age") +alive_states = mp_subset(state, Vital = "alive") +alive_groups = mp_indices( + mp_labels(alive_states, "Loc.Age"), + mp_labels(state_strata) +) +alive = mp_indices( + mp_labels(alive_states), + mp_labels(state) +) +N_vector = Vector(state_strata) +state_vector = Vector(state) +state_vector$set_numbers(Epi = c(I = 1), Loc.Age = "ham.young") +S_numbers = mp_zero_vector(state, "Loc.Age", Epi = "S") +S_numbers[] = round(abs(rnorm(length(S_numbers), 1000, 1000))) +state_vector$set_numbers(Loc.Age = S_numbers, Epi = "S") + +alive_groups +alive +engine_eval( + ~ groupSums(state[alive], alive_groups, n_strata), + state = state_vector$numbers(), + alive = alive, + alive_groups = alive_groups, + n_strata = length(N_vector) +) movement_flows = mp_join( - mp_filter(state, "from", Vital = "alive"), - mp_filter(state, "to", Vital = "alive"), - mp_filter(flow_rates, "rate", Epi = ""), + mp_choose(state, "from", Vital = "alive"), + mp_choose(state, "to", Vital = "alive"), + mp_choose(flow_rates, "rate", Epi = ""), from.to = "Epi.Age", from.rate = "Loc", to.rate = "Loc" ~ "Move" ) infection_flows = mp_join( - mp_filter(state, "from", Epi = "S"), - mp_filter(state, "to", Epi = "I"), - mp_filter(flow_rates, "rate", Epi = "lambda"), + mp_choose(state, "from", Epi = "S"), + mp_choose(state, "to", Epi = "I"), + mp_choose(flow_rates, "rate", Epi = "lambda"), from.to = "Loc.Age", from.rate = "Loc.Age" ) recovery_flows = mp_join( - mp_filter(state, "from", Epi = "I"), - mp_filter(state, "to", Epi = "R"), - mp_filter(flow_rates, "rate", Epi = "gamma"), + mp_choose(state, "from", Epi = "I"), + mp_choose(state, "to", Epi = "R"), + mp_choose(flow_rates, "rate", Epi = "gamma"), from.to = "Loc.Age" ) death_flows = mp_join( - mp_filter(state, "from", Vital = "alive"), - mp_filter(state, "to", Vital = "dead"), - mp_filter(flow_rates, "rate", Epi = "mu"), + mp_choose(state, "from", Vital = "alive"), + mp_choose(state, "to", Vital = "dead"), + mp_choose(flow_rates, "rate", Epi = "mu"), from.to = "Loc.Age", from.rate = "Age" ) @@ -92,9 +127,9 @@ flows = rbind( , death_flows$labels_frame() ) influences = mp_join( - mp_filter(state, "infectious", Epi = "I"), - mp_filter(flow_rates, "infection", Epi = "lambda"), - mp_filter(trans_rates, "rate", Epi = "beta"), + mp_choose(state, "infectious", Epi = "I"), + mp_choose(flow_rates, "infection", Epi = "lambda"), + mp_choose(trans_rates, "rate", Epi = "beta"), infectious.infection = "Loc", infectious.rate = "Age.Loc", infection.rate = "Loc.Age" ~ "Loc.Contact" @@ -128,7 +163,7 @@ vec$state$set_numbers( ) N = mp_select(state, "N", "Loc") -alive = mp_filter(state, "alive", Epi = c("S", "I", "R")) +alive = mp_choose(state, "alive", Epi = c("S", "I", "R")) VectorMerged = function(labeller) { self = Base() @@ -150,7 +185,7 @@ mp_indices(alive$labels_for$alive(), state$labels()) -I = mp_filter(state, "I", Epi = "I") +I = mp_choose(state, "I", Epi = "I") @@ -166,15 +201,15 @@ flow_file = CSVReader("inst/model_library/sir_age/flows.csv")$read() vars_file = CSVReader("inst/model_library/sir_age/variables.csv")$read() settings_file = JSONReader("inst/model_library/sir_age/settings.json")$read() -make_basis = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { +make_descriptors = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { vec_labs = to_labels(vars_table[settings_list[[vec_part_field]]]) - basis( + descriptors( vars_table[vec_labs == vec_name, , drop = FALSE], labelling_names = to_name(settings_list[[lab_part_field]]) ) } -state = make_basis("state", vars_file, settings_file, "vec_partition", "labelling_partition") -flow_rates = make_basis("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") +state = make_descriptors("state", vars_file, settings_file, "vec_partition", "labelling_partition") +flow_rates = make_descriptors("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") @@ -185,20 +220,20 @@ flow_file i = 1L flow = flow_file[i,,drop=FALSE] mp_join( - mp_filter(state, "from", flow$from_partition), - mp_filter(state, "to", Epi = flow$to), - mp_filter(flow_rates, "flow", Epi = flow$flow), + mp_choose(state, "from", flow$from_partition), + mp_choose(state, "to", Epi = flow$to), + mp_choose(flow_rates, "flow", Epi = flow$flow), from.to = "Epi" ) N = mp_select(state, "N", "Loc") -active = mp_filter(state, "active", Epi = c("S", "I")) -infectious = mp_filter(state, "infectious", Epi = "I") +active = mp_choose(state, "active", Epi = c("S", "I")) +infectious = mp_choose(state, "infectious", Epi = "I") mp_join(N, infectious, N.infectious = "Loc") mp_join(xx, yy, N.norm_inf = "Loc")$labels_frame() -mp_join(N, mp_filter(state, "infectious", Epi = "I")) +mp_join(N, mp_choose(state, "infectious", Epi = "I")) match(N$labels_frame()$active, state$labels()) - 1L @@ -206,7 +241,7 @@ match(N$labels_frame()$N, unique(N$labels_frame()$N)) - 1L groupSums(state[active], strata, length(active)) xx = mp_join( - mp_filter(state, "state"), + mp_choose(state, "state"), mp_select(state, "N", "Loc"), state.N = "Loc" ) @@ -222,24 +257,24 @@ mp_init_vec = function(components, ...) { } mp_init_vec(state, S.tor = 99) mp_join( - mp_filter(state, "infectious", Epi = "I"), - mp_filter(flow_rates, "infection", Epi = "lambda"), + mp_choose(state, "infectious", Epi = "I"), + mp_choose(flow_rates, "infection", Epi = "lambda"), infectious.infection = "Loc" )$labels_frame() mp_union( mp_join( - mp_filter(state, "from") - , mp_filter(state, "to") - , mp_filter(flow_rates, "flow", Epi = "") + mp_choose(state, "from") + , mp_choose(state, "to") + , mp_choose(flow_rates, "flow", Epi = "") , from.to = "Epi" , from.flow = "Loc" , to.flow = "Loc" ~ "Move" ), mp_join( - mp_filter(state, "from", Epi = "S") - , mp_filter(state, "to", Epi = "I") - , mp_filter(flow_rates, "flow", Epi = "lambda") + mp_choose(state, "from", Epi = "S") + , mp_choose(state, "to", Epi = "I") + , mp_choose(flow_rates, "flow", Epi = "lambda") , from.to = "Loc" , from.flow = "Loc" ) From cc49fbcc95f7e0c354a3166fbe2627589a476430 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 02:42:14 -0400 Subject: [PATCH 045/332] banff --- NAMESPACE | 56 ++++-- R/formula_data.R | 12 ++ R/formula_utils.R | 44 ++++ R/frame_utils.R | 36 ++++ R/index.R | 161 +++++++++++++++ R/{relationship.R => link.R} | 193 +++++++++++++----- R/math.R | 5 +- R/model_shape.R | 37 ---- R/mp.R | 246 +++++++++++++++++++++-- R/name_utils.R | 6 +- inst/model_library/sir/derivations.json | 1 - inst/model_library/sir/flows.csv | 3 - inst/model_library/sir/infection.csv | 2 - inst/model_library/sir/model_structure.R | 54 +++++ inst/model_library/sir/settings.json | 5 - inst/model_library/sir/variables.csv | 6 - inst/model_library/sir/vectors.csv | 8 - man/Index.Rd | 124 ++++++++++++ man/Link.Rd | 29 +++ man/Partition.Rd | 7 +- man/Relationship.Rd | 11 - man/mp_index.Rd | 29 +++ misc/dev/dev.cpp | 20 +- misc/experiments/refactorcpp.R | 141 +++++++++++-- src/macpan2.cpp | 21 +- 25 files changed, 1065 insertions(+), 192 deletions(-) create mode 100644 R/formula_data.R create mode 100644 R/frame_utils.R create mode 100644 R/index.R rename R/{relationship.R => link.R} (59%) delete mode 100644 R/model_shape.R delete mode 100644 inst/model_library/sir/derivations.json delete mode 100644 inst/model_library/sir/flows.csv delete mode 100644 inst/model_library/sir/infection.csv create mode 100644 inst/model_library/sir/model_structure.R delete mode 100644 inst/model_library/sir/settings.json delete mode 100644 inst/model_library/sir/variables.csv delete mode 100644 inst/model_library/sir/vectors.csv create mode 100644 man/Index.Rd create mode 100644 man/Link.Rd delete mode 100644 man/Relationship.Rd create mode 100644 man/mp_index.Rd diff --git a/NAMESPACE b/NAMESPACE index cd57735a..ba757651 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,34 +1,45 @@ # Generated by roxygen2: do not edit by hand -S3method(Descriptors,Descriptors) -S3method(Descriptors,Partition) -S3method(Descriptors,data.frame) +S3method(Index,Index) +S3method(Index,Partition) +S3method(Index,data.frame) +S3method(as.data.frame,Index) +S3method(as.data.frame,Link) S3method(c,String) S3method(c,StringData) -S3method(head,Relationship) +S3method(head,Link) +S3method(labelling_names,Index) +S3method(labelling_names,Link) +S3method(labels,Index) S3method(length,Vector) -S3method(mp_labels,Descriptors) -S3method(mp_labels,Relationship) -S3method(names,Descriptors) +S3method(mp_labels,Index) +S3method(mp_labels,Link) +S3method(mp_union,Index) +S3method(mp_union,Link) +S3method(names,Index) S3method(names,IntVecs) +S3method(names,Link) S3method(names,MatsList) S3method(names,MethList) S3method(names,Partition) -S3method(print,Descriptors) +S3method(print,FormulaData) +S3method(print,Index) +S3method(print,Link) S3method(print,MathExpression) S3method(print,Partition) S3method(print,Quantities) -S3method(print,Relationship) S3method(print,String) S3method(print,StringData) S3method(print,Vector) -S3method(print,summary.Relationship) +S3method(print,summary.Link) S3method(split_by,character) S3method(split_by,formula) -S3method(str,Relationship) -S3method(summary,Relationship) -S3method(tail,Relationship) -S3method(to_labels,Descriptors) +S3method(str,Link) +S3method(summary,Link) +S3method(swap_sides,character) +S3method(swap_sides,formula) +S3method(tail,Link) +S3method(to_labels,Index) S3method(to_labels,Labels) S3method(to_labels,Partition) S3method(to_labels,Scalar) @@ -36,14 +47,14 @@ S3method(to_labels,StringData) S3method(to_labels,Vector) S3method(to_labels,character) S3method(to_labels,data.frame) -S3method(to_name,Descriptors) +S3method(to_name,Index) S3method(to_name,Names) S3method(to_name,Partition) S3method(to_name,Scalar) S3method(to_name,StringData) S3method(to_name,character) S3method(to_names,"NULL") -S3method(to_names,Descriptors) +S3method(to_names,Index) S3method(to_names,Names) S3method(to_names,Partition) S3method(to_names,Scalar) @@ -57,7 +68,6 @@ export(CompartmentalSimulator) export(DerivationExtractor) export(Derivations) export(Derivations2ExprList) -export(Descriptors) export(EngineMethods) export(Euler) export(ExprList) @@ -65,9 +75,11 @@ export(Files) export(FlowExpander) export(Flows) export(Formula) +export(Index) export(Infection) export(IntVecs) export(JSONReader) +export(Link) export(Log) export(LogFile) export(Logit) @@ -87,7 +99,6 @@ export(Partition) export(Products) export(Quantities) export(Reader) -export(Relationship) export(Scalar2Vector) export(SimulatorConstructor) export(Simulators) @@ -108,7 +119,6 @@ export(all_not_equal) export(atomic_partition) export(cartesian) export(cartesian_self) -export(descriptors) export(empty_matrix) export(engine_eval) export(finalizer_char) @@ -118,20 +128,26 @@ export(initial_valid_vars) export(join_partitions) export(labelled_frame) export(labelled_zero_vector) +export(labelling_names) export(make_expr_parser) export(model_starter) +export(mp_aggregate) export(mp_cartesian) export(mp_choose) export(mp_choose_out) +export(mp_decompose) export(mp_expr_binop) export(mp_expr_group_sum) -export(mp_group) +export(mp_formula_data) +export(mp_index) export(mp_indicator) export(mp_indices) export(mp_join) export(mp_labels) export(mp_linear) export(mp_map_to_selection) +export(mp_rbind) +export(mp_rename) export(mp_select) export(mp_square) export(mp_subset) diff --git a/R/formula_data.R b/R/formula_data.R new file mode 100644 index 00000000..9bcb50be --- /dev/null +++ b/R/formula_data.R @@ -0,0 +1,12 @@ +FormulaData = function(frame, reference_index_list) { + self = Base() + self$frame = frame + self$reference_index_list = reference_index_list + + return_object(self, "FormulaData") +} + +#' @export +print.FormulaData = function(x, ...) { + print(x$frame, row.names = FALSE) +} diff --git a/R/formula_utils.R b/R/formula_utils.R index 5c6ae1ab..5826b147 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -64,6 +64,15 @@ to_assign = function(formula, dummy = "dummy") { as.formula(do.call(sprintf, args)) } +## update formula symbolically with additional formulas +## in ..., each of which has a lhs matching a symbol in +## the focal formula and a rhs to replace that symbol with +update_formula = function(formula, replacers) { + nms = lapply(replacers, lhs_char) + l = lapply(replacers, rhs_expr) |> setNames(nms) + do.call('substitute', list(formula, l)) +} + ## for character vectors lhs and rhs, return a formula one_sided = function(rhs) { as.formula(sprintf("~ %s", as.character(rhs))) @@ -72,6 +81,25 @@ two_sided = function(lhs, rhs) { as.formula(sprintf("%s ~ %s", as.character(lhs), as.character(rhs))) } + + +swap_sides = function(x) UseMethod("swap_sides") + +#' @export +swap_sides.formula = function(x) { + stopifnot(is_two_sided(x)) + formula = x + formula[[2L]] = x[[3L]] + formula[[3L]] = x[[2L]] + formula +} + +#' @export +swap_sides.character = function(x) { + stopifnot(length(x) == 1L) + x +} + ## how many sides does a formula have? is_zero_sided = function(formula) { (length(formula) == 1L) & inherits(formula, "formula") @@ -96,6 +124,18 @@ lhs = function(formula) { formula } +rhs_expr = function(formula) { + if (is_two_sided(formula)) return(formula[[3L]]) + formula[[2L]] +} +lhs_expr = function(formula) { + if (is_two_sided(formula)) return(formula[[2L]]) + msg_colon( + "There is no left-hand-side in this formula", + deparse(formula, width.cutoff = 500) + ) |> stop() +} + lhs_char = function(formula) { if (is_two_sided(formula)) { return(deparse(formula[[2L]], 500)) @@ -109,6 +149,10 @@ rhs_char = function(formula) { deparse(formula[[i]], 500) } +formula_as_character = function(formula) { + sprintf("%s ~ %s", lhs_char(formula), rhs_char(formula)) +} + # formula parsing in macpan2 works one side at a time. but sometimes # it is helpful to parse two-sided formulas. this function does so # by parsing each side at a time and rbinding the results. diff --git a/R/frame_utils.R b/R/frame_utils.R new file mode 100644 index 00000000..3f116f7d --- /dev/null +++ b/R/frame_utils.R @@ -0,0 +1,36 @@ +## from poorman + +is_nested <- function(lst) vapply(lst, function(x) inherits(x[1L], "list"), FALSE) + +flatten <- function(lst) { + nested <- is_nested(lst) + res <- c(lst[!nested], unlist(lst[nested], recursive = FALSE)) + if (sum(nested)) Recall(res) else return(res) +} + +bind_rows <- function(..., .id = NULL) { + lsts <- list(...) + lsts <- flatten(lsts) + lsts <- Filter(Negate(is.null), lsts) + + if (!missing(.id)) { + lsts <- lapply(seq_along(lsts), function(i) { + nms <- names(lsts) + id_df <- data.frame(id = if (is.null(nms)) as.character(i) else nms[i], stringsAsFactors = FALSE) + colnames(id_df) <- .id + cbind(id_df, lsts[[i]]) + }) + } + + nms <- unique(unlist(lapply(lsts, names))) + lsts <- lapply( + lsts, + function(x) { + if (!is.data.frame(x)) x <- data.frame(as.list(x), stringsAsFactors = FALSE) + for (i in nms[!nms %in% names(x)]) x[[i]] <- "" + x + } + ) + names(lsts) <- NULL + do.call(rbind, lsts) +} diff --git a/R/index.R b/R/index.R new file mode 100644 index 00000000..fc8db9c4 --- /dev/null +++ b/R/index.R @@ -0,0 +1,161 @@ +#' Index +#' +#' Make an object to represent descriptions of model components and their +#' dimensions of variation. \code{Index} objects generalize and wrap +#' \code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like +#' object is an entry in the index, and each column provides a description of +#' the entries. For example, the following \code{Index} object describes the +#' state variables of an age-structured SIR model. Each row corresponds to a +#' state variable and each state variable is described by columns `Epi` and +#' `Age`. +#' ```{r, echo = FALSE} +#' sir = mp_index(Epi = c("S", "I", "R")) +#' age = mp_index(Age = c("young", "old")) +#' mp_cartesian(sir, age) +#' ``` +#' +#' @param partition A data frame (or data frame-like) object containing +#' definitions of the index. This object can be a \code{\link{data.frame}}, +#' \code{\link{Partition}}, or another \code{\link{Index}} object. +#' @param labelling_names A \code{\link{character}} vector of the names of the +#' index that will be used to label the model components (i.e. rows) being +#' described. The \code{labelling_names} cannot have duplicates and must +#' contain at least one name. The index given by the \code{labelling_names} +#' must uniquely identify each row. +#' @param reference_index (Advanced) An optional partition to use when +#' computing subset indices. +#' @param x \code{Index} object. +#' @param ... For consistency with existing S3 methods. +#' +#' @seealso [mp_index()] +#' +#' @examples +#' sir = data.frame( +#' Epi = c("S", "I", "R", "D" ), +#' Vital = c("alive", "alive", "alive", "dead") +#' ) |> Index(labelling_names = "Epi") + +#' age = data.frame( +#' Age = c("young", "old") +#' ) |> Index() +#' state_index = mp_cartesian(sir, age) +#' labels(state_index) +#' (state_index +#' |> mp_subset(Vital = "alive") +#' |> labels() +#' ) +#' +#' @export +Index = function(partition, labelling_names = names(partition), reference_index = NULL) { + UseMethod("Index") +} + +#' @describeIn Index Create a \code{Index} object from a +#' \code{\link{Partition}} object. These \code{\link{Partition}} objects +#' wrap \code{\link{data.frame}}s. These data frames must follow certain +#' restrictions. +#' @export +Index.Partition = function(partition + , labelling_names = names(partition) + , reference_index = NULL + ) { + self = Base() + + ## Args + self$partition = partition + self$labelling_names = to_names(labelling_names) + + ## Private Arg + self$.reference_index = reference_index + + ## Getters and Setters + self$reference_index = function() { + if (is.null(self$.reference_index)) return(self) + self$.reference_index + } + self$set_reference_index = function(index) { + self$.reference_index = index + } + + ## Standard Methods + self$labels = function() self$partition$select(self$labelling_names)$labels() + self$partial_labels = function(...) self$partition$partial_labels(...) + + return_object(self, "Index") +} + +#' @describeIn Index Create a \code{Index} object from a +#' \code{\link{data.frame}} object that follows certain restrictions +#' documented in the help page for \code{\link{Partition}}s. +#' @export +Index.data.frame = function(partition, labelling_names = names(partition), reference_index = NULL) { + partition |> Partition() |> Index(labelling_names, reference_index) +} + +#' @describeIn Index Create another \code{Index} object from an +#' existing \code{Index} object, possibly with new \code{labelling_names}. +#' @export +Index.Index = function(partition, labelling_names = names(partition), reference_index = NULL) { + partition$partition |> Index(labelling_names, reference_index) +} + +#' @describeIn Index Print a \code{Index} object. +#' @export +print.Index = function(x, ...) print(x$partition) + +#' @describeIn Index Get the names of the index +#' (i.e. columns) of a \code{Index} object. +#' @export +names.Index = function(x) x$partition$names() + +#' @export +as.data.frame.Index = function(x, row.names = NULL, optional = FALSE, ...) { + x$partition$frame() +} + +#' @export +labelling_names = function(x) UseMethod("labelling_names") + +#' @describeIn Index Retrieve the \code{labelling_names} of +#' a \code{Index} object. These \code{labelling_names} +#' are the names of the index that are used to label the model +#' components. +#' @export +labelling_names.Index = function(x) x$labelling_names + +#' @describeIn Index Convert a \code{Index} object into +#' a character vector giving labels associated with each model component +#' (i.e. row) being described. +#' @export +to_labels.Index = function(x) x$labels() + +#' @describeIn Index Convert a \code{Index} object into +#' a character vector giving labels associated with each model component +#' (i.e. row) being described. +#' @export +labels.Index = function(x, ...) x$labels() + + +#' Create Index Object +#' +#' Create an \code{\link{Index}} object from a set of +#' character vectors. This function is analogous to the +#' \code{\link{data.frame}} function for creating data frames. +#' +#' @param ... Character vectors to combine into an \code{\link{Index}}. +#' object. +#' @param labelling_names See \code{\link{Index}}. +#' +#' @seealso [Index()] +#' @examples +#' mp_index( +#' Epi = c("S", "I", "S", "I"), +#' Age = c("young", "young", "old", "old") +#' ) +#' +#' @export +mp_index = function(..., labelling_names) { + f = data.frame(...) + if (missing(labelling_names)) labelling_names = names(f) + Index(f, to_names(labelling_names)) +} diff --git a/R/relationship.R b/R/link.R similarity index 59% rename from R/relationship.R rename to R/link.R index d8e7071e..858db46b 100644 --- a/R/relationship.R +++ b/R/link.R @@ -1,25 +1,48 @@ -#' Relationship +#' Link +#' +#' Make an object to describe links between the entities in an +#' \code{\link{Index}}. \code{Link} object are created by operating on existing +#' \code{\link{Index}} objects. For example, here the \code{\link{mp_join}} +#' combines two +#' ```{r} +#' age = mp_index(Age = c("young", "old")) +#' state = mp_cartesian( +#' mp_index(Epi = c("S", "I", "R")), +#' age +#' ) +#' mp_join( +#' mp_choose(state, "from", Epi = "S"), +#' mp_choose(state, "to", Epi = "I"), +#' from.to = "Age" +#' ) +#' ``` #' #' @export -Relationship = function(frame, column_map, labelling_names_list) { +Link = function(frame, column_map, reference_index_list) { self = Base() self$frame = frame self$column_map = column_map - self$labelling_names_list = labelling_names_list + self$reference_index_list = reference_index_list + self$labelling_names_list = function() { + lapply(self$reference_index_list, getElement, "labelling_names") + } + self$table_names = function() names(self$column_map) self$labels_for = list() self$labels_frame = function() { l = list() for (d in names(self$column_map)) l[[d]] = self$labels_for[[d]]() l |> as.data.frame() } - self$frame_by_dim = list() + self$frame_for = list() self$labels_for = list() - self$partition_by_dim = list() + self$partition_for = list() + self$index_for = list() for (d in names(self$column_map)) { getter = FrameGetter(self, d) - self$frame_by_dim[[d]] = getter$get_frame + self$frame_for[[d]] = getter$get_frame self$labels_for[[d]] = getter$get_labels - self$partition_by_dim[[d]] = getter$get_partition + self$partition_for[[d]] = getter$get_partition + self$index_for[[d]] = getter$get_index } self$column_by_dim = list() for (d in names(self$column_map)) { @@ -30,8 +53,8 @@ Relationship = function(frame, column_map, labelling_names_list) { } self$partition_list = function() { l = list() - for (dn in names(self$partition_by_dim)) { - l[[dn]] = self$partition_by_dim[[dn]]() + for (dn in names(self$partition_for)) { + l[[dn]] = self$partition_for[[dn]]() } l } @@ -39,22 +62,22 @@ Relationship = function(frame, column_map, labelling_names_list) { self$filter = function(condition) { condition = substitute(condition) i = eval(condition, envir = c(self$column_by_dim, self$frame)) - Relationship(self$frame[i, , drop = FALSE] + Link(self$frame[i, , drop = FALSE] , column_map = self$column_map - , labelling_names_list = self$labelling_names_list + , reference_index_list = self$reference_index_list ) } - self$partition = self$partition_by_dim[[1L]]() ## hack! should probably have a method and then change the partition field in Descriptors to a method as well - return_object(self, "Relationship") + self$partition = self$partition_for[[1L]]() ## hack! should probably have a method and then change the partition field in Index to a method as well + return_object(self, "Link") } -ColumnGetter = function(relationship, dimension_name, column_name) { +ColumnGetter = function(link, dimension_name, column_name) { self = Base() - self$relationship = relationship + self$link = link self$dimension_name = dimension_name self$column_name = column_name self$get = function() { - lp = self$relationship + lp = self$link i = lp$column_map[[self$dimension_name]][[self$column_name]] lp$frame[[i]] } @@ -62,23 +85,27 @@ ColumnGetter = function(relationship, dimension_name, column_name) { self$get } -FrameGetter = function(relationship, dimension_name) { +FrameGetter = function(link, dimension_name) { self = Base() - self$relationship = relationship + self$link = link self$dimension_name = dimension_name self$get_frame = function() { i = unlist( - self$relationship$column_map[[self$dimension_name]], + self$link$column_map[[self$dimension_name]], use.names = FALSE ) setNames( - self$relationship$frame[, i, drop = FALSE], - names(self$relationship$column_map[[self$dimension_name]]) + self$link$frame[, i, drop = FALSE], + names(self$link$column_map[[self$dimension_name]]) ) } self$get_partition = function() self$get_frame() |> Partition() + self$get_index = function() Index( + self$get_partition(), + self$link$labelling_names_list()[[self$dimension_name]] + ) self$get_labels = function() { - i = self$relationship$labelling_names_list[[self$dimension_name]] + i = self$link$labelling_names_list()[[self$dimension_name]] f = self$get_frame()[, i, drop = FALSE] l = as.list(f) paste_args = c(l, sep = ".") @@ -94,6 +121,11 @@ initial_column_map = function(column_names, dimension_name) { ) } +initial_reference_index_list = function(index, dimension_name) { + setNames(list(index), dimension_name) +} + +## TODO XXX initial_labelling_names_list = function(labelling_names, dimension_name) { setNames( list(labelling_names), @@ -172,15 +204,15 @@ merge_util = function(x, y, by.x, by.y) { } z_column_map = c(x_cmap, y_cmap) - z_lab_names_list = c(x$labelling_names_list, y$labelling_names_list) + z_reference_index_list = c(x$reference_index_list, y$reference_index_list) ## ---- ## wrap up the result with provenance-preserving column map ## ---- - Relationship( + Link( z, z_column_map, - z_lab_names_list + z_reference_index_list ) } @@ -198,10 +230,10 @@ filter_by_list = function(x_orig, y_orig, by_list) { } #' @export -init_merge = function(frame, dimension_name, labelling_names) { - Relationship(frame +init_merge = function(frame, dimension_name, reference_index) { + Link(frame , initial_column_map(names(frame), dimension_name) - , initial_labelling_names_list(labelling_names, dimension_name) + , initial_reference_index_list(reference_index, dimension_name) ) } @@ -230,6 +262,45 @@ apply_col_map = function(map, orig_table_nm, by) { map[[orig_table_nm]][by] |> unlist(use.names = FALSE) } + +is_provenance_implicit = function(x, col_nm) { + (x$column_map + |> lapply(getElement, col_nm) + |> Filter(f = Negate(is.null)) + |> vapply(`==`, logical(1L), col_nm) + ) +} + +explicit_provenance = function(x, col_nm) { + m = x$column_map + implicit = is_provenance_implicit(x, col_nm) + if (length(implicit) == 0L) { + macpan2:::msg_colon( + macpan2:::msg("Column", col_nm, "not found in any of the original tables"), + macpan2:::msg_indent(names(m)) + ) |> stop() + } + if (!any(implicit)) return(x) + + tabs_to_fix = (implicit + |> Filter(f = isTRUE) + |> names() + ) + + f = x$frame + l = x$labelling_names_list() + + orig_col = f[[col_nm]] + for (tab_nm in tabs_to_fix) { + new_col_nm = sprintf("%s:%s", col_nm, tab_nm) + f[[new_col_nm]] = orig_col + m[[tab_nm]][[col_nm]] = new_col_nm + } + f[[col_nm]] = NULL + Link(f, m, l) +} + + merge_generic_by_util = function(x, y, ...) { by = filter_by_list( names(x$column_map), @@ -256,23 +327,38 @@ merge_generic_by_util = function(x, y, ...) { ) } by = data.frame(x = by.x, y = by.y) |> unique() - merge_util(x, y, by$x, by$y) + + dup.x = duplicated(by$x) + if (any(dup.x)) { + cols_to_fix = by$x[dup.x] + for (col_nm in cols_to_fix) { + x = explicit_provenance(x, col_nm) + } + merge_generic_by_util(x, y, ...) + } else { + merge_util(x, y, by$x, by$y) + } } #' @export -summary.Relationship = function(object, ...) { +as.data.frame.Link = function(x, row.names = NULL, optional = FALSE, ...) { + x$labels_frame() +} + +#' @export +summary.Link = function(object, ...) { formats = c("name", "combined") structure( - sapply(formats, relationship_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), - class = "summary.Relationship" + sapply(formats, link_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), + class = "summary.Link" ) } #' @export -print.summary.Relationship = function(x, ...) { +print.summary.Link = function(x, ...) { msg_hline() |> message() msg( - "Relationship object from macpan2 describing", + "Link object from macpan2 describing", "an aspect of model shape" ) |> message() msg_hline() |> message() @@ -280,35 +366,40 @@ print.summary.Relationship = function(x, ...) { print(x$combined, row.names = FALSE) } -#names.Relationship = function(x) to_names(x$labelling_names()) +#' @export +names.Link = function(x) names(x$frame) + +#' @export +labelling_names.Link = function(x) x$labelling_names_list() + -relationship_format_picker = function(x - , format = c("labels", "relationship", "combined", "separate") +link_format_picker = function(x + , format = c("labels", "link", "combined", "separate") ) { - switch (match.arg(format) + switch(match.arg(format) , labels = x$labels_frame() - , relationship = x$frame + , link = x$frame , combined = cbind(x$labels_frame(), x$frame) , separate = x$frame_list() ) } #' @export -print.Relationship = function(x - , format = c("labels", "relationship", "combined", "separate") +print.Link = function(x + , format = c("labels", "link", "combined", "separate") , ... ) { - x = relationship_format_picker(x, format) + x = link_format_picker(x, format) print(x, row.names = FALSE, ...) } #' @export -head.Relationship = function(x +head.Link = function(x , n = 6L - , format = c("labels", "relationship", "combined", "separate") + , format = c("labels", "link", "combined", "separate") , ... ) { - x = relationship_format_picker(x, format) + x = link_format_picker(x, format) if (format == "separate") { return(lapply(x, head, n = n, ...)) } else { @@ -317,12 +408,12 @@ head.Relationship = function(x } #' @export -tail.Relationship = function(x +tail.Link = function(x , n = 6L - , format = c("labels", "relationship", "combined", "separate") + , format = c("labels", "link", "combined", "separate") , ... ) { - x = relationship_format_picker(x, format) + x = link_format_picker(x, format) if (format == "separate") { return(lapply(x, tail, n = n, ...)) } else { @@ -331,10 +422,10 @@ tail.Relationship = function(x } #' @export -str.Relationship = function(x - , format = c("labels", "relationship", "combined", "separate") +str.Link = function(x + , format = c("labels", "link", "combined", "separate") , ... ) { - x = relationship_format_picker(x, format) + x = link_format_picker(x, format) str(x, ...) } diff --git a/R/math.R b/R/math.R index b132021b..fd709171 100644 --- a/R/math.R +++ b/R/math.R @@ -35,7 +35,10 @@ SymbolicMath = function() { if (y == "") return(self$wrap(paste("-", x, sep = ""))) ## actual binop -- binary operator -- case - self$wrap(paste(x, y, sep = op)) + z = paste(x, y, sep = op) + + ## wrapping makes stuff ugly + self$wrap(z) } ## 1. all functions in self take string (i.e. length-1 character vector) diff --git a/R/model_shape.R b/R/model_shape.R deleted file mode 100644 index fae3f841..00000000 --- a/R/model_shape.R +++ /dev/null @@ -1,37 +0,0 @@ -#' @export -descriptors = function(..., labelling_names) { - f = data.frame(...) - if (missing(labelling_names)) labelling_names = names(f) - Descriptors(f, to_names(labelling_names)) -} - -#' @export -Descriptors = function(partition, labelling_names = names(frame)) { - UseMethod("Descriptors") -} - -#' @export -Descriptors.Partition = function(partition, labelling_names = names(frame)) { - self = Base() - self$partition = partition - self$labelling_names = to_names(labelling_names) - self$labels = function() self$partition$select(self$labelling_names)$labels() - self$partial_labels = function(...) self$partition$partial_labels(...) - return_object(self, "Descriptors") -} - -#' @export -Descriptors.data.frame = function(partition, labelling_names = names(frame)) { - partition |> Partition() |> Descriptors(labelling_names) -} - -#' @export -Descriptors.Descriptors = function(partition, labelling_names = names(frame)) { - partition$partition |> Descriptors(labelling_names) -} - -#' @export -print.Descriptors = function(x, ...) print(x$partition) - -#' @export -names.Descriptors = function(x) x$partition$names() diff --git a/R/mp.R b/R/mp.R index c0208e4b..7bdcbd7d 100644 --- a/R/mp.R +++ b/R/mp.R @@ -22,15 +22,22 @@ mp_map_to_selection = function(x, filter_cond, select_cond) { mp_cartesian = function(x, y) { labelling_names = union(x$labelling_names, y$labelling_names) f = join_partitions(x$partition$frame(), y$partition$frame()) - Descriptors(f, labelling_names = labelling_names) + Index(f, labelling_names = labelling_names) } #' @export -mp_square = function(x, y_labelling_names) { - nms = to_names(y_labelling_names) +mp_square = function(x, suffixes = c("A", "B")) { + l1 = sprintf("%s%s", x$labelling_names, suffixes[1L]) + l2 = sprintf("%s%s", x$labelling_names, suffixes[2L]) + n1 = sprintf("%s%s", names(x), suffixes[1L]) + n2 = sprintf("%s%s", names(x), suffixes[2L]) + x = (x$partition$frame() + |> setNames(n1) + |> Index(l1) + ) y = (x$partition$frame() - |> setNames(nms) - |> Descriptors(nms) + |> setNames(n2) + |> Index(l2) ) mp_cartesian(x, y) } @@ -58,7 +65,7 @@ mp_triangle = function(x, y_labelling_names, exclude_diag = TRUE, lower_tri = FA f[i, , drop = FALSE], g[j, , drop = FALSE] ) - Descriptors(f, names(f)) + Index(f, names(f)) } #' @export @@ -80,7 +87,7 @@ mp_symmetric = function(x, y_labelling_names, exclude_diag = TRUE) { f[i, , drop = FALSE], g[j, , drop = FALSE] ) - Descriptors(f, names(f)) + Index(f, names(f)) } #' @export @@ -97,17 +104,20 @@ mp_linear = function(x, y_labelling_names) { f[i, , drop = FALSE], g[j, , drop = FALSE] ) - Descriptors(f, names(f)) + Index(f, names(f)) } #' @export mp_subset = function(x, ...) { partition = mp_choose(x, "pick", ...)$partition - Descriptors(partition, x$labelling_names) + Index(partition, x$labelling_names, x) } #' @export -mp_union = function(...) { +mp_union = function(...) UseMethod("mp_union") + +#' @export +mp_union.Index = function(...) { l = list(...) partitions = lapply(l, getElement, "partition") labelling_names = (l @@ -115,9 +125,61 @@ mp_union = function(...) { |> unlist(recursive = FALSE, use.names = FALSE) |> unique() ) - Descriptors(do.call(union_vars, partitions)$frame(), labelling_names) + Index(do.call(union_vars, partitions)$frame(), labelling_names) +} + +#' @export +mp_union.Link = function(...) { + l = list(...) + column_map = lapply(l, getElement, "column_map") |> unique() + if (length(column_map) != 1L) { + msg_colon( + msg( + "Union of inconsistent Link objects.", + "All Link objects must have the same", + "column_map, but the following distinct", + "maps were found:" + ), + msg_indent_break(lapply(column_map, unlist)) + ) |> stop() + } + ## TODO: should really be checking for reference_index_list + labelling_names_list = method_apply(l, "labelling_names_list") |> unique() + if (length(labelling_names_list) != 1L) { + msg_colon( + msg( + "Union of inconsistent Link objects.", + "All Link objects must have the same", + "labelling_names_list, but the following", + "distinct maps were found:" + ), + msg_indent_break(lapply(labelling_names_list, unlist)) + ) |> stop() + } + frame = mp_rbind(...) + FormulaData(frame, l[[1L]]$reference_index_list) +} + +#' @export +mp_rbind = function(...) { + (list(...) + |> lapply(as.data.frame) + |> do.call(what = bind_rows) ## bind_rows is in frame_utils.R + ) } +## how does this work for Index objects? +## might be ok because it is just another +## way to get columns for formulas. on the +## other hand it doesn't sound right at all. +## should probably throw an error here if +## Indices are passed. +#' @export +mp_formula_data = function(...) { + mp_union(...) +} + + #' @export mp_choose = function(x, subset_name, ...) { l = list(...) @@ -132,7 +194,7 @@ mp_choose = function(x, subset_name, ...) { p = p$filter(vals, .wrt = cc) } } - init_merge(p$frame(), subset_name, x$labelling_names) + init_merge(p$frame(), subset_name, x$reference_index()) } #' @export @@ -149,10 +211,41 @@ mp_choose_out = function(x, subset_name, ...) { #' @export mp_join = function(...) { args = list(...) - is_basis = vapply(args, inherits, logical(1L), "Descriptors") - is_relationship = vapply(args, inherits, logical(1L), "Relationship") - table_list = args[is_basis | is_relationship] - by_list = args[!is_basis & !is_relationship] + is_index = vapply(args, inherits, logical(1L), "Index") + is_link = vapply(args, inherits, logical(1L), "Link") + table_list = args[is_index | is_link] + by_list = args[!is_index & !is_link] + by_nms = names(by_list) |> strsplit(".", fixed = TRUE) + table_nms = names(table_list) + if (!is.null(table_nms)) { + for (nm in names(table_list)) { + if (nm != "" & inherits(table_list[[nm]], "Index")) { + table_list[[nm]] = mp_choose(table_list[[nm]], nm) + } + } + } + orig_tab_nms = (table_list + |> method_apply("table_names") + |> unname() + |> unlist(recursive = FALSE) + ) + table_nm_diffs = (by_nms + |> lapply(factor, levels = orig_tab_nms) + |> lapply(as.integer) + |> vapply(diff, integer(1L)) + ) + bad_by_args = table_nm_diffs < 1L ## table names in the wrong order + if (any(bad_by_args)) { + fixed_by_nms = (by_nms[bad_by_args] + |> lapply(rev) + |> lapply(paste0, collapse = ".") + ) + fixed_by_args = (by_list[bad_by_args] + |> lapply(swap_sides) + ) + by_list[bad_by_args] = fixed_by_args + names(by_list)[bad_by_args] = fixed_by_nms + } z = table_list[[1L]] for (i in 2:length(table_list)) { args = c( @@ -165,16 +258,127 @@ mp_join = function(...) { } #' @export -mp_group = function(group, group_nm, group_labelling_names, ...) { - filter = list(...) +mp_aggregate = function(formula + , group_by + , index + , ... +) { + prototypes = list( + group_sums = macpan2:::MethodPrototype(y ~ groupSums(x), c("x", "y"), character()) + ) + consistent_agg_meths = (prototypes + |> method_apply("consistent", formula) + |> vapply(c, logical(1L), USE.NAMES = TRUE) + ) + if (!any(consistent_agg_meths)) { + f = method_apply(prototypes, "as_character") + msg_break( + msg_colon( + "The following aggregation formula", + msg_indent(formula_as_character(formula)) + ), + msg_colon( + msg( + "was not consistent with any of the", + "available aggregation prototypes" + ), + msg_indent_break(f) + ) + ) |> stop() + } + agg_meth = (consistent_agg_meths + |> which() + |> names() + |> getElement(1L) ## first match takes precedence + ) + agg = prototypes[[agg_meth]] + strata = mp_select(x, stratify_by) + subset = mp_subset(x, ...) + grouping_indices = mp_indices( + mp_labels(subset, stratify_by), + mp_labels(strata) + ) + subset_indices = mp_indices( + mp_labels(subset), + mp_labels(x) + ) + +} + + +#' @export +mp_decompose = function(formula, index, decomp_name, ...) { + input_formula = formula + table_args = list(...) + output_name = lhs_char(formula) + by_args = table_args + names(by_args) = sprintf("%s.%s", output_name, names(table_args)) + for (nm in names(table_args)) { + table_args[[nm]] = mp_select(index, table_args[[nm]]) + } + linked_indices = do.call(mp_join, + c(setNames(list(index), output_name), table_args, by_args) + ) + int_vecs = setNames( + vector(mode = "list", length(table_args)), + sprintf("%s_%s", names(table_args), decomp_name) + ) + iv_nms = names(int_vecs) + tab_nms = names(table_args) + expand_iv_nms = sprintf("%s[%s]", tab_nms, iv_nms) + replacement_formulas = mapply(two_sided + , tab_nms, expand_iv_nms + , SIMPLIFY = FALSE, USE.NAMES = FALSE + ) + + for (i in seq_along(table_args)) { + int_vecs[[iv_nms[i]]] = mp_indices( + linked_indices$labels_for[[tab_nms[i]]](), + linked_indices$partition_for[[tab_nms[i]]]()$labels() + ) + } + formula = update_formula(formula, replacement_formulas) + nlist( + formula, + input_formula, + linked_indices, + int_vecs + ) +} + + +#' @export +mp_rename = function(x, ...) { + l = list(...) + new_nms = names(l) + old_nms = unlist(l, recursive = FALSE, use.names = FALSE) + f = x$partition$frame() + labs = x$labelling_names + i = match(old_nms, names(f)) + if (any(is.na(i))) { + msg_break( + msg_colon( + "Attempted to replace the following names that do not exist", + msg_indent(old_nms[is.na(i)]) + ), + msg_colon( + "These are the only names that are available", + msg_indent(names(f)) + ) + ) |> stop() + } + j = match(old_nms, labs) + names(f)[i] = new_nms + labs[j[!is.na(j)]] = new_nms[!is.na(j)] + Index(f, labs) } #' @export mp_select = function(basis, grouping_dimension) { frame = basis$partition$select(to_names(grouping_dimension))$frame() nms = names(frame)[names(frame) %in% basis$labelling_names] - frame |> Descriptors(labelling_names = nms) + frame |> Index(labelling_names = nms, basis$partition) } #' @export @@ -197,7 +401,7 @@ mp_labels = function(x, labelling_names) { } #' @export -mp_labels.Descriptors = function(x, labelling_names) { +mp_labels.Index = function(x, labelling_names) { if (missing(labelling_names)) return(x$labels()) x$partial_labels(labelling_names) } @@ -212,7 +416,7 @@ mp_zero_vector = function(x, labelling_names, ...) { } #' @export -mp_labels.Relationship = function(x, labelling_names) { +mp_labels.Link = function(x, labelling_names) { x$labels_for[[labelling_names]]() } diff --git a/R/name_utils.R b/R/name_utils.R index 5c87286f..e206c3a9 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -14,8 +14,6 @@ to_labels.character = function(x) valid_dotted$assert(x) #' @export to_labels.Partition = function(x) x$labels() -#' @export -to_labels.Descriptors = function(x) x$labels() #' @export to_labels.data.frame = function(x) StringDataFromFrame(x)$dot()$labels()$value() @@ -63,7 +61,7 @@ to_names.character = function(x) { to_names.Partition = function(x) x$names() #' @export -to_names.Descriptors = function(x) x$labelling_names +to_names.Index = function(x) x$labelling_names #' @export to_names.StringData = function(x) x$undot()$names()$value() @@ -101,7 +99,7 @@ to_name.character = function(x) { to_name.Partition = function(x) x$name() #' @export -to_name.Descriptors = function(x) to_names(x) |> to_name() +to_name.Index = function(x) to_names(x) |> to_name() #' @export to_name.StringData = function(x) x$dot()$names()$value() diff --git a/inst/model_library/sir/derivations.json b/inst/model_library/sir/derivations.json deleted file mode 100644 index 0637a088..00000000 --- a/inst/model_library/sir/derivations.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/inst/model_library/sir/flows.csv b/inst/model_library/sir/flows.csv deleted file mode 100644 index aa5c2e0d..00000000 --- a/inst/model_library/sir/flows.csv +++ /dev/null @@ -1,3 +0,0 @@ -from ,to ,flow ,type -S ,I ,lambda ,per_capita -I ,R ,gamma ,per_capita diff --git a/inst/model_library/sir/infection.csv b/inst/model_library/sir/infection.csv deleted file mode 100644 index e514ad6d..00000000 --- a/inst/model_library/sir/infection.csv +++ /dev/null @@ -1,2 +0,0 @@ -flow ,state ,norm ,type ,flow_partition ,state_partition ,norm_partition ,flow_state_partition ,flow_norm_partition ,state_norm_partition -lambda ,I ,N ,state_normalized ,Epi ,Epi ,Epi , , ,Null diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R new file mode 100644 index 00000000..11e0ac3c --- /dev/null +++ b/inst/model_library/sir/model_structure.R @@ -0,0 +1,54 @@ +library(macpan2) + +state = mp_index(Epi = c("S", "I", "R")) +rates = mp_index(Epi = c("lambda", "gamma", "beta")) + +infection = mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + link = mp_subset(rates, Epi = "lambda") +) + +recovery = mp_join( + from = mp_subset(state, Epi = "I"), + to = mp_subset(state, Epi = "S"), + link = mp_subset(rates, Epi = "gamma") +) + +flow = mp_formula_data(infection, recovery) +flow$reference_index_list$from$partial_labels() +infection$labelling_names_list() + +mp_flow( + flow ~ rates[link] * state[from] + , inflow ~ groupSums(flow, to, state) + , outflow ~ groupSums(flow, from, state) + , state ~ state + inflow - outflow + , formula_data = flow +) + +flows = mp_union(infection, recovery) +flows$labels_frame() +flows$labelling_names_list() + +l = list(infection, recovery) +lapply(l, getElement, "column_map") |> unique() +lapply(l, getElement, "reference_index_list") |> unique() +lapply(l, getElement, "frame") |> do.call(what = rbind) + +mp_flow( + flow ~ rate * from + , inflow ~ groupSums(flow, ) + , links = +) + + +mp_subset(state, Epi = "I") + +transmission = mp_join( + state = mp_subset(state, Epi = "I"), + flow = mp_subset(rates, Epi = "lambda"), + rate = mp_subset(rates, Epi = "beta") +) + +mp_union(infection, recovery, transmission) diff --git a/inst/model_library/sir/settings.json b/inst/model_library/sir/settings.json deleted file mode 100644 index f91b0908..00000000 --- a/inst/model_library/sir/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "labelling_partition" : ["Epi"], - "null_partition" : "Null", - -} diff --git a/inst/model_library/sir/variables.csv b/inst/model_library/sir/variables.csv deleted file mode 100644 index f49c77e1..00000000 --- a/inst/model_library/sir/variables.csv +++ /dev/null @@ -1,6 +0,0 @@ -Epi ,Type ,Vec -S ,susceptible ,state -I ,infectious ,state -R ,immune ,state -lambda ,infection ,flow -gamma ,recovery ,flow diff --git a/inst/model_library/sir/vectors.csv b/inst/model_library/sir/vectors.csv deleted file mode 100644 index 3138d4d6..00000000 --- a/inst/model_library/sir/vectors.csv +++ /dev/null @@ -1,8 +0,0 @@ -vector ,subset ,dimension ,partition -state , ,state ,Vec -state ,infectious_state ,infectious ,Infect -flow , ,flow ,Vec -flow ,infection_flows ,infection ,Infect -trans , ,infectious ,Infect -trans , ,infected ,Infect -N , ,norm ,Vec diff --git a/man/Index.Rd b/man/Index.Rd new file mode 100644 index 00000000..010a15b6 --- /dev/null +++ b/man/Index.Rd @@ -0,0 +1,124 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/index.R +\name{Index} +\alias{Index} +\alias{Index.Partition} +\alias{Index.data.frame} +\alias{Index.Index} +\alias{print.Index} +\alias{names.Index} +\alias{labelling_names.Index} +\alias{to_labels.Index} +\alias{labels.Index} +\title{Index} +\usage{ +Index(partition, labelling_names = names(partition), reference_index = NULL) + +\method{Index}{Partition}(partition, labelling_names = names(partition), reference_index = NULL) + +\method{Index}{data.frame}(partition, labelling_names = names(partition), reference_index = NULL) + +\method{Index}{Index}(partition, labelling_names = names(partition), reference_index = NULL) + +\method{print}{Index}(x, ...) + +\method{names}{Index}(x) + +\method{labelling_names}{Index}(x) + +\method{to_labels}{Index}(x) + +\method{labels}{Index}(x, ...) +} +\arguments{ +\item{partition}{A data frame (or data frame-like) object containing +definitions of the index. This object can be a \code{\link{data.frame}}, +\code{\link{Partition}}, or another \code{\link{Index}} object.} + +\item{labelling_names}{A \code{\link{character}} vector of the names of the +index that will be used to label the model components (i.e. rows) being +described. The \code{labelling_names} cannot have duplicates and must +contain at least one name. The index given by the \code{labelling_names} +must uniquely identify each row.} + +\item{reference_index}{(Advanced) An optional partition to use when +computing subset indices.} + +\item{x}{\code{Index} object.} + +\item{...}{For consistency with existing S3 methods.} +} +\description{ +Make an object to represent descriptions of model components and their +dimensions of variation. \code{Index} objects generalize and wrap +\code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like +object is an entry in the index, and each column provides a description of +the entries. For example, the following \code{Index} object describes the +state variables of an age-structured SIR model. Each row corresponds to a +state variable and each state variable is described by columns \code{Epi} and +\code{Age}. + +\if{html}{\out{
    }}\preformatted{#> Epi Age +#> S young +#> I young +#> R young +#> S old +#> I old +#> R old +}\if{html}{\out{
    }} +} +\section{Methods (by class)}{ +\itemize{ +\item \code{Index(Partition)}: Create a \code{Index} object from a +\code{\link{Partition}} object. These \code{\link{Partition}} objects +wrap \code{\link{data.frame}}s. These data frames must follow certain +restrictions. + +\item \code{Index(data.frame)}: Create a \code{Index} object from a +\code{\link{data.frame}} object that follows certain restrictions +documented in the help page for \code{\link{Partition}}s. + +\item \code{Index(Index)}: Create another \code{Index} object from an +existing \code{Index} object, possibly with new \code{labelling_names}. + +}} +\section{Methods (by generic)}{ +\itemize{ +\item \code{print(Index)}: Print a \code{Index} object. + +\item \code{names(Index)}: Get the names of the index +(i.e. columns) of a \code{Index} object. + +\item \code{labelling_names(Index)}: Retrieve the \code{labelling_names} of +a \code{Index} object. These \code{labelling_names} +are the names of the index that are used to label the model +components. + +\item \code{to_labels(Index)}: Convert a \code{Index} object into +a character vector giving labels associated with each model component +(i.e. row) being described. + +\item \code{labels(Index)}: Convert a \code{Index} object into +a character vector giving labels associated with each model component +(i.e. row) being described. + +}} +\examples{ +sir = data.frame( + Epi = c("S", "I", "R", "D" ), + Vital = c("alive", "alive", "alive", "dead") +) |> Index(labelling_names = "Epi") +age = data.frame( + Age = c("young", "old") +) |> Index() +state_index = mp_cartesian(sir, age) +labels(state_index) +(state_index + |> mp_subset(Vital = "alive") + |> labels() +) + +} +\seealso{ +\code{\link[=mp_index]{mp_index()}} +} diff --git a/man/Link.Rd b/man/Link.Rd new file mode 100644 index 00000000..4e12c608 --- /dev/null +++ b/man/Link.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/link.R +\name{Link} +\alias{Link} +\title{Link} +\usage{ +Link(frame, column_map, reference_index_list) +} +\description{ +Make an object to describe links between the entities in an +\code{\link{Index}}. \code{Link} object are created by operating on existing +\code{\link{Index}} objects. For example, here the \code{\link{mp_join}} +combines two + +\if{html}{\out{
    }}\preformatted{age = mp_index(Age = c("young", "old")) +state = mp_cartesian( + mp_index(Epi = c("S", "I", "R")), + age +) +mp_join( + mp_choose(state, "from", Epi = "S"), + mp_choose(state, "to", Epi = "I"), + from.to = "Age" +) +#> from to +#> S.old I.old +#> S.young I.young +}\if{html}{\out{
    }} +} diff --git a/man/Partition.Rd b/man/Partition.Rd index 94521930..6f49ea3c 100644 --- a/man/Partition.Rd +++ b/man/Partition.Rd @@ -7,7 +7,12 @@ Partition(frame) } \arguments{ -\item{frame}{Data frame representing the partition.} +\item{frame}{Data frame representing the partition. The column names must +consist only of letters, numbers, and start with a letter. The columns of +the data frame must be character vectors such that each value is composed +entirely of letters, numbers, underscore, and must start with a letter +unless it is a blank string. Missing values are not allowed, but blank +strings are. Each row must be unique.} } \value{ Object of class \code{Partition} with the following methods. diff --git a/man/Relationship.Rd b/man/Relationship.Rd deleted file mode 100644 index 15395af4..00000000 --- a/man/Relationship.Rd +++ /dev/null @@ -1,11 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/relationship.R -\name{Relationship} -\alias{Relationship} -\title{Relationship} -\usage{ -Relationship(frame, column_map, labelling_names_list) -} -\description{ -Relationship -} diff --git a/man/mp_index.Rd b/man/mp_index.Rd new file mode 100644 index 00000000..c43797a3 --- /dev/null +++ b/man/mp_index.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/index.R +\name{mp_index} +\alias{mp_index} +\title{Create Index Object} +\usage{ +mp_index(..., labelling_names) +} +\arguments{ +\item{...}{Character vectors to combine into an \code{\link{Index}}. +object.} + +\item{labelling_names}{See \code{\link{Index}}.} +} +\description{ +Create an \code{\link{Index}} object from a set of +character vectors. This function is analogous to the +\code{\link{data.frame}} function for creating data frames. +} +\examples{ +mp_index( + Epi = c("S", "I", "S", "I"), + Age = c("young", "young", "old", "old") +) + +} +\seealso{ +\code{\link[=Index]{Index()}} +} diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index b3e3e725..b780b9b8 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -457,6 +457,24 @@ class ArgList { } } + int get_as_int(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + if (std::holds_alternative>(items_[i])) { + std::vector v = get_as_int_vec(i); + return v[0]; + } else { + matrix m = get_as_mat(i); + int j = m.rows(); + if (j == 1) { + std::vector v = get_as_int_vec(i); + return v[0]; + } + return j; + } + } + matrix operator[](int i) { return get_as_mat(i); } @@ -1490,7 +1508,7 @@ class ExprEvaluator { m = args[0]; v1 = args.get_as_int_vec(1); - rows = args.get_as_int_vec(2)[0]; + rows = args.get_as_int(2); m1 = matrix::Zero(rows, 1); for (int i = 0; i < m.rows(); i++) { diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 0731116e..d984780b 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -3,24 +3,21 @@ library(macpan2helpers) library(oor) library(ggplot2) library(dplyr) -# m = Compartmental(file.path("inst", "starter_models", "sir")) -# m$simulators$tmb(100 -# , state = c(S = 99, I = 1) -# ) -## factor model bases --------------- -state_sir = descriptors( +## factor model indices --------------- + +state_sir = mp_index( Epi = c("S", "I", "R", "D"), Vital = c("alive", "alive", "alive", "dead"), labelling_names = "Epi" ) -flow_rates_sir = descriptors(Epi = c("lambda", "gamma", "mu")) -trans_rates_sir = descriptors(Epi = "beta") -cities = descriptors(Loc = c("cal", "ham", "que")) +flow_rates_sir = mp_index(Epi = c("lambda", "gamma", "mu")) +trans_rates_sir = mp_index(Epi = "beta") +cities = mp_index(Loc = c("cal", "ham", "que")) movement = mp_linear(cities, "Move") -age = descriptors(Age = c("young", "old")) -contact = mp_square(age, "Contact") +age = mp_index(Age = c("young", "old")) +contact = mp_square(age, c("Infectious", "Infection")) ## product model bases ------------- @@ -31,8 +28,8 @@ state = (state_sir flow_rates = mp_union( (flow_rates_sir |> mp_subset(Epi = "lambda") - |> mp_cartesian(age) |> mp_cartesian(cities) + |> mp_cartesian(age) ), (flow_rates_sir |> mp_subset(Epi = "mu") @@ -42,17 +39,123 @@ flow_rates = mp_union( movement ) trans_rates = (trans_rates_sir - |> mp_subset(Epi = "beta") |> mp_cartesian(cities) |> mp_cartesian(contact) ) +mp_join( + alive = mp_subset(state, Vital = "alive"), + N = mp_select(state, "Loc.Age"), + alive.N = "Loc.Age" +) + +mp_choose(mp_select(state, "Loc.Age"), "N") + + +yy = Vector(state) +yy$numbers() +yy$set_numbers(Loc = c(cal = 1000, ham = 2000, que = 3000), Epi = "S") +yy$set_numbers(Epi = c(I = 1), Loc.Age = "ham.young") + +infection_flows = mp_join( + to = mp_subset(state, Epi = "S") + , from = mp_subset(state, Epi = "I") + , rate = mp_subset(flow_rates, Epi = "lambda") + , to.rate = "Age.Loc" + , rate.from = "Age.Loc" + , from.to = "Age.Loc" +) + +influence = mp_join( + state = mp_subset(state, Epi = "I") + , flow = mp_subset(flow_rates, Epi = "lambda") + , trans = mp_subset(trans_rates, Epi = "beta") + + ## can't infect someone at a different location + , state.flow = "Loc" + + ## location and age specific transmission rates + ## for each infectious state + , state.trans = "Loc.Age" ~ "Loc.AgeInfectious" + + ## location and age specific transmission rates + ## for each infection flow + , flow.trans = "Loc.Age" ~ "Loc.AgeInfection" +) + +# mp_aggregate = function( +# formula, index +# ) + +# mp_expr_group_sum(state +# , stratify_by = "Loc.Age" +# , output_name = "N" +# , vector_name = "state" +# , subset_name = "alive" +# , grouping_name = sprintf("%s_groups", output_name) +# , length_name = sprintf("%s_length", output_name) +# , Vital = "alive" +# ) + +mp_aggregate(N ~ groupSums(state) + , group_by = "Loc.Age" + , index = state + , Vital = "alive" +) + + + + +trans_decomp = mp_decompose( + beta ~ contact * infectivity * susceptibility + , index = mp_subset(trans_rates, Epi = "beta") + , decomp_name = "trans" + + ## define dimensions over-which each component of the + ## decomposition will vary. these all must be dimensions + ## in the index. + , contact = "Loc.AgeInfectious.AgeInfection" + , infectivity = "Loc" + , susceptibility = "AgeInfection" +) +trans_decomp$linked_indices + +trans_decomp$formula +trans_decomp$input_formula +macpan2:::update_formula(g ~ y * x * z, g ~ friend) + + + +hh = function(e) { + e = substitute(e) + eval(e, list(x = as.symbol("lkjg"), y = as.symbol("LKJDSF"))) +} +hh(x + y) + +x = function(e, ...) { + self = new.env(parent = emptyenv()) + self$`*` = function(...) paste(..., sep = " * ") + self$e +} +y = x(A * B * C, A[friend], "A") +y$e + +trans_decomp$formula +trans_decomp$int_vecs +trans_decomp$labels_for$beta() +trans_decomp$labels_for$infectivity() + +mp_indices( + trans_decomp$labels_for$susceptibility(), + trans_decomp$partition_for$susceptibility()$labels() +) + N_expr = mp_expr_group_sum(state , "Loc.Age" , "N", "state", "alive", "alive_groups", "length_N" , Vital = "alive" ) -N_expr$strata$labels() + xx = mp_join( mp_choose(N_expr$strata, "N"), @@ -201,15 +304,15 @@ flow_file = CSVReader("inst/model_library/sir_age/flows.csv")$read() vars_file = CSVReader("inst/model_library/sir_age/variables.csv")$read() settings_file = JSONReader("inst/model_library/sir_age/settings.json")$read() -make_descriptors = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { +make_mp_index = function(vec_name, vars_table, settings_list, vec_part_field, lab_part_field) { vec_labs = to_labels(vars_table[settings_list[[vec_part_field]]]) - descriptors( + mp_index( vars_table[vec_labs == vec_name, , drop = FALSE], labelling_names = to_name(settings_list[[lab_part_field]]) ) } -state = make_descriptors("state", vars_file, settings_file, "vec_partition", "labelling_partition") -flow_rates = make_descriptors("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") +state = make_mp_index("state", vars_file, settings_file, "vec_partition", "labelling_partition") +flow_rates = make_mp_index("flow_rates", vars_file, settings_file, "vec_partition", "labelling_partition") @@ -438,7 +541,7 @@ prod = Quantities( cartesian(sir$flow_rates, loc$state_variables), loc_flow_rates ), - labelling_descriptors = "Epi.Loc.Move" + labelling_index = "Epi.Loc.Move" ) ## learned lots about joining and a bit about filtering: diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 3a0f88ff..dfa5dce1 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -458,6 +458,25 @@ class ArgList { } } + int get_as_int(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + if (std::holds_alternative>(items_[i])) { + std::vector v = get_as_int_vec(i); + return v[0]; + } else { + matrix m = get_as_mat(i); + int j = m.rows(); + if (j == 1) { + std::vector v = get_as_int_vec(i); + // std::cout << "HI" << v[0] << std::endl; + return v[0]; + } + return j; + } + } + matrix operator[](int i) { return get_as_mat(i); } @@ -1491,7 +1510,7 @@ class ExprEvaluator { m = args[0]; v1 = args.get_as_int_vec(1); - rows = args.get_as_int_vec(2)[0]; + rows = args.get_as_int(2); m1 = matrix::Zero(rows, 1); for (int i = 0; i < m.rows(); i++) { From da7186baf9aead38d11dbd469b54d9d12776e631 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 04:16:16 -0400 Subject: [PATCH 046/332] definition and windows compiling --- .github/workflows/R-CMD-check.yaml | 2 ++ Makefile | 6 +++--- R/formula_data.R | 4 ++-- R/link.R | 28 +++++++++++++++++----------- R/mp.R | 16 ++++++++-------- man/Link.Rd | 2 +- misc/experiments/refactorcpp.R | 10 +++++++--- src/macpan2.cpp | 1 - 8 files changed, 40 insertions(+), 29 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index bf5284cf..2023aed5 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -52,3 +52,5 @@ jobs: - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true + build_args: 'c("--no-manual", "--no-build-vignettes")' + error_on: '"error"' diff --git a/Makefile b/Makefile index 686a47ef..642d69ce 100644 --- a/Makefile +++ b/Makefile @@ -114,15 +114,15 @@ doc-update: R/*.R misc/dev/dev.cpp pkg-build:: ../macpan2_$(VERSION).tar.gz ../macpan2_$(VERSION).tar.gz: DESCRIPTION man/*.Rd R/*.R src/*.cpp tests/testthat/test-*.R tests/testthat.R inst/starter_models/**/*.csv inst/starter_models/**/*.json - R CMD build --no-build-vignettes . + cd .. && R CMD build --no-build-vignettes macpan2 pkg-check: ../macpan2_$(VERSION).tar.gz - R CMD check ../macpan2_$(VERSION).tar.gz + cd .. && R CMD check macpan2_$(VERSION).tar.gz pkg-install: ../macpan2_$(VERSION).tar.gz - R CMD INSTALL --no-multiarch --install-tests ../macpan2_$(VERSION).tar.gz + cd .. && R CMD INSTALL --no-multiarch --install-tests macpan2_$(VERSION).tar.gz compile-dev: misc/dev/dev.cpp diff --git a/R/formula_data.R b/R/formula_data.R index 9bcb50be..b3000c49 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -1,8 +1,8 @@ -FormulaData = function(frame, reference_index_list) { +FormulaData = function(frame, reference_index_list, labelling_names_list) { self = Base() self$frame = frame self$reference_index_list = reference_index_list - + self$labelling_names_list = labelling_names_list return_object(self, "FormulaData") } diff --git a/R/link.R b/R/link.R index 858db46b..e25e4437 100644 --- a/R/link.R +++ b/R/link.R @@ -18,14 +18,15 @@ #' ``` #' #' @export -Link = function(frame, column_map, reference_index_list) { +Link = function(frame, column_map, reference_index_list, labelling_names_list) { self = Base() self$frame = frame self$column_map = column_map self$reference_index_list = reference_index_list - self$labelling_names_list = function() { - lapply(self$reference_index_list, getElement, "labelling_names") - } + self$labelling_names_list = labelling_names_list + # function() { + # lapply(self$reference_index_list, getElement, "labelling_names") + # } self$table_names = function() names(self$column_map) self$labels_for = list() self$labels_frame = function() { @@ -65,6 +66,7 @@ Link = function(frame, column_map, reference_index_list) { Link(self$frame[i, , drop = FALSE] , column_map = self$column_map , reference_index_list = self$reference_index_list + , labelling_names_list = self$labelling_names_list ) } self$partition = self$partition_for[[1L]]() ## hack! should probably have a method and then change the partition field in Index to a method as well @@ -102,10 +104,10 @@ FrameGetter = function(link, dimension_name) { self$get_partition = function() self$get_frame() |> Partition() self$get_index = function() Index( self$get_partition(), - self$link$labelling_names_list()[[self$dimension_name]] + self$link$labelling_names_list[[self$dimension_name]] ) self$get_labels = function() { - i = self$link$labelling_names_list()[[self$dimension_name]] + i = self$link$labelling_names_list[[self$dimension_name]] f = self$get_frame()[, i, drop = FALSE] l = as.list(f) paste_args = c(l, sep = ".") @@ -125,7 +127,7 @@ initial_reference_index_list = function(index, dimension_name) { setNames(list(index), dimension_name) } -## TODO XXX + initial_labelling_names_list = function(labelling_names, dimension_name) { setNames( list(labelling_names), @@ -205,6 +207,7 @@ merge_util = function(x, y, by.x, by.y) { z_column_map = c(x_cmap, y_cmap) z_reference_index_list = c(x$reference_index_list, y$reference_index_list) + z_lab_names_list = c(x$labelling_names_list, y$labelling_names_list) ## ---- ## wrap up the result with provenance-preserving column map @@ -212,7 +215,8 @@ merge_util = function(x, y, by.x, by.y) { Link( z, z_column_map, - z_reference_index_list + z_reference_index_list, + z_lab_names_list ) } @@ -230,10 +234,11 @@ filter_by_list = function(x_orig, y_orig, by_list) { } #' @export -init_merge = function(frame, dimension_name, reference_index) { +init_merge = function(frame, dimension_name, reference_index, labelling_names) { Link(frame , initial_column_map(names(frame), dimension_name) , initial_reference_index_list(reference_index, dimension_name) + , initial_labelling_names_list(labelling_names, dimension_name) ) } @@ -288,7 +293,7 @@ explicit_provenance = function(x, col_nm) { ) f = x$frame - l = x$labelling_names_list() + l = x$labelling_names_list orig_col = f[[col_nm]] for (tab_nm in tabs_to_fix) { @@ -297,6 +302,7 @@ explicit_provenance = function(x, col_nm) { m[[tab_nm]][[col_nm]] = new_col_nm } f[[col_nm]] = NULL + ## TODO: update with four-arg form of Link Link(f, m, l) } @@ -370,7 +376,7 @@ print.summary.Link = function(x, ...) { names.Link = function(x) names(x$frame) #' @export -labelling_names.Link = function(x) x$labelling_names_list() +labelling_names.Link = function(x) x$labelling_names_list link_format_picker = function(x diff --git a/R/mp.R b/R/mp.R index 7bdcbd7d..ceda672f 100644 --- a/R/mp.R +++ b/R/mp.R @@ -144,7 +144,7 @@ mp_union.Link = function(...) { ) |> stop() } ## TODO: should really be checking for reference_index_list - labelling_names_list = method_apply(l, "labelling_names_list") |> unique() + labelling_names_list = lapply(l, getElement, "labelling_names_list") |> unique() if (length(labelling_names_list) != 1L) { msg_colon( msg( @@ -157,7 +157,7 @@ mp_union.Link = function(...) { ) |> stop() } frame = mp_rbind(...) - FormulaData(frame, l[[1L]]$reference_index_list) + FormulaData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_names_list) } #' @export @@ -194,7 +194,7 @@ mp_choose = function(x, subset_name, ...) { p = p$filter(vals, .wrt = cc) } } - init_merge(p$frame(), subset_name, x$reference_index()) + init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_names) } #' @export @@ -205,7 +205,7 @@ mp_choose_out = function(x, subset_name, ...) { vals = l[[cc]] p = p$filter_out(vals, .wrt = cc) } - init_merge(p$frame(), subset_name, x$labelling_names) + init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_names) } #' @export @@ -375,10 +375,10 @@ mp_rename = function(x, ...) { } #' @export -mp_select = function(basis, grouping_dimension) { - frame = basis$partition$select(to_names(grouping_dimension))$frame() - nms = names(frame)[names(frame) %in% basis$labelling_names] - frame |> Index(labelling_names = nms, basis$partition) +mp_select = function(index, grouping_dimension) { + frame = index$partition$select(to_names(grouping_dimension))$frame() + nms = names(frame)[names(frame) %in% index$labelling_names] + frame |> Index(labelling_names = nms, index) } #' @export diff --git a/man/Link.Rd b/man/Link.Rd index 4e12c608..aba1a54d 100644 --- a/man/Link.Rd +++ b/man/Link.Rd @@ -4,7 +4,7 @@ \alias{Link} \title{Link} \usage{ -Link(frame, column_map, reference_index_list) +Link(frame, column_map, reference_index_list, labelling_names_list) } \description{ Make an object to describe links between the entities in an diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index d984780b..78675e46 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -43,15 +43,19 @@ trans_rates = (trans_rates_sir |> mp_cartesian(contact) ) -mp_join( +xx = mp_join( alive = mp_subset(state, Vital = "alive"), N = mp_select(state, "Loc.Age"), alive.N = "Loc.Age" ) -mp_choose(mp_select(state, "Loc.Age"), "N") +mp_labels +xx$reference_index_list$N$labelling_names +yy = mp_formula_data(xx) +yy$reference_index_list + yy = Vector(state) yy$numbers() yy$set_numbers(Loc = c(cal = 1000, ham = 2000, que = 3000), Epi = "S") @@ -118,8 +122,8 @@ trans_decomp = mp_decompose( , infectivity = "Loc" , susceptibility = "AgeInfection" ) -trans_decomp$linked_indices +trans_decomp trans_decomp$formula trans_decomp$input_formula macpan2:::update_formula(g ~ y * x * z, g ~ friend) diff --git a/src/macpan2.cpp b/src/macpan2.cpp index dfa5dce1..e0951f41 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -470,7 +470,6 @@ class ArgList { int j = m.rows(); if (j == 1) { std::vector v = get_as_int_vec(i); - // std::cout << "HI" << v[0] << std::endl; return v[0]; } return j; From f55678f838f95c5ac51a6a83c9018fac052a10e0 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 04:31:43 -0400 Subject: [PATCH 047/332] dev tooling --- .github/workflows/R-CMD-check.yaml | 2 +- Makefile | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 2023aed5..c155c79d 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -53,4 +53,4 @@ jobs: with: upload-snapshots: true build_args: 'c("--no-manual", "--no-build-vignettes")' - error_on: '"error"' + error-on: '"error"' diff --git a/Makefile b/Makefile index 642d69ce..99fcb8a4 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,7 @@ TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") all: - make src-update - make enum-update - make enum-meth-update - make engine-doc-update - make doc-update - make pkg-build - make pkg-install + make full-install make pkg-check From 04c44dcb8bcad6d7bf69e8196284601c6967f3a0 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 04:40:00 -0400 Subject: [PATCH 048/332] working to get windows binaries --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index c155c79d..925e3eda 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -53,4 +53,4 @@ jobs: with: upload-snapshots: true build_args: 'c("--no-manual", "--no-build-vignettes")' - error-on: '"error"' + error-on: '"never"' From fd0a84e353ff8480396e1b0f6656bd2258697500 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 08:30:59 -0400 Subject: [PATCH 049/332] intermediate --- Makefile | 9 ++++++--- NAMESPACE | 1 - R/mp.R | 6 ------ R/settings.R | 6 ++++-- inst/starter_models/sir/settings.json | 4 ++-- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 99fcb8a4..02dc038f 100644 --- a/Makefile +++ b/Makefile @@ -104,18 +104,21 @@ R/engine_functions.R: src/macpan2.cpp doc-update: R/*.R misc/dev/dev.cpp echo "suppressWarnings(roxygen2::roxygenize(\".\",roclets = c(\"collate\", \"rd\", \"namespace\")))" | R --slave + touch doc-update pkg-build:: ../macpan2_$(VERSION).tar.gz -../macpan2_$(VERSION).tar.gz: DESCRIPTION man/*.Rd R/*.R src/*.cpp tests/testthat/test-*.R tests/testthat.R inst/starter_models/**/*.csv inst/starter_models/**/*.json +../macpan2_$(VERSION).tar.gz: DESCRIPTION man/*.Rd R/*.R src/*.cpp tests/testthat/test-*.R tests/testthat.R inst/starter_models/**/*.csv inst/starter_models/**/*.json doc-update cd .. && R CMD build --no-build-vignettes macpan2 + touch pkg-build -pkg-check: ../macpan2_$(VERSION).tar.gz +pkg-check: ../macpan2_$(VERSION).tar.gz pkg-build cd .. && R CMD check macpan2_$(VERSION).tar.gz + touch pkg-check -pkg-install: ../macpan2_$(VERSION).tar.gz +pkg-install: ../macpan2_$(VERSION).tar.gz pkg-build cd .. && R CMD INSTALL --no-multiarch --install-tests macpan2_$(VERSION).tar.gz diff --git a/NAMESPACE b/NAMESPACE index ba757651..a96279af 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -145,7 +145,6 @@ export(mp_indices) export(mp_join) export(mp_labels) export(mp_linear) -export(mp_map_to_selection) export(mp_rbind) export(mp_rename) export(mp_select) diff --git a/R/mp.R b/R/mp.R index ceda672f..9ddd56f4 100644 --- a/R/mp.R +++ b/R/mp.R @@ -11,12 +11,6 @@ mp = function(mp_func) { f } -#' @export -mp_map_to_selection = function(x, filter_cond, select_cond) { - filter = mp("filter")(x, "filter", filter_cond, select_cond) - replacement = mp_select(filter, "select", names(select_cond)) - mp_join(filter, replacement, filter.select = names(select_cond)) -} #' @export mp_cartesian = function(x, y) { diff --git a/R/settings.R b/R/settings.R index 374c007d..2cf8b57e 100644 --- a/R/settings.R +++ b/R/settings.R @@ -27,13 +27,15 @@ Settings = function(model) { |> stop() ) } - nms[which.min(i)] # take the first synonym found + nms[min(which(i))] # take the first synonym found } self$lab_part = function() self$.resolve_synonyms("lab_part") self$name = function() to_name(self$.settings()[[self$lab_part()]]) self$names = function() to_names(self$.settings()[[self$lab_part()]]) self$null = function() self$.settings()$null_partition - self$var_part = function() self$.resolve_synonyms("var_part") + self$var_part = function() { + self$.settings()[[self$.resolve_synonyms("var_part")]] + } self$variable = function(type) { type_nm = sprintf("%s_variables", type) var_nms = self$.settings()[[type_nm]] diff --git a/inst/starter_models/sir/settings.json b/inst/starter_models/sir/settings.json index 94152e70..f4eb7eb6 100644 --- a/inst/starter_models/sir/settings.json +++ b/inst/starter_models/sir/settings.json @@ -2,8 +2,8 @@ "required_partitions" : ["Epi"], "null_partition" : "Null", "vector_partition" : ["Vec"], - "state" : ["state"], - "flow_rates" : ["flow_rates"], + "state_variables" : ["state"], + "flow_variables" : ["flow_rates"], "infection_flow_variables" : ["foi"], "infectious_state_variables" : ["I"] } From b87902214af5c3f351fe0f320eff9b36b992a173 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 08:31:13 -0400 Subject: [PATCH 050/332] intermediate --- doc-update | 0 pkg-build | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc-update create mode 100644 pkg-build diff --git a/doc-update b/doc-update new file mode 100644 index 00000000..e69de29b diff --git a/pkg-build b/pkg-build new file mode 100644 index 00000000..e69de29b From 02f8c1da6f36cd949c820a9cf1795e4fe05fdba4 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 1 Nov 2023 11:30:13 -0400 Subject: [PATCH 051/332] avoid variants for c++14 compatibility --- misc/dev/dev.cpp | 87 +++++++++++++++++++++++++++++++++++++++--------- src/macpan2.cpp | 87 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 144 insertions(+), 30 deletions(-) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index b780b9b8..d9711fad 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -12,7 +12,6 @@ // https://github.com/kaskr/adcomp/wiki/Development#distributing-code #include #include -#include //////////////////////////////////////////////////////////////////////////////// @@ -417,15 +416,24 @@ matrix getNthMat( template class ArgList { public: - using ItemType = std::variant, std::vector>; + enum class ItemType { Matrix, IntVector }; ArgList(int size) : items_(size), size_(size) {} - void set(int index, const ItemType& item) { + void set(int index, const matrix& mat) { if (index < 0 || index >= size_) { throw std::out_of_range("Index out of range"); } - items_[index] = item; + items_[index].type = ItemType::Matrix; + items_[index].mat = mat; + } + + void set(int index, const std::vector& intVec) { + if (index < 0 || index >= size_) { + throw std::out_of_range("Index out of range"); + } + items_[index].type = ItemType::IntVector; + items_[index].intVec = intVec; } matrix get_as_mat(int i) const { @@ -433,8 +441,8 @@ class ArgList { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { - return std::get>(items_[i]); + if (items_[i].type == ItemType::Matrix) { + return items_[i].mat; } else { throw std::runtime_error("Item at index is not a matrix"); } @@ -445,36 +453,75 @@ class ArgList { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { - return std::get>(items_[i]); + if (items_[i].type == ItemType::IntVector) { + return items_[i].intVec; } else { matrix m = get_as_mat(i); std::vector v(m.rows()); - for (int i=0; i= size_) { + // throw std::out_of_range("Index out of range"); + // } + // items_[index] = item; + // } + // + // matrix get_as_mat(int i) const { + // if (i < 0 || i >= items_.size()) { + // throw std::out_of_range("Index out of range"); + // } + // + // if (std::holds_alternative>(items_[i])) { + // return std::get>(items_[i]); + // } else { + // throw std::runtime_error("Item at index is not a matrix"); + // } + // } + // + // std::vector get_as_int_vec(int i) { + // if (i < 0 || i >= items_.size()) { + // throw std::out_of_range("Index out of range"); + // } + // + // if (std::holds_alternative>(items_[i])) { + // return std::get>(items_[i]); + // } else { + // matrix m = get_as_mat(i); + // std::vector v(m.rows()); + // for (int i=0; i= items_.size()) { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { + + if (items_[i].type == ItemType::IntVector) { std::vector v = get_as_int_vec(i); return v[0]; } else { matrix m = get_as_mat(i); int j = m.rows(); if (j == 1) { - std::vector v = get_as_int_vec(i); - return v[0]; + std::vector v = get_as_int_vec(i); + return v[0]; } return j; } } + matrix operator[](int i) { return get_as_mat(i); } @@ -630,12 +677,22 @@ class ArgList { return error_code_; } - private: - std::vector items_; + struct Item { + ItemType type; + matrix mat; + std::vector intVec; + }; + + std::vector items_; int size_; int error_code_ = 0; // Initialize the error code to 0 (no error) by default }; +// private: +// std::vector items_; +// int size_; +// int error_code_ = 0; // Initialize the error code to 0 (no error) by default +// }; template diff --git a/src/macpan2.cpp b/src/macpan2.cpp index e0951f41..ae30ab7d 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -13,7 +13,6 @@ // https://github.com/kaskr/adcomp/wiki/Development#distributing-code #include #include -#include //////////////////////////////////////////////////////////////////////////////// @@ -418,15 +417,24 @@ matrix getNthMat( template class ArgList { public: - using ItemType = std::variant, std::vector>; + enum class ItemType { Matrix, IntVector }; ArgList(int size) : items_(size), size_(size) {} - void set(int index, const ItemType& item) { + void set(int index, const matrix& mat) { if (index < 0 || index >= size_) { throw std::out_of_range("Index out of range"); } - items_[index] = item; + items_[index].type = ItemType::Matrix; + items_[index].mat = mat; + } + + void set(int index, const std::vector& intVec) { + if (index < 0 || index >= size_) { + throw std::out_of_range("Index out of range"); + } + items_[index].type = ItemType::IntVector; + items_[index].intVec = intVec; } matrix get_as_mat(int i) const { @@ -434,8 +442,8 @@ class ArgList { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { - return std::get>(items_[i]); + if (items_[i].type == ItemType::Matrix) { + return items_[i].mat; } else { throw std::runtime_error("Item at index is not a matrix"); } @@ -446,36 +454,75 @@ class ArgList { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { - return std::get>(items_[i]); + if (items_[i].type == ItemType::IntVector) { + return items_[i].intVec; } else { matrix m = get_as_mat(i); std::vector v(m.rows()); - for (int i=0; i= size_) { + // throw std::out_of_range("Index out of range"); + // } + // items_[index] = item; + // } + // + // matrix get_as_mat(int i) const { + // if (i < 0 || i >= items_.size()) { + // throw std::out_of_range("Index out of range"); + // } + // + // if (std::holds_alternative>(items_[i])) { + // return std::get>(items_[i]); + // } else { + // throw std::runtime_error("Item at index is not a matrix"); + // } + // } + // + // std::vector get_as_int_vec(int i) { + // if (i < 0 || i >= items_.size()) { + // throw std::out_of_range("Index out of range"); + // } + // + // if (std::holds_alternative>(items_[i])) { + // return std::get>(items_[i]); + // } else { + // matrix m = get_as_mat(i); + // std::vector v(m.rows()); + // for (int i=0; i= items_.size()) { throw std::out_of_range("Index out of range"); } - if (std::holds_alternative>(items_[i])) { + + if (items_[i].type == ItemType::IntVector) { std::vector v = get_as_int_vec(i); return v[0]; } else { matrix m = get_as_mat(i); int j = m.rows(); if (j == 1) { - std::vector v = get_as_int_vec(i); - return v[0]; + std::vector v = get_as_int_vec(i); + return v[0]; } return j; } } + matrix operator[](int i) { return get_as_mat(i); } @@ -631,12 +678,22 @@ class ArgList { return error_code_; } - private: - std::vector items_; + struct Item { + ItemType type; + matrix mat; + std::vector intVec; + }; + + std::vector items_; int size_; int error_code_ = 0; // Initialize the error code to 0 (no error) by default }; +// private: +// std::vector items_; +// int size_; +// int error_code_ = 0; // Initialize the error code to 0 (no error) by default +// }; template From d9699e7d3b6b0551c2785fe4eb0fd793b73ad8f3 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 3 Nov 2023 02:57:57 -0400 Subject: [PATCH 052/332] new model definitions! with sir example --- NAMESPACE | 16 +- R/expr_list.R | 11 + R/formula_data.R | 120 +++++++- R/index.R | 25 +- R/index_to_tmb.R | 45 +++ R/link.R | 42 ++- R/lists.R | 11 + R/mp.R | 52 ++-- R/simple_sims.R | 11 +- R/vector.R | 94 ++++++- inst/model_library/sir/model_structure.R | 85 +++--- inst/model_library/sir/numerical_inputs.R | 22 ++ inst/model_library/sir/simulator.R | 11 + man/IndexedExpressions.Rd | 29 ++ man/Link.Rd | 5 +- man/mp_formula_data.Rd | 17 ++ man/simple_sims.Rd | 2 +- misc/dev/dev.cpp | 101 ------- misc/experiments/refactorcpp.R | 318 +++++++++++++++++----- src/macpan2.cpp | 101 ------- 20 files changed, 756 insertions(+), 362 deletions(-) create mode 100644 R/index_to_tmb.R create mode 100644 inst/model_library/sir/numerical_inputs.R create mode 100644 inst/model_library/sir/simulator.R create mode 100644 man/IndexedExpressions.Rd create mode 100644 man/mp_formula_data.Rd diff --git a/NAMESPACE b/NAMESPACE index a96279af..b5664206 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,8 +3,12 @@ S3method(Index,Index) S3method(Index,Partition) S3method(Index,data.frame) +S3method(Vector,Index) +S3method(Vector,data.frame) +S3method(Vector,numeric) S3method(as.data.frame,Index) S3method(as.data.frame,Link) +S3method(as.matrix,Vector) S3method(c,String) S3method(c,StringData) S3method(head,Link) @@ -12,6 +16,8 @@ S3method(labelling_names,Index) S3method(labelling_names,Link) S3method(labels,Index) S3method(length,Vector) +S3method(mp_index,character) +S3method(mp_index,data.frame) S3method(mp_labels,Index) S3method(mp_labels,Link) S3method(mp_union,Index) @@ -22,7 +28,6 @@ S3method(names,Link) S3method(names,MatsList) S3method(names,MethList) S3method(names,Partition) -S3method(print,FormulaData) S3method(print,Index) S3method(print,Link) S3method(print,MathExpression) @@ -76,6 +81,7 @@ export(FlowExpander) export(Flows) export(Formula) export(Index) +export(IndexedExpressions) export(Infection) export(IntVecs) export(JSONReader) @@ -138,8 +144,11 @@ export(mp_choose_out) export(mp_decompose) export(mp_expr_binop) export(mp_expr_group_sum) +export(mp_expr_list) export(mp_formula_data) +export(mp_group) export(mp_index) +export(mp_indexed_exprs) export(mp_indicator) export(mp_indices) export(mp_join) @@ -147,12 +156,15 @@ export(mp_labels) export(mp_linear) export(mp_rbind) export(mp_rename) -export(mp_select) +export(mp_set_numbers) +export(mp_setdiff) export(mp_square) export(mp_subset) export(mp_symmetric) +export(mp_tmb_simulator) export(mp_triangle) export(mp_union) +export(mp_vector) export(mp_zero_vector) export(nlist) export(not_all_equal) diff --git a/R/expr_list.R b/R/expr_list.R index 8845f99d..21916f37 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -159,6 +159,14 @@ ExprList = function( if (is.null(nms)) nms = rep("", length(self$formula_list())) nms } + self$all_formula_vars = function() { + (self$formula_list() + |> lapply(formula_components) + |> lapply(getElement, "variables") + |> unlist(use.names = FALSE, recursive = FALSE) + |> unique() + ) + } self$data_arg = function() { r = c( @@ -230,3 +238,6 @@ ExprList = function( return_object(self, "ExprList") } + +#' @export +mp_expr_list = ExprList diff --git a/R/formula_data.R b/R/formula_data.R index b3000c49..51fc81b6 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -1,12 +1,120 @@ -FormulaData = function(frame, reference_index_list, labelling_names_list) { +FormulaData = function(...) { self = Base() - self$frame = frame - self$reference_index_list = reference_index_list - self$labelling_names_list = labelling_names_list + self$link_list = list(...) + + labelling_names_list = (self$link_list + |> lapply(getElement, "labelling_names_list") + |> unname() + |> unique() + ) + stopifnot(length(labelling_names_list) == 1L) + self$labelling_names_list = labelling_names_list[[1L]] + + reference_index_list = (self$link_list + |> lapply(getElement, "reference_index_list") + |> unname() + |> unique() + ) + stopifnot(length(reference_index_list) == 1L) + self$reference_index_list = reference_index_list[[1L]] + + table_names = (self$link_list + |> method_apply("table_names") + |> unname() + |> unique() + ) + stopifnot(length(table_names) == 1L) + self$table_names = table_names[[1L]] + + self$labels_frame = function() { + (self$link_list + |> method_apply("labels_frame") + |> bind_rows() + ) + } + + self$positions_frame = function(zero_based = FALSE) { + positions_list = list() + for (i in seq_along(self$link_list)) { + positions_list[[i]] = list() + for (d in self$table_names) { + positions_list[[i]][[d]] = self$link_list[[i]]$positions_for[[d]](zero_based) + } + positions_list[[i]] = as.data.frame(positions_list[[i]]) + } + bind_rows(positions_list) + } + return_object(self, "FormulaData") } +#' #' @export +#' print.FormulaData = function(x, ...) { +#' print(x$frame, row.names = FALSE) +#' } + + #' @export -print.FormulaData = function(x, ...) { - print(x$frame, row.names = FALSE) +mp_formula_data = function(...) FormulaData(...) + +#' Indexed Expressions +#' +#' @param ... Formula objects that reference the columns in the +#' \code{index_data}, the vectors in \code{vector_list} and the matrices +#' in \code{unstructured_matrix_list}. +#' @param index_data An object produced using \code{\link{mp_formula_data}}. +#' @param vector_list Named list of objected produced using +#' \code{\link{mp_vector}}. +#' @param unstructured_matrix_list Named list of objects that can be coerced +#' to a matrix. +#' +#' @export +IndexedExpressions = function(... + , index_data + , vector_list = list() + , unstructured_matrix_list = list() + ) { + self = Base() + self$formulas = list(...) + self$index_data = index_data + self$vector_list = vector_list + self$unstructured_matrix_list = unstructured_matrix_list + self$int_vecs = function(zero_based = FALSE) { + self$index_data$positions_frame(zero_based) |> as.list() + } + self$mats_list = function() { + all_vars = (self$formulas + |> lapply(macpan2:::formula_components) + |> lapply(getElement, "variables") + |> unlist(use.names = FALSE, recursive = FALSE) + |> unique() + ) + derived = setdiff(all_vars, c( + names(self$vector_list), + self$index_data$table_names, + names(self$unstructured_matrix_list) + )) + derived = (empty_matrix + |> list() + |> rep(length(derived)) + |> setNames(derived) + ) + vectors = method_apply(self$vector_list, "numbers") + unstruc = self$unstructured_matrix_list + c(vectors, unstruc, derived) + } + self$simulate = function(time_steps = 1L) { + simple_sims( + self$formulas, + time_steps, + self$int_vecs(zero_based = TRUE), + self$mats_list() + ) + } + return_object(self, "IndexedExpressions") } + + +#' @export +mp_indexed_exprs = IndexedExpressions + diff --git a/R/index.R b/R/index.R index fc8db9c4..5fa0fd29 100644 --- a/R/index.R +++ b/R/index.R @@ -80,6 +80,19 @@ Index.Partition = function(partition ## Standard Methods self$labels = function() self$partition$select(self$labelling_names)$labels() self$partial_labels = function(...) self$partition$partial_labels(...) + self$reference_labels = function() { + self$reference_index()$partial_labels(self$labelling_names) + } + self$reference_positions = function(zero_based = FALSE) { + i = match(self$reference_labels(), self$labels()) + if (zero_based) i = i - 1L + i + } + self$positions = function(zero_based = FALSE) { + i = match(self$labels(), self$reference_labels()) + if (zero_based) i = i - 1L + i + } return_object(self, "Index") } @@ -154,8 +167,18 @@ labels.Index = function(x, ...) x$labels() #' ) #' #' @export -mp_index = function(..., labelling_names) { +mp_index = function(..., labelling_names) UseMethod("mp_index") + +#' @export +mp_index.character = function(..., labelling_names) { f = data.frame(...) if (missing(labelling_names)) labelling_names = names(f) Index(f, to_names(labelling_names)) } + +#' @export +mp_index.data.frame = function(..., labelling_names) { + f = list(...)[[1L]] + if (missing(labelling_names)) labelling_names = names(f) + Index(f, to_names(labelling_names)) +} diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R new file mode 100644 index 00000000..322b05fc --- /dev/null +++ b/R/index_to_tmb.R @@ -0,0 +1,45 @@ +#' @export +mp_tmb_simulator = function(expr_list = ExprList() + , index_data = list() + , indexed_vecs = list() + , unstruc_mats = list() + , time_steps = 0L + , mats_to_save = names(indexed_vecs) + , mats_to_return = names(indexed_vecs) + , ... +) { + int_vecs = (index_data + |> method_apply("positions_frame", zero_based = TRUE) + |> lapply(as.list) + |> unname() + |> unlist(recursive = FALSE) + ) + indexed_mats = lapply(indexed_vecs, as.matrix) + + all_vars = expr_list$all_formula_vars() + + derived_nms = setdiff(all_vars, c( + names(int_vecs), names(indexed_mats), names(unstruc_mats) + )) + + derived_mats = (empty_matrix + |> list() + |> rep(length(derived_nms)) + |> setNames(derived_nms) + ) + + mats = c(indexed_mats, unstruc_mats, derived_mats) + mats_list_options = list( + .mats_to_save = mats_to_save, + .mats_to_return = mats_to_return + ) + engine_methods = EngineMethods(int_vecs = do.call(IntVecs, int_vecs)) + tmb_model = TMBModel( + init_mats = do.call(MatsList, c(mats, mats_list_options)) + , expr_list = expr_list + , engine_methods = engine_methods + , time_steps = Time(time_steps) + , ... + ) + tmb_model$simulator() +} diff --git a/R/link.R b/R/link.R index e25e4437..6c0c8f08 100644 --- a/R/link.R +++ b/R/link.R @@ -35,15 +35,21 @@ Link = function(frame, column_map, reference_index_list, labelling_names_list) { l |> as.data.frame() } self$frame_for = list() - self$labels_for = list() self$partition_for = list() self$index_for = list() + self$labels_for = list() + self$reference_labels_for = list() + self$positions_for = list() + self$reference_positions_for = list() for (d in names(self$column_map)) { getter = FrameGetter(self, d) self$frame_for[[d]] = getter$get_frame self$labels_for[[d]] = getter$get_labels self$partition_for[[d]] = getter$get_partition self$index_for[[d]] = getter$get_index + self$reference_labels_for[[d]] = getter$get_reference_labels + self$positions_for[[d]] = getter$get_positions + self$reference_positions_for[[d]] = getter$get_reference_positions } self$column_by_dim = list() for (d in names(self$column_map)) { @@ -69,6 +75,10 @@ Link = function(frame, column_map, reference_index_list, labelling_names_list) { , labelling_names_list = self$labelling_names_list ) } + self$expr = function(condition) { + substitute(condition) + eval(condition, envir = c(self$column_by_dim, self$frame)) + } self$partition = self$partition_for[[1L]]() ## hack! should probably have a method and then change the partition field in Index to a method as well return_object(self, "Link") } @@ -102,10 +112,13 @@ FrameGetter = function(link, dimension_name) { ) } self$get_partition = function() self$get_frame() |> Partition() - self$get_index = function() Index( - self$get_partition(), - self$link$labelling_names_list[[self$dimension_name]] - ) + self$get_index = function() { + Index( + self$get_partition(), + self$link$labelling_names_list[[self$dimension_name]], + self$link$reference_index_list[[self$dimension_name]] + ) + } self$get_labels = function() { i = self$link$labelling_names_list[[self$dimension_name]] f = self$get_frame()[, i, drop = FALSE] @@ -113,6 +126,19 @@ FrameGetter = function(link, dimension_name) { paste_args = c(l, sep = ".") do.call(paste, paste_args) } + self$get_reference_labels = function() { + self$get_index()$reference_labels() + } + self$get_positions = function(zero_based = FALSE) { + i = match(self$get_labels(), self$get_reference_labels()) + if (zero_based) i = i - 1L + i + } + self$get_reference_positions = function(zero_based = FALSE) { + i = match(self$get_reference_labels(), self$get_labels()) + if (zero_based) i = i - 1L + i + } return_object(self, "FrameGetter") } @@ -280,9 +306,9 @@ explicit_provenance = function(x, col_nm) { m = x$column_map implicit = is_provenance_implicit(x, col_nm) if (length(implicit) == 0L) { - macpan2:::msg_colon( - macpan2:::msg("Column", col_nm, "not found in any of the original tables"), - macpan2:::msg_indent(names(m)) + msg_colon( + msg("Column", col_nm, "not found in any of the original tables"), + msg_indent(names(m)) ) |> stop() } if (!any(implicit)) return(x) diff --git a/R/lists.R b/R/lists.R index 7a0dfe27..eb835d79 100644 --- a/R/lists.R +++ b/R/lists.R @@ -16,3 +16,14 @@ nlist = function(...) { } setNames(L, nm) } + +assert_named_list = function(l) { + if (is.null(names(l))) { + if (length(l) == 0L) { + l = setNames(list(), character()) + } else { + stop("Developer error: missing names") + } + } + l +} diff --git a/R/mp.R b/R/mp.R index 9ddd56f4..f625a1d6 100644 --- a/R/mp.R +++ b/R/mp.R @@ -107,6 +107,12 @@ mp_subset = function(x, ...) { Index(partition, x$labelling_names, x) } +#' @export +mp_setdiff = function(x, ...) { + partition = mp_choose_out(x, "pick", ...)$partition + Index(partition, x$labelling_names, x) +} + #' @export mp_union = function(...) UseMethod("mp_union") @@ -162,16 +168,6 @@ mp_rbind = function(...) { ) } -## how does this work for Index objects? -## might be ok because it is just another -## way to get columns for formulas. on the -## other hand it doesn't sound right at all. -## should probably throw an error here if -## Indices are passed. -#' @export -mp_formula_data = function(...) { - mp_union(...) -} #' @export @@ -202,15 +198,33 @@ mp_choose_out = function(x, subset_name, ...) { init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_names) } + #' @export -mp_join = function(...) { - args = list(...) - is_index = vapply(args, inherits, logical(1L), "Index") - is_link = vapply(args, inherits, logical(1L), "Link") - table_list = args[is_index | is_link] - by_list = args[!is_index & !is_link] +mp_join = function(..., by = list()) { + table_list = list(...) + by_list = assert_named_list(by) by_nms = names(by_list) |> strsplit(".", fixed = TRUE) table_nms = names(table_list) + good_by_nms = (by_nms + |> lapply(`%in%`, table_nms) + |> vapply(all, logical(1L)) + ) + if (any(!good_by_nms)) { + msg_break( + msg_colon( + "Indices to join were given the following names for the join output", + table_nms + ), + msg_colon( + msg( + "But the names of arguments that specify what columns will be", + "The following arguments were supplied to", + "determine what columns to join on" + ), + msg_indent_break(by_nms[!good_by_nms]) + ), + ) |> stop() + } if (!is.null(table_nms)) { for (nm in names(table_list)) { if (nm != "" & inherits(table_list[[nm]], "Index")) { @@ -369,10 +383,10 @@ mp_rename = function(x, ...) { } #' @export -mp_select = function(index, grouping_dimension) { - frame = index$partition$select(to_names(grouping_dimension))$frame() +mp_group = function(index, by) { + frame = index$partition$select(to_names(by))$frame() nms = names(frame)[names(frame) %in% index$labelling_names] - frame |> Index(labelling_names = nms, index) + Index(frame, labelling_names = nms) } #' @export diff --git a/R/simple_sims.R b/R/simple_sims.R index 18ba5cb8..2af54e86 100644 --- a/R/simple_sims.R +++ b/R/simple_sims.R @@ -12,14 +12,13 @@ #' method in \code{\link{TMBSimulator}}. #' #' @export -simple_sims = function(iteration_exprs, time_steps, ...) { - mat_names = names(list(...)) +simple_sims = function(iteration_exprs, time_steps, int_vecs = list(), mats = list()) { + mat_names = names(mats) + TMBModel( - init_mats = MatsList(... - , .mats_to_return = mat_names - , .mats_to_save = mat_names - ), + init_mats = do.call(MatsList, c(mats, list(.mats_to_return = mat_names, .mats_to_save = mat_names))), expr_list = ExprList(during = iteration_exprs), + engine_methods = EngineMethods(int_vecs = do.call(IntVecs, int_vecs)), time_steps = Time(time_steps) )$simulator()$report(.phases = c("before", "during")) } diff --git a/R/vector.R b/R/vector.R index b27f6991..4d4d2187 100644 --- a/R/vector.R +++ b/R/vector.R @@ -1,19 +1,82 @@ #' @export -Vector = function(labeller) { +Vector = function(x, ...) UseMethod("Vector") + +#' @export +Vector.data.frame = function(x, index, values_name = "values", ...) { + nms = setdiff(names(x), values_name) + bad_names = !nms %in% names(index) + if (any(bad_names)) { + msg_break( + msg_colon( + msg( + "The following names were found in the values data", + "but not in the model index" + ), + msg_indent(nms[bad_names]) + ), + msg_colon( + "The only names in the index are the following", + msg_indent(names(index)) + ) + ) |> stop() + } + + index_names = !names(x) %in% values_name + test_index = try(Index(x[, index_names, drop = FALSE])) + if (inherits(test_index, "try-error")) stop("Incompatible values data frame") + bad_labels = !test_index$labels() %in% index$partial_labels(test_index$labelling_names) + if (any(bad_labels)) { + msg_colon( + msg( + "The following labels found in the values data", + "are being ignored because they are not in the model index" + ), + msg_indent(test_index$labels()[bad_labels]) + ) |> warning() + } + + f = merge(index$partition$frame(), x, all.x = TRUE, sort = FALSE) ## left join + index_names = !names(f) %in% values_name ## redefine index_names after merge + mangled_order = Index( + f[, index_names, drop = FALSE], + labelling_names = index$labelling_names + )$labels() + sorted_positions = match(index$labels(), mangled_order) + values = f[[values_name]][sorted_positions] + values[is.na(values)] = 0 ## that's the way we roll + f = f[sorted_positions, index_names, drop = FALSE] + index = Index(f, labelling_names = index$labelling_names) + v = Vector(index) + v$set_all_numbers(values) +} + +#' @export +Vector.numeric = function(x, index, ...) { + v = Vector(index) + args = setNames(list(x), to_name(index$labelling_names)) + do.call(v$set_numbers, args) +} + +#' @export +Vector.Index = function(x, ...) { self = Base() - self$labeller = labeller - self$.numbers = zero_vector(self$labeller$labels()) + self$index = x + self$.numbers = zero_vector(self$index$labels()) self$numbers = function(...) { l = list(...) if (length(l) == 0L) return(self$.numbers) - i = mp_subset(self$labeller, ...)$labels() + i = mp_subset(self$index, ...)$labels() self$.numbers[i] } + self$set_all_numbers = function(values) { + self$.numbers = setNames(values, names(self$.numbers)) + self + } self$set_numbers = function(...) { ## generate list with: value, filter_cond, select_cond, select_nm l = process_grouping_dots(...) - args = c(list(self$labeller), l$filter_cond, l$select_cond) + args = c(list(self$index), l$filter_cond, l$select_cond) filter = do.call(mp_subset, args) replacement_values = setNames( l$value[filter$partition$partial_labels(l$select_nm)], @@ -23,12 +86,20 @@ Vector = function(labeller) { self$.numbers[names(replacement_values)] = replacement_values self } - self$frame = function() { + self$labels_frame = function() { data.frame( - labels = self$labeller$labels(), + labels = self$index$labels(), values = unname(self$numbers()) ) } + self$frame = function() { + f = self$index$partition$frame() + f$values = self$numbers() + f + } + self$csv = function(file) { + write.csv(self$frame(), file, row.names = FALSE, quote = FALSE) + } self$length = function() length(self$.numbers) return_object(self, "Vector") } @@ -51,4 +122,13 @@ length.Vector = function(x) x$length() #' @export print.Vector = function(x, ...) print(x$numbers()) +#' @export +as.matrix.Vector = function(x, ...) x$numbers() |> as.matrix() + zero_vector = function(labels) setNames(rep(0, length(labels)), labels) + +#' @export +mp_vector = Vector + +#' @export +mp_set_numbers = function(vector, ...) vector$set_numbers(...) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index 11e0ac3c..f9ea40f7 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -1,54 +1,49 @@ library(macpan2) +## basic model indexes ------------------------- + state = mp_index(Epi = c("S", "I", "R")) rates = mp_index(Epi = c("lambda", "gamma", "beta")) -infection = mp_join( - from = mp_subset(state, Epi = "S"), - to = mp_subset(state, Epi = "I"), - link = mp_subset(rates, Epi = "lambda") -) - -recovery = mp_join( - from = mp_subset(state, Epi = "I"), - to = mp_subset(state, Epi = "S"), - link = mp_subset(rates, Epi = "gamma") -) - -flow = mp_formula_data(infection, recovery) -flow$reference_index_list$from$partial_labels() -infection$labelling_names_list() - -mp_flow( - flow ~ rates[link] * state[from] - , inflow ~ groupSums(flow, to, state) - , outflow ~ groupSums(flow, from, state) - , state ~ state + inflow - outflow - , formula_data = flow +## model products (none in this model) -------- + +## subset and grouping indexes ----------------- + +### note: this section is more interesting in structured models. +### in particular these are all single row indexes. also +### there are no grouping indexes, only subsets. + +S = mp_subset(state, Epi = "S") +I = mp_subset(state, Epi = "I") +R = mp_subset(state, Epi = "R") +lambda = mp_subset(rates, Epi = "lambda") +gamma = mp_subset(rates, Epi = "gamma") +beta = mp_subset(rates, Epi = "beta") + +## links between entries in the indexes ----------- + +index_data = list( + flow = mp_formula_data( + infection = mp_join(from = S, to = I, flow = lambda), + recovery = mp_join(from = I, to = R, flow = gamma) + ), + influences = mp_formula_data( + transmission = mp_join( + infectious = I, + infection = lambda, + transmission = beta + ) + ) ) -flows = mp_union(infection, recovery) -flows$labels_frame() -flows$labelling_names_list() +## expressions that define model dynamics --------- -l = list(infection, recovery) -lapply(l, getElement, "column_map") |> unique() -lapply(l, getElement, "reference_index_list") |> unique() -lapply(l, getElement, "frame") |> do.call(what = rbind) - -mp_flow( - flow ~ rate * from - , inflow ~ groupSums(flow, ) - , links = -) - - -mp_subset(state, Epi = "I") - -transmission = mp_join( - state = mp_subset(state, Epi = "I"), - flow = mp_subset(rates, Epi = "lambda"), - rate = mp_subset(rates, Epi = "beta") +expr_list = mp_expr_list( + during = list( + rates[infection] ~ state[infectious] * rates[transmission] / N + , flows_per_time ~ state[from] * rates[flow] + , inflow ~ groupSums(flows_per_time, to, state) + , outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + inflow - outflow + ) ) - -mp_union(infection, recovery, transmission) diff --git a/inst/model_library/sir/numerical_inputs.R b/inst/model_library/sir/numerical_inputs.R new file mode 100644 index 00000000..25c325e1 --- /dev/null +++ b/inst/model_library/sir/numerical_inputs.R @@ -0,0 +1,22 @@ +source("model_structure.R") + +## numeric vectors labelled with particular indexes ----------- + +indexed_vecs = list( + state = mp_vector( + c(S = 999, I = 1, R = 0), + state + ), + rates = mp_vector( + c(lambda = NA, gamma = 0.1, beta = 0.25), + rates + ) +) + +## unstructured matrices -------------------------- + +unstruc_mats = list(N = 1000) + +## number of time steps to run in simulations ------ + +time_steps = 100L diff --git a/inst/model_library/sir/simulator.R b/inst/model_library/sir/simulator.R new file mode 100644 index 00000000..ce6f8337 --- /dev/null +++ b/inst/model_library/sir/simulator.R @@ -0,0 +1,11 @@ +source("numerical_inputs.R") + +## collect information into a simulator ----------------------- + +sir = mp_tmb_simulator( + expr_list + , index_data + , indexed_vecs + , unstruc_mats + , time_steps +) diff --git a/man/IndexedExpressions.Rd b/man/IndexedExpressions.Rd new file mode 100644 index 00000000..0949452b --- /dev/null +++ b/man/IndexedExpressions.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/formula_data.R +\name{IndexedExpressions} +\alias{IndexedExpressions} +\title{Indexed Expressions} +\usage{ +IndexedExpressions( + ..., + index_data, + vector_list = list(), + unstructured_matrix_list = list() +) +} +\arguments{ +\item{...}{Formula objects that reference the columns in the +\code{index_data}, the vectors in \code{vector_list} and the matrices +in \code{unstructured_matrix_list}.} + +\item{index_data}{An object produced using \code{\link{mp_formula_data}}.} + +\item{vector_list}{Named list of objected produced using +\code{\link{mp_vector}}.} + +\item{unstructured_matrix_list}{Named list of objects that can be coerced +to a matrix.} +} +\description{ +Indexed Expressions +} diff --git a/man/Link.Rd b/man/Link.Rd index aba1a54d..fcec6c18 100644 --- a/man/Link.Rd +++ b/man/Link.Rd @@ -12,7 +12,7 @@ Make an object to describe links between the entities in an \code{\link{Index}} objects. For example, here the \code{\link{mp_join}} combines two -\if{html}{\out{
    }}\preformatted{age = mp_index(Age = c("young", "old")) +\if{html}{\out{
    }}\preformatted{age = mp_index(Age = c("young", "old")) state = mp_cartesian( mp_index(Epi = c("S", "I", "R")), age @@ -22,8 +22,5 @@ mp_join( mp_choose(state, "to", Epi = "I"), from.to = "Age" ) -#> from to -#> S.old I.old -#> S.young I.young }\if{html}{\out{
    }} } diff --git a/man/mp_formula_data.Rd b/man/mp_formula_data.Rd new file mode 100644 index 00000000..ec1bf898 --- /dev/null +++ b/man/mp_formula_data.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/formula_data.R +\name{mp_formula_data} +\alias{mp_formula_data} +\title{#' @export +print.FormulaData = function(x, ...) { +print(x$frame, row.names = FALSE) +}} +\usage{ +mp_formula_data(...) +} +\description{ +#' @export +print.FormulaData = function(x, ...) { +print(x$frame, row.names = FALSE) +} +} diff --git a/man/simple_sims.Rd b/man/simple_sims.Rd index 3ffec31e..0d3e5f85 100644 --- a/man/simple_sims.Rd +++ b/man/simple_sims.Rd @@ -4,7 +4,7 @@ \alias{simple_sims} \title{Simple Iterated Simulation} \usage{ -simple_sims(iteration_exprs, time_steps, ...) +simple_sims(iteration_exprs, time_steps, int_vecs = list(), mats = list()) } \arguments{ \item{iteration_exprs}{List of expressions to pass to the diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index d9711fad..a9205640 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -148,70 +148,6 @@ void printMatrix(const matrix& mat) { } -// Helper function -// template -// int CheckIndices( -// matrix& x, -// matrix& rowIndices, -// matrix& colIndices -// ) { -// int rows = x.rows(); -// int cols = x.cols(); -// Type maxRowIndex = rowIndices.maxCoeff(); -// Type maxColIndex = colIndices.maxCoeff(); -// Type minRowIndex = rowIndices.minCoeff(); -// Type minColIndex = colIndices.minCoeff(); -// -// if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { -// return 0; -// } -// return 1; -// } - -// // Helper function -// template -// int RecycleInPlace( -// matrix& mat, -// int rows, -// int cols -// ) { -// #ifdef MP_VERBOSE -// std::cout << "recycling ... " << std::endl; -// #endif -// if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. -// return 0; -// -// matrix m(rows, cols); -// if (mat.rows()==1 && mat.cols()==1) { -// m = matrix::Constant(rows, cols, mat.coeff(0,0)); -// } -// else if (mat.rows()==rows) { -// if (mat.cols()==1) { -// #ifdef MP_VERBOSE -// std::cout << "recycling columns ... " << std::endl; -// #endif -// for (int i=0; i struct ListOfMatrices { // below is a vector of matrices that passed from R @@ -465,43 +401,6 @@ class ArgList { } } - - // void set(int index, const ItemType& item) { - // if (index < 0 || index >= size_) { - // throw std::out_of_range("Index out of range"); - // } - // items_[index] = item; - // } - // - // matrix get_as_mat(int i) const { - // if (i < 0 || i >= items_.size()) { - // throw std::out_of_range("Index out of range"); - // } - // - // if (std::holds_alternative>(items_[i])) { - // return std::get>(items_[i]); - // } else { - // throw std::runtime_error("Item at index is not a matrix"); - // } - // } - // - // std::vector get_as_int_vec(int i) { - // if (i < 0 || i >= items_.size()) { - // throw std::out_of_range("Index out of range"); - // } - // - // if (std::holds_alternative>(items_[i])) { - // return std::get>(items_[i]); - // } else { - // matrix m = get_as_mat(i); - // std::vector v(m.rows()); - // for (int i=0; i= items_.size()) { throw std::out_of_range("Index out of range"); diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 78675e46..4dcf3020 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -4,8 +4,99 @@ library(oor) library(ggplot2) library(dplyr) +sir = mp_index(Epi = c("S", "I", "R")) +sir_two_strains = (mp_rename(sir, A = "Epi") + |> mp_cartesian(mp_rename(sir, B = "Epi")) + |> mp_cartesian(mp_rename(sir, C = "Epi")) + |> mp_setdiff(A.B = "I.I", A.C = "I.I", B.C = "I.I") +) + +## replacement model + +n_strains = 10L +strain_nms = letters[seq_len(n_strains)] + +strains = mp_index( + Strain = strain_nms[-n_strains], + Replace = strain_nms[-1L], + labelling_names = "Strain" +) + +replacement_state = mp_union( + + ## naive part + mp_index(Epi = "S", Strain = "", Replace = "a" + , labelling_names = "Epi.Strain" + ), + + ## non-naive part + mp_cartesian( + mp_index(Epi = c("I", "E", "R")), + strains + ) +) + + +xx = macpan2:::FormulaData( + infection = mp_join( + from = mp_subset(replacement_state, Epi = c("S", "R")), + to = mp_subset(replacement_state, Epi = "E"), + by = list(from.to = "Replace" ~ "Strain") + ), + progression = mp_join( + from = mp_subset(replacement_state, Epi = "E"), + to = mp_subset(replacement_state, Epi = "I"), + by = list(from.to = "Strain") + ) +) + + +replacement_infection$labelling_names_list +replacement_progression$labelling_names_list + +replacement_infection$column_map +replacement_progression$column_map + +macpan2:::bind_rows( + replacement_infection$frame, + replacement_progression$frame +) -## factor model indices --------------- + +replacement_progression$frame + + +mp_union(replacement_infection, replacement_progression) + +replacement_progression$reference_positions_for$from() + +mp_vector(c(S. = 1000, I.a = 1), replacement_state) + + +replacement_state$labels()[replacement_progression$positions_for$from()] +initial_replacement_state = data.frame( + Epi = c("S", "I"), + Strain = c("", "a"), + values = c(1000, 1 ) +) +state_vector = mp_vector(initial_replacement_state, replacement_state) +state_vector +state_vector$frame() +state_vector$csv("state-test-write.csv") + + +xx = mp_vector(replacement_state) +xx$set_numbers(Strain = "a", Epi = c(I = 1, E = 1)) + + +mp_subset(replacement_state, Epi = "E")$position() +mp_subset(replacement_state, Epi = "E")$reference_position() + +xx = mp_group(replacement_state, "Strain") +xx$reference_labels() +xx$labels() + +## atomic model indices --------------- state_sir = mp_index( Epi = c("S", "I", "R", "D"), @@ -19,7 +110,7 @@ movement = mp_linear(cities, "Move") age = mp_index(Age = c("young", "old")) contact = mp_square(age, c("Infectious", "Infection")) -## product model bases ------------- +## structured model indices ------------- state = (state_sir |> mp_cartesian(cities) @@ -37,20 +128,153 @@ flow_rates = mp_union( ), mp_subset(flow_rates_sir, Epi = "gamma"), movement -) +) |> mp_group("Epi.Age.Loc.Move") trans_rates = (trans_rates_sir |> mp_cartesian(cities) |> mp_cartesian(contact) ) -xx = mp_join( - alive = mp_subset(state, Vital = "alive"), - N = mp_select(state, "Loc.Age"), - alive.N = "Loc.Age" +## index groups and subsets of indexes ------------ + +strata = mp_group(state, by = "Loc.Age") +alive = mp_subset(state, Epi = c("S", "I", "R")) +infectious = mp_subset(state, Epi = "I") +beta = mp_subset(trans_rates, Epi = "beta") +lambda = mp_subset(flow_rates, Epi = "lambda") + +## create vectors for particular indexes ----------- + +state_vector = Vector(state) +state_vector$ + set_numbers(Epi = c(I = 1))$ + set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) +N = Vector(strata) +flow_rate_vector = Vector(flow_rates) +flow_rate_vector$set_numbers(Epi = c(lambda = 0.15, gamma = 0.1, mu = 0.01)) + +## links between entries in the indexes ----------- + +movement_flows = mp_join( + from = mp_subset(state, Epi = c("S", "I", "R")), + to = mp_subset(state, Epi = c("S", "I","R")), + link = mp_subset(flow_rates, Epi = ""), + by = list( + from.to = "Epi.Age", + from.link = "Loc", + to.link = "Loc" ~ "Move" + ) +) +infection_flows = mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + link = mp_subset(flow_rates, Epi = "lambda"), + by = list( + from.to = "Loc.Age", + from.link = "Loc.Age" + ) +) +recovery_flows = mp_join( + from = mp_subset(state, Epi = "I"), + to = mp_subset(state, Epi = "R"), + link = mp_subset(flow_rates, Epi = "gamma"), + by = list(from.to = "Loc.Age") +) +death_flows = mp_join( + from = mp_subset(state, Vital = "alive"), + to = mp_subset(state, Vital = "dead"), + link = mp_subset(flow_rates, Epi = "mu"), + by = list( + from.to = "Loc.Age", + from.link = "Age" + ) +) + +flows = mp_formula_data( + movement_flows + , infection_flows + , recovery_flows + , death_flows +) + +vv = IndexedExpressions( + flows_per_time ~ state[from] * flow_rates[link] + , inflow ~ groupSums(flows_per_time, to, state) + , outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + inflow - outflow + , index_data = flows + , vector_list = list( + state = state_vector + , flow_rates = flow_rate_vector + ) +) +vv$mats_list() +vv$int_vecs(TRUE) +filter(vv$simulate(20), matrix == "state", row == "S.cal.young") + + +state_aggregation = mp_join( + alive = alive, + group_by = strata, + by = list(alive.group_by = "Loc.Age") +) |> mp_formula_data() +## N ~ groupSums(state[alive], group_by, N) +state_vector = Vector(state) +state_vector$set_numbers(Epi = c(I = 1))$set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) +hh = IndexedExpressions( + N ~ groupSums(state[alive], group_by, N) + , index_data = state_aggregation + , vector_list = list( + N = Vector(strata) + , state = state_vector + ) +) +filter(hh$simulate(10), matrix == "N") +state_normalization = mp_join( + infectious = infectious, + strata = strata, + by = list(infectious.strata = "Loc.Age") +) +## Inorm ~ state[infectious] / N[strata] + +transmission = mp_join( + infectious = infectious, + infection = lambda, + transmission = beta, + ) + + +## performance sanity check --------- +if (FALSE) { + cities = mp_index(Loc = letters) + state_sir = mp_cartesian( + mp_index(EpiA = rep(LETTERS)), + mp_index(EpiB = rep(LETTERS)) + ) |> mp_cartesian(cities) + flow_rates = mp_cartesian( + mp_index(EpiA = paste(letters[1:25], letters[2:26], sep = "")), + mp_index(EpiB = paste(letters[1:25], letters[2:26], sep = "")) + ) |> mp_cartesian(cities) + flow = mp_join( + from = mp_subset(state, EpiA = "S"), + to = mp_subset(state, EpiA = "I"), + rate = mp_subset(flow_rates, EpiA = "gh"), + from.to = "Loc", + from.rate = "Loc", + to.rate = "Loc" + ) + flow +} + + + +self$reference_index_list$N +self$frame +self$reference_index_list$N$labels() +groupSums() mp_labels xx$reference_index_list$N$labelling_names yy = mp_formula_data(xx) @@ -162,14 +386,14 @@ N_expr = mp_expr_group_sum(state xx = mp_join( - mp_choose(N_expr$strata, "N"), - mp_choose(state, "infectious", Epi = "I"), + mp_subset(N_expr$strata, "N"), + mp_subset(state, "infectious", Epi = "I"), N.infectious = N_expr$strata$labelling_names ) xx$labels_for$infectious() xx$frame -state_strata = mp_select(state, "Loc.Age") +state_strata = mp_group(state, "Loc.Age") alive_states = mp_subset(state, Vital = "alive") alive_groups = mp_indices( mp_labels(alive_states, "Loc.Age"), @@ -197,34 +421,6 @@ engine_eval( ) -movement_flows = mp_join( - mp_choose(state, "from", Vital = "alive"), - mp_choose(state, "to", Vital = "alive"), - mp_choose(flow_rates, "rate", Epi = ""), - from.to = "Epi.Age", - from.rate = "Loc", - to.rate = "Loc" ~ "Move" -) -infection_flows = mp_join( - mp_choose(state, "from", Epi = "S"), - mp_choose(state, "to", Epi = "I"), - mp_choose(flow_rates, "rate", Epi = "lambda"), - from.to = "Loc.Age", - from.rate = "Loc.Age" -) -recovery_flows = mp_join( - mp_choose(state, "from", Epi = "I"), - mp_choose(state, "to", Epi = "R"), - mp_choose(flow_rates, "rate", Epi = "gamma"), - from.to = "Loc.Age" -) -death_flows = mp_join( - mp_choose(state, "from", Vital = "alive"), - mp_choose(state, "to", Vital = "dead"), - mp_choose(flow_rates, "rate", Epi = "mu"), - from.to = "Loc.Age", - from.rate = "Age" -) flows = rbind( @@ -234,9 +430,9 @@ flows = rbind( , death_flows$labels_frame() ) influences = mp_join( - mp_choose(state, "infectious", Epi = "I"), - mp_choose(flow_rates, "infection", Epi = "lambda"), - mp_choose(trans_rates, "rate", Epi = "beta"), + mp_subset(state, "infectious", Epi = "I"), + mp_subset(flow_rates, "infection", Epi = "lambda"), + mp_subset(trans_rates, "rate", Epi = "beta"), infectious.infection = "Loc", infectious.rate = "Age.Loc", infection.rate = "Loc.Age" ~ "Loc.Contact" @@ -269,8 +465,8 @@ vec$state$set_numbers( Epi = "S" ) -N = mp_select(state, "N", "Loc") -alive = mp_choose(state, "alive", Epi = c("S", "I", "R")) +N = mp_group(state, "N", "Loc") +alive = mp_subset(state, "alive", Epi = c("S", "I", "R")) VectorMerged = function(labeller) { self = Base() @@ -292,7 +488,7 @@ mp_indices(alive$labels_for$alive(), state$labels()) -I = mp_choose(state, "I", Epi = "I") +I = mp_subset(state, "I", Epi = "I") @@ -327,20 +523,20 @@ flow_file i = 1L flow = flow_file[i,,drop=FALSE] mp_join( - mp_choose(state, "from", flow$from_partition), - mp_choose(state, "to", Epi = flow$to), - mp_choose(flow_rates, "flow", Epi = flow$flow), + mp_subset(state, "from", flow$from_partition), + mp_subset(state, "to", Epi = flow$to), + mp_subset(flow_rates, "flow", Epi = flow$flow), from.to = "Epi" ) -N = mp_select(state, "N", "Loc") -active = mp_choose(state, "active", Epi = c("S", "I")) -infectious = mp_choose(state, "infectious", Epi = "I") +N = mp_group(state, "N", "Loc") +active = mp_subset(state, "active", Epi = c("S", "I")) +infectious = mp_subset(state, "infectious", Epi = "I") mp_join(N, infectious, N.infectious = "Loc") mp_join(xx, yy, N.norm_inf = "Loc")$labels_frame() -mp_join(N, mp_choose(state, "infectious", Epi = "I")) +mp_join(N, mp_subset(state, "infectious", Epi = "I")) match(N$labels_frame()$active, state$labels()) - 1L @@ -348,8 +544,8 @@ match(N$labels_frame()$N, unique(N$labels_frame()$N)) - 1L groupSums(state[active], strata, length(active)) xx = mp_join( - mp_choose(state, "state"), - mp_select(state, "N", "Loc"), + mp_subset(state, "state"), + mp_group(state, "N", "Loc"), state.N = "Loc" ) xx$labels_for$state() @@ -364,24 +560,24 @@ mp_init_vec = function(components, ...) { } mp_init_vec(state, S.tor = 99) mp_join( - mp_choose(state, "infectious", Epi = "I"), - mp_choose(flow_rates, "infection", Epi = "lambda"), + mp_subset(state, "infectious", Epi = "I"), + mp_subset(flow_rates, "infection", Epi = "lambda"), infectious.infection = "Loc" )$labels_frame() mp_union( mp_join( - mp_choose(state, "from") - , mp_choose(state, "to") - , mp_choose(flow_rates, "flow", Epi = "") + mp_subset(state, "from") + , mp_subset(state, "to") + , mp_subset(flow_rates, "flow", Epi = "") , from.to = "Epi" , from.flow = "Loc" , to.flow = "Loc" ~ "Move" ), mp_join( - mp_choose(state, "from", Epi = "S") - , mp_choose(state, "to", Epi = "I") - , mp_choose(flow_rates, "flow", Epi = "lambda") + mp_subset(state, "from", Epi = "S") + , mp_subset(state, "to", Epi = "I") + , mp_subset(flow_rates, "flow", Epi = "lambda") , from.to = "Loc" , from.flow = "Loc" ) diff --git a/src/macpan2.cpp b/src/macpan2.cpp index ae30ab7d..1156ae31 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -149,70 +149,6 @@ void printMatrix(const matrix& mat) { } -// Helper function -// template -// int CheckIndices( -// matrix& x, -// matrix& rowIndices, -// matrix& colIndices -// ) { -// int rows = x.rows(); -// int cols = x.cols(); -// Type maxRowIndex = rowIndices.maxCoeff(); -// Type maxColIndex = colIndices.maxCoeff(); -// Type minRowIndex = rowIndices.minCoeff(); -// Type minColIndex = colIndices.minCoeff(); -// -// if ((maxRowIndex < rows) & (maxColIndex < cols) && (minRowIndex > -0.1) && (minColIndex > -0.1)) { -// return 0; -// } -// return 1; -// } - -// // Helper function -// template -// int RecycleInPlace( -// matrix& mat, -// int rows, -// int cols -// ) { -// #ifdef MP_VERBOSE -// std::cout << "recycling ... " << std::endl; -// #endif -// if (mat.rows()==rows && mat.cols()==cols) // don't need to do anything. -// return 0; -// -// matrix m(rows, cols); -// if (mat.rows()==1 && mat.cols()==1) { -// m = matrix::Constant(rows, cols, mat.coeff(0,0)); -// } -// else if (mat.rows()==rows) { -// if (mat.cols()==1) { -// #ifdef MP_VERBOSE -// std::cout << "recycling columns ... " << std::endl; -// #endif -// for (int i=0; i struct ListOfMatrices { // below is a vector of matrices that passed from R @@ -466,43 +402,6 @@ class ArgList { } } - - // void set(int index, const ItemType& item) { - // if (index < 0 || index >= size_) { - // throw std::out_of_range("Index out of range"); - // } - // items_[index] = item; - // } - // - // matrix get_as_mat(int i) const { - // if (i < 0 || i >= items_.size()) { - // throw std::out_of_range("Index out of range"); - // } - // - // if (std::holds_alternative>(items_[i])) { - // return std::get>(items_[i]); - // } else { - // throw std::runtime_error("Item at index is not a matrix"); - // } - // } - // - // std::vector get_as_int_vec(int i) { - // if (i < 0 || i >= items_.size()) { - // throw std::out_of_range("Index out of range"); - // } - // - // if (std::holds_alternative>(items_[i])) { - // return std::get>(items_[i]); - // } else { - // matrix m = get_as_mat(i); - // std::vector v(m.rows()); - // for (int i=0; i= items_.size()) { throw std::out_of_range("Index out of range"); From fef37cdeb1aba63bb236c9c48a5229f896f02e42 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 3 Nov 2023 03:01:51 -0400 Subject: [PATCH 053/332] clean up sir_age example --- inst/model_library/sir_age/derivations.json | 1 - inst/model_library/sir_age/flows.csv | 4 ---- inst/model_library/sir_age/indices.csv | 0 inst/model_library/sir_age/infection.csv | 2 -- inst/model_library/sir_age/mappings.csv | 9 --------- inst/model_library/sir_age/normalization.csv | 0 inst/model_library/sir_age/settings.json | 7 ------- inst/model_library/sir_age/variables.csv | 12 ------------ inst/model_library/sir_age/vectors.csv | 7 ------- 9 files changed, 42 deletions(-) delete mode 100644 inst/model_library/sir_age/derivations.json delete mode 100644 inst/model_library/sir_age/flows.csv delete mode 100644 inst/model_library/sir_age/indices.csv delete mode 100644 inst/model_library/sir_age/infection.csv delete mode 100644 inst/model_library/sir_age/mappings.csv delete mode 100644 inst/model_library/sir_age/normalization.csv delete mode 100644 inst/model_library/sir_age/settings.json delete mode 100644 inst/model_library/sir_age/variables.csv delete mode 100644 inst/model_library/sir_age/vectors.csv diff --git a/inst/model_library/sir_age/derivations.json b/inst/model_library/sir_age/derivations.json deleted file mode 100644 index 0637a088..00000000 --- a/inst/model_library/sir_age/derivations.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/inst/model_library/sir_age/flows.csv b/inst/model_library/sir_age/flows.csv deleted file mode 100644 index 36b8dacd..00000000 --- a/inst/model_library/sir_age/flows.csv +++ /dev/null @@ -1,4 +0,0 @@ -from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition -S ,I ,lambda ,per_capita ,Epi ,Epi ,Epi , , ,Null -I ,R ,gamma ,per_capita ,Epi ,Epi ,Epi , , ,Null -young ,old ,aging ,per_capita ,Age ,Age ,Age , , ,Null diff --git a/inst/model_library/sir_age/indices.csv b/inst/model_library/sir_age/indices.csv deleted file mode 100644 index e69de29b..00000000 diff --git a/inst/model_library/sir_age/infection.csv b/inst/model_library/sir_age/infection.csv deleted file mode 100644 index 8a1f46fb..00000000 --- a/inst/model_library/sir_age/infection.csv +++ /dev/null @@ -1,2 +0,0 @@ -flow ,state ,flow_partition ,state_partition ,flow_state_partition -lambda ,I ,Epi ,Epi , diff --git a/inst/model_library/sir_age/mappings.csv b/inst/model_library/sir_age/mappings.csv deleted file mode 100644 index ee60b6ab..00000000 --- a/inst/model_library/sir_age/mappings.csv +++ /dev/null @@ -1,9 +0,0 @@ -mapping ,state ,flow -trans ,I.young ,lambda.young -trans ,I.old ,lambda.young -trans ,I.young ,lambda.old -trans ,I.old ,lambda.old -contact ,I.young ,lambda.young -contact ,I.old ,lambda.young -contact ,I.young ,lambda.old -contact ,I.old ,lambda.old diff --git a/inst/model_library/sir_age/normalization.csv b/inst/model_library/sir_age/normalization.csv deleted file mode 100644 index e69de29b..00000000 diff --git a/inst/model_library/sir_age/settings.json b/inst/model_library/sir_age/settings.json deleted file mode 100644 index 610e3708..00000000 --- a/inst/model_library/sir_age/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "labelling_partition" : ["Epi", "Age"], - "null_partition" : "Null", - "vec_partition" : "Vec", - "state" : ["state"], - "flow" : ["flow_rates"] -} diff --git a/inst/model_library/sir_age/variables.csv b/inst/model_library/sir_age/variables.csv deleted file mode 100644 index 8d29fcea..00000000 --- a/inst/model_library/sir_age/variables.csv +++ /dev/null @@ -1,12 +0,0 @@ -Epi ,Age ,Type ,Vec -S ,young ,susceptible ,state -I ,young ,infectious ,state -R ,young ,immune ,state -S ,old ,susceptible ,state -I ,old ,infectious ,state -R ,old ,immune ,state -lambda ,young ,infection ,flow_rates -gamma ,young ,recovery ,flow_rates -lambda ,old ,infection ,flow_rates -gamma ,old ,recovery ,flow_rates - ,aging ,aging ,flow_rates diff --git a/inst/model_library/sir_age/vectors.csv b/inst/model_library/sir_age/vectors.csv deleted file mode 100644 index 0c6e5606..00000000 --- a/inst/model_library/sir_age/vectors.csv +++ /dev/null @@ -1,7 +0,0 @@ -vector ,subset ,components ,partition -state , ,state ,Vec -state ,infectious ,infectious ,Infect -flow , ,flow ,Vec -flow ,infection ,infection ,Infect -trans , ,infectious ,Infect -norm , ,norm ,Vec From 3d4f3295e0e66a457bb3055cebe2a4bee8455946 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 3 Nov 2023 12:35:51 -0400 Subject: [PATCH 054/332] new model defs with product example and more docs --- NAMESPACE | 16 +- R/expr_list.R | 4 +- R/formula_data.R | 12 +- R/formula_utils.R | 13 +- R/index.R | 190 ++++++++++-------- R/index_to_tmb.R | 2 +- R/link.R | 38 ++-- R/mp.R | 151 ++++++++++---- R/name_utils.R | 2 +- R/vector.R | 36 +++- inst/model_library/sir/model_structure.R | 4 +- inst/model_library/sir_age/experimentation.R | 12 ++ inst/model_library/sir_age/main.R | 5 + inst/model_library/sir_age/model_structure.R | 130 ++++++++++++ inst/model_library/sir_age/numerical_inputs.R | 44 ++++ inst/model_library/sir_age/simulator.R | 10 + man/Index.Rd | 124 ------------ man/IndexedExpressions.Rd | 2 +- man/Link.Rd | 2 +- man/mp_cartesian.Rd | 44 ++++ man/mp_index.Rd | 132 +++++++++++- man/{mp_formula_data.Rd => mp_index_data.Rd} | 6 +- man/mp_subset.Rd | 22 ++ man/mp_union.Rd | 11 + misc/experiments/refactorcpp.R | 22 +- 25 files changed, 720 insertions(+), 314 deletions(-) create mode 100644 inst/model_library/sir_age/experimentation.R create mode 100644 inst/model_library/sir_age/main.R create mode 100644 inst/model_library/sir_age/model_structure.R create mode 100644 inst/model_library/sir_age/numerical_inputs.R create mode 100644 inst/model_library/sir_age/simulator.R delete mode 100644 man/Index.Rd create mode 100644 man/mp_cartesian.Rd rename man/{mp_formula_data.Rd => mp_index_data.Rd} (81%) create mode 100644 man/mp_subset.Rd create mode 100644 man/mp_union.Rd diff --git a/NAMESPACE b/NAMESPACE index b5664206..cde86cf9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,8 +12,8 @@ S3method(as.matrix,Vector) S3method(c,String) S3method(c,StringData) S3method(head,Link) -S3method(labelling_names,Index) -S3method(labelling_names,Link) +S3method(labelling_column_names,Index) +S3method(labelling_column_names,Link) S3method(labels,Index) S3method(length,Vector) S3method(mp_index,character) @@ -21,7 +21,11 @@ S3method(mp_index,data.frame) S3method(mp_labels,Index) S3method(mp_labels,Link) S3method(mp_union,Index) -S3method(mp_union,Link) +S3method(mp_vector,Index) +S3method(mp_vector,Link) +S3method(mp_vector,character) +S3method(mp_vector,data.frame) +S3method(mp_vector,numeric) S3method(names,Index) S3method(names,IntVecs) S3method(names,Link) @@ -134,7 +138,7 @@ export(initial_valid_vars) export(join_partitions) export(labelled_frame) export(labelled_zero_vector) -export(labelling_names) +export(labelling_column_names) export(make_expr_parser) export(model_starter) export(mp_aggregate) @@ -145,16 +149,16 @@ export(mp_decompose) export(mp_expr_binop) export(mp_expr_group_sum) export(mp_expr_list) -export(mp_formula_data) +export(mp_extract) export(mp_group) export(mp_index) +export(mp_index_data) export(mp_indexed_exprs) export(mp_indicator) export(mp_indices) export(mp_join) export(mp_labels) export(mp_linear) -export(mp_rbind) export(mp_rename) export(mp_set_numbers) export(mp_setdiff) diff --git a/R/expr_list.R b/R/expr_list.R index 21916f37..ffae5538 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -159,9 +159,9 @@ ExprList = function( if (is.null(nms)) nms = rep("", length(self$formula_list())) nms } - self$all_formula_vars = function() { + self$all_formula_vars = function(side = c("both", "left", "right")) { (self$formula_list() - |> lapply(formula_components) + |> lapply(formula_components, side) |> lapply(getElement, "variables") |> unlist(use.names = FALSE, recursive = FALSE) |> unique() diff --git a/R/formula_data.R b/R/formula_data.R index 51fc81b6..8e5bdbe0 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -2,13 +2,13 @@ FormulaData = function(...) { self = Base() self$link_list = list(...) - labelling_names_list = (self$link_list - |> lapply(getElement, "labelling_names_list") + labelling_column_names_list = (self$link_list + |> lapply(getElement, "labelling_column_names_list") |> unname() |> unique() ) - stopifnot(length(labelling_names_list) == 1L) - self$labelling_names_list = labelling_names_list[[1L]] + stopifnot(length(labelling_column_names_list) == 1L) + self$labelling_column_names_list = labelling_column_names_list[[1L]] reference_index_list = (self$link_list |> lapply(getElement, "reference_index_list") @@ -55,14 +55,14 @@ FormulaData = function(...) { #' @export -mp_formula_data = function(...) FormulaData(...) +mp_index_data = function(...) FormulaData(...) #' Indexed Expressions #' #' @param ... Formula objects that reference the columns in the #' \code{index_data}, the vectors in \code{vector_list} and the matrices #' in \code{unstructured_matrix_list}. -#' @param index_data An object produced using \code{\link{mp_formula_data}}. +#' @param index_data An object produced using \code{\link{mp_index_data}}. #' @param vector_list Named list of objected produced using #' \code{\link{mp_vector}}. #' @param unstructured_matrix_list Named list of objects that can be coerced diff --git a/R/formula_utils.R b/R/formula_utils.R index 5826b147..0a465a56 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -156,9 +156,14 @@ formula_as_character = function(formula) { # formula parsing in macpan2 works one side at a time. but sometimes # it is helpful to parse two-sided formulas. this function does so # by parsing each side at a time and rbinding the results. -concat_parse_table = function(formula) { +concat_parse_table = function(formula, side = c("both", "left", "right")) { + side = match.arg(side) if (is_one_sided(formula)) return(method_parser(formula)) - rbind(method_parser(lhs(formula)), method_parser(rhs(formula))) + switch(side + , both = rbind(method_parser(lhs(formula)), method_parser(rhs(formula))) + , left = method_parser(lhs(formula)) + , right = method_parser(rhs(formula)) + ) } # When looking at a formula without any additional information (e.g. @@ -173,8 +178,8 @@ concat_parse_table = function(formula) { # # This function returns a list with three components each giving a list of # the components of that type. -formula_components = function(formula) { - parse_table = concat_parse_table(formula) +formula_components = function(formula, side = c("both", "left", "right")) { + parse_table = concat_parse_table(formula, side) is_var_or_lit = parse_table$n == 0L is_func = parse_table$n > 0L is_lit = grepl("^[0-9]*\\.?[0-9]*$", parse_table$x) diff --git a/R/index.R b/R/index.R index 5fa0fd29..226912a3 100644 --- a/R/index.R +++ b/R/index.R @@ -1,26 +1,101 @@ -#' Index +#' Model Component Index #' -#' Make an object to represent descriptions of model components and their -#' dimensions of variation. \code{Index} objects generalize and wrap +#' Make an index object to represent descriptions of model components and their +#' dimensions of variation. These objects generalize and wrap #' \code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like #' object is an entry in the index, and each column provides a description of -#' the entries. For example, the following \code{Index} object describes the +#' the entries. Each of these entries describes an element of a model component. +#' For example, the following index describes the #' state variables of an age-structured SIR model. Each row corresponds to a #' state variable and each state variable is described by columns `Epi` and #' `Age`. #' ```{r, echo = FALSE} #' sir = mp_index(Epi = c("S", "I", "R")) #' age = mp_index(Age = c("young", "old")) -#' mp_cartesian(sir, age) +#' prod = mp_cartesian(sir, age) +#' prod +#' ``` +#' Each index can produce labels for the elements by dot-concatenating the +#' values in each row. The labels of the state variables in this +#' age-structured SIR example model are as follows. +#' ```{r, echo = FALSE} +#' labels(prod) #' ``` +#' These labels can be used to create 'multidimensional' names for the elements +#' of vectors. Here is the above example expressed in vector form. +#' ```{r, echo = FALSE} +#' v = Vector(prod) +#' v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") +#' ``` +#' This example vector could be stored as a 3-by-2 matrix. But other examples +#' cannot, making this indexing approach more general. For example, consider +#' the following index. +#' ```{r, echo = FALSE} +#' symp = mp_index( +#' Epi = c("S", "I", "I", "R"), +#' Symptoms = c("", "mild", "severe", "") +#' ) +#' symp +#' ``` +#' This index has an associated indexed vector that cannot be expressed +#' as a matrix. +#' ```{r, echo = FALSE} +#' mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") +#' ``` +#' Dots are not allowed in the index so that the labels can be inverted to +#' reproduce the original index (provided that the column names can be +#' retrieved). See the details below for more restrictions. +#' +#' This function is analogous to the +#' \code{\link{data.frame}} function for creating data frames. +#' +#' All values in the index must contain only letters, numbers, and underscores, +#' and blank empty string entries are also allowed. No value can be missing. +#' These restrictions ensure that the dot-concatenation of each row can be +#' unambiguously inverted and that these dot-concatenations produce +#' syntactically valid names (see \code{\link{make.names}}). These +#' dot-concatenations are used to provide labels for the model components. +#' +#' By convention, it is recommended to use CamalCase for the columns of +#' indexes and either snake_case (aging_rate) or single uppercase +#' letters (e.g. S, I). This helps when reading code that contains +#' references to both column names and values in an index. +#' +#' @param ... Character vectors to combine to produce an index. +#' @param labelling_column_names A \code{\link{character}} vector of the names +#' of the index that will be used to label the model components (i.e. rows) +#' being described. The \code{labelling_column_names} cannot have duplicates +#' and must contain at least one name. The index given by the +#' \code{labelling_column_names} must uniquely identify each row. +#' +#' @examples +#' state = mp_index( +#' Epi = c("S", "I", "S", "I"), +#' Age = c("young", "young", "old", "old") +#' ) +#' print(state) +#' labels(state) +#' state_vector = (state +#' |> mp_vector() +#' |> mp_set_numbers(Epi = c(S = 1000)) +#' |> mp_set_numbers(Epi = c(I = 1), Age = "old") +#' ) +#' print(state_vector) +#' mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) +#' +#' @export +mp_index = function(..., labelling_column_names) UseMethod("mp_index") + + +#' Index #' #' @param partition A data frame (or data frame-like) object containing #' definitions of the index. This object can be a \code{\link{data.frame}}, #' \code{\link{Partition}}, or another \code{\link{Index}} object. -#' @param labelling_names A \code{\link{character}} vector of the names of the +#' @param labelling_column_names A \code{\link{character}} vector of the names of the #' index that will be used to label the model components (i.e. rows) being -#' described. The \code{labelling_names} cannot have duplicates and must -#' contain at least one name. The index given by the \code{labelling_names} +#' described. The \code{labelling_column_names} cannot have duplicates and must +#' contain at least one name. The index given by the \code{labelling_column_names} #' must uniquely identify each row. #' @param reference_index (Advanced) An optional partition to use when #' computing subset indices. @@ -28,42 +103,23 @@ #' @param ... For consistency with existing S3 methods. #' #' @seealso [mp_index()] -#' -#' @examples -#' sir = data.frame( -#' Epi = c("S", "I", "R", "D" ), -#' Vital = c("alive", "alive", "alive", "dead") -#' ) |> Index(labelling_names = "Epi") - -#' age = data.frame( -#' Age = c("young", "old") -#' ) |> Index() -#' state_index = mp_cartesian(sir, age) -#' labels(state_index) -#' (state_index -#' |> mp_subset(Vital = "alive") -#' |> labels() -#' ) -#' +#' @noRd +#' @keywords internal #' @export -Index = function(partition, labelling_names = names(partition), reference_index = NULL) { +Index = function(partition, labelling_column_names = names(partition), reference_index = NULL) { UseMethod("Index") } -#' @describeIn Index Create a \code{Index} object from a -#' \code{\link{Partition}} object. These \code{\link{Partition}} objects -#' wrap \code{\link{data.frame}}s. These data frames must follow certain -#' restrictions. #' @export Index.Partition = function(partition - , labelling_names = names(partition) + , labelling_column_names = names(partition) , reference_index = NULL ) { self = Base() ## Args self$partition = partition - self$labelling_names = to_names(labelling_names) + self$labelling_column_names = to_names(labelling_column_names) ## Private Arg self$.reference_index = reference_index @@ -76,12 +132,13 @@ Index.Partition = function(partition self$set_reference_index = function(index) { self$.reference_index = index } + self$reset_reference_index = function() self$.reference_index = NULL ## Standard Methods - self$labels = function() self$partition$select(self$labelling_names)$labels() + self$labels = function() self$partition$select(self$labelling_column_names)$labels() self$partial_labels = function(...) self$partition$partial_labels(...) self$reference_labels = function() { - self$reference_index()$partial_labels(self$labelling_names) + self$reference_index()$partial_labels(self$labelling_column_names) } self$reference_positions = function(zero_based = FALSE) { i = match(self$reference_labels(), self$labels()) @@ -97,27 +154,21 @@ Index.Partition = function(partition return_object(self, "Index") } -#' @describeIn Index Create a \code{Index} object from a -#' \code{\link{data.frame}} object that follows certain restrictions -#' documented in the help page for \code{\link{Partition}}s. #' @export -Index.data.frame = function(partition, labelling_names = names(partition), reference_index = NULL) { - partition |> Partition() |> Index(labelling_names, reference_index) +Index.data.frame = function(partition, labelling_column_names = names(partition), reference_index = NULL) { + partition |> Partition() |> Index(labelling_column_names, reference_index) } -#' @describeIn Index Create another \code{Index} object from an -#' existing \code{Index} object, possibly with new \code{labelling_names}. #' @export -Index.Index = function(partition, labelling_names = names(partition), reference_index = NULL) { - partition$partition |> Index(labelling_names, reference_index) +Index.Index = function(partition, labelling_column_names = names(partition), reference_index = NULL) { + partition$partition |> Index(labelling_column_names, reference_index) } -#' @describeIn Index Print a \code{Index} object. +#' @describeIn mp_index Print an index. #' @export print.Index = function(x, ...) print(x$partition) -#' @describeIn Index Get the names of the index -#' (i.e. columns) of a \code{Index} object. +#' @describeIn mp_index Get the names of the columns of an index. #' @export names.Index = function(x) x$partition$names() @@ -127,58 +178,37 @@ as.data.frame.Index = function(x, row.names = NULL, optional = FALSE, ...) { } #' @export -labelling_names = function(x) UseMethod("labelling_names") +labelling_column_names = function(x) UseMethod("labelling_column_names") -#' @describeIn Index Retrieve the \code{labelling_names} of -#' a \code{Index} object. These \code{labelling_names} -#' are the names of the index that are used to label the model -#' components. +#' @describeIn mp_index Retrieve the \code{labelling_column_names} of +#' an index. These are the names of the columns that are used to label +#' the model components. #' @export -labelling_names.Index = function(x) x$labelling_names +labelling_column_names.Index = function(x) x$labelling_column_names -#' @describeIn Index Convert a \code{Index} object into +#' @describeIn mp_index Convert an index into #' a character vector giving labels associated with each model component #' (i.e. row) being described. #' @export to_labels.Index = function(x) x$labels() -#' @describeIn Index Convert a \code{Index} object into +#' @describeIn mp_index Convert an index into #' a character vector giving labels associated with each model component #' (i.e. row) being described. #' @export labels.Index = function(x, ...) x$labels() -#' Create Index Object -#' -#' Create an \code{\link{Index}} object from a set of -#' character vectors. This function is analogous to the -#' \code{\link{data.frame}} function for creating data frames. -#' -#' @param ... Character vectors to combine into an \code{\link{Index}}. -#' object. -#' @param labelling_names See \code{\link{Index}}. -#' -#' @seealso [Index()] -#' @examples -#' mp_index( -#' Epi = c("S", "I", "S", "I"), -#' Age = c("young", "young", "old", "old") -#' ) -#' -#' @export -mp_index = function(..., labelling_names) UseMethod("mp_index") - #' @export -mp_index.character = function(..., labelling_names) { +mp_index.character = function(..., labelling_column_names) { f = data.frame(...) - if (missing(labelling_names)) labelling_names = names(f) - Index(f, to_names(labelling_names)) + if (missing(labelling_column_names)) labelling_column_names = names(f) + Index(f, to_names(labelling_column_names)) } #' @export -mp_index.data.frame = function(..., labelling_names) { +mp_index.data.frame = function(..., labelling_column_names) { f = list(...)[[1L]] - if (missing(labelling_names)) labelling_names = names(f) - Index(f, to_names(labelling_names)) + if (missing(labelling_column_names)) labelling_column_names = names(f) + Index(f, to_names(labelling_column_names)) } diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 322b05fc..37ceff3c 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -5,7 +5,7 @@ mp_tmb_simulator = function(expr_list = ExprList() , unstruc_mats = list() , time_steps = 0L , mats_to_save = names(indexed_vecs) - , mats_to_return = names(indexed_vecs) + , mats_to_return = mats_to_save , ... ) { int_vecs = (index_data diff --git a/R/link.R b/R/link.R index 6c0c8f08..cb5cf4b2 100644 --- a/R/link.R +++ b/R/link.R @@ -18,14 +18,14 @@ #' ``` #' #' @export -Link = function(frame, column_map, reference_index_list, labelling_names_list) { +Link = function(frame, column_map, reference_index_list, labelling_column_names_list) { self = Base() self$frame = frame self$column_map = column_map self$reference_index_list = reference_index_list - self$labelling_names_list = labelling_names_list + self$labelling_column_names_list = labelling_column_names_list # function() { - # lapply(self$reference_index_list, getElement, "labelling_names") + # lapply(self$reference_index_list, getElement, "labelling_column_names") # } self$table_names = function() names(self$column_map) self$labels_for = list() @@ -72,7 +72,7 @@ Link = function(frame, column_map, reference_index_list, labelling_names_list) { Link(self$frame[i, , drop = FALSE] , column_map = self$column_map , reference_index_list = self$reference_index_list - , labelling_names_list = self$labelling_names_list + , labelling_column_names_list = self$labelling_column_names_list ) } self$expr = function(condition) { @@ -115,12 +115,12 @@ FrameGetter = function(link, dimension_name) { self$get_index = function() { Index( self$get_partition(), - self$link$labelling_names_list[[self$dimension_name]], + self$link$labelling_column_names_list[[self$dimension_name]], self$link$reference_index_list[[self$dimension_name]] ) } self$get_labels = function() { - i = self$link$labelling_names_list[[self$dimension_name]] + i = self$link$labelling_column_names_list[[self$dimension_name]] f = self$get_frame()[, i, drop = FALSE] l = as.list(f) paste_args = c(l, sep = ".") @@ -154,9 +154,9 @@ initial_reference_index_list = function(index, dimension_name) { } -initial_labelling_names_list = function(labelling_names, dimension_name) { +initial_labelling_column_names_list = function(labelling_column_names, dimension_name) { setNames( - list(labelling_names), + list(labelling_column_names), dimension_name ) } @@ -180,7 +180,8 @@ merge_util = function(x, y, by.x, by.y) { z = merge( x$frame, y$frame, by.x = by.x, by.y = by.y, - suffixes = suffixes + suffixes = suffixes, + sort = FALSE ) ## ---- @@ -233,7 +234,7 @@ merge_util = function(x, y, by.x, by.y) { z_column_map = c(x_cmap, y_cmap) z_reference_index_list = c(x$reference_index_list, y$reference_index_list) - z_lab_names_list = c(x$labelling_names_list, y$labelling_names_list) + z_lab_names_list = c(x$labelling_column_names_list, y$labelling_column_names_list) ## ---- ## wrap up the result with provenance-preserving column map @@ -260,11 +261,11 @@ filter_by_list = function(x_orig, y_orig, by_list) { } #' @export -init_merge = function(frame, dimension_name, reference_index, labelling_names) { +init_merge = function(frame, dimension_name, reference_index, labelling_column_names) { Link(frame , initial_column_map(names(frame), dimension_name) , initial_reference_index_list(reference_index, dimension_name) - , initial_labelling_names_list(labelling_names, dimension_name) + , initial_labelling_column_names_list(labelling_column_names, dimension_name) ) } @@ -293,7 +294,8 @@ apply_col_map = function(map, orig_table_nm, by) { map[[orig_table_nm]][by] |> unlist(use.names = FALSE) } - +## @param x Link object +## @param col_nm Name of a column to check for implicit provenance is_provenance_implicit = function(x, col_nm) { (x$column_map |> lapply(getElement, col_nm) @@ -302,6 +304,7 @@ is_provenance_implicit = function(x, col_nm) { ) } +## @param x Link object explicit_provenance = function(x, col_nm) { m = x$column_map implicit = is_provenance_implicit(x, col_nm) @@ -319,7 +322,8 @@ explicit_provenance = function(x, col_nm) { ) f = x$frame - l = x$labelling_names_list + l = x$labelling_column_names_list + ii = x$reference_index_list orig_col = f[[col_nm]] for (tab_nm in tabs_to_fix) { @@ -329,7 +333,8 @@ explicit_provenance = function(x, col_nm) { } f[[col_nm]] = NULL ## TODO: update with four-arg form of Link - Link(f, m, l) + ## frame, column_map, reference_index_list, labelling_column_names_list + Link(f, m, ii, l) } @@ -402,7 +407,7 @@ print.summary.Link = function(x, ...) { names.Link = function(x) names(x$frame) #' @export -labelling_names.Link = function(x) x$labelling_names_list +labelling_column_names.Link = function(x) x$labelling_column_names_list link_format_picker = function(x @@ -461,3 +466,4 @@ str.Link = function(x x = link_format_picker(x, format) str(x, ...) } + diff --git a/R/mp.R b/R/mp.R index f625a1d6..cf87697a 100644 --- a/R/mp.R +++ b/R/mp.R @@ -12,17 +12,66 @@ mp = function(mp_func) { } +#' Cartesian Product of Indexes +#' +#' Produce a new index by taking all possible pairwise combinations +#' of the input indexes. This is useful for producing product models +#' that expand model components through stratification. +#' +#' @param x,y Objects produced by \code{\link{mp_index}} or derived +#' from such an object using one of (TODO: list the functions that +#' will preserve indexness). +#' +#' @examples +#' mp_cartesian( +#' mp_index(Epi = c("S", "I")), +#' mp_index(Age = c("young", "old")) +#' ) +#' +#' si = mp_index(Epi = c("S", "I")) +#' age = mp_index(Age = c("young", "old")) +#' loc = mp_index(City = c("hamilton", "toronto")) +#' vax = mp_index(Vax = c("unvax", "vax")) +#' (si +#' |> mp_cartesian(age) +#' |> mp_cartesian(loc) +#' |> mp_cartesian(vax) +#' ) +#' +#' flow_rates = mp_index(Epi = c("infection", "recovery")) +#' mp_union( +#' mp_cartesian( +#' mp_subset(flow_rates, Epi = "infection"), +#' age +#' ), +#' mp_subset(flow_rates, Epi = "recovery") +#' ) +#' #' @export mp_cartesian = function(x, y) { - labelling_names = union(x$labelling_names, y$labelling_names) + shared_columns = intersect(names(x), names(y)) + if (length(shared_columns) != 0) { + msg_break( + msg_colon( + msg( + "Cannot take the Cartesian product of two indexes that", + "share columns names. But the input indexes share the", + "following columns" + ), + msg_indent(shared_columns) + ), + msg("Perhaps mp_join is more suitable?") + ) |> stop() + } + labelling_column_names = union(x$labelling_column_names, y$labelling_column_names) f = join_partitions(x$partition$frame(), y$partition$frame()) - Index(f, labelling_names = labelling_names) + Index(f, labelling_column_names = labelling_column_names) } #' @export mp_square = function(x, suffixes = c("A", "B")) { - l1 = sprintf("%s%s", x$labelling_names, suffixes[1L]) - l2 = sprintf("%s%s", x$labelling_names, suffixes[2L]) + l1 = sprintf("%s%s", x$labelling_column_names, suffixes[1L]) + l2 = sprintf("%s%s", x$labelling_column_names, suffixes[2L]) n1 = sprintf("%s%s", names(x), suffixes[1L]) n2 = sprintf("%s%s", names(x), suffixes[2L]) x = (x$partition$frame() @@ -37,9 +86,9 @@ mp_square = function(x, suffixes = c("A", "B")) { } #' @export -mp_triangle = function(x, y_labelling_names, exclude_diag = TRUE, lower_tri = FALSE) { +mp_triangle = function(x, y_labelling_column_names, exclude_diag = TRUE, lower_tri = FALSE) { f = x$partition$frame() - g = setNames(f, y_labelling_names) + g = setNames(f, y_labelling_column_names) n = nrow(f) if (exclude_diag) { k = 2:n @@ -63,9 +112,9 @@ mp_triangle = function(x, y_labelling_names, exclude_diag = TRUE, lower_tri = FA } #' @export -mp_symmetric = function(x, y_labelling_names, exclude_diag = TRUE) { +mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { f = x$partition$frame() - g = setNames(f, y_labelling_names) + g = setNames(f, y_labelling_column_names) n = nrow(f) k = seq_len(n) @@ -85,9 +134,9 @@ mp_symmetric = function(x, y_labelling_names, exclude_diag = TRUE) { } #' @export -mp_linear = function(x, y_labelling_names) { +mp_linear = function(x, y_labelling_column_names) { f = x$partition$frame() - g = setNames(f, y_labelling_names) + g = setNames(f, y_labelling_column_names) n = nrow(f) k = c(1L, rep(2L, n - 2L), 1L) @@ -101,18 +150,30 @@ mp_linear = function(x, y_labelling_names) { Index(f, names(f)) } +#' Subset of Indexes +#' +#' Take a subset of the rows of an index to produce another index. +#' +#' @param x Model index. +#' @param ... Tagged character vectors used to determine the subset that is +#' taken. The tags refer to columns (or sets of columns using dot-concatenation) +#' in \code{x} and the values of the character vectors refer to labels with +#' respect to those columns. +#' #' @export mp_subset = function(x, ...) { partition = mp_choose(x, "pick", ...)$partition - Index(partition, x$labelling_names, x) + Index(partition, x$labelling_column_names, x) } +#' @rdname mp_subset #' @export mp_setdiff = function(x, ...) { partition = mp_choose_out(x, "pick", ...)$partition - Index(partition, x$labelling_names, x) + Index(partition, x$labelling_column_names, x) } +#' Union of Indexes #' @export mp_union = function(...) UseMethod("mp_union") @@ -120,15 +181,15 @@ mp_union = function(...) UseMethod("mp_union") mp_union.Index = function(...) { l = list(...) partitions = lapply(l, getElement, "partition") - labelling_names = (l - |> lapply(getElement, "labelling_names") + labelling_column_names = (l + |> lapply(getElement, "labelling_column_names") |> unlist(recursive = FALSE, use.names = FALSE) |> unique() ) - Index(do.call(union_vars, partitions)$frame(), labelling_names) + Index(do.call(union_vars, partitions)$frame(), labelling_column_names) } -#' @export +## not used anymore? mp_union.Link = function(...) { l = list(...) column_map = lapply(l, getElement, "column_map") |> unique() @@ -144,23 +205,23 @@ mp_union.Link = function(...) { ) |> stop() } ## TODO: should really be checking for reference_index_list - labelling_names_list = lapply(l, getElement, "labelling_names_list") |> unique() - if (length(labelling_names_list) != 1L) { + labelling_column_names_list = lapply(l, getElement, "labelling_column_names_list") |> unique() + if (length(labelling_column_names_list) != 1L) { msg_colon( msg( "Union of inconsistent Link objects.", "All Link objects must have the same", - "labelling_names_list, but the following", + "labelling_column_names_list, but the following", "distinct maps were found:" ), - msg_indent_break(lapply(labelling_names_list, unlist)) + msg_indent_break(lapply(labelling_column_names_list, unlist)) ) |> stop() } frame = mp_rbind(...) - FormulaData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_names_list) + FormulaData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) } -#' @export +## not used anymore? mp_rbind = function(...) { (list(...) |> lapply(as.data.frame) @@ -173,6 +234,7 @@ mp_rbind = function(...) { #' @export mp_choose = function(x, subset_name, ...) { l = list(...) + if (length(l) != 0L) valid$named_list$check(l) p = x$partition for (cc in names(l)) { vals = l[[cc]] @@ -184,7 +246,7 @@ mp_choose = function(x, subset_name, ...) { p = p$filter(vals, .wrt = cc) } } - init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_names) + init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) } #' @export @@ -195,16 +257,23 @@ mp_choose_out = function(x, subset_name, ...) { vals = l[[cc]] p = p$filter_out(vals, .wrt = cc) } - init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_names) + init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) } #' @export mp_join = function(..., by = list()) { - table_list = list(...) - by_list = assert_named_list(by) - by_nms = names(by_list) |> strsplit(".", fixed = TRUE) + table_list = valid$named_list$assert(list(...)) table_nms = names(table_list) + if (length(table_nms) < 2L) stop("cannot join fewer than two index objects.") + if (is.character(by)) { + if (length(table_nms) != 2L) { + stop("joining more than one index requires a list-valued by argument.") + } + by = setNames(list(by), to_name(table_nms)) + } + by_list = valid$named_list$assert(by) + by_nms = names(by_list) |> strsplit(".", fixed = TRUE) good_by_nms = (by_nms |> lapply(`%in%`, table_nms) |> vapply(all, logical(1L)) @@ -355,6 +424,12 @@ mp_decompose = function(formula, index, decomp_name, ...) { ) } +#' @export +mp_extract = function(x, dimension_name) { + ii = x$index_for[[dimension_name]]() + ii$reset_reference_index() + ii +} #' @export mp_rename = function(x, ...) { @@ -362,7 +437,7 @@ mp_rename = function(x, ...) { new_nms = names(l) old_nms = unlist(l, recursive = FALSE, use.names = FALSE) f = x$partition$frame() - labs = x$labelling_names + labs = x$labelling_column_names i = match(old_nms, names(f)) if (any(is.na(i))) { msg_break( @@ -385,8 +460,8 @@ mp_rename = function(x, ...) { #' @export mp_group = function(index, by) { frame = index$partition$select(to_names(by))$frame() - nms = names(frame)[names(frame) %in% index$labelling_names] - Index(frame, labelling_names = nms) + nms = names(frame)[names(frame) %in% index$labelling_column_names] + Index(frame, labelling_column_names = nms) } #' @export @@ -404,28 +479,28 @@ mp_indices = function(x, table) { } #' @export -mp_labels = function(x, labelling_names) { +mp_labels = function(x, labelling_column_names) { UseMethod("mp_labels") } #' @export -mp_labels.Index = function(x, labelling_names) { - if (missing(labelling_names)) return(x$labels()) - x$partial_labels(labelling_names) +mp_labels.Index = function(x, labelling_column_names) { + if (missing(labelling_column_names)) return(x$labels()) + x$partial_labels(labelling_column_names) } #' @export -mp_zero_vector = function(x, labelling_names, ...) { +mp_zero_vector = function(x, labelling_column_names, ...) { (x |> mp_subset(...) - |> mp_labels(labelling_names) + |> mp_labels(labelling_column_names) |> zero_vector() ) } #' @export -mp_labels.Link = function(x, labelling_names) { - x$labels_for[[labelling_names]]() +mp_labels.Link = function(x, labelling_column_names) { + x$labels_for[[labelling_column_names]]() } #' @export diff --git a/R/name_utils.R b/R/name_utils.R index e206c3a9..69866c3d 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -61,7 +61,7 @@ to_names.character = function(x) { to_names.Partition = function(x) x$names() #' @export -to_names.Index = function(x) x$labelling_names +to_names.Index = function(x) x$labelling_column_names #' @export to_names.StringData = function(x) x$undot()$names()$value() diff --git a/R/vector.R b/R/vector.R index 4d4d2187..551846b5 100644 --- a/R/vector.R +++ b/R/vector.R @@ -2,8 +2,9 @@ Vector = function(x, ...) UseMethod("Vector") #' @export -Vector.data.frame = function(x, index, values_name = "values", ...) { +Vector.data.frame = function(x, index = NULL, values_name = "values", ...) { nms = setdiff(names(x), values_name) + if (is.null(index)) index = mp_index(x[, nms, drop = FALSE]) bad_names = !nms %in% names(index) if (any(bad_names)) { msg_break( @@ -24,7 +25,7 @@ Vector.data.frame = function(x, index, values_name = "values", ...) { index_names = !names(x) %in% values_name test_index = try(Index(x[, index_names, drop = FALSE])) if (inherits(test_index, "try-error")) stop("Incompatible values data frame") - bad_labels = !test_index$labels() %in% index$partial_labels(test_index$labelling_names) + bad_labels = !test_index$labels() %in% index$partial_labels(test_index$labelling_column_names) if (any(bad_labels)) { msg_colon( msg( @@ -39,13 +40,13 @@ Vector.data.frame = function(x, index, values_name = "values", ...) { index_names = !names(f) %in% values_name ## redefine index_names after merge mangled_order = Index( f[, index_names, drop = FALSE], - labelling_names = index$labelling_names + labelling_column_names = index$labelling_column_names )$labels() sorted_positions = match(index$labels(), mangled_order) values = f[[values_name]][sorted_positions] values[is.na(values)] = 0 ## that's the way we roll f = f[sorted_positions, index_names, drop = FALSE] - index = Index(f, labelling_names = index$labelling_names) + index = Index(f, labelling_column_names = index$labelling_column_names) v = Vector(index) v$set_all_numbers(values) } @@ -53,7 +54,7 @@ Vector.data.frame = function(x, index, values_name = "values", ...) { #' @export Vector.numeric = function(x, index, ...) { v = Vector(index) - args = setNames(list(x), to_name(index$labelling_names)) + args = setNames(list(x), to_name(index$labelling_column_names)) do.call(v$set_numbers, args) } @@ -101,6 +102,10 @@ Vector.Index = function(x, ...) { write.csv(self$frame(), file, row.names = FALSE, quote = FALSE) } self$length = function() length(self$.numbers) + self$clone = function() { + new = Vector(self$index) + new$set_all_numbers(self$numbers()) + } return_object(self, "Vector") } @@ -128,7 +133,24 @@ as.matrix.Vector = function(x, ...) x$numbers() |> as.matrix() zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' @export -mp_vector = Vector +mp_vector = function(x, ...) UseMethod("mp_vector") + +#' @export +mp_vector.Index = Vector.Index + +#' @export +mp_vector.data.frame = Vector.data.frame + +#' @export +mp_vector.numeric = Vector.numeric + +#' @export +mp_vector.character = function(x, ...) zero_vector(x) + +#' @export +mp_vector.Link = function(x, dimension_name, ...) { + mp_vector(x$labels_for[[dimension_name]]()) +} #' @export -mp_set_numbers = function(vector, ...) vector$set_numbers(...) +mp_set_numbers = function(vector, ...) vector$clone()$set_numbers(...) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index f9ea40f7..3581dcae 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -23,11 +23,11 @@ beta = mp_subset(rates, Epi = "beta") ## links between entries in the indexes ----------- index_data = list( - flow = mp_formula_data( + flow = mp_index_data( infection = mp_join(from = S, to = I, flow = lambda), recovery = mp_join(from = I, to = R, flow = gamma) ), - influences = mp_formula_data( + influences = mp_index_data( transmission = mp_join( infectious = I, infection = lambda, diff --git a/inst/model_library/sir_age/experimentation.R b/inst/model_library/sir_age/experimentation.R new file mode 100644 index 00000000..375f7919 --- /dev/null +++ b/inst/model_library/sir_age/experimentation.R @@ -0,0 +1,12 @@ +library(dplyr) +library(ggplot2) +library(tidyr) + +(sir$report() + |> filter(matrix == "state") + |> separate(row, c("epi", "age")) + |> mutate(epi = factor(epi, levels = c("S", "I", "R"))) + |> ggplot() + + geom_line(aes(time, value, colour = age)) + + facet_wrap(~epi) +) diff --git a/inst/model_library/sir_age/main.R b/inst/model_library/sir_age/main.R new file mode 100644 index 00000000..0aec59d7 --- /dev/null +++ b/inst/model_library/sir_age/main.R @@ -0,0 +1,5 @@ +proj_dir = "inst/model_library/sir_age" + +proj_dir |> file.path("model_structure.R") |> source() +proj_dir |> file.path("numerical_inputs.R") |> source() +proj_dir |> file.path("simulator.R") |> source() diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R new file mode 100644 index 00000000..03858098 --- /dev/null +++ b/inst/model_library/sir_age/model_structure.R @@ -0,0 +1,130 @@ +library(macpan2) + +## basic model indexes ------------------------- + +state_sir = mp_index(Epi = c("S", "I", "R")) +flow_rates_sir = mp_index(Epi = c("lambda", "gamma")) +trans_rates_sir = mp_index(Epi = "beta") + +## model strata indexes ------------------------ + +age = mp_index(Age = sprintf("lb%s", seq(from = 0, to = 90, by = 10))) +age_contact = mp_cartesian( + mp_rename(age, AgeInfectious = "Age"), + mp_rename(age, AgeSusceptible = "Age") +) + +## structured model -------- + +state = mp_cartesian(state_sir, age) +flow_rates = mp_union( + mp_cartesian( + mp_subset(flow_rates_sir, Epi = "lambda"), + age + ), + mp_subset(flow_rates_sir, Epi = "gamma") +) +trans_rates = mp_cartesian(trans_rates_sir, age_contact) + +## subset and grouping indexes ----------------- + +S = mp_subset(state, Epi = "S") +I = mp_subset(state, Epi = "I") +R = mp_subset(state, Epi = "R") + +lambda = mp_subset(flow_rates, Epi = "lambda") +gamma = mp_subset(flow_rates, Epi = "gamma") + +alive = mp_subset(state, Epi = c("S", "I", "R")) +strata = mp_group(state, "Age") + +susceptibility = mp_group(trans_rates, "AgeSusceptible") +contact = mp_group(trans_rates, "AgeInfectious.AgeSusceptible") +infectivity = mp_group(trans_rates, "AgeInfectious") + +## aggregations, normalizations, summarizations --------------- + +### lining up vectors that are involved +aggregation = mp_join(alive = alive, groups = strata, by = "Age") +normalization = mp_join(norm = I , denominator = strata, by = "Age") + +### creating new indexes for some of these vectors +N = mp_extract(aggregation , "groups") +norm_state = mp_extract(normalization, "norm") + +## decompositions ----------------------------- + +trans_decomposition = mp_join( + decomp = trans_rates, + s = susceptibility, + si = contact, + i = infectivity, + by = list( + decomp.s = "AgeSusceptible" + , decomp.si = "AgeInfectious.AgeSusceptible" + , decomp.i = "AgeInfectious" + ) +) + + +## linking states and rates ----------- + +transmission = mp_join( + infectious = norm_state, + infection = lambda, + transmission = trans_rates, + by = list( + infectious.transmission = "Age" ~ "AgeInfectious", + infection.transmission = "Age" ~ "AgeSusceptible" + ) +) +infection = mp_join( + from = S, to = I, edge = lambda, + by = list(from.to = "Age", from.edge = "Age") +) +recovery = mp_join( + from = I, to = R, edge = gamma, + by = list(from.to = "Age") +) + +index_data = list( + mp_index_data(trans_decomposition) + , mp_index_data(aggregation) + , mp_index_data(normalization) + , mp_index_data(infection, recovery) ## flows + , mp_index_data(transmission) +) + +## expressions that define model dynamics --------- + +## these expressions are very general and should remain robust +## as model structure changes +expr_list = mp_expr_list( + before = list( + trans_rates ~ susceptibility[s] * contact[si] * infectivity[i] + ), + during = list( + N ~ groupSums(state[alive], groups, N) + , norm_state ~ state[norm] / N[denominator] + , flow_rates[infection] ~ norm_state[infectious] * trans_rates[transmission] + , flows_per_time ~ state[from] * flow_rates[edge] + , inflow ~ groupSums(flows_per_time, to, state) + , outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + inflow - outflow + ) +) + +## instantiate numeric vectors labelled with particular indexes ----------- + +## TODO: give automatic advice or just automatically initialize +## vectors required given an analysis of the expression graph +indexed_vecs = list( + state = mp_vector(state), + flow_rates = mp_vector(flow_rates), + trans_rates = mp_vector(trans_rates), + N = mp_vector(mp_extract(aggregation, "groups")), + norm_state = mp_vector(mp_extract(normalization, "norm")), + susceptibility = mp_vector(susceptibility), + contact = mp_vector(contact), + infectivity = mp_vector(infectivity) +) diff --git a/inst/model_library/sir_age/numerical_inputs.R b/inst/model_library/sir_age/numerical_inputs.R new file mode 100644 index 00000000..928117c8 --- /dev/null +++ b/inst/model_library/sir_age/numerical_inputs.R @@ -0,0 +1,44 @@ +library(dplyr) +library(tidyr) + +## insert non-zero values + +indexed_vecs$state = (indexed_vecs$state + |> mp_set_numbers(Epi = c(S = 1000)) ## ridiculous even age distribution + |> mp_set_numbers(Epi = c(I = 1), Age = "lb90") ## initial infectious 90-year-old +) +indexed_vecs$flow_rates = (indexed_vecs$flow_rates + |> mp_set_numbers(Epi = c(lambda = NA, gamma = 0.1)) +) +indexed_vecs$susceptibility = (susceptibility + |> as.data.frame() + |> mutate(values = seq(from = 0.01, by = 0.1, length.out = n())) + |> mp_vector(susceptibility) +) +indexed_vecs$contact = (contact + |> as.data.frame() + |> mutate( + i = as.numeric(sub("^lb", "", AgeInfectious)), + s = as.numeric(sub("^lb", "", AgeSusceptible)), + values = exp(-((i - s)/20)^2) ## ridiculous kernel + ) + |> group_by(AgeInfectious) + |> mutate(values = values / sum(values)) + |> ungroup() + |> select(-i, -s) + |> as.data.frame() + |> mp_vector(contact) +) +indexed_vecs$infectivity = (infectivity + |> as.data.frame() + |> mutate(values = seq(from = 0.01, by = 0.1, length.out = n())) + |> mp_vector(infectivity) +) + +## unstructured matrices -------------------------- + +unstruc_mats = list() + +## number of time steps to run in simulations ------ + +time_steps = 100L diff --git a/inst/model_library/sir_age/simulator.R b/inst/model_library/sir_age/simulator.R new file mode 100644 index 00000000..891f6b18 --- /dev/null +++ b/inst/model_library/sir_age/simulator.R @@ -0,0 +1,10 @@ +## collect information into a simulator ----------------------- + +sir = mp_tmb_simulator( + expr_list + , index_data + , indexed_vecs + , unstruc_mats + , time_steps + , mats_to_save = "state" +) diff --git a/man/Index.Rd b/man/Index.Rd deleted file mode 100644 index 010a15b6..00000000 --- a/man/Index.Rd +++ /dev/null @@ -1,124 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/index.R -\name{Index} -\alias{Index} -\alias{Index.Partition} -\alias{Index.data.frame} -\alias{Index.Index} -\alias{print.Index} -\alias{names.Index} -\alias{labelling_names.Index} -\alias{to_labels.Index} -\alias{labels.Index} -\title{Index} -\usage{ -Index(partition, labelling_names = names(partition), reference_index = NULL) - -\method{Index}{Partition}(partition, labelling_names = names(partition), reference_index = NULL) - -\method{Index}{data.frame}(partition, labelling_names = names(partition), reference_index = NULL) - -\method{Index}{Index}(partition, labelling_names = names(partition), reference_index = NULL) - -\method{print}{Index}(x, ...) - -\method{names}{Index}(x) - -\method{labelling_names}{Index}(x) - -\method{to_labels}{Index}(x) - -\method{labels}{Index}(x, ...) -} -\arguments{ -\item{partition}{A data frame (or data frame-like) object containing -definitions of the index. This object can be a \code{\link{data.frame}}, -\code{\link{Partition}}, or another \code{\link{Index}} object.} - -\item{labelling_names}{A \code{\link{character}} vector of the names of the -index that will be used to label the model components (i.e. rows) being -described. The \code{labelling_names} cannot have duplicates and must -contain at least one name. The index given by the \code{labelling_names} -must uniquely identify each row.} - -\item{reference_index}{(Advanced) An optional partition to use when -computing subset indices.} - -\item{x}{\code{Index} object.} - -\item{...}{For consistency with existing S3 methods.} -} -\description{ -Make an object to represent descriptions of model components and their -dimensions of variation. \code{Index} objects generalize and wrap -\code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like -object is an entry in the index, and each column provides a description of -the entries. For example, the following \code{Index} object describes the -state variables of an age-structured SIR model. Each row corresponds to a -state variable and each state variable is described by columns \code{Epi} and -\code{Age}. - -\if{html}{\out{
    }}\preformatted{#> Epi Age -#> S young -#> I young -#> R young -#> S old -#> I old -#> R old -}\if{html}{\out{
    }} -} -\section{Methods (by class)}{ -\itemize{ -\item \code{Index(Partition)}: Create a \code{Index} object from a -\code{\link{Partition}} object. These \code{\link{Partition}} objects -wrap \code{\link{data.frame}}s. These data frames must follow certain -restrictions. - -\item \code{Index(data.frame)}: Create a \code{Index} object from a -\code{\link{data.frame}} object that follows certain restrictions -documented in the help page for \code{\link{Partition}}s. - -\item \code{Index(Index)}: Create another \code{Index} object from an -existing \code{Index} object, possibly with new \code{labelling_names}. - -}} -\section{Methods (by generic)}{ -\itemize{ -\item \code{print(Index)}: Print a \code{Index} object. - -\item \code{names(Index)}: Get the names of the index -(i.e. columns) of a \code{Index} object. - -\item \code{labelling_names(Index)}: Retrieve the \code{labelling_names} of -a \code{Index} object. These \code{labelling_names} -are the names of the index that are used to label the model -components. - -\item \code{to_labels(Index)}: Convert a \code{Index} object into -a character vector giving labels associated with each model component -(i.e. row) being described. - -\item \code{labels(Index)}: Convert a \code{Index} object into -a character vector giving labels associated with each model component -(i.e. row) being described. - -}} -\examples{ -sir = data.frame( - Epi = c("S", "I", "R", "D" ), - Vital = c("alive", "alive", "alive", "dead") -) |> Index(labelling_names = "Epi") -age = data.frame( - Age = c("young", "old") -) |> Index() -state_index = mp_cartesian(sir, age) -labels(state_index) -(state_index - |> mp_subset(Vital = "alive") - |> labels() -) - -} -\seealso{ -\code{\link[=mp_index]{mp_index()}} -} diff --git a/man/IndexedExpressions.Rd b/man/IndexedExpressions.Rd index 0949452b..1eb9ac05 100644 --- a/man/IndexedExpressions.Rd +++ b/man/IndexedExpressions.Rd @@ -16,7 +16,7 @@ IndexedExpressions( \code{index_data}, the vectors in \code{vector_list} and the matrices in \code{unstructured_matrix_list}.} -\item{index_data}{An object produced using \code{\link{mp_formula_data}}.} +\item{index_data}{An object produced using \code{\link{mp_index_data}}.} \item{vector_list}{Named list of objected produced using \code{\link{mp_vector}}.} diff --git a/man/Link.Rd b/man/Link.Rd index fcec6c18..dc218387 100644 --- a/man/Link.Rd +++ b/man/Link.Rd @@ -4,7 +4,7 @@ \alias{Link} \title{Link} \usage{ -Link(frame, column_map, reference_index_list, labelling_names_list) +Link(frame, column_map, reference_index_list, labelling_column_names_list) } \description{ Make an object to describe links between the entities in an diff --git a/man/mp_cartesian.Rd b/man/mp_cartesian.Rd new file mode 100644 index 00000000..2de814cc --- /dev/null +++ b/man/mp_cartesian.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_cartesian} +\alias{mp_cartesian} +\title{Cartesian Product of Indexes} +\usage{ +mp_cartesian(x, y) +} +\arguments{ +\item{x, y}{Objects produced by \code{\link{mp_index}} or derived +from such an object using one of (TODO: list the functions that +will preserve indexness).} +} +\description{ +Produce a new index by taking all possible pairwise combinations +of the input indexes. This is useful for producing product models +that expand model components through stratification. +} +\examples{ +mp_cartesian( + mp_index(Epi = c("S", "I")), + mp_index(Age = c("young", "old")) +) + +si = mp_index(Epi = c("S", "I")) +age = mp_index(Age = c("young", "old")) +loc = mp_index(City = c("hamilton", "toronto")) +vax = mp_index(Vax = c("unvax", "vax")) +(si + |> mp_cartesian(age) + |> mp_cartesian(loc) + |> mp_cartesian(vax) +) + +flow_rates = mp_index(Epi = c("infection", "recovery")) +mp_union( + mp_cartesian( + mp_subset(flow_rates, Epi = "infection"), + age + ), + mp_subset(flow_rates, Epi = "recovery") +) + +} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index c43797a3..df7379a0 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -2,28 +2,138 @@ % Please edit documentation in R/index.R \name{mp_index} \alias{mp_index} -\title{Create Index Object} +\alias{print.Index} +\alias{names.Index} +\alias{labelling_column_names.Index} +\alias{to_labels.Index} +\alias{labels.Index} +\title{Model Component Index} \usage{ -mp_index(..., labelling_names) +mp_index(..., labelling_column_names) + +\method{print}{Index}(x, ...) + +\method{names}{Index}(x) + +\method{labelling_column_names}{Index}(x) + +\method{to_labels}{Index}(x) + +\method{labels}{Index}(x, ...) } \arguments{ -\item{...}{Character vectors to combine into an \code{\link{Index}}. -object.} +\item{...}{Character vectors to combine to produce an index.} -\item{labelling_names}{See \code{\link{Index}}.} +\item{labelling_column_names}{A \code{\link{character}} vector of the names +of the index that will be used to label the model components (i.e. rows) +being described. The \code{labelling_column_names} cannot have duplicates +and must contain at least one name. The index given by the +\code{labelling_column_names} must uniquely identify each row.} } \description{ -Create an \code{\link{Index}} object from a set of -character vectors. This function is analogous to the +Make an index object to represent descriptions of model components and their +dimensions of variation. These objects generalize and wrap +\code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like +object is an entry in the index, and each column provides a description of +the entries. Each of these entries describes an element of a model component. +For example, the following index describes the +state variables of an age-structured SIR model. Each row corresponds to a +state variable and each state variable is described by columns \code{Epi} and +\code{Age}. + +\if{html}{\out{
    }}\preformatted{#> Epi Age +#> S young +#> I young +#> R young +#> S old +#> I old +#> R old +}\if{html}{\out{
    }} + +Each index can produce labels for the elements by dot-concatenating the +values in each row. The labels of the state variables in this +age-structured SIR example model are as follows. + +\if{html}{\out{
    }}\preformatted{#> [1] "S.young" "I.young" "R.young" "S.old" "I.old" "R.old" +}\if{html}{\out{
    }} + +These labels can be used to create 'multidimensional' names for the elements +of vectors. Here is the above example expressed in vector form. + +\if{html}{\out{
    }}\preformatted{#> S.young I.young R.young S.old I.old R.old +#> 1000 0 0 1000 1 0 +}\if{html}{\out{
    }} + +This example vector could be stored as a 3-by-2 matrix. But other examples +cannot, making this indexing approach more general. For example, consider +the following index. + +\if{html}{\out{
    }}\preformatted{#> Epi Symptoms +#> S +#> I mild +#> I severe +#> R +}\if{html}{\out{
    }} + +This index has an associated indexed vector that cannot be expressed +as a matrix. + +\if{html}{\out{
    }}\preformatted{#> S. I.mild I.severe R. +#> 1000 0 1 0 +}\if{html}{\out{
    }} + +Dots are not allowed in the index so that the labels can be inverted to +reproduce the original index (provided that the column names can be +retrieved). See the details below for more restrictions. +} +\details{ +This function is analogous to the \code{\link{data.frame}} function for creating data frames. + +All values in the index must contain only letters, numbers, and underscores, +and blank empty string entries are also allowed. No value can be missing. +These restrictions ensure that the dot-concatenation of each row can be +unambiguously inverted and that these dot-concatenations produce +syntactically valid names (see \code{\link{make.names}}). These +dot-concatenations are used to provide labels for the model components. + +By convention, it is recommended to use CamalCase for the columns of +indexes and either snake_case (aging_rate) or single uppercase +letters (e.g. S, I). This helps when reading code that contains +references to both column names and values in an index. } +\section{Functions}{ +\itemize{ +\item \code{print(Index)}: Print an index. + +\item \code{names(Index)}: Get the names of the columns of an index. + +\item \code{labelling_column_names(Index)}: Retrieve the \code{labelling_column_names} of +an index. These are the names of the columns that are used to label +the model components. + +\item \code{to_labels(Index)}: Convert an index into +a character vector giving labels associated with each model component +(i.e. row) being described. + +\item \code{labels(Index)}: Convert an index into +a character vector giving labels associated with each model component +(i.e. row) being described. + +}} \examples{ -mp_index( +state = mp_index( Epi = c("S", "I", "S", "I"), Age = c("young", "young", "old", "old") ) +print(state) +labels(state) +state_vector = (state + |> mp_vector() + |> mp_set_numbers(Epi = c(S = 1000)) + |> mp_set_numbers(Epi = c(I = 1), Age = "old") +) +print(state_vector) +mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) } -\seealso{ -\code{\link[=Index]{Index()}} -} diff --git a/man/mp_formula_data.Rd b/man/mp_index_data.Rd similarity index 81% rename from man/mp_formula_data.Rd rename to man/mp_index_data.Rd index ec1bf898..f807c0bb 100644 --- a/man/mp_formula_data.Rd +++ b/man/mp_index_data.Rd @@ -1,13 +1,13 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/formula_data.R -\name{mp_formula_data} -\alias{mp_formula_data} +\name{mp_index_data} +\alias{mp_index_data} \title{#' @export print.FormulaData = function(x, ...) { print(x$frame, row.names = FALSE) }} \usage{ -mp_formula_data(...) +mp_index_data(...) } \description{ #' @export diff --git a/man/mp_subset.Rd b/man/mp_subset.Rd new file mode 100644 index 00000000..3995e74c --- /dev/null +++ b/man/mp_subset.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_subset} +\alias{mp_subset} +\alias{mp_setdiff} +\title{Subset of Indexes} +\usage{ +mp_subset(x, ...) + +mp_setdiff(x, ...) +} +\arguments{ +\item{x}{Model index.} + +\item{...}{Tagged character vectors used to determine the subset that is +taken. The tags refer to columns (or sets of columns using dot-concatenation) +in \code{x} and the values of the character vectors refer to labels with +respect to those columns.} +} +\description{ +Take a subset of the rows of an index to produce another index. +} diff --git a/man/mp_union.Rd b/man/mp_union.Rd new file mode 100644 index 00000000..165cc44f --- /dev/null +++ b/man/mp_union.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_union} +\alias{mp_union} +\title{Union of Indexes} +\usage{ +mp_union(...) +} +\description{ +Union of Indexes +} diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 4dcf3020..6b03919c 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -19,14 +19,14 @@ strain_nms = letters[seq_len(n_strains)] strains = mp_index( Strain = strain_nms[-n_strains], Replace = strain_nms[-1L], - labelling_names = "Strain" + labelling_column_names = "Strain" ) replacement_state = mp_union( ## naive part mp_index(Epi = "S", Strain = "", Replace = "a" - , labelling_names = "Epi.Strain" + , labelling_column_names = "Epi.Strain" ), ## non-naive part @@ -51,8 +51,8 @@ xx = macpan2:::FormulaData( ) -replacement_infection$labelling_names_list -replacement_progression$labelling_names_list +replacement_infection$labelling_column_names_list +replacement_progression$labelling_column_names_list replacement_infection$column_map replacement_progression$column_map @@ -101,7 +101,7 @@ xx$labels() state_sir = mp_index( Epi = c("S", "I", "R", "D"), Vital = c("alive", "alive", "alive", "dead"), - labelling_names = "Epi" + labelling_column_names = "Epi" ) flow_rates_sir = mp_index(Epi = c("lambda", "gamma", "mu")) trans_rates_sir = mp_index(Epi = "beta") @@ -189,7 +189,7 @@ death_flows = mp_join( ) ) -flows = mp_formula_data( +flows = mp_index_data( movement_flows , infection_flows , recovery_flows @@ -216,7 +216,7 @@ state_aggregation = mp_join( alive = alive, group_by = strata, by = list(alive.group_by = "Loc.Age") -) |> mp_formula_data() +) |> mp_index_data() ## N ~ groupSums(state[alive], group_by, N) state_vector = Vector(state) state_vector$set_numbers(Epi = c(I = 1))$set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) @@ -276,8 +276,8 @@ self$frame self$reference_index_list$N$labels() groupSums() mp_labels -xx$reference_index_list$N$labelling_names -yy = mp_formula_data(xx) +xx$reference_index_list$N$labelling_column_names +yy = mp_index_data(xx) yy$reference_index_list yy = Vector(state) @@ -388,7 +388,7 @@ N_expr = mp_expr_group_sum(state xx = mp_join( mp_subset(N_expr$strata, "N"), mp_subset(state, "infectious", Epi = "I"), - N.infectious = N_expr$strata$labelling_names + N.infectious = N_expr$strata$labelling_column_names ) xx$labels_for$infectious() xx$frame @@ -508,7 +508,7 @@ make_mp_index = function(vec_name, vars_table, settings_list, vec_part_field, la vec_labs = to_labels(vars_table[settings_list[[vec_part_field]]]) mp_index( vars_table[vec_labs == vec_name, , drop = FALSE], - labelling_names = to_name(settings_list[[lab_part_field]]) + labelling_column_names = to_name(settings_list[[lab_part_field]]) ) } state = make_mp_index("state", vars_file, settings_file, "vec_partition", "labelling_partition") From ea3801b57f61e46c3adf4635ea4244a65288ea48 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 3 Nov 2023 12:48:50 -0400 Subject: [PATCH 055/332] ggplot fussing --- inst/model_library/sir_age/experimentation.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inst/model_library/sir_age/experimentation.R b/inst/model_library/sir_age/experimentation.R index 375f7919..607793cd 100644 --- a/inst/model_library/sir_age/experimentation.R +++ b/inst/model_library/sir_age/experimentation.R @@ -1,10 +1,12 @@ library(dplyr) library(ggplot2) library(tidyr) +library(stringr) (sir$report() |> filter(matrix == "state") |> separate(row, c("epi", "age")) + |> mutate(age = ordered(as.integer(str_remove(age, "^lb")))) |> mutate(epi = factor(epi, levels = c("S", "I", "R"))) |> ggplot() + geom_line(aes(time, value, colour = age)) From 6f5c79d5c598b3d64e3c080a90f14db3011c0434 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 3 Nov 2023 13:20:46 -0400 Subject: [PATCH 056/332] pushing --- R/index.R | 9 +++++--- R/mp.R | 20 ++++++++++++---- inst/model_library/sir_age/numerical_inputs.R | 2 +- man/mp_cartesian.Rd | 8 +++++++ man/mp_index.Rd | 14 +++++++++-- man/mp_rename.Rd | 23 +++++++++++++++++++ man/mp_subset.Rd | 16 +++++++++---- man/mp_union.Rd | 11 +++++++++ man/roxygen/meta.R | 3 +++ 9 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 man/mp_rename.Rd create mode 100644 man/roxygen/meta.R diff --git a/R/index.R b/R/index.R index 226912a3..09edb12d 100644 --- a/R/index.R +++ b/R/index.R @@ -61,7 +61,10 @@ #' letters (e.g. S, I). This helps when reading code that contains #' references to both column names and values in an index. #' -#' @param ... Character vectors to combine to produce an index. +#' @param ... Character vectors to combine to produce an index. Alternatively, +#' any number of data frames of character-valued columns. If data frames are +#' supplied, their rows will be binded and the result converted to an index +#' if possible. #' @param labelling_column_names A \code{\link{character}} vector of the names #' of the index that will be used to label the model components (i.e. rows) #' being described. The \code{labelling_column_names} cannot have duplicates @@ -82,7 +85,7 @@ #' ) #' print(state_vector) #' mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) -#' +#' @family indexes #' @export mp_index = function(..., labelling_column_names) UseMethod("mp_index") @@ -208,7 +211,7 @@ mp_index.character = function(..., labelling_column_names) { #' @export mp_index.data.frame = function(..., labelling_column_names) { - f = list(...)[[1L]] + f = list(...) |> bind_rows() if (missing(labelling_column_names)) labelling_column_names = names(f) Index(f, to_names(labelling_column_names)) } diff --git a/R/mp.R b/R/mp.R index cf87697a..8fceb58e 100644 --- a/R/mp.R +++ b/R/mp.R @@ -47,6 +47,7 @@ mp = function(mp_func) { #' mp_subset(flow_rates, Epi = "recovery") #' ) #' +#' @family indexes #' @export mp_cartesian = function(x, y) { shared_columns = intersect(names(x), names(y)) @@ -155,11 +156,12 @@ mp_linear = function(x, y_labelling_column_names) { #' Take a subset of the rows of an index to produce another index. #' #' @param x Model index. -#' @param ... Tagged character vectors used to determine the subset that is -#' taken. The tags refer to columns (or sets of columns using dot-concatenation) -#' in \code{x} and the values of the character vectors refer to labels with -#' respect to those columns. +#' @param ... Name-value pairs. The names are columns (or sets of columns +#' using dot-concatenation) in \code{x} and the values are character vectors +#' that refer to labels with respect to those columns. These values +#' determine the resulting subset. #' +#' @family indexes #' @export mp_subset = function(x, ...) { partition = mp_choose(x, "pick", ...)$partition @@ -174,6 +176,10 @@ mp_setdiff = function(x, ...) { } #' Union of Indexes +#' +#' @param ... Indexes. +#' +#' @family indexes #' @export mp_union = function(...) UseMethod("mp_union") @@ -431,6 +437,12 @@ mp_extract = function(x, dimension_name) { ii } +#' Rename Index Columns +#' +#' @param ... Name-value pairs. The name gives the new name and the value +#' is a character vector giving the old name. +#' +#' @family indexes #' @export mp_rename = function(x, ...) { l = list(...) diff --git a/inst/model_library/sir_age/numerical_inputs.R b/inst/model_library/sir_age/numerical_inputs.R index 928117c8..04d1c1c2 100644 --- a/inst/model_library/sir_age/numerical_inputs.R +++ b/inst/model_library/sir_age/numerical_inputs.R @@ -5,7 +5,7 @@ library(tidyr) indexed_vecs$state = (indexed_vecs$state |> mp_set_numbers(Epi = c(S = 1000)) ## ridiculous even age distribution - |> mp_set_numbers(Epi = c(I = 1), Age = "lb90") ## initial infectious 90-year-old + |> mp_set_numbers(Epi = c(I = 1), Age = "lb90") ## initial infectious 80-year-old ) indexed_vecs$flow_rates = (indexed_vecs$flow_rates |> mp_set_numbers(Epi = c(lambda = NA, gamma = 0.1)) diff --git a/man/mp_cartesian.Rd b/man/mp_cartesian.Rd index 2de814cc..166861b3 100644 --- a/man/mp_cartesian.Rd +++ b/man/mp_cartesian.Rd @@ -42,3 +42,11 @@ mp_union( ) } +\seealso{ +Other functions that return indexes +\code{\link{mp_index}()}, +\code{\link{mp_rename}()}, +\code{\link{mp_subset}()}, +\code{\link{mp_union}()} +} +\concept{indexes} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index df7379a0..3be3cf9e 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -22,7 +22,10 @@ mp_index(..., labelling_column_names) \method{labels}{Index}(x, ...) } \arguments{ -\item{...}{Character vectors to combine to produce an index.} +\item{...}{Character vectors to combine to produce an index. Alternatively, +any number of data frames of character-valued columns. If data frames are +supplied, their rows will be binded and the result converted to an index +if possible.} \item{labelling_column_names}{A \code{\link{character}} vector of the names of the index that will be used to label the model components (i.e. rows) @@ -135,5 +138,12 @@ state_vector = (state ) print(state_vector) mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) - } +\seealso{ +Other functions that return indexes +\code{\link{mp_cartesian}()}, +\code{\link{mp_rename}()}, +\code{\link{mp_subset}()}, +\code{\link{mp_union}()} +} +\concept{indexes} diff --git a/man/mp_rename.Rd b/man/mp_rename.Rd new file mode 100644 index 00000000..4cc8b4b5 --- /dev/null +++ b/man/mp_rename.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_rename} +\alias{mp_rename} +\title{Rename Index Columns} +\usage{ +mp_rename(x, ...) +} +\arguments{ +\item{...}{Name-value pairs. The name gives the new name and the value +is a character vector giving the old name.} +} +\description{ +Rename Index Columns +} +\seealso{ +Other functions that return indexes +\code{\link{mp_cartesian}()}, +\code{\link{mp_index}()}, +\code{\link{mp_subset}()}, +\code{\link{mp_union}()} +} +\concept{indexes} diff --git a/man/mp_subset.Rd b/man/mp_subset.Rd index 3995e74c..7d1613de 100644 --- a/man/mp_subset.Rd +++ b/man/mp_subset.Rd @@ -12,11 +12,19 @@ mp_setdiff(x, ...) \arguments{ \item{x}{Model index.} -\item{...}{Tagged character vectors used to determine the subset that is -taken. The tags refer to columns (or sets of columns using dot-concatenation) -in \code{x} and the values of the character vectors refer to labels with -respect to those columns.} +\item{...}{Name-value pairs. The names are columns (or sets of columns +using dot-concatenation) in \code{x} and the values are character vectors +that refer to labels with respect to those columns. These values +determine the resulting subset.} } \description{ Take a subset of the rows of an index to produce another index. } +\seealso{ +Other functions that return indexes +\code{\link{mp_cartesian}()}, +\code{\link{mp_index}()}, +\code{\link{mp_rename}()}, +\code{\link{mp_union}()} +} +\concept{indexes} diff --git a/man/mp_union.Rd b/man/mp_union.Rd index 165cc44f..ab3746b6 100644 --- a/man/mp_union.Rd +++ b/man/mp_union.Rd @@ -6,6 +6,17 @@ \usage{ mp_union(...) } +\arguments{ +\item{...}{Indexes.} +} \description{ Union of Indexes } +\seealso{ +Other functions that return indexes +\code{\link{mp_cartesian}()}, +\code{\link{mp_index}()}, +\code{\link{mp_rename}()}, +\code{\link{mp_subset}()} +} +\concept{indexes} diff --git a/man/roxygen/meta.R b/man/roxygen/meta.R new file mode 100644 index 00000000..63290ef2 --- /dev/null +++ b/man/roxygen/meta.R @@ -0,0 +1,3 @@ +list( + rd_family_title = list(indexes = "Other functions that return indexes") +) From bc2dcac5258e1f4562231ef1f5d9707f57607dfc Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sat, 4 Nov 2023 11:46:17 -0400 Subject: [PATCH 057/332] describe sir_age model and decmop bug fix --- inst/model_library/sir/model_structure.R | 11 +++--- inst/model_library/sir_age/README.md | 28 ++++++------- inst/model_library/sir_age/experimentation.R | 4 +- inst/model_library/sir_age/model_structure.R | 39 +++++++++++++------ inst/model_library/sir_age/numerical_inputs.R | 30 +++++++------- inst/model_library/sir_age/simulator.R | 14 ++++++- 6 files changed, 78 insertions(+), 48 deletions(-) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index 3581dcae..e3737584 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -22,11 +22,12 @@ beta = mp_subset(rates, Epi = "beta") ## links between entries in the indexes ----------- -index_data = list( - flow = mp_index_data( - infection = mp_join(from = S, to = I, flow = lambda), - recovery = mp_join(from = I, to = R, flow = gamma) - ), +mp_join( + infection = mp_join(from = S, to = I, flow = lambda), + recovery = mp_join(from = I, to = R, flow = gamma) +) + + influences = mp_index_data( transmission = mp_join( infectious = I, diff --git a/inst/model_library/sir_age/README.md b/inst/model_library/sir_age/README.md index 237bd318..a06eefb0 100644 --- a/inst/model_library/sir_age/README.md +++ b/inst/model_library/sir_age/README.md @@ -1,20 +1,22 @@ -# Model Specification Format Challenges +--- +title: "Age-Structured SIR" +index_entry: "Basic SIR Model with Age Groups" +author: Steve Walker +--- -The `variables.csv` file contains two types variables, state and flow, as well as others that do not fit as nicely. For example, what do we do about transmission variables that need to map states to flows? +This model directory has the following files. -* So we are tempted to place these items into another kind of file, say `mappings.csv`. But now when resolving `flows.csv` we need to look in both `variables.csv` and `mappings.csv`. -* An alternative is to have lots of blank cells in `variables.csv`. But this how do we name the mappings without lots of dots in the basic state and flow variables? -* An alternative is to break the rule that all labels have the same numbers of dots, but this breaks the long-existing labelling standard. -* An alternative is to add mapping variables to `variables.csv` as vectors (or matrices?), but this causes problems for lining up the elements of these matrices through mechanisms like `flows.csv`. +1. `model_structure.R` -- Defines the structure of the model with neither numerical inputs nor numerical outputs. -All this to say that from the user's perspective what we want is really the first suggestion, and bite the bullet as a developer for resolving `flows.csv` in multiple locations. +2. `numerical_inputs.R` -- Assigns numerical values to vectors. -Or actually, maybe the better thing is to have smarter labelling standards that check to see if labels can be truncated without destroying invertibility? +3. `simulator.R` -- Constructs a simulator object that can be used to run simulations. -Well ... maybe we can save the labelling standards by letting the mapping component names get split into the atomic labels first to produce valid labels that can be inverted. +4. `experimentation.R` -- Example code for using the simulator object. Currently it just produces a graph. -Well ... this doesn't need to happen, because in the mappings file we might be able to keep them split up. +5. `main.R` -- Runs 1-3 above. -``` -flow[infection] ~ groupSums(beta * state[trans_infectious], trans_infection, length(infection)) -``` + +All of these files should be run from the root directory of `macpan2`. + +Files 1-4 depend on each other in sequential order. diff --git a/inst/model_library/sir_age/experimentation.R b/inst/model_library/sir_age/experimentation.R index 607793cd..22a77f76 100644 --- a/inst/model_library/sir_age/experimentation.R +++ b/inst/model_library/sir_age/experimentation.R @@ -6,9 +6,7 @@ library(stringr) (sir$report() |> filter(matrix == "state") |> separate(row, c("epi", "age")) - |> mutate(age = ordered(as.integer(str_remove(age, "^lb")))) |> mutate(epi = factor(epi, levels = c("S", "I", "R"))) |> ggplot() - + geom_line(aes(time, value, colour = age)) - + facet_wrap(~epi) + + geom_line(aes(time, value, colour = epi, linetype = age)) ) diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index 03858098..c3d09ca9 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -8,7 +8,7 @@ trans_rates_sir = mp_index(Epi = "beta") ## model strata indexes ------------------------ -age = mp_index(Age = sprintf("lb%s", seq(from = 0, to = 90, by = 10))) +age = mp_index(Age = c("young", "old")) age_contact = mp_cartesian( mp_rename(age, AgeInfectious = "Age"), mp_rename(age, AgeSusceptible = "Age") @@ -16,7 +16,9 @@ age_contact = mp_cartesian( ## structured model -------- -state = mp_cartesian(state_sir, age) +state = (state_sir + |> mp_cartesian(age) +) flow_rates = mp_union( mp_cartesian( mp_subset(flow_rates_sir, Epi = "lambda"), @@ -24,7 +26,9 @@ flow_rates = mp_union( ), mp_subset(flow_rates_sir, Epi = "gamma") ) -trans_rates = mp_cartesian(trans_rates_sir, age_contact) +trans_rates = (trans_rates_sir + |> mp_cartesian(age_contact) +) ## subset and grouping indexes ----------------- @@ -35,9 +39,10 @@ R = mp_subset(state, Epi = "R") lambda = mp_subset(flow_rates, Epi = "lambda") gamma = mp_subset(flow_rates, Epi = "gamma") -alive = mp_subset(state, Epi = c("S", "I", "R")) -strata = mp_group(state, "Age") +alive = mp_subset(state, Epi = c("S", "I", "R")) ## all states are alive in this model +strata = mp_group(state, "Age") ## stratify by age for normalizing I +## for decomposing beta into three components susceptibility = mp_group(trans_rates, "AgeSusceptible") contact = mp_group(trans_rates, "AgeInfectious.AgeSusceptible") infectivity = mp_group(trans_rates, "AgeInfectious") @@ -70,12 +75,12 @@ trans_decomposition = mp_join( ## linking states and rates ----------- transmission = mp_join( - infectious = norm_state, - infection = lambda, + infectious_state = norm_state, + infection_flow = lambda, transmission = trans_rates, by = list( - infectious.transmission = "Age" ~ "AgeInfectious", - infection.transmission = "Age" ~ "AgeSusceptible" + infectious_state.transmission = "Age" ~ "AgeInfectious", + infection_flow.transmission = "Age" ~ "AgeSusceptible" ) ) infection = mp_join( @@ -87,12 +92,18 @@ recovery = mp_join( by = list(from.to = "Age") ) +## wrapping the results of joins ------------ + +## wrap up decomposition, aggregation, normalization, +## flows, and influences into a data structure that +## is used by the dynamic model expressions that appear below. + index_data = list( mp_index_data(trans_decomposition) , mp_index_data(aggregation) , mp_index_data(normalization) , mp_index_data(infection, recovery) ## flows - , mp_index_data(transmission) + , mp_index_data(transmission) ## influences ) ## expressions that define model dynamics --------- @@ -101,12 +112,12 @@ index_data = list( ## as model structure changes expr_list = mp_expr_list( before = list( - trans_rates ~ susceptibility[s] * contact[si] * infectivity[i] + trans_rates[decomp] ~ infectivity[i] * contact[si] * susceptibility[s] ), during = list( N ~ groupSums(state[alive], groups, N) , norm_state ~ state[norm] / N[denominator] - , flow_rates[infection] ~ norm_state[infectious] * trans_rates[transmission] + , flow_rates[infection_flow] ~ norm_state[infectious_state] * trans_rates[transmission] , flows_per_time ~ state[from] * flow_rates[edge] , inflow ~ groupSums(flows_per_time, to, state) , outflow ~ groupSums(flows_per_time, from, state) @@ -128,3 +139,7 @@ indexed_vecs = list( contact = mp_vector(contact), infectivity = mp_vector(infectivity) ) + +## vectors and matrices without model structure +## (e.g. scalar factor like beta0) +unstruc_mats = list() diff --git a/inst/model_library/sir_age/numerical_inputs.R b/inst/model_library/sir_age/numerical_inputs.R index 04d1c1c2..b34b308d 100644 --- a/inst/model_library/sir_age/numerical_inputs.R +++ b/inst/model_library/sir_age/numerical_inputs.R @@ -5,39 +5,41 @@ library(tidyr) indexed_vecs$state = (indexed_vecs$state |> mp_set_numbers(Epi = c(S = 1000)) ## ridiculous even age distribution - |> mp_set_numbers(Epi = c(I = 1), Age = "lb90") ## initial infectious 80-year-old + |> mp_set_numbers(Epi = c(I = 1), Age = "old") ## initial infectious ) indexed_vecs$flow_rates = (indexed_vecs$flow_rates |> mp_set_numbers(Epi = c(lambda = NA, gamma = 0.1)) ) -indexed_vecs$susceptibility = (susceptibility - |> as.data.frame() - |> mutate(values = seq(from = 0.01, by = 0.1, length.out = n())) - |> mp_vector(susceptibility) + +## young people are less susceptible +indexed_vecs$susceptibility = (indexed_vecs$susceptibility + |> mp_set_numbers(AgeSusceptible = c(young = 0.1, old = 1)) ) + +## symmetric contact matrix with 0.6 on the diagonal and 0.4 off +## of the diagonal. indexed_vecs$contact = (contact |> as.data.frame() |> mutate( - i = as.numeric(sub("^lb", "", AgeInfectious)), - s = as.numeric(sub("^lb", "", AgeSusceptible)), - values = exp(-((i - s)/20)^2) ## ridiculous kernel + values = if_else(AgeInfectious == AgeSusceptible, 0.6, 0.4) ) |> group_by(AgeInfectious) |> mutate(values = values / sum(values)) |> ungroup() - |> select(-i, -s) |> as.data.frame() |> mp_vector(contact) ) -indexed_vecs$infectivity = (infectivity - |> as.data.frame() - |> mutate(values = seq(from = 0.01, by = 0.1, length.out = n())) - |> mp_vector(infectivity) + +## set infectivity = 1 for both age groups +indexed_vecs$infectivity = (indexed_vecs$infectivity + |> mp_set_numbers(AgeInfectious = c(young = 1, old = 1)) ) + + ## unstructured matrices -------------------------- -unstruc_mats = list() +unstruc_mats = list() ## none in this model ## number of time steps to run in simulations ------ diff --git a/inst/model_library/sir_age/simulator.R b/inst/model_library/sir_age/simulator.R index 891f6b18..65b7b126 100644 --- a/inst/model_library/sir_age/simulator.R +++ b/inst/model_library/sir_age/simulator.R @@ -1,10 +1,22 @@ ## collect information into a simulator ----------------------- +### pick vectors and matrices to be included in the +### simulation output. +to_return = c("state", "flows_per_time") + +### here is the list of vectors and matrices in the +### model that could be included in the +### simulation output. +setdiff( + expr_list$all_formula_vars(), + lapply(index_data, getElement, "table_names") |> unlist() +) + sir = mp_tmb_simulator( expr_list , index_data , indexed_vecs , unstruc_mats , time_steps - , mats_to_save = "state" + , mats_to_save = to_return ) From b3d0e7271034de3974947e3ce2f3695caa5c9039 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 6 Nov 2023 04:47:53 -0500 Subject: [PATCH 058/332] sir model structure file --- inst/model_library/sir/model_structure.R | 59 ++++++++++++++---------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index e3737584..0f8a205f 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -1,9 +1,25 @@ library(macpan2) +## dynamics --------------------------------------------- + +expr_list = mp_expr_list( + before = list( + N ~ sum(state) + ), + during = list( + flow_rates[infection_flow] ~ state[infectious_state] * trans_rates[transmission] / N + , flows_per_time ~ state[from] * flow_rates[edge] + , total_inflow ~ groupSums(flows_per_time, to, state) + , total_outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + total_inflow - total_outflow + ) +) + ## basic model indexes ------------------------- state = mp_index(Epi = c("S", "I", "R")) -rates = mp_index(Epi = c("lambda", "gamma", "beta")) +flow_rates = mp_index(Epi = c("lambda", "gamma")) +trans_rates = mp_index(Epi = "beta") ## model products (none in this model) -------- @@ -16,35 +32,28 @@ rates = mp_index(Epi = c("lambda", "gamma", "beta")) S = mp_subset(state, Epi = "S") I = mp_subset(state, Epi = "I") R = mp_subset(state, Epi = "R") -lambda = mp_subset(rates, Epi = "lambda") -gamma = mp_subset(rates, Epi = "gamma") -beta = mp_subset(rates, Epi = "beta") +lambda = mp_subset(flow_rates, Epi = "lambda") +gamma = mp_subset(flow_rates, Epi = "gamma") +beta = mp_subset(trans_rates, Epi = "beta") ## links between entries in the indexes ----------- -mp_join( - infection = mp_join(from = S, to = I, flow = lambda), - recovery = mp_join(from = I, to = R, flow = gamma) -) +infection = mp_join(from = S, to = I, edge = lambda) +recovery = mp_join(from = I, to = R, edge = gamma) - - influences = mp_index_data( - transmission = mp_join( - infectious = I, - infection = lambda, - transmission = beta - ) - ) +transmission = mp_join( + infectious_state = I, + infection_flow = lambda, + transmission = beta ) -## expressions that define model dynamics --------- +index_data = list( + transmission = mp_index_data(transmission) ## influences + , update = mp_index_data(infection, recovery) ## flows +) -expr_list = mp_expr_list( - during = list( - rates[infection] ~ state[infectious] * rates[transmission] / N - , flows_per_time ~ state[from] * rates[flow] - , inflow ~ groupSums(flows_per_time, to, state) - , outflow ~ groupSums(flows_per_time, from, state) - , state ~ state + inflow - outflow - ) +vecs = list( + state = mp_vector(state), + flow_rates = mp_vector(flow_rates), + trans_rates = mp_vector(trans_rates) ) From e634a0e15c0f6057684191c93dc61659c4368df3 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 6 Nov 2023 10:00:29 -0500 Subject: [PATCH 059/332] birs --- NAMESPACE | 11 +- R/dev_tools.R | 10 ++ R/expr_list.R | 3 + R/formula_data.R | 10 +- R/index_to_tmb.R | 82 +++++++++-- R/link.R | 28 ++++ R/lists.R | 2 + R/model_def_run.R | 73 ++++++++++ R/mp.R | 4 +- R/readers.R | 8 ++ R/vector.R | 35 +++++ inst/model_library/sir/README.Rmd | 47 +++++++ inst/model_library/sir/README.md | 39 ++++++ inst/model_library/sir/model_structure.R | 40 ++++-- inst/model_library/sir/numerical_inputs.R | 21 ++- inst/model_library/sir/settings.json | 4 + inst/model_library/sir/simulator.R | 5 +- inst/model_library/sir_age/experimentation.R | 2 +- inst/model_library/sir_age/model_structure.R | 132 +++++++++++++----- inst/model_library/sir_age/numerical_inputs.R | 12 +- inst/model_library/sir_age/settings.json | 4 + inst/model_library/sir_age/simulator.R | 23 +-- man/IndexedExpressions.Rd | 2 +- man/Reader.Rd | 5 + man/{mp_index_data.Rd => mp_link_data.Rd} | 10 +- misc/experiments/refactorcpp.R | 8 +- 26 files changed, 505 insertions(+), 115 deletions(-) create mode 100644 R/model_def_run.R create mode 100644 inst/model_library/sir/README.Rmd create mode 100644 inst/model_library/sir/README.md create mode 100644 inst/model_library/sir/settings.json create mode 100644 inst/model_library/sir_age/settings.json rename man/{mp_index_data.Rd => mp_link_data.Rd} (61%) diff --git a/NAMESPACE b/NAMESPACE index cde86cf9..3cc72708 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,6 +20,8 @@ S3method(mp_index,character) S3method(mp_index,data.frame) S3method(mp_labels,Index) S3method(mp_labels,Link) +S3method(mp_tmb_simulator,DynamicModel) +S3method(mp_tmb_simulator,ModelDefRun) S3method(mp_union,Index) S3method(mp_vector,Index) S3method(mp_vector,Link) @@ -32,6 +34,7 @@ S3method(names,Link) S3method(names,MatsList) S3method(names,MethList) S3method(names,Partition) +S3method(print,ExprList) S3method(print,Index) S3method(print,Link) S3method(print,MathExpression) @@ -73,10 +76,12 @@ export(BinaryOperator) export(CSVReader) export(Collection) export(Compartmental) +export(Compartmental2) export(CompartmentalSimulator) export(DerivationExtractor) export(Derivations) export(Derivations2ExprList) +export(DynamicModel) export(EngineMethods) export(Euler) export(ExprList) @@ -90,6 +95,7 @@ export(Infection) export(IntVecs) export(JSONReader) export(Link) +export(LinkList) export(Log) export(LogFile) export(Logit) @@ -108,6 +114,7 @@ export(OptParamsList) export(Partition) export(Products) export(Quantities) +export(RReader) export(Reader) export(Scalar2Vector) export(SimulatorConstructor) @@ -123,6 +130,7 @@ export(Time) export(Transform) export(UserExpr) export(Vector) +export(VectorList) export(all_consistent) export(all_equal) export(all_not_equal) @@ -152,14 +160,15 @@ export(mp_expr_list) export(mp_extract) export(mp_group) export(mp_index) -export(mp_index_data) export(mp_indexed_exprs) export(mp_indicator) export(mp_indices) export(mp_join) export(mp_labels) export(mp_linear) +export(mp_link_data) export(mp_rename) +export(mp_report) export(mp_set_numbers) export(mp_setdiff) export(mp_square) diff --git a/R/dev_tools.R b/R/dev_tools.R index e3282911..0031ddd6 100644 --- a/R/dev_tools.R +++ b/R/dev_tools.R @@ -40,3 +40,13 @@ dev_compile = function(suffix = "", ext = "cpp") { TMB::compile(ff) dyn.load(TMB::dynlib(dev_obj(suffix = suffix, ext = ext))) } + +render_model_readme = function(file) { + rmarkdown::render(input = file, output_format = "md_document", intermediates_dir = NULL) + f = basename(file) |> tools::file_path_sans_ext() + output_file = sprintf("%s.md", f) + d = dirname(file) + readLines(output_file) + readLines(file) + +} diff --git a/R/expr_list.R b/R/expr_list.R index ffae5538..5f3a62b1 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -241,3 +241,6 @@ ExprList = function( #' @export mp_expr_list = ExprList + +#' @export +print.ExprList = function(x, ...) x$print_exprs() diff --git a/R/formula_data.R b/R/formula_data.R index 8e5bdbe0..19c6c8fe 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -1,4 +1,4 @@ -FormulaData = function(...) { +LinkData = function(...) { self = Base() self$link_list = list(...) @@ -45,24 +45,24 @@ FormulaData = function(...) { bind_rows(positions_list) } - return_object(self, "FormulaData") + return_object(self, "LinkData") } #' #' @export -#' print.FormulaData = function(x, ...) { +#' print.LinkData = function(x, ...) { #' print(x$frame, row.names = FALSE) #' } #' @export -mp_index_data = function(...) FormulaData(...) +mp_link_data = function(...) LinkData(...) #' Indexed Expressions #' #' @param ... Formula objects that reference the columns in the #' \code{index_data}, the vectors in \code{vector_list} and the matrices #' in \code{unstructured_matrix_list}. -#' @param index_data An object produced using \code{\link{mp_index_data}}. +#' @param index_data An object produced using \code{\link{mp_link_data}}. #' @param vector_list Named list of objected produced using #' \code{\link{mp_vector}}. #' @param unstructured_matrix_list Named list of objects that can be coerced diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 37ceff3c..0c802b7a 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -1,20 +1,56 @@ +#' @importFrom oor method_apply #' @export -mp_tmb_simulator = function(expr_list = ExprList() - , index_data = list() - , indexed_vecs = list() - , unstruc_mats = list() +mp_tmb_simulator = function(dynamic_model , time_steps = 0L - , mats_to_save = names(indexed_vecs) + , vectors = NULL + , unstruc_mats = NULL + , mats_to_save = names(vectors) , mats_to_return = mats_to_save + , params = OptParamsList(0) + , random = OptParamsList() + , obj_fn = ObjectiveFunction(~0) + , log_file = LogFile() + , do_pred_sdreport = TRUE + , tmb_cpp = "macpan2" + , initialize_ad_fun = TRUE , ... ) { - int_vecs = (index_data + UseMethod("mp_tmb_simulator") +} + +#' @export +mp_tmb_simulator.DynamicModel = function(dynamic_model + , time_steps = 0L + , vectors = NULL + , unstruc_mats = NULL + , mats_to_save = names(vectors) + , mats_to_return = mats_to_save + , params = OptParamsList(0) + , random = OptParamsList() + , obj_fn = ObjectiveFunction(~0) + , log_file = LogFile() + , do_pred_sdreport = TRUE + , tmb_cpp = "macpan2" + , initialize_ad_fun = TRUE + , ... +) { + link_data = dynamic_model$link_data + expr_list = dynamic_model$expr_list + int_vecs = (link_data |> method_apply("positions_frame", zero_based = TRUE) |> lapply(as.list) |> unname() |> unlist(recursive = FALSE) ) - indexed_mats = lapply(indexed_vecs, as.matrix) + if (is.null(vectors)) { + indexed_mats = dynamic_model$init_vecs + mats_to_save = names(indexed_mats) + } else { + indexed_mats = lapply(vectors, as.matrix) + } + if (is.null(unstruc_mats)) { + unstruc_mats = dynamic_model$unstruc_mats + } all_vars = expr_list$all_formula_vars() @@ -39,7 +75,35 @@ mp_tmb_simulator = function(expr_list = ExprList() , expr_list = expr_list , engine_methods = engine_methods , time_steps = Time(time_steps) - , ... + , params = params + , random = random + , obj_fn = obj_fn + , log_file = log_file + , do_pred_sdreport = do_pred_sdreport ) - tmb_model$simulator() + tmb_model$simulator(tmb_cpp = tmb_cpp, initialize_ad_fun = initialize_ad_fun) +} + +#' @export +mp_tmb_simulator.ModelDefRun = function(dynamic_model + , time_steps = 0L + , vectors = NULL + , unstruc_mats = NULL + , mats_to_save = names(vectors) + , mats_to_return = mats_to_save + , params = OptParamsList(0) + , random = OptParamsList() + , obj_fn = ObjectiveFunction(~0) + , log_file = LogFile() + , do_pred_sdreport = TRUE + , tmb_cpp = "macpan2" + , initialize_ad_fun = TRUE + , ... +) { + args = c(as.list(environment()), list(...)) + args$dynamic_model = dynamic_model$dynamic_model + do.call(mp_tmb_simulator, args) } + +#' @export +mp_report = function(simulator, ...) simulator$report() diff --git a/R/link.R b/R/link.R index cb5cf4b2..f35744af 100644 --- a/R/link.R +++ b/R/link.R @@ -467,3 +467,31 @@ str.Link = function(x str(x, ...) } + +#' @export +LinkList = function() { + self = Base() + self$list = list() |> setNames(as.character()) + self$add = function(new_link, ...) { + new_links = list(...) + if (missing(new_link)) { + macpan2:::valid$named_list$check(new_links) + } else if(length(new_links) == 0L) { + new_nm = deparse1(substitute(new_link)) + new_links = setNames(list(new_link), new_nm) + } else { + stop("If supplying more than one join result, please name them with {name} = {join_result}") + } + + for (nm in names(new_links)) { + if (nm %in% names(self$list)) { + msg( + "Join result", nm, "is already in the list.", + "Overwriting the existing one." + ) |> message() + } + self$list[[nm]] = new_links[[nm]] + } + } + return_object(self, "LinkList") +} diff --git a/R/lists.R b/R/lists.R index eb835d79..3d13611c 100644 --- a/R/lists.R +++ b/R/lists.R @@ -17,6 +17,8 @@ nlist = function(...) { setNames(L, nm) } +empty_named_list = function() list() |> setNames(character(0L)) + assert_named_list = function(l) { if (is.null(names(l))) { if (length(l) == 0L) { diff --git a/R/model_def_run.R b/R/model_def_run.R new file mode 100644 index 00000000..4dd4cc15 --- /dev/null +++ b/R/model_def_run.R @@ -0,0 +1,73 @@ +#' @export +Compartmental2 = function(model_directory) { + self = Base() + self$model_directory = model_directory + self$files = Files(model_directory + , reader_spec("settings.json", JSONReader) + ) + self$settings = function() self$files$get("settings") + self$def_env = new.env(parent = parent.frame()) + sys.source(file.path(model_directory, self$settings()[["structure_file"]]) + , envir = self$def_env + , chdir = TRUE + ) + self$dynamic_model = self$def_env[[self$settings()[["model_object"]]]] + + self$link_data_type = function(type) { + self$dynamic_model$link_data[[self$settings()[[type]]]] + } + self$index_data_type = function(type) { + x = self$def_env[[self$settings()[[type]]]] + stopifnot(inherits(x, "Index")) + x + } + self$flow_links = function() self$dynamic_model$link_data$flows + self$influence_links = function() self$dynamic_model$link_data$influences + #self$normalization_links = function() self$link_data_type("normalization") + #self$aggregation_links = function() self$link_data_type("aggregation") + + self$flows = function() self$flow_links()$labels_frame() + self$influences = function() self$influence_links()$labels_frame() + #self$normalization = function() self$normalization_links()$labels_frame() + #self$aggregation = function() self$aggregation_links()$labels_frame() + self$expr_list = function() self$def_env[[self$settings()[["expr_list"]]]] + + ## back-compatibility + self$variables = VariablesScripts(self) + self$labels = LabelsScripts(self) + + return_object(self, "ModelDefRun") +} + +VariablesScripts = function(model) { + self = Base() + self$model = model + self$state = function() self$model$index_data_type("state") + self$flow_rates = function() self$model$index_data_type("flow_rates") + self$influence_rates = function() self$model$index_data_type("influence_rates") + self$aggregated_states = function() self$model$index_data_type("aggregated_states") + self$normalized_state = function() self$model$index_data_type("normalized_state") + return_object(self, "VariablesScripts") +} + +LabelsScripts = function(model) { + self = Base() + self$model = model + self$variables = model$variables + self$state = function() self$variables$state()$labels() + self$flow_rates = function() self$variables$flow_rates()$labels() + self$influence_rates = function() self$variables$influence_rates()$labels() + self$aggregated_states = function() self$variables$aggregated_states()$labels() + self$normalized_state = function() self$variables$normalized_state()$labels() + return_object(self, "LabelsScripts") +} + +#' @export +DynamicModel = function(expr_list = ExprList(), link_data = list(), init_vecs = list(), unstruc_mats = list()) { + self = Base() + self$expr_list = expr_list + self$link_data = link_data + self$init_vecs = init_vecs + self$unstruc_mats = unstruc_mats + return_object(self, "DynamicModel") +} diff --git a/R/mp.R b/R/mp.R index 8fceb58e..e84871bb 100644 --- a/R/mp.R +++ b/R/mp.R @@ -224,7 +224,7 @@ mp_union.Link = function(...) { ) |> stop() } frame = mp_rbind(...) - FormulaData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) + LinkData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) } ## not used anymore? @@ -268,7 +268,7 @@ mp_choose_out = function(x, subset_name, ...) { #' @export -mp_join = function(..., by = list()) { +mp_join = function(..., by = empty_named_list()) { table_list = valid$named_list$assert(list(...)) table_nms = names(table_list) if (length(table_nms) < 2L) stop("cannot join fewer than two index objects.") diff --git a/R/readers.R b/R/readers.R index 09a6b05d..094a3f9b 100644 --- a/R/readers.R +++ b/R/readers.R @@ -77,6 +77,14 @@ TXTReader = function(...) { return_object(self, "TXTReader") } +#' @describeIn Reader Read R files. +#' @export +RReader = function(...) { + self = Reader(...) + self$read_base = function() readLines(self$file) + return_object(self, "RReader") +} + #' @describeIn Reader Placeholder reader that always returns \code{NULL}. #' @export NULLReader = function(...) { diff --git a/R/vector.R b/R/vector.R index 551846b5..f6ac3de9 100644 --- a/R/vector.R +++ b/R/vector.R @@ -154,3 +154,38 @@ mp_vector.Link = function(x, dimension_name, ...) { #' @export mp_set_numbers = function(vector, ...) vector$clone()$set_numbers(...) + + + + +#' @export +VectorList = function() { + self = Base() + self$list = list() |> setNames(as.character()) + self$add = function(new_vec, ...) { + new_vecs = list(...) + if (missing(new_vec)) { + macpan2:::valid$named_list$check(new_vecs) + } else if(length(new_vecs) == 0L) { + new_nm = deparse1(substitute(new_vec)) + new_vecs = setNames(list(new_vec), new_nm) + } else { + stop("If supplying more than one vector, please name them with {name} = {vector}") + } + + for (nm in names(new_vecs)) { + if (nm %in% names(self$list)) { + msg( + "Vector", nm, "is already in the list.", + "Overwriting the existing one." + ) |> message() + } + if (inherits(new_vecs[[nm]], "Index")) { + new_vecs[[nm]] = mp_vector(new_vecs[[nm]]) + } + self$list[[nm]] = new_vecs[[nm]] + } + } + return_object(self, "VectorList") +} + diff --git a/inst/model_library/sir/README.Rmd b/inst/model_library/sir/README.Rmd new file mode 100644 index 00000000..9bac2286 --- /dev/null +++ b/inst/model_library/sir/README.Rmd @@ -0,0 +1,47 @@ +--- +title: "Basic SIR" +index_entry: "A very simple epidemic model" +author: Steve Walker +--- + +This is (nearly) the simplest possible 'vanilla' epidemic model, +implemented as an example. + +```{r read_model, message = FALSE} +library(macpan2) +library(dplyr) +library(ggplot2) +sir_dir = system.file("model_library", "sir", package = "macpan2") +sir_mod = Compartmental2(sir_dir) +sir_sim = mp_tmb_simulator(sir_mod + , vectors = list( + state = c(S = 999, I = 1, R = 0), + flow_rates = c(lambda = NA, gamma = 0.1), + trans_rates = c(beta = 0.25) + ) + , time_steps = 100L +) +``` + +The state variables and rates in this model are as follows. + +```{r print_vars_and_rates} + +``` + +```{r print_flows_and_influences} +sir_mod$flows() +sir_mod$influences() +``` + + +```{r plot_model} +(sir_sim + |> mp_report() + |> filter(matrix == "state") + |> mutate(state = factor(row, levels = c("S", "I", "R"))) + |> ggplot() + + geom_line(aes(time, value, colour = state)) +) +``` + diff --git a/inst/model_library/sir/README.md b/inst/model_library/sir/README.md new file mode 100644 index 00000000..5713ddb1 --- /dev/null +++ b/inst/model_library/sir/README.md @@ -0,0 +1,39 @@ +This is (nearly) the simplest possible ‘vanilla’ epidemic model, +implemented as an example. + + library(macpan2) + library(dplyr) + library(ggplot2) + sir_dir = system.file("model_library", "sir", package = "macpan2") + sir_mod = Compartmental2(sir_dir) + sir_sim = mp_tmb_simulator(sir_mod + , vectors = list( + state = c(S = 999, I = 1, R = 0), + flow_rates = c(lambda = NA, gamma = 0.1), + trans_rates = c(beta = 0.25) + ) + , time_steps = 100L + ) + +The state variables and rates in this model are as follows. + + sir_mod$flows() + + ## from to edge + ## 1 S I lambda + ## 2 I R gamma + + sir_mod$influences() + + ## infectious_state infection_flow transmission + ## 1 I lambda beta + + (sir_sim + |> mp_report() + |> filter(matrix == "state") + |> mutate(state = factor(row, levels = c("S", "I", "R"))) + |> ggplot() + + geom_line(aes(time, value, colour = state)) + ) + +![](README_files/figure-markdown_strict/plot_model-1.png) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index 0f8a205f..b8817e5e 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -4,11 +4,18 @@ library(macpan2) expr_list = mp_expr_list( before = list( + ## aggregations N ~ sum(state) ), during = list( - flow_rates[infection_flow] ~ state[infectious_state] * trans_rates[transmission] / N + ## influences + flow_rates[infection_flow] ~ + state[infectious_state] * trans_rates[transmission] / N + + ## flows , flows_per_time ~ state[from] * flow_rates[edge] + + ## state update , total_inflow ~ groupSums(flows_per_time, to, state) , total_outflow ~ groupSums(flows_per_time, from, state) , state ~ state + total_inflow - total_outflow @@ -21,13 +28,7 @@ state = mp_index(Epi = c("S", "I", "R")) flow_rates = mp_index(Epi = c("lambda", "gamma")) trans_rates = mp_index(Epi = "beta") -## model products (none in this model) -------- - -## subset and grouping indexes ----------------- - -### note: this section is more interesting in structured models. -### in particular these are all single row indexes. also -### there are no grouping indexes, only subsets. +## subset indexes ----------------- S = mp_subset(state, Epi = "S") I = mp_subset(state, Epi = "I") @@ -38,22 +39,37 @@ beta = mp_subset(trans_rates, Epi = "beta") ## links between entries in the indexes ----------- +# flows_per_time ~ state[from] * flow_rates[edge] infection = mp_join(from = S, to = I, edge = lambda) recovery = mp_join(from = I, to = R, edge = gamma) + +# flow_rates[infection_flow] ~ +# state[infectious_state] * trans_rates[transmission] / N transmission = mp_join( infectious_state = I, infection_flow = lambda, transmission = beta ) -index_data = list( - transmission = mp_index_data(transmission) ## influences - , update = mp_index_data(infection, recovery) ## flows +link_data = list( + influences = mp_link_data(transmission) + , flows = mp_link_data(infection, recovery) ) -vecs = list( +## Initialize indexed vectors (to all zeros) -------------- + +init_vecs = list( state = mp_vector(state), flow_rates = mp_vector(flow_rates), trans_rates = mp_vector(trans_rates) ) + +## Final output ----------------- + +dynamic_model = DynamicModel( + expr_list = expr_list, + link_data = link_data, + init_vecs = init_vecs, + unstruc_mats = list() +) diff --git a/inst/model_library/sir/numerical_inputs.R b/inst/model_library/sir/numerical_inputs.R index 25c325e1..28375688 100644 --- a/inst/model_library/sir/numerical_inputs.R +++ b/inst/model_library/sir/numerical_inputs.R @@ -1,21 +1,18 @@ -source("model_structure.R") - ## numeric vectors labelled with particular indexes ----------- -indexed_vecs = list( - state = mp_vector( - c(S = 999, I = 1, R = 0), - state - ), - rates = mp_vector( - c(lambda = NA, gamma = 0.1, beta = 0.25), - rates - ) +vecs$state = mp_set_numbers(vecs$state + , Epi = c(S = 999, I = 1, R = 0) +) +vecs$flow_rates = mp_set_numbers(vecs$flow_rates + , Epi = c(lambda = NA, gamma = 0.1) +) +vecs$trans_rates = mp_set_numbers(vecs$trans_rates + , Epi = c(beta = 0.25) ) ## unstructured matrices -------------------------- -unstruc_mats = list(N = 1000) +unstruc_mats = list() ## number of time steps to run in simulations ------ diff --git a/inst/model_library/sir/settings.json b/inst/model_library/sir/settings.json new file mode 100644 index 00000000..077a01a7 --- /dev/null +++ b/inst/model_library/sir/settings.json @@ -0,0 +1,4 @@ +{ + "structure_file" : "model_structure.R", + "model_object" : "dynamic_model" +} diff --git a/inst/model_library/sir/simulator.R b/inst/model_library/sir/simulator.R index ce6f8337..2861eb3c 100644 --- a/inst/model_library/sir/simulator.R +++ b/inst/model_library/sir/simulator.R @@ -1,11 +1,10 @@ -source("numerical_inputs.R") - ## collect information into a simulator ----------------------- sir = mp_tmb_simulator( expr_list , index_data - , indexed_vecs + , vecs , unstruc_mats , time_steps ) +sir$report() diff --git a/inst/model_library/sir_age/experimentation.R b/inst/model_library/sir_age/experimentation.R index 22a77f76..11f90f4c 100644 --- a/inst/model_library/sir_age/experimentation.R +++ b/inst/model_library/sir_age/experimentation.R @@ -3,7 +3,7 @@ library(ggplot2) library(tidyr) library(stringr) -(sir$report() +(mp_report(sir_sim) |> filter(matrix == "state") |> separate(row, c("epi", "age")) |> mutate(epi = factor(epi, levels = c("S", "I", "R"))) diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index c3d09ca9..194fe167 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -1,5 +1,44 @@ library(macpan2) +## dynamics --------------------------------------------- + +expr_list = mp_expr_list( + before = list( + + sub_population_sizes = + N ~ groupSums(state[alive], groups, N) + + , decompose_transmission_rate = + trans_rates[decomp] ~ infectivity[i] * contact[si] * susceptibility[s] + ), + during = list( + + normalize_infectious_states = + norm_state ~ state[norm] / N[denominator] + + , update_force_of_infection = + flow_rates[infection_flow] ~ + norm_state[infectious_state] * trans_rates[transmission] + + , update_flows_per_time = + flows_per_time ~ state[from] * flow_rates[edge] + + , update_inflows_per_time = + total_inflow ~ groupSums(flows_per_time, to, state) + + , update_outflows_per_time = + total_outflow ~ groupSums(flows_per_time, from, state) + + , update_state = + state ~ state + total_inflow - total_outflow + ) +) + +## going to add all required vectors to this list as we go through +## each set of expressions in the dynamics above +#vecs = VectorList() +#joins = LinkList() + ## basic model indexes ------------------------- state_sir = mp_index(Epi = c("S", "I", "R")) @@ -30,6 +69,39 @@ trans_rates = (trans_rates_sir |> mp_cartesian(age_contact) ) + +## Vectorizing sub-population size computations ------- + +#expr_list$before$sub_population_sizes +## N ~ groupSums(state[alive], groups, N) + +#N = mp_group(state, "Age") +#state_alive = mp_subset(state, Epi = c("S", "I", "R")) +#joins$add(sub_population_sizes = +# mp_join(groups = N, alive = state_alive, by = "Age") +#) +#vecs$add(N) +#vecs$add(state) + +#oor::method_apply(joins$list, "labels_frame") |> lapply(as.list) |> unlist(use.names = FALSE, recursive = FALSE) +#joins$list$sub_population_sizes + +## grouped indices ---------- + +### stratify by age for normalizing I +N = mp_group(state, "Age") + +### decomposing transmission into three components: +#### 1. component that characterizes the susceptible categories +susceptibility = mp_group(trans_rates, "AgeSusceptible") +#### 2. component that characterizes the infectious-susceptible pairs +contact = mp_group(trans_rates, "AgeInfectious.AgeSusceptible") +#### 3. component that characterizes the infectious categories +infectivity = mp_group(trans_rates, "AgeInfectious") + + +## initialize vectors + ## subset and grouping indexes ----------------- S = mp_subset(state, Epi = "S") @@ -39,22 +111,21 @@ R = mp_subset(state, Epi = "R") lambda = mp_subset(flow_rates, Epi = "lambda") gamma = mp_subset(flow_rates, Epi = "gamma") -alive = mp_subset(state, Epi = c("S", "I", "R")) ## all states are alive in this model -strata = mp_group(state, "Age") ## stratify by age for normalizing I - -## for decomposing beta into three components -susceptibility = mp_group(trans_rates, "AgeSusceptible") -contact = mp_group(trans_rates, "AgeInfectious.AgeSusceptible") -infectivity = mp_group(trans_rates, "AgeInfectious") ## aggregations, normalizations, summarizations --------------- +### N ~ groupSums(state[alive], groups, N) +### norm_state ~ state[norm] / N[denominator] + +alive = mp_subset(state, Epi = c("S", "I", "R")) ## all states are alive in this model + ### lining up vectors that are involved -aggregation = mp_join(alive = alive, groups = strata, by = "Age") -normalization = mp_join(norm = I , denominator = strata, by = "Age") +aggregation = mp_join(alive = alive, groups = N, by = "Age") +normalization = mp_join(norm = I , denominator = N, by = "Age") ### creating new indexes for some of these vectors -N = mp_extract(aggregation , "groups") +#N = mp_index(Age = c("old", "young")) +#N = mp_extract(aggregation , "groups") ## necessary? norm_state = mp_extract(normalization, "norm") ## decompositions ----------------------------- @@ -98,48 +169,39 @@ recovery = mp_join( ## flows, and influences into a data structure that ## is used by the dynamic model expressions that appear below. -index_data = list( - mp_index_data(trans_decomposition) - , mp_index_data(aggregation) - , mp_index_data(normalization) - , mp_index_data(infection, recovery) ## flows - , mp_index_data(transmission) ## influences +link_data = list( + decomposition = mp_link_data(trans_decomposition) + , aggregation = mp_link_data(aggregation) + , normalization = mp_link_data(normalization) + , influences = mp_link_data(transmission) + , flows = mp_link_data(infection, recovery) ) ## expressions that define model dynamics --------- ## these expressions are very general and should remain robust ## as model structure changes -expr_list = mp_expr_list( - before = list( - trans_rates[decomp] ~ infectivity[i] * contact[si] * susceptibility[s] - ), - during = list( - N ~ groupSums(state[alive], groups, N) - , norm_state ~ state[norm] / N[denominator] - , flow_rates[infection_flow] ~ norm_state[infectious_state] * trans_rates[transmission] - , flows_per_time ~ state[from] * flow_rates[edge] - , inflow ~ groupSums(flows_per_time, to, state) - , outflow ~ groupSums(flows_per_time, from, state) - , state ~ state + inflow - outflow - ) -) ## instantiate numeric vectors labelled with particular indexes ----------- ## TODO: give automatic advice or just automatically initialize ## vectors required given an analysis of the expression graph -indexed_vecs = list( +init_vecs = list( state = mp_vector(state), flow_rates = mp_vector(flow_rates), trans_rates = mp_vector(trans_rates), - N = mp_vector(mp_extract(aggregation, "groups")), + N = mp_vector(N), + #N = mp_vector(mp_extract(aggregation, "groups")), norm_state = mp_vector(mp_extract(normalization, "norm")), susceptibility = mp_vector(susceptibility), contact = mp_vector(contact), infectivity = mp_vector(infectivity) ) -## vectors and matrices without model structure -## (e.g. scalar factor like beta0) -unstruc_mats = list() + +dynamic_model = DynamicModel( + expr_list = expr_list, + link_data = link_data, + init_vecs = init_vecs, + unstruc_mats = list() +) diff --git a/inst/model_library/sir_age/numerical_inputs.R b/inst/model_library/sir_age/numerical_inputs.R index b34b308d..0a381e1a 100644 --- a/inst/model_library/sir_age/numerical_inputs.R +++ b/inst/model_library/sir_age/numerical_inputs.R @@ -3,22 +3,22 @@ library(tidyr) ## insert non-zero values -indexed_vecs$state = (indexed_vecs$state - |> mp_set_numbers(Epi = c(S = 1000)) ## ridiculous even age distribution +init_vecs$state = (init_vecs$state + |> mp_set_numbers(Epi.Age = c(S.young = 1000, S.old = 3000)) |> mp_set_numbers(Epi = c(I = 1), Age = "old") ## initial infectious ) -indexed_vecs$flow_rates = (indexed_vecs$flow_rates +init_vecs$flow_rates = (init_vecs$flow_rates |> mp_set_numbers(Epi = c(lambda = NA, gamma = 0.1)) ) ## young people are less susceptible -indexed_vecs$susceptibility = (indexed_vecs$susceptibility +init_vecs$susceptibility = (init_vecs$susceptibility |> mp_set_numbers(AgeSusceptible = c(young = 0.1, old = 1)) ) ## symmetric contact matrix with 0.6 on the diagonal and 0.4 off ## of the diagonal. -indexed_vecs$contact = (contact +init_vecs$contact = (contact |> as.data.frame() |> mutate( values = if_else(AgeInfectious == AgeSusceptible, 0.6, 0.4) @@ -31,7 +31,7 @@ indexed_vecs$contact = (contact ) ## set infectivity = 1 for both age groups -indexed_vecs$infectivity = (indexed_vecs$infectivity +init_vecs$infectivity = (init_vecs$infectivity |> mp_set_numbers(AgeInfectious = c(young = 1, old = 1)) ) diff --git a/inst/model_library/sir_age/settings.json b/inst/model_library/sir_age/settings.json new file mode 100644 index 00000000..a47877ef --- /dev/null +++ b/inst/model_library/sir_age/settings.json @@ -0,0 +1,4 @@ +{ + "structure_file" : "model_structure.R", + "link_data" : "index_data" +} diff --git a/inst/model_library/sir_age/simulator.R b/inst/model_library/sir_age/simulator.R index 65b7b126..4c354023 100644 --- a/inst/model_library/sir_age/simulator.R +++ b/inst/model_library/sir_age/simulator.R @@ -1,22 +1,7 @@ ## collect information into a simulator ----------------------- -### pick vectors and matrices to be included in the -### simulation output. -to_return = c("state", "flows_per_time") - -### here is the list of vectors and matrices in the -### model that could be included in the -### simulation output. -setdiff( - expr_list$all_formula_vars(), - lapply(index_data, getElement, "table_names") |> unlist() -) - -sir = mp_tmb_simulator( - expr_list - , index_data - , indexed_vecs - , unstruc_mats - , time_steps - , mats_to_save = to_return +sir_sim = mp_tmb_simulator(dynamic_model + , vectors = init_vecs + , time_steps = 100L ) +mp_report(sir_sim) diff --git a/man/IndexedExpressions.Rd b/man/IndexedExpressions.Rd index 1eb9ac05..052a8c91 100644 --- a/man/IndexedExpressions.Rd +++ b/man/IndexedExpressions.Rd @@ -16,7 +16,7 @@ IndexedExpressions( \code{index_data}, the vectors in \code{vector_list} and the matrices in \code{unstructured_matrix_list}.} -\item{index_data}{An object produced using \code{\link{mp_index_data}}.} +\item{index_data}{An object produced using \code{\link{mp_link_data}}.} \item{vector_list}{Named list of objected produced using \code{\link{mp_vector}}.} diff --git a/man/Reader.Rd b/man/Reader.Rd index f72579fe..b5a0b79b 100644 --- a/man/Reader.Rd +++ b/man/Reader.Rd @@ -5,6 +5,7 @@ \alias{CSVReader} \alias{JSONReader} \alias{TXTReader} +\alias{RReader} \alias{NULLReader} \title{Reader} \usage{ @@ -16,6 +17,8 @@ JSONReader(...) TXTReader(...) +RReader(...) + NULLReader(...) } \arguments{ @@ -32,6 +35,8 @@ Construct objects for reading data. \item \code{TXTReader()}: Read TXT files. +\item \code{RReader()}: Read R files. + \item \code{NULLReader()}: Placeholder reader that always returns \code{NULL}. }} diff --git a/man/mp_index_data.Rd b/man/mp_link_data.Rd similarity index 61% rename from man/mp_index_data.Rd rename to man/mp_link_data.Rd index f807c0bb..e62e8bfc 100644 --- a/man/mp_index_data.Rd +++ b/man/mp_link_data.Rd @@ -1,17 +1,17 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/formula_data.R -\name{mp_index_data} -\alias{mp_index_data} +\name{mp_link_data} +\alias{mp_link_data} \title{#' @export -print.FormulaData = function(x, ...) { +print.LinkData = function(x, ...) { print(x$frame, row.names = FALSE) }} \usage{ -mp_index_data(...) +mp_link_data(...) } \description{ #' @export -print.FormulaData = function(x, ...) { +print.LinkData = function(x, ...) { print(x$frame, row.names = FALSE) } } diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 6b03919c..836516df 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -37,7 +37,7 @@ replacement_state = mp_union( ) -xx = macpan2:::FormulaData( +xx = macpan2:::LinkData( infection = mp_join( from = mp_subset(replacement_state, Epi = c("S", "R")), to = mp_subset(replacement_state, Epi = "E"), @@ -189,7 +189,7 @@ death_flows = mp_join( ) ) -flows = mp_index_data( +flows = mp_link_data( movement_flows , infection_flows , recovery_flows @@ -216,7 +216,7 @@ state_aggregation = mp_join( alive = alive, group_by = strata, by = list(alive.group_by = "Loc.Age") -) |> mp_index_data() +) |> mp_link_data() ## N ~ groupSums(state[alive], group_by, N) state_vector = Vector(state) state_vector$set_numbers(Epi = c(I = 1))$set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) @@ -277,7 +277,7 @@ self$reference_index_list$N$labels() groupSums() mp_labels xx$reference_index_list$N$labelling_column_names -yy = mp_index_data(xx) +yy = mp_link_data(xx) yy$reference_index_list yy = Vector(state) From 7d637b0062bdd5ed5e0134b2c522316bc56bad12 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 6 Nov 2023 10:02:47 -0500 Subject: [PATCH 060/332] out of date --- inst/model_library/sir/numerical_inputs.R | 19 ------------------- inst/model_library/sir/simulator.R | 10 ---------- 2 files changed, 29 deletions(-) delete mode 100644 inst/model_library/sir/numerical_inputs.R delete mode 100644 inst/model_library/sir/simulator.R diff --git a/inst/model_library/sir/numerical_inputs.R b/inst/model_library/sir/numerical_inputs.R deleted file mode 100644 index 28375688..00000000 --- a/inst/model_library/sir/numerical_inputs.R +++ /dev/null @@ -1,19 +0,0 @@ -## numeric vectors labelled with particular indexes ----------- - -vecs$state = mp_set_numbers(vecs$state - , Epi = c(S = 999, I = 1, R = 0) -) -vecs$flow_rates = mp_set_numbers(vecs$flow_rates - , Epi = c(lambda = NA, gamma = 0.1) -) -vecs$trans_rates = mp_set_numbers(vecs$trans_rates - , Epi = c(beta = 0.25) -) - -## unstructured matrices -------------------------- - -unstruc_mats = list() - -## number of time steps to run in simulations ------ - -time_steps = 100L diff --git a/inst/model_library/sir/simulator.R b/inst/model_library/sir/simulator.R deleted file mode 100644 index 2861eb3c..00000000 --- a/inst/model_library/sir/simulator.R +++ /dev/null @@ -1,10 +0,0 @@ -## collect information into a simulator ----------------------- - -sir = mp_tmb_simulator( - expr_list - , index_data - , vecs - , unstruc_mats - , time_steps -) -sir$report() From a943589af68d9b839a2883ee9fd7a15920ca2ef0 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 6 Nov 2023 11:23:27 -0500 Subject: [PATCH 061/332] clean up sir_age a bit --- inst/model_library/sir_age/model_structure.R | 120 ++++++++----------- 1 file changed, 47 insertions(+), 73 deletions(-) diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index 194fe167..de036e64 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -34,11 +34,6 @@ expr_list = mp_expr_list( ) ) -## going to add all required vectors to this list as we go through -## each set of expressions in the dynamics above -#vecs = VectorList() -#joins = LinkList() - ## basic model indexes ------------------------- state_sir = mp_index(Epi = c("S", "I", "R")) @@ -69,27 +64,7 @@ trans_rates = (trans_rates_sir |> mp_cartesian(age_contact) ) - -## Vectorizing sub-population size computations ------- - -#expr_list$before$sub_population_sizes -## N ~ groupSums(state[alive], groups, N) - -#N = mp_group(state, "Age") -#state_alive = mp_subset(state, Epi = c("S", "I", "R")) -#joins$add(sub_population_sizes = -# mp_join(groups = N, alive = state_alive, by = "Age") -#) -#vecs$add(N) -#vecs$add(state) - -#oor::method_apply(joins$list, "labels_frame") |> lapply(as.list) |> unlist(use.names = FALSE, recursive = FALSE) -#joins$list$sub_population_sizes - -## grouped indices ---------- - -### stratify by age for normalizing I -N = mp_group(state, "Age") +## decomposing transmission ---------- ### decomposing transmission into three components: #### 1. component that characterizes the susceptible categories @@ -99,37 +74,6 @@ contact = mp_group(trans_rates, "AgeInfectious.AgeSusceptible") #### 3. component that characterizes the infectious categories infectivity = mp_group(trans_rates, "AgeInfectious") - -## initialize vectors - -## subset and grouping indexes ----------------- - -S = mp_subset(state, Epi = "S") -I = mp_subset(state, Epi = "I") -R = mp_subset(state, Epi = "R") - -lambda = mp_subset(flow_rates, Epi = "lambda") -gamma = mp_subset(flow_rates, Epi = "gamma") - - -## aggregations, normalizations, summarizations --------------- - -### N ~ groupSums(state[alive], groups, N) -### norm_state ~ state[norm] / N[denominator] - -alive = mp_subset(state, Epi = c("S", "I", "R")) ## all states are alive in this model - -### lining up vectors that are involved -aggregation = mp_join(alive = alive, groups = N, by = "Age") -normalization = mp_join(norm = I , denominator = N, by = "Age") - -### creating new indexes for some of these vectors -#N = mp_index(Age = c("old", "young")) -#N = mp_extract(aggregation , "groups") ## necessary? -norm_state = mp_extract(normalization, "norm") - -## decompositions ----------------------------- - trans_decomposition = mp_join( decomp = trans_rates, s = susceptibility, @@ -143,8 +87,37 @@ trans_decomposition = mp_join( ) -## linking states and rates ----------- +## subset indexes ----------------- + +S = mp_subset(state, Epi = "S") +I = mp_subset(state, Epi = "I") +R = mp_subset(state, Epi = "R") + +lambda = mp_subset(flow_rates, Epi = "lambda") +gamma = mp_subset(flow_rates, Epi = "gamma") + +## aggregating states --------------- + +### N ~ groupSums(state[alive], groups, N) +aggregation = mp_join( + alive = mp_subset(state, Epi = c("S", "I", "R")) ## all states are alive in this model + , groups = mp_group(state, "Age") + , by = "Age" +) +N = mp_extract(aggregation, "groups") + +## normalizing states ----------- + +### norm_state ~ state[norm] / N[denominator] +normalization = mp_join(norm = I, denominator = N, by = "Age") + + +## influences (i.e. states directly influencing flow rates) ----------- +### update_force_of_infection = +### flow_rates[infection_flow] ~ +### norm_state[infectious_state] * trans_rates[transmission] +norm_state = mp_extract(normalization, "norm") transmission = mp_join( infectious_state = norm_state, infection_flow = lambda, @@ -154,6 +127,18 @@ transmission = mp_join( infection_flow.transmission = "Age" ~ "AgeSusceptible" ) ) + + +## flows (i.e. movement from one state to another) --------- + +### update_flows_per_time = +### flows_per_time ~ state[from] * flow_rates[edge] +### update_inflows_per_time = +### total_inflow ~ groupSums(flows_per_time, to, state) +### update_outflows_per_time = +### total_outflow ~ groupSums(flows_per_time, from, state) +### update_state = +### state ~ state + total_inflow - total_outflow infection = mp_join( from = S, to = I, edge = lambda, by = list(from.to = "Age", from.edge = "Age") @@ -163,11 +148,7 @@ recovery = mp_join( by = list(from.to = "Age") ) -## wrapping the results of joins ------------ - -## wrap up decomposition, aggregation, normalization, -## flows, and influences into a data structure that -## is used by the dynamic model expressions that appear below. +## wrapping up the links defined by the joins above ------------ link_data = list( decomposition = mp_link_data(trans_decomposition) @@ -177,27 +158,20 @@ link_data = list( , flows = mp_link_data(infection, recovery) ) -## expressions that define model dynamics --------- - -## these expressions are very general and should remain robust -## as model structure changes - ## instantiate numeric vectors labelled with particular indexes ----------- -## TODO: give automatic advice or just automatically initialize -## vectors required given an analysis of the expression graph init_vecs = list( state = mp_vector(state), flow_rates = mp_vector(flow_rates), trans_rates = mp_vector(trans_rates), - N = mp_vector(N), - #N = mp_vector(mp_extract(aggregation, "groups")), - norm_state = mp_vector(mp_extract(normalization, "norm")), + N = mp_vector(mp_extract(aggregation, "groups")), + norm_state = mp_vector(norm_state), susceptibility = mp_vector(susceptibility), contact = mp_vector(contact), infectivity = mp_vector(infectivity) ) +## wrap up the above definition ----------------- dynamic_model = DynamicModel( expr_list = expr_list, From 06994e2d7e455a2f259aafae71e9a4ea3a7ff9dd Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 6 Nov 2023 12:06:58 -0500 Subject: [PATCH 062/332] readme tests --- inst/model_library/sir/README.Rmd | 4 +--- inst/model_library/sir/README.md | 5 +++++ inst/model_library/sir/header.yaml | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 inst/model_library/sir/header.yaml diff --git a/inst/model_library/sir/README.Rmd b/inst/model_library/sir/README.Rmd index 9bac2286..00edd759 100644 --- a/inst/model_library/sir/README.Rmd +++ b/inst/model_library/sir/README.Rmd @@ -1,7 +1,5 @@ --- -title: "Basic SIR" -index_entry: "A very simple epidemic model" -author: Steve Walker +output: md_document --- This is (nearly) the simplest possible 'vanilla' epidemic model, diff --git a/inst/model_library/sir/README.md b/inst/model_library/sir/README.md index 5713ddb1..886a5321 100644 --- a/inst/model_library/sir/README.md +++ b/inst/model_library/sir/README.md @@ -1,3 +1,8 @@ +--- +title: "Basic SIR" +index_entry: "A very simple epidemic model" +author: Steve Walker +--- This is (nearly) the simplest possible ‘vanilla’ epidemic model, implemented as an example. diff --git a/inst/model_library/sir/header.yaml b/inst/model_library/sir/header.yaml new file mode 100644 index 00000000..05a7b2c2 --- /dev/null +++ b/inst/model_library/sir/header.yaml @@ -0,0 +1,5 @@ +--- +title: "Basic SIR" +index_entry: "A very simple epidemic model" +author: Steve Walker +--- From 06723036cb2d7b89f31aee9076d2ebc0603a40ad Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 6 Nov 2023 12:07:46 -0500 Subject: [PATCH 063/332] image --- .../figure-markdown_strict/plot_model-1.png | Bin 0 -> 40866 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 inst/model_library/sir/README_files/figure-markdown_strict/plot_model-1.png diff --git a/inst/model_library/sir/README_files/figure-markdown_strict/plot_model-1.png b/inst/model_library/sir/README_files/figure-markdown_strict/plot_model-1.png new file mode 100644 index 0000000000000000000000000000000000000000..1ea40dc317f67396de3775b0396bfe8954559ecf GIT binary patch literal 40866 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 P0k1o%P?d^X7Qz1m_d?y6 literal 0 HcmV?d00001 From 5ad7ab2455122de8aa738d2041758d6c34fb38e5 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Mon, 6 Nov 2023 17:25:29 -0500 Subject: [PATCH 064/332] checkpoint new quickstart draft - trying to simplify model specification... - need to discussion "flow rates" vs "influence rates" nomenclature - maybe start with sketch of expr_list to motivate all the labelling and subsetting, etc. --- vignettes/quickstart.Rmd | 450 ++++++++++++++++----------------------- 1 file changed, 178 insertions(+), 272 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 5eb39f56..d2a504a7 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -11,340 +11,246 @@ editor_options: chunk_output_type: console --- -[![status](https://img.shields.io/badge/status-mature%20draft-yellow)](https://canmod.github.io/macpan2/articles/vignette-status#mature-draft) +[![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) -```{r pkgs, include = FALSE} +```{r setup, echo = FALSE} library(macpan2) -library(macpan2helpers) -library(dplyr) -library(ggplot2) -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -cat_file = function(...) cat(readLines(file.path(...)), sep = "\n") -``` - -```{r show_visnet, echo = FALSE} -# function to get around issues with displaying visNetwork objects in an html vignette -show_visnet <- function(vn) { - vn = visNetwork::visInteraction(vn - , dragNodes = TRUE - , dragView = TRUE - , zoomView = FALSE - ) - visNetwork::visSave(vn, file = paste0("visnet.html")) - hh <- htmltools::includeHTML("visnet.html") |> - ## https://stackoverflow.com/questions/47577894/r-markdown-visnetwork-not-displayed-in-html-output-when-using-for-loops-or-fun - gsub(pattern = "", replacement = "", fixed = TRUE) |> - sub(pattern = "\n", replacement = "") - cat(hh) - invisible(file.remove("visnet.html")) -} -``` - -This quickstart guide is meant to show a new user how to specify and simulate a simple compartmental model in `macpan2` (it does assume you have already installed the package: see [these installation instructions](https://github.com/canmod/macpan2/#installation)). - - - -`macpan2` is extremely flexible in how models are specified, and so there can be several equivalent ways of specifying the same model. This feature is important to keep in mind when trying to understand or build more complicated models, such as the one demonstrated in Quickstart Guide, part 2 (see `vignette('quickstart2', package = 'macpan2')`). - -## Model Definition Files - -Models are defined by a set of four [model definition files](https://canmod.github.io/macpan2/articles/model_definitions), all housed in the same directory: - -``` -model_name -├── variables.csv -├── derivations.json -├── settings.json -└── flows.csv ``` -Installations of `macpan2` contain several starter models: -```{r list_starters} -list.files(system.file("starter_models", package = "macpan2")) -``` +# Motivation/nomenclature -The simplest is [the SIR model](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model), which can be found in this directory: +- goal is to teach a user how models are specified in `macpan2` via a familiar and simple model +- `macpan2`'s model specification is extremely flexible, which is also what can make it difficult to learn to work with at first -```{r sir_model_path} -print(sir_dir <- system.file("starter_models", "sir", package = "macpan2")) -``` +- unstructured model = a pure epidemic compartmental model, where each state is only denoted using a disease status (_e.g._ $S$ = susceptible, $I$ = infected, etc.) +- structured model = an epidemic model **crossed** with something else, like locations to generate a meta-population model or age groups to generate an age-stratified model -In this article, we work through the specification and simulation of the SIR sample model. You can discover [more examples here](https://canmod.github.io/macpan2/articles/example_models). To make your own copy of a model (e.g. because you want to modify it), you can use `model_starter(starter_name, dir_name)`, e.g. `model_starter("sir", "~/my_projects/macpan2/sir")` (if the target directory doesn't exist, `model_starter()` will refuse to overwrite it). - -### variables.csv {#variables} - -`variables.csv` includes a list of the model variables: - -```{r vars, echo=FALSE, eval=TRUE} -cat_file(sir_dir, "variables.csv") -``` +- while the model specification may seem like overkill for a simple model, we will demonstrate later how tthe model specification can be used to quickly and efficiently generate a complicated, structured model by expanding a simple model -(The first row of this file is a header row, which classifies the variables below. More on this later.) +# A simple, unstructured model -In `macpan2`, we use "variables" as a general term to refer to +We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models. The SIR model is generally cast as a system of ordinary differential equations: -- **State variables**, such as: - - `S`: the number of susceptible individuals at each point in time; - - `I`: the number of infectious individuals; - - `R`: the number of removed individuals. -- **Flow variables**, such as: - - `beta`: the contact rate (transmission per time per susceptible per infective); - - `gamma`: the per-capita recovery rate of infectious individuals. -- **Derived variables**, to be calculated using state and/or flow variables, such as: - - `N`: population size, calculated by summing all of the state variables `S + I + R`. - - `foi`: the force of infection, `beta * I / N` in this model. - -Formulas for the derived quantities are defined in [derivations.json](#derivations). +\begin{align} +\frac{dS}{dt} &= -\beta S(t) I(t) \\ +\frac{dI}{dt} &= \beta S(t) I(t) - \gamma I(t) \\ +\frac{dR}{dt} &= \gamma I(t) +\label{eqn:SIR-ode} +\end{align} -In simple models like this one, `variables.csv` is a single-column `.csv` file. The header row contains one column name, `Epi`, which labels the variables below it as epidemiological variables. Such variable category labels, or **variable partitions**, aren't useful for simple models like the basic SIR, which have only a single variable category. However, partitions are essential as part of ["product" models](https://canmod.github.io/macpan2/articles/hello_products.html) that combine multiple model structures into more complex models (for example, an SEIR model where the infectious class is divided by level of symptoms: see `vignette('quickstart2', package = 'macpan2')`. +However, it will be more useful to think of the SIR model in terms of a system of difference equations as that is how we will simulate it with `macpan2`: -### derivations.json {#derivations} +\begin{align} +S_{t+1} &= -\beta S_t I_t \\ +I_{t+1} &= \beta S_t I_t - \gamma I_t \\ +R_{t+1} &= \gamma I_t +\label{eqn:SIR-difference} +\end{align} -`derivations.json` includes expressions for computing derived variables: +Most flows in SIR-type compartmental models are of the form $rX$ where $r$ is a flow and $X$ is the name of the "from" compartment, that is the compartment from which the flow originates. We call the rates $r$ in such terms **flow rates**, and we could re-cast (\ref{eqn:SIR-difference}) to be purely in terms of flow rates, by setting $\lambda = \beta I_t$: -```{r, echo=FALSE, eval=TRUE} -cat_file(sir_dir, "derivations.json") -``` +\begin{align} +S_{t+1} &= -\lambda S_t \\ +I_{t+1} &= \lambda S_t - \gamma I_t \\ +R_{t+1} &= \gamma I_t +\label{eqn:SIR-difference} +\end{align} -In this example, the derived variables are the total population size, `N`, and the force of infection, `foi`. Each derived variable is defined in a [JSON](https://en.wikipedia.org/wiki/JSON) object. Each such JSON object contains several key-value pairs. Here the keys are: +Now the model is stated purely in terms of the **flow rates** $\lambda$ and $\gamma$.^[In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.] -- `output_names`: The name(s) of the derived variable(s); in this simple model, each JSON object only defines a single derived variable. -- `simulation_phase`: The phase during which this derived variable is calculated; Here we have: - - `before`, which means the variable is calculated from the initial variable values, before the simulation begins; - - `during_pre_update`, which means the variable is calculated at each simulation step, before the state variables are updated for the next time step. -- `arguments`: An array of the variables (delimited by brackets `[]`) used to calculate the derived variable. -- `expression`: A string storing a mathematical expression used to calculate the derived variable using the `arguments`. +However, $\lambda$ is special from a simulation standpoint as it depends on a state variable ($I_t$), so it must be recomputed at every step of the simulation (unlike $\gamma$, which is generally fixed and chosen by the modeller when specifying the model). $\lambda$ also depends on $\beta$, which we call an **influence rate** as it is involved in a term -The keys described above have more options; supplementary keys are used for defining more complicated models. These are fully catalogued in the [model definitions](https://canmod.github.io/macpan2/articles/model_definitions.html#flow-between-compartments) article. Mathematical operations and functions available to use in the `expression` string are listed in the [engine functions](https://canmod.github.io/macpan2/reference/engine_functions.html) reference article. +In order to specify a model, we must first decide on the labels we will use for model components. For the SIR model (and most compartmental models), these components will be: -In the example above, we could have defined `foi` as `beta * I / (S + I + R)`. However, this expression is harder to read, and less computationally efficient. As defined above, `N` is computed once, before the simulation begins (`"simulation_phase": "before"`), since the population size is fixed in this model. `foi` is computed from its arguments at each simulation time step (`"simulation_phase": "during_pre_update"`) in order to update the numerator `I` using the latest prevalence. The denominator `(S + I + R)` would then also be re-computed needlessly during each time step. +- **States**: Names of the compartments. +- **Flow rates**: Names of rates used in flow expressions of the form $rX$, where $r$ is the rate and $X$ is the "from" state---the state from which the flow originates. In the SIR model above, these would be $\gamma$ and $\lambda$. +- **Influence rates**: Names of rates used in flow expressions that additionally include a state other than the "from" state, such as (but not limited to) those of the form $rYX$, where $r$ is the influence rate, $X$ is the "from" state and $Y$ is the "to" state. In the SIR model above, $\beta$ would be an influence rate. -### settings.json +The distinction between flow and influence rates is not always made when working with compartmental epidemic models (_i.e._ $\beta$ is sometimes called a flow rate), but flow and influence rates are processed differently in `macpan2` for the purpose of simulating the model, and so we must also make this distinction when specifying the model. -`settings.json` gives information about how to interpret the variables defined in [variables.csv](#variables). +For the SIR model, the labels (_indices_) are generated as follows: -```{r, echo=FALSE, eval=TRUE} -cat_file(sir_dir, "settings.json") +```{r} +state = mp_index(Epi = c("S", "I", "R")) +flow_rates = mp_index(Epi = c("lambda", "gamma")) +influence_rates = mp_index(Epi = "beta") ``` -This file contains a single JSON object, with several key-value pairs. The mandatory keys are: - -- `required_partitions`: An array of variable partition names (from the header row of [variables.csv](#variables)) required to uniquely identify each variable. -- `null_partition`: Leave this entry as `"Null"`; this key may be used to expand `macpan2`'s functionality in the future, but it is currently unused. -- `state_variables`: An array of state variables. -- `flow_variables`: An array of flow variables. - -### flows.csv - -`flows.csv` defines flows between state variable compartments: +We start by -```{r, echo=FALSE, eval=TRUE} -cat_file(sir_dir, "flows.csv") +```{r} +state ``` -This file also starts with a header row, including the following columns: - -- `from`: The name of the flow origin compartment. -- `to`: The name of the flow destination compartment. -- `flow`: The variable name storing the value of the flow rate. -- `type`: The type of the flow. +```{r model-working} +expr_list = mp_expr_list( + before = list( + ## aggregations + N ~ sum(state) + ), + during = list( + ## influences + flow_rates[infection_flow] ~ + state[infectious_state] * influence_rates[transmission] / N + + ## flows + , flows_per_time ~ state[from] * flow_rates[edge] + + ## state update + , total_inflow ~ groupSums(flows_per_time, to, state) + , total_outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + total_inflow - total_outflow + ) +) -Here, the flow type is `per_capita`: i.e., the flow at time step $t+1$ equals `flow` times the number of individuals in the `from` compartment at time $t$. In the example above, the flow from `S` to `I` equals `foi * S` (referring back to the derivations file, this is equal to `beta * S * I/ N`). +## basic model indexes ------------------------- -All available flow types are given in the [model definitions](https://canmod.github.io/macpan2/articles/model_definitions.html#flow-between-compartments) article. +state = mp_index(Epi = c("S", "I", "R")) +# flow_rates = mp_index(Epi = c("lambda", "gamma", "beta")) +flow_rates = mp_index(Epi = c("lambda", "gamma")) +influence_rates = mp_index(Epi = "beta") -## Side-Note: `macpan2` is Object-Oriented +## subset indexes ----------------- -Before we demonstrate how to import and use a model into R, note that `macpan2` is [object-oriented](https://en.wikipedia.org/wiki/Object-oriented_programming). All you really need to know about this is that when using `macpan2`, we routinely create ("initialize") special R objects that have specific functions ("methods") attached that perform useful actions. These objects are generally just named lists, where some of the list elements are actually methods (functions). As an example, with made-up general names: +S = mp_subset(state, Epi = "S") +I = mp_subset(state, Epi = "I") +R = mp_subset(state, Epi = "R") +lambda = mp_subset(flow_rates, Epi = "lambda") +gamma = mp_subset(flow_rates, Epi = "gamma") +beta = mp_subset(influence_rates, Epi = "beta") +# beta = mp_subset(flow_rates, Epi = "beta") -``` -myObject <- functionToInitializeObject(argumentsToSetUpMyObject) -names(myObject) -> [1] "objectInfo" "objectMethod" -myObject$objectMethod(argumentsForUsingObjectMethod) -``` +## links between entries in the indexes ----------- -The next section will make this idea more concrete. +# flows_per_time ~ state[from] * flow_rates[edge] +infection = mp_join(from = S, to = I, edge = lambda) +recovery = mp_join(from = I, to = R, edge = gamma) -## Getting Model Definitions into R +# flow_rates[infection_flow] ~ +# state[infectious_state] * influence_rates[transmission] / N +transmission = mp_join( + infectious_state = I, + infection_flow = lambda, + transmission = beta +) -Now that we have a directory that completely defines a compartmental model, we can use it to create a **model structure** object in R using the `Compartmental()` function. `Compartmental()` is a function that initializes `macpan2` models (which also include `Compartmental` in their class): +link_data = list( + influences = mp_link_data(transmission) + , flows = mp_link_data(infection, recovery) +) -```{r Compartmental} -sir = Compartmental(sir_dir) -class(sir) -``` +## Initialize indexed vectors (to all zeros) -------------- -Visualizing a `Compartmental` object with a function from the [macpan2helpers](https://github.com/canmod/macpan2helpers) package: +init_vecs = list( + state = mp_vector(state), + flow_rates = mp_vector(flow_rates), + influence_rates = mp_vector(influence_rates) +) -```{r make_visnet} -vn = macpan2helpers::visCompartmental(sir, label_flows = TRUE) -``` +## Final output ----------------- -```{r show_visnet_sir, results = 'asis', echo = FALSE} -show_visnet(vn) -``` +dynamic_model = DynamicModel( + expr_list = expr_list, + link_data = link_data, + init_vecs = init_vecs, + unstruc_mats = list() +) -We can display the components of `macpan2` models (`Compartmental` objects), including the methods that operate on them, by listing their names: +sir_sim = mp_tmb_simulator(dynamic_model + , vectors = list( + state = c(S = 999, I = 1, R = 0), + flow_rates = c(lambda = NA, gamma = 0.1), + influence_rates = c(beta = 0.25) + ) + , time_steps = 100L +) -```{r} -names(sir) +mp_report(sir_sim) ``` -The documentation for `Compartmental` objects (`?Compartmental()`) gives a full description of these components, but for the next section, we focus on `sir$simulators`. - -## Inputting Values to Create Model Simulators - -Model structure objects, like `sir`, are very general: they don't contain any specific values for their variables, such as initial conditions for the state variables or values for flow rates. In order to simulate a model, we need to create a **model simulator** object by combining a model structure object with numerical values for its variables. - -The `sir$simulators` list element contains methods for generating model simulators. In this article, we use the `sir$simulators$tmb()` method, which is currently the only simulation engine available. - -We attach specific values for model variables when initializing the model simulator, `sir_simulator`, using the `sir$simulators$tmb()` function: - -```{r sim_values} -sir_simulator = sir$simulators$tmb( - time_steps = 100, - state = c(S = 99, I = 1, R = 0), - flow = c(foi = NA, gamma = 0.1), - beta = 0.2, - N = empty_matrix # explained below +```{r model-edited} +expr_list = mp_expr_list( + before = list( + ## aggregations + N ~ sum(state) + ), + during = list( + ## influences + flow_rates[infection_flow] ~ + state[infectious_state] * flow_rates[transmission] / N + + ## flows + , flows_per_time ~ state[from] * flow_rates[edge] + + ## state update + , total_inflow ~ groupSums(flows_per_time, to, state) + , total_outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + total_inflow - total_outflow + ) ) -``` -(flow variables that are recalculated before every time step, as specified in `derivations.json`, can be set to `NA` - these are placeholder values and don't affect the simulation) +## basic model indexes ------------------------- -The simulator initializer function, `sir$simulators$tmb()`, uses `sir` as the model structure, and additionally takes the following arguments: +state = mp_index(Epi = c("S", "I", "R")) +flow_rates = mp_index(Epi = c("lambda", "gamma", "beta")) +# flow_rates = mp_index(Epi = c("lambda", "gamma")) +# influence_rates = mp_index(Epi = "beta") -- `time_steps`: How many time steps should the epidemic simulator run for? -- `state`: A named vector containing the initial values of the state variables defined in `settings.json`. In this example, the units of the states are "number of individuals". -- `flow`: A named vector containing the values of the flow variables defined in `settings.json` - here, just `foi` and `gamma`. -- `...`: Any other named variables that are required for your model definition, _i.e._, variables used in `derivations.json` that are not included in `settings.json`. Here, `beta` and `N` are used in expressions for derived variables but not explicitly as flow variables. +## subset indexes ----------------- -Always think explicitly about the units of your flow variables. For example, perhaps you know that infected individuals recover in ten days on average. You could then set the per-capita recovery rate, `gamma`, equal to `1/10 = 0.1`, thus implicitly specifying your time step as one day. If you set `gamma = 1/240` instead, then each time step would represent one hour. You must define all flow variables using consistent time units! +S = mp_subset(state, Epi = "S") +I = mp_subset(state, Epi = "I") +R = mp_subset(state, Epi = "R") +lambda = mp_subset(flow_rates, Epi = "lambda") +gamma = mp_subset(flow_rates, Epi = "gamma") +# beta = mp_subset(influence_rates, Epi = "beta") +beta = mp_subset(flow_rates, Epi = "beta") -In our example, we use the `empty_matrix` placeholder value to initialize a value for the population size `N`. `N` will be computed before the simulation starts using the given initial conditions, as specified in `derivations.json` with `"simulation_phase": "before"`. In order to create a model simulator object, we must give a value for every variable in the model definition. `macpan2` does not check ahead of time whether `N` will be pre-computed. Thus, we initialize `N` with the placeholder value `macpan2::empty_matrix`, which is simply an empty matrix. (Although `N` is a scalar value, `macpan2` represents all variables as [matrices](https://canmod.github.io/macpan2/articles/cpp_side#matrices), including scalars, in this case treated as 1 x 1 matrices.) +## links between entries in the indexes ----------- -What if we had specified a hard-coded value for `N` above? For instance: +# flows_per_time ~ state[from] * flow_rates[edge] +infection = mp_join(from = S, to = I, edge = lambda) +recovery = mp_join(from = I, to = R, edge = gamma) -```{r sim_values2, eval = FALSE} -sir_simulator2 = sir$simulators$tmb( - time_steps = 100, - state = c(S = 99, I = 1, R = 0), - flow = c(foi = NA, gamma = 0.1), - beta = 0.2, - N = 100 +# flow_rates[infection_flow] ~ +# state[infectious_state] * influence_rates[transmission] / N +transmission = mp_join( + infectious_state = I, + infection_flow = lambda, + transmission = beta ) -``` - -There is actually no problem with this specification; `macpan2` will compute `N` using the expression `S + I + R` from `derivations.json` and the initial conditions from the `state` argument above before the simulation and overwrite the user-input value of 100. Thus, if one were to change the initial condition to `state = c(S = 999, I = 1, R = 0)` without updating the population to `N = 1000`, the simulator would still use the correct population size. However, the code is more readable if you explicitly use `N = empty_matrix`, because it reminds you that `N` will be computed by the simulator. - -## Simulation - -Now that we have a simulation engine object, `sir_simulator`, we use it to generate simulation results using one of its methods: `report()`. The results come out in [long (or "narrow") format](https://en.wikipedia.org/wiki/Wide_and_narrow_data), where there is exactly one value per row: - -```{r run_sims} -sir_results = sir_simulator$report() -head(sir_results, 9) -``` - -The simulation results are output as a data frame with the following columns: - -- `matrix`: Which matrix does a value come from? By default, the simulation results will only include values for the state variables (coming from the `state` matrix). This behaviour can be changed with the `.mats_to_return` argument of `$simulators$tmb()` (see `?Simulators()`) -- `time`: The time index, where the initial conditions are returned for `time` 0. -- `row`: The name of the variable, corresponding to the row names of the `state` matrix. -- `col`: A placeholder for variable name components in more complicated models (not covered in this article). -- `value`: The simulated value for a particular state and time step. - -## Processing Results -`macpan2` does not provide any data manipulation or plotting tools (although there are a few in `macpan2helpers`). The philosophy is to focus on the engine and modelling interface, but to provide outputs in formats that are easy to use with other data processing packages, like `ggplot2`, `dplyr`, and `tidyr`, all of which readily make use of data in long format. - -For example, we can generate a `ggplot` with: - -```{r ggplot_ex, fig.width = 6} -(sir_results - |> mutate(state = factor(row, levels = sir$labels$state())) - |> ggplot() - + geom_line(aes(time, value, colour = state)) - + theme_bw() +# IP: what is this for? +link_data = list( + influences = mp_link_data(transmission) + , flows = mp_link_data(infection, recovery) ) -``` -The `mutate` step is optional, and is included to enforce the logical ordering of the state variables in the legend of the plot -- otherwise we would have the alphabetically-ordered IRS model instead of the flow-ordered SIR. +## Initialize indexed vectors (to all zeros) -------------- -If you want to use base R plots, you can convert the long format data to wide format: - -```{r pivot_wider} -sir_results_wide <- (sir_results - ## drop 'matrix' (has only a single value) and 'col' (empty) - |> dplyr::select(-c(matrix, col)) - |> tidyr::pivot_wider(id_cols = time, names_from = row) +init_vecs = list( + state = mp_vector(state), + flow_rates = mp_vector(flow_rates) ) -head(sir_results_wide, n = 3) -``` +## Final output ----------------- -(Above, we used the [base R pipe operator](https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/#pipes), `|>`.) - -```{r base_plot_ex, fig.width = 6} -with(sir_results_wide, - plot(x = time, - y = I) +dynamic_model = DynamicModel( + expr_list = expr_list, + link_data = link_data, + init_vecs = init_vecs, + unstruc_mats = list() ) -``` - -or - -```{r base_matplot_ex, fig.width = 6} -par(las = 1) ## horizontal y-axis ticks -matplot(sir_results_wide[, 1], - sir_results_wide[,-1], - type = "l", - log = "y", - xlab = "time", ylab = "") -legend("bottom", col = 1:3, lty = 1:3, legend = sir$labels$state()) -``` - - -## Incidence - -All of the above simulations give the prevalence of the disease but not incidence, which is the total number of new individuals entering the `I`-box at each time step. Incidence is more commonly reported than prevalence, and so it is important to be able to get incidence numbers. - -The total number of new individuals entering all boxes is stored in the `total_inflow` vector that is automatically constructed by the `Compartmental` function. This vector is as long as `state` and each element to the inflow into each corresponding box (there is an analogous `total_outflow` vector as well). To return this vector, one must declare that it is a matrix to return as in the following code (by default `state` is the only matrix to be returned). -```{r incidence} -sir_simulator3 = sir$simulators$tmb( - time_steps = 100, - state = c(S = 99, I = 1, R = 0), - flow = c(foi = NA, gamma = 0.1), - beta = 0.2, - N = empty_matrix, - - ## telling the simulator to return the inflows into each box - ## including `I`, which corresponds to incidence - ## (note: in the near future the .dimnames part will be automatic) - .mats_to_return = c("state", "total_inflow"), - .dimnames = list(total_inflow = list(sir$labels$state(), "")) +sir_sim = mp_tmb_simulator(dynamic_model + , vectors = list( + state = c(S = 999, I = 1, R = 0), + flow_rates = c(lambda = NA, gamma = 0.1, beta = 0.25) + ) + , time_steps = 100L ) -``` - -The plotting code used above can be modified to plot inflow rather than state. -```{r inflow_plot, fig.width = 6} -(sir_simulator3$report() - |> filter(matrix == "total_inflow") - |> mutate(state = factor(row, levels = sir$labels$state())) - |> ggplot() - + geom_line(aes(time, value, colour = state)) - + ggtitle("Total Inflow into Each State") - + theme_bw() -) +mp_report(sir_sim) ``` -In this figure the incidence is the inflow into the `I` box. The `S` line is flat because in this model there is no flow into `S`. - -Note that this approach to calculating incidence only works if the time period over which incidence is measured corresponds to the length of one time step. If this assumption is not met -- for example if the time step is one day but incidence data are reported every week -- then other approaches to computing simulated incidence must be taken. The simplest approach is to use accumulator variables, which are not covered here (TODO: write a vignette on accumulator variables). From 87ecbffd4c17fd21853068a5ba6124ca5c7c0d13 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 7 Nov 2023 12:13:34 -0500 Subject: [PATCH 065/332] working away --- Makefile | 6 ++ NAMESPACE | 14 +++++ R/compartmental.R | 1 - R/dev_tools.R | 11 +++- R/engine_eval.R | 9 +-- R/formula_data.R | 1 + R/formula_utils.R | 2 +- R/index_to_tmb.R | 8 ++- R/lists.R | 2 + R/mats_list.R | 16 +++++- R/model_def_run.R | 60 +++++++++++++++++--- R/mp.R | 22 +++++++ R/obj_utils.R | 3 + R/tmb_model.R | 41 +++++++++++++ R/tmb_model_editors.R | 31 +++++----- R/variables.R | 6 ++ R/vector.R | 3 + R/zzz.R | 1 + inst/model_library/sir/README.Rmd | 2 +- inst/model_library/sir/model_structure.R | 14 ++++- inst/model_library/sir_age/model_structure.R | 8 +-- man/MatsList.Rd | 3 +- vignettes/calibration.Rmd | 36 ++++++------ 23 files changed, 239 insertions(+), 61 deletions(-) create mode 100644 R/obj_utils.R diff --git a/Makefile b/Makefile index 02dc038f..20810939 100644 --- a/Makefile +++ b/Makefile @@ -128,3 +128,9 @@ compile-dev: misc/dev/dev.cpp misc/dev/%.run: misc/dev/%.R cd misc/dev; Rscript ../../$^ + +inst/model_library/%/README.md: inst/model_library/%/README.Rmd + echo "rmarkdown::render(input = \"$^\", intermediates_dir = NULL)" | R --slave + cat $(dir $@)/header.yaml $(dir $@)/README.md > $(dir $@)/tmp.md + cp $(dir $@)/tmp.md $(dir $@)/README.md + rm $(dir $@)/tmp.md diff --git a/NAMESPACE b/NAMESPACE index 3cc72708..534c5816 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,8 +14,19 @@ S3method(c,StringData) S3method(head,Link) S3method(labelling_column_names,Index) S3method(labelling_column_names,Link) +S3method(labels,Compartmental) +S3method(labels,DynamicModel) S3method(labels,Index) +S3method(labels,LabelsDynamic) +S3method(labels,LabelsScripts) +S3method(labels,ModelDefRun) +S3method(labels,TMBCompartmentalSimulator) +S3method(labels,TMBDynamicSimulator) +S3method(labels,VariableLabels) S3method(length,Vector) +S3method(mp_extract,DynamicModel) +S3method(mp_extract,Link) +S3method(mp_extract,ModelDefRun) S3method(mp_index,character) S3method(mp_index,data.frame) S3method(mp_labels,Index) @@ -34,6 +45,8 @@ S3method(names,Link) S3method(names,MatsList) S3method(names,MethList) S3method(names,Partition) +S3method(names,Vector) +S3method(print,DynamicModel) S3method(print,ExprList) S3method(print,Index) S3method(print,Link) @@ -154,6 +167,7 @@ export(mp_cartesian) export(mp_choose) export(mp_choose_out) export(mp_decompose) +export(mp_dynamic_model) export(mp_expr_binop) export(mp_expr_group_sum) export(mp_expr_list) diff --git a/R/compartmental.R b/R/compartmental.R index e79a1572..f57588b7 100644 --- a/R/compartmental.R +++ b/R/compartmental.R @@ -161,7 +161,6 @@ CompartmentalMatsList = function( , absolute_outflow = empty_matrix , total_inflow = empty_matrix , total_outflow = empty_matrix - , dummy = empty_matrix , .mats_to_save = .mats_to_save , .mats_to_return = .mats_to_return , .dimnames = .dimnames diff --git a/R/dev_tools.R b/R/dev_tools.R index 0031ddd6..7194beef 100644 --- a/R/dev_tools.R +++ b/R/dev_tools.R @@ -46,7 +46,12 @@ render_model_readme = function(file) { f = basename(file) |> tools::file_path_sans_ext() output_file = sprintf("%s.md", f) d = dirname(file) - readLines(output_file) - readLines(file) - + output_lines = readLines(file.path(d, output_file)) + input_lines = readLines(file) + potential_yaml = which(grepl("^---", input_lines)) + if (potential_yaml[1L] == 1L) { + if (length(potential_yaml) > 1L) { + file[seq_len(potential_yaml[2L])] + } + } } diff --git a/R/engine_eval.R b/R/engine_eval.R index 9624f00b..639ab926 100644 --- a/R/engine_eval.R +++ b/R/engine_eval.R @@ -58,10 +58,11 @@ engine_eval = function(e, ..., .matrix_to_return, .tmb_cpp = getOption("macpan2_ ) if (!inherits(.structure_labels, "NullLabels")) { - component_list = list( - state = .structure_labels$state(), - flow = .structure_labels$flow() - ) + component_list = .structure_labels$component_list() + # list( + # state = .structure_labels$state(), + # flow = .structure_labels$flow() + # ) e = to_special_vecs(e, component_list, c(left_hand_side, names(dot_mats))) } diff --git a/R/formula_data.R b/R/formula_data.R index 19c6c8fe..3e586328 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -57,6 +57,7 @@ LinkData = function(...) { #' @export mp_link_data = function(...) LinkData(...) + #' Indexed Expressions #' #' @param ... Formula objects that reference the columns in the diff --git a/R/formula_utils.R b/R/formula_utils.R index 0a465a56..d5e65f20 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -16,7 +16,7 @@ #' @noRd to_special_vecs = function( formula, component_list, matrix_list, - component_vec_by = c(state = "state", flow = "flow") + component_vec_by = getOption("macpan2_vec_by") ) { arg_signature = c( unlist(component_list, use.names = FALSE, recursive = FALSE), diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 0c802b7a..fa5c70e7 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -67,7 +67,8 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model mats = c(indexed_mats, unstruc_mats, derived_mats) mats_list_options = list( .mats_to_save = mats_to_save, - .mats_to_return = mats_to_return + .mats_to_return = mats_to_return, + .structure_labels = dynamic_model$labels ) engine_methods = EngineMethods(int_vecs = do.call(IntVecs, int_vecs)) tmb_model = TMBModel( @@ -81,7 +82,10 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model , log_file = log_file , do_pred_sdreport = do_pred_sdreport ) - tmb_model$simulator(tmb_cpp = tmb_cpp, initialize_ad_fun = initialize_ad_fun) + TMBDynamicSimulator( + tmb_model$simulator(tmb_cpp = tmb_cpp, initialize_ad_fun = initialize_ad_fun), + dynamic_model + ) } #' @export diff --git a/R/lists.R b/R/lists.R index 3d13611c..881d0d1c 100644 --- a/R/lists.R +++ b/R/lists.R @@ -29,3 +29,5 @@ assert_named_list = function(l) { } l } + +self_named_vector = function(...) c(...) |> setNames(c(...)) diff --git a/R/mats_list.R b/R/mats_list.R index f295293d..7cfa275d 100644 --- a/R/mats_list.R +++ b/R/mats_list.R @@ -44,6 +44,7 @@ MatsList = function(... , .mats_to_return = character(0L) , .dimnames = list() , .structure_labels = NullLabels() + , .dummy = "dummy" ) { self = Base() @@ -60,9 +61,22 @@ MatsList = function(... self$.mats_to_return = .mats_to_return self$.dimnames = .dimnames self$.structure_labels = .structure_labels + self$.dummy = .dummy # Static - self$.initial_mats = lapply(list(...), as.matrix) + l = list(...) + + ## FIXME: still required because int_vecs and not literals are required on + ## the lhs so we still need the dummy ~ assign... construction for those + ## cases + if (!is.null(.dummy)) { + if (!.dummy %in% names(l)) { + l = c(l, setNames(list(empty_matrix), .dummy)) + } + } + + self$.initial_mats = lapply(l, as.matrix) + if (length(self$.initial_mats) == 0L) names(self$.initial_mats) = character() self$mats_save_hist = function() names(self$.initial_mats) %in% self$.mats_to_save diff --git a/R/model_def_run.R b/R/model_def_run.R index 4dd4cc15..7bbdff91 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -23,8 +23,8 @@ Compartmental2 = function(model_directory) { } self$flow_links = function() self$dynamic_model$link_data$flows self$influence_links = function() self$dynamic_model$link_data$influences - #self$normalization_links = function() self$link_data_type("normalization") - #self$aggregation_links = function() self$link_data_type("aggregation") + self$normalization_links = function() self$dynamic_model$link_data$normalization + self$aggregation_links = function() self$dynamic_model$link_data$aggregation self$flows = function() self$flow_links()$labels_frame() self$influences = function() self$influence_links()$labels_frame() @@ -54,14 +54,50 @@ LabelsScripts = function(model) { self = Base() self$model = model self$variables = model$variables - self$state = function() self$variables$state()$labels() - self$flow_rates = function() self$variables$flow_rates()$labels() - self$influence_rates = function() self$variables$influence_rates()$labels() - self$aggregated_states = function() self$variables$aggregated_states()$labels() - self$normalized_state = function() self$variables$normalized_state()$labels() + + self$dynamic_model = model$dynamic_model + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) self[[nm]] = LabelsGetter(self$dynamic_model, nm) + self$component_list = function() { + l = list() + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) l[[nm]] = self[[nm]]() + l + } + + # self$state = function() self$variables$state()$labels() + # self$flow_rates = function() self$variables$flow_rates()$labels() + # self$influence_rates = function() self$variables$influence_rates()$labels() + # self$aggregated_states = function() self$variables$aggregated_states()$labels() + # self$normalized_state = function() self$variables$normalized_state()$labels() + # self$component_list = function() { + # + # } return_object(self, "LabelsScripts") } +LabelsGetter = function(dynamic_model, vector_name) { + self = Base() + self$dynamic_model = dynamic_model + self$vector_name = vector_name + self$get = function() self$dynamic_model$init_vecs[[self$vector_name]] |> names() + self$get +} + +LabelsDynamic = function(dynamic_model) { + self = Base() + self$dynamic_model = dynamic_model + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) self[[nm]] = LabelsGetter(self$dynamic_model, nm) + self$component_list = function() { + l = list() + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) l[[nm]] = self[[nm]]() + l + } + return_object(self, "LabelsDynamic") +} + #' @export DynamicModel = function(expr_list = ExprList(), link_data = list(), init_vecs = list(), unstruc_mats = list()) { self = Base() @@ -69,5 +105,15 @@ DynamicModel = function(expr_list = ExprList(), link_data = list(), init_vecs = self$link_data = link_data self$init_vecs = init_vecs self$unstruc_mats = unstruc_mats + self$labels = LabelsDynamic(self) return_object(self, "DynamicModel") } + + +#' @export +mp_dynamic_model = DynamicModel + +#' @export +print.DynamicModel = function(x, ...) { + print(x$expr_list) +} diff --git a/R/mp.R b/R/mp.R index e84871bb..c5d827f0 100644 --- a/R/mp.R +++ b/R/mp.R @@ -432,11 +432,33 @@ mp_decompose = function(formula, index, decomp_name, ...) { #' @export mp_extract = function(x, dimension_name) { + UseMethod("mp_extract") +} + +#' @export +mp_extract.Link = function(x, dimension_name) { ii = x$index_for[[dimension_name]]() ii$reset_reference_index() ii } +#' @export +mp_extract.DynamicModel = function(x, dimension_name) { + y = try(x$init_vecs[[dimension_name]]$index, silent = TRUE) + if (!inherits(y, "Index")) { + msg( + "Failed to find an index for", + dimension_name, "in this object" + ) |> stop() + } + y +} + +#' @export +mp_extract.ModelDefRun = function(x, dimension_name) { + mp_extract(x$dynamic_model, dimension_name) +} + #' Rename Index Columns #' #' @param ... Name-value pairs. The name gives the new name and the value diff --git a/R/obj_utils.R b/R/obj_utils.R new file mode 100644 index 00000000..bd1c09fb --- /dev/null +++ b/R/obj_utils.R @@ -0,0 +1,3 @@ +method_names = function(x) { + names(which(unlist(eapply(x, is.function)))) +} diff --git a/R/tmb_model.R b/R/tmb_model.R index 405daf9a..5262e3e7 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -174,6 +174,47 @@ TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { return_object(self, "TMBCompartmentalSimulator") } +TMBDynamicSimulator = function(tmb_simulator, dynamic_model) { + self = tmb_simulator + self$dynamic_model = dynamic_model + return_object(self, "TMBDynamicSimulator") +} + +#' @export +labels.VariableLabels = function(object, ...) object$component_list() + +#' @export +labels.Compartmental = function(object, ...) labels(object$labels) + +#' @export +labels.TMBCompartmentalSimulator = function(object, ...) { + labels(object$compartmental_model) +} + +#' @export +labels.ModelDefRun = function(object, ...) { + labels(object$labels) +} + +#' @export +labels.TMBDynamicSimulator = function(object, ...) { + labels(object$dynamic_model) +} + +#' @export +labels.DynamicModel = function(object, ...) { + labels(object$labels) +} + +#' @export +labels.LabelsDynamic = function(object, ...) { + object$component_list() +} + +#' @export +labels.LabelsScripts = function(object, ...) { + object$component_list() +} TMBSimulationUtils = function() { self = Base() diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index 205e30c6..8eaeb7a8 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -54,22 +54,21 @@ TMBSimulatorInserter = function(simulator) { , .at = 1L , .phase = c("before", "during", "after") , .simulate_exprs = character(0L) - , .vec_by_states = "state" - , .vec_by_flows = "flow" + , .vec_by = getOption("macpan2_vec_by") ) { - if (.vec_by_states == "") .vec_by_states = "...RAW...INDICES..." - if (.vec_by_flows == "") .vec_by_flows = "...RAW...INDICES..." - component_vec_by = c(state = .vec_by_states, flow = .vec_by_flows) + for (v in names(.vec_by)) { + if (.vec_by[v] == "") .vec_by[v] = "...RAW...INDICES..." + } + # if (.vec_by_states == "") .vec_by_states = "...RAW...INDICES..." + # if (.vec_by_flows == "") .vec_by_flows = "...RAW...INDICES..." + #component_vec_by = c(state = .vec_by_states, flow = .vec_by_flows) if (inherits(self$model$init_mats$.structure_labels, "NullLabels")) { args = list(...) } else { mat_names = names(self$model$init_mats) - component_list = list( - state = self$model$init_mats$.structure_labels$state(), - flow = self$model$init_mats$.structure_labels$flow() - ) + component_list = self$model$init_mats$.structure_labels$component_list() args = (list(...) - |> lapply(to_special_vecs, component_list, mat_names, component_vec_by) + |> lapply(to_special_vecs, component_list, mat_names, .vec_by) |> lapply(to_assign) ) } @@ -78,7 +77,7 @@ TMBSimulatorInserter = function(simulator) { args$.simulate_exprs = .simulate_exprs (self$model$expr_list$insert |> do.call(args) - |> self$model$refresh_expr_list() + |> self$model$refresh$expr_list() ) self$simulator$cache$invalidate() invisible(self$simulator) @@ -118,7 +117,7 @@ TMBSimulatorAdder = function(simulator) { , .mats_to_save = .mats_to_save , .mats_to_return = .mats_to_return , .dimnames = .dimnames - ) |> self$model$refresh_init_mats() + ) |> self$model$refresh$init_mats() self$simulator$cache$invalidate() invisible(self$simulator) } @@ -161,7 +160,7 @@ TMBReplacer = function(model) { self$obj_fn = function(obj_fn_expr) { (obj_fn_expr |> ObjectiveFunction() - |> self$model$refresh_obj_fn() + |> self$model$refresh$obj_fn() ) self$model } @@ -174,14 +173,14 @@ TMBSimulatorReplacer = function(simulator) { self$obj_fn = function(obj_fn_expr) { (obj_fn_expr |> ObjectiveFunction() - |> self$model$refresh_obj_fn() + |> self$model$refresh$obj_fn() ) self$simulator$cache$invalidate() invisible(self$simulator) } self$params_frame = function(frame) { new_params = OptParamsFrame(frame, self$model$init_mats$dimnames()) - self$model$refresh_params(new_params) + self$model$refresh$params(new_params) self$simulator$cache$invalidate() valid$consistency_params_mats$check(self$model) invisible(self$simulator) @@ -191,7 +190,7 @@ TMBSimulatorReplacer = function(simulator) { } self$random_frame = function(frame) { new_random = OptParamsFrame(frame, self$model$init_mats$dimnames()) - self$model$refresh_random(new_random) + self$model$refresh$random(new_random) self$simulator$cache$invalidate() valid$consistency_random_mats$check(self$model) invisible(self$simulator) diff --git a/R/variables.R b/R/variables.R index fbffbd0d..8474d8b3 100644 --- a/R/variables.R +++ b/R/variables.R @@ -72,6 +72,12 @@ VariableLabels = function(variables) { if (is.null(v)) return(v) v$labels() } + self$component_list = function() { + list( + state = self$state(), + flow = self$flow() + ) + } self$rp = function() self$variables$model$settings$names() self$all = function() self$variables$all()$select(self$rp())$labels() self$flow = function() self$variables$flow()$select(self$rp())$labels() diff --git a/R/vector.R b/R/vector.R index f6ac3de9..ef9f2ee6 100644 --- a/R/vector.R +++ b/R/vector.R @@ -127,6 +127,9 @@ length.Vector = function(x) x$length() #' @export print.Vector = function(x, ...) print(x$numbers()) +#' @export +names.Vector = function(x) x$numbers() |> names() + #' @export as.matrix.Vector = function(x, ...) x$numbers() |> as.matrix() diff --git a/R/zzz.R b/R/zzz.R index 4e3872d9..694b72e3 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,6 +1,7 @@ .onLoad <- function(lib, pkg) { options( macpan2_dll = "macpan2" + , macpan2_vec_by = c("state", "flow_rates", "trans_rates") |> self_named_vector() #, macpan2_memoise = TRUE ) } diff --git a/inst/model_library/sir/README.Rmd b/inst/model_library/sir/README.Rmd index 00edd759..95281489 100644 --- a/inst/model_library/sir/README.Rmd +++ b/inst/model_library/sir/README.Rmd @@ -24,7 +24,7 @@ sir_sim = mp_tmb_simulator(sir_mod The state variables and rates in this model are as follows. ```{r print_vars_and_rates} - +mp_extract(sir_mod, "state") ``` ```{r print_flows_and_influences} diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index b8817e5e..5d30141b 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -7,7 +7,7 @@ expr_list = mp_expr_list( ## aggregations N ~ sum(state) ), - during = list( + during = list( ## this is a for loop over time steps ## influences flow_rates[infection_flow] ~ state[infectious_state] * trans_rates[transmission] / N @@ -22,6 +22,16 @@ expr_list = mp_expr_list( ) ) +data.frame( + flow = runif(10), + from = rep(1:4, times = 1:4) +) + +data.frame( + flow = runif(10), + from = rep(1:2, 5) +) + ## basic model indexes ------------------------- state = mp_index(Epi = c("S", "I", "R")) @@ -67,7 +77,7 @@ init_vecs = list( ## Final output ----------------- -dynamic_model = DynamicModel( +dynamic_model = mp_dynamic_model( expr_list = expr_list, link_data = link_data, init_vecs = init_vecs, diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index de036e64..6ff089ce 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -151,9 +151,9 @@ recovery = mp_join( ## wrapping up the links defined by the joins above ------------ link_data = list( - decomposition = mp_link_data(trans_decomposition) - , aggregation = mp_link_data(aggregation) - , normalization = mp_link_data(normalization) + decompositions = mp_link_data(trans_decomposition) + , aggregations = mp_link_data(aggregation) + , normalizations = mp_link_data(normalization) , influences = mp_link_data(transmission) , flows = mp_link_data(infection, recovery) ) @@ -173,7 +173,7 @@ init_vecs = list( ## wrap up the above definition ----------------- -dynamic_model = DynamicModel( +dynamic_model = mp_dynamic_model( expr_list = expr_list, link_data = link_data, init_vecs = init_vecs, diff --git a/man/MatsList.Rd b/man/MatsList.Rd index 36567062..163723af 100644 --- a/man/MatsList.Rd +++ b/man/MatsList.Rd @@ -9,7 +9,8 @@ MatsList( .mats_to_save = character(0L), .mats_to_return = character(0L), .dimnames = list(), - .structure_labels = NullLabels() + .structure_labels = NullLabels(), + .dummy = "dummy" ) } \arguments{ diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index ed9ef23b..f1432c9f 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -48,23 +48,23 @@ First set up the model from the quickstart guide. For convenience (since we will ```{r sir_setup} mk_sim <- function(init_state = c(S = 99, I = 1, R = 0)) { - sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) - sim <- sir$simulators$tmb( + #sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) + sir = Compartmental2(system.file("model_library", "sir", package = "macpan2")) + sim <- mp_tmb_simulator(sir, time_steps = 100, - state = init_state, - flow = c(foi = NA, gamma = 0.1), - beta = 0.2, - N = empty_matrix + vectors = list( + state = init_state, + flow_rates = c(foi = NA, gamma = 0.1), + trans_rates = c(beta = 0.2) ) + ) return(sim) } sir_simulator <- mk_sim() ## `.phases = "during"` is important so that the number of observations matches the number of time steps -sir_results = sir_simulator$report(.phases = "during") +sir_results = sir_simulator$report() ``` -**fixme**: providing mismatched time series (e.g. by forgetting to specify `.phases = "during"` when generating simulated data) gives cryptic/confusing errors and behaviour (warnings about failure to recycle, objective function values equal to zero). (We can make a reprex for this by leaving out `.phases = "during"` above ... - Add some noise to the prevalence (`I`) value: ```{r sir_noise} @@ -86,14 +86,14 @@ Now we'll use the *experimental* `mk_calibrate()` function from `macpan2helpers` ```{r mk_calibrate} mk_calibrate(sir_simulator, - data = data.frame(I_obs = sir_prevalence$obs_val), - params = list(beta = 1, I_sd = 1), - transforms = list(beta = "log", I_sd = "log"), - exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), - ) + data = data.frame(I_obs = sir_prevalence$obs_val), + params = list(trans_rates = c(beta = 1), I_sd = 1), + transforms = list(trans_rates = c(beta = "log"), I_sd = "log"), + exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)) +) ``` -Unlike typical R functions, this function modifies the `sim` object *in place* (!!) +Unlike typical R functions, this function modifies the `sim` object *in place* (!!) `FIXME: do not do this in place -- make a clone, which is easy with oor` A sanity check: make sure that the starting values give a reasonable-looking trajectory. @@ -118,7 +118,7 @@ Setting $\log(\beta)=0$ instead gives us a trajectory that is still very unreali Let's replace the starting value for `log_beta` with 0: ```{r repl_param} -sir_simulator$replace$params(c(0, 1), c("log_beta", "log_I_sd")) +sir_simulator$replace$params(c(0, 1), c("log_trans_rates", "log_I_sd")) ``` (In a proper workflow we might prefer to go back upstream to wherever we defined the default values, rather than resetting the value on the fly ...) @@ -184,8 +184,8 @@ library(outbreaks) sir_simulator <- mk_sim(init_state = c(S = 760, I = 3, R = 0)) mk_calibrate(sir_simulator, data = data.frame(I_obs = influenza_england_1978_school$in_bed), - params = list(beta = 1, gamma = 0.5, I_disp = 1), - transforms = list(beta = "log", gamma = "log", I_disp = "log"), + params = list(trans_rates = c(beta = 1), flow_rates = c(gamma = 0.5), I_disp = 1), + transforms = list(trans_rates = c(beta = "log"), flow_rates = c(gamma = "log"), I_disp = "log"), exprs = list(log_lik ~ dnbinom(I_obs, I, I_disp)) ) ## basic sanity check From 7f295edc009f94b8f514424c52261c9e9dd44edc Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 7 Nov 2023 17:48:18 -0500 Subject: [PATCH 066/332] checkpoint draft before big edit --- vignettes/quickstart.Rmd | 205 +++++++++++++++++++++++++-------------- 1 file changed, 130 insertions(+), 75 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index d2a504a7..a8464cf0 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -21,25 +21,32 @@ library(macpan2) # Motivation/nomenclature - goal is to teach a user how models are specified in `macpan2` via a familiar and simple model -- `macpan2`'s model specification is extremely flexible, which is also what can make it difficult to learn to work with at first + +`macpan2`'s model specification is extremely flexible, which is also what can make it difficult to learn to work with at first. The key is to understand that there is no built-in or prescribed method to perform simulations; for instance, when simulating a compartmental model, how each state variables is updated must be specified by the user. This can be difficult to - unstructured model = a pure epidemic compartmental model, where each state is only denoted using a disease status (_e.g._ $S$ = susceptible, $I$ = infected, etc.) - structured model = an epidemic model **crossed** with something else, like locations to generate a meta-population model or age groups to generate an age-stratified model -- while the model specification may seem like overkill for a simple model, we will demonstrate later how tthe model specification can be used to quickly and efficiently generate a complicated, structured model by expanding a simple model +As we explore the model specification for a simple, unstructured model in the [next section]{#SIR}, it will likely seem overly complicated at first. However, when we then expand this simple model to include more structure (stratification) [later](#SIR-age) may seem like overkill for a simple model, we will demonstrate later how the model specification can be used to quickly and efficiently generate a complicated, structured model by expanding a simple model -# A simple, unstructured model +# A simple, unstructured model {#SIR} -We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models. The SIR model is generally cast as a system of ordinary differential equations: +We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models. The SIR model is generally cast as a system of ordinary differential equations \begin{align} -\frac{dS}{dt} &= -\beta S(t) I(t) \\ -\frac{dI}{dt} &= \beta S(t) I(t) - \gamma I(t) \\ -\frac{dR}{dt} &= \gamma I(t) +\frac{dS}{dt} &= -\beta S(t) I(t)/N, \\ +\frac{dI}{dt} &= \beta S(t) I(t)/N - \gamma I(t), \\ +\frac{dR}{dt} &= \gamma I(t), \label{eqn:SIR-ode} \end{align} -However, it will be more useful to think of the SIR model in terms of a system of difference equations as that is how we will simulate it with `macpan2`: +where +- $S$, $I$, and $R$ are the number of susceptible, infected, and recovered individuals, respectively, +- $N$ is the total population size ($S + I + R$), +- $\beta$ is the transmission rate, +- $\gamma$ is the recovery rate. + +For a system of ordinary differential equations, we assume time is continuous, but it will be more useful to think of the SIR model in terms of a system of difference equations (in discrete time) as that is how we will simulate it numerically: \begin{align} S_{t+1} &= -\beta S_t I_t \\ @@ -48,7 +55,10 @@ R_{t+1} &= \gamma I_t \label{eqn:SIR-difference} \end{align} -Most flows in SIR-type compartmental models are of the form $rX$ where $r$ is a flow and $X$ is the name of the "from" compartment, that is the compartment from which the flow originates. We call the rates $r$ in such terms **flow rates**, and we could re-cast (\ref{eqn:SIR-difference}) to be purely in terms of flow rates, by setting $\lambda = \beta I_t$: +Most flows in SIR-type compartmental models are of the form $rX$ where $r$ is a flow and $X$ is the name of the "from" compartment, that is the compartment from which the flow originates. These flows translate to + + +We call the rates $r$ in such terms **flow rates**, and we could re-cast (\ref{eqn:SIR-difference}) to be purely in terms of flow rates, by setting $\lambda = \beta I_t$: \begin{align} S_{t+1} &= -\lambda S_t \\ @@ -57,9 +67,9 @@ R_{t+1} &= \gamma I_t \label{eqn:SIR-difference} \end{align} -Now the model is stated purely in terms of the **flow rates** $\lambda$ and $\gamma$.^[In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.] +Now the model is stated purely in terms of the **flow rates** $\lambda$ and $\gamma$.^[In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.] This framing enables us to start building our simulator. -However, $\lambda$ is special from a simulation standpoint as it depends on a state variable ($I_t$), so it must be recomputed at every step of the simulation (unlike $\gamma$, which is generally fixed and chosen by the modeller when specifying the model). $\lambda$ also depends on $\beta$, which we call an **influence rate** as it is involved in a term +However, $\lambda$ is special from a simulation standpoint as it depends on a state variable ($I_t$), so it must be recomputed at every step of the simulation (unlike $\gamma$, which is generally fixed and chosen by the modeller when specifying the model). $\lambda$ also depends on $\beta$, which is what a modeller would actually want to specify. So we In order to specify a model, we must first decide on the labels we will use for model components. For the SIR model (and most compartmental models), these components will be: @@ -83,6 +93,12 @@ We start by state ``` +# Stratifying a simple model with age structure {#SIR-age} + +# Interlude: simplifying model spec + +This is just me trying to see if we can simplify the model specification further to lower the barrier to entry. + ```{r model-working} expr_list = mp_expr_list( before = list( @@ -124,8 +140,8 @@ beta = mp_subset(influence_rates, Epi = "beta") ## links between entries in the indexes ----------- # flows_per_time ~ state[from] * flow_rates[edge] -infection = mp_join(from = S, to = I, edge = lambda) -recovery = mp_join(from = I, to = R, edge = gamma) +infection = mp_join(from = S, to = I, rate = lambda) +recovery = mp_join(from = I, to = R, rate = gamma) # flow_rates[infection_flow] ~ # state[infectious_state] * influence_rates[transmission] / N @@ -166,91 +182,130 @@ sir_sim = mp_tmb_simulator(dynamic_model , time_steps = 100L ) -mp_report(sir_sim) +sim.working = mp_report(sir_sim) ``` ```{r model-edited} -expr_list = mp_expr_list( - before = list( - ## aggregations - N ~ sum(state) - ), - during = list( - ## influences - flow_rates[infection_flow] ~ - state[infectious_state] * flow_rates[transmission] / N - - ## flows - , flows_per_time ~ state[from] * flow_rates[edge] - - ## state update - , total_inflow ~ groupSums(flows_per_time, to, state) - , total_outflow ~ groupSums(flows_per_time, from, state) - , state ~ state + total_inflow - total_outflow - ) -) - -## basic model indexes ------------------------- +## indices (labels) for model quantities ------------------------- state = mp_index(Epi = c("S", "I", "R")) -flow_rates = mp_index(Epi = c("lambda", "gamma", "beta")) -# flow_rates = mp_index(Epi = c("lambda", "gamma")) -# influence_rates = mp_index(Epi = "beta") - -## subset indexes ----------------- +flow_rates = mp_index(Epi = c("beta", "gamma", "lambda")) -S = mp_subset(state, Epi = "S") -I = mp_subset(state, Epi = "I") -R = mp_subset(state, Epi = "R") -lambda = mp_subset(flow_rates, Epi = "lambda") -gamma = mp_subset(flow_rates, Epi = "gamma") -# beta = mp_subset(influence_rates, Epi = "beta") -beta = mp_subset(flow_rates, Epi = "beta") +## connect simulation calculations to indices ----------- -## links between entries in the indexes ----------- +# there are three main calculation archetypes in simulating the SIR model +# that a modeller may want to have control over to expand later: +# 1. infection: the flow between susceptible and infected classes (if there are multiple) +# 2. recovery: the flow between infected and recovered classes +# 3. force of infection: the prevalence-dependent rate involved in calculating infection above -# flows_per_time ~ state[from] * flow_rates[edge] -infection = mp_join(from = S, to = I, edge = lambda) -recovery = mp_join(from = I, to = R, edge = gamma) +# for each of these calculation archetypes, we need to write down a ledger cataloguing each specific calculation +# the SIR model is so simple that there will only be one infection flow calculation, one recovery flow calculation, and one force of infection calculation, but we will see with a structured model that these ledgers give us the flexbility to easily expand a model without being redundant -# flow_rates[infection_flow] ~ -# state[infectious_state] * influence_rates[transmission] / N -transmission = mp_join( - infectious_state = I, - infection_flow = lambda, - transmission = beta +# infection +infection = mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + rate = mp_subset(flow_rates, Epi = "lambda") ) -# IP: what is this for? -link_data = list( - influences = mp_link_data(transmission) - , flows = mp_link_data(infection, recovery) +# recovery +recovery = mp_join( + from = mp_subset(state, Epi = "I"), + to = mp_subset(state, Epi = "R"), + rate = mp_subset(flow_rates, Epi = "gamma") ) -## Initialize indexed vectors (to all zeros) -------------- +# infection additionally involves the calculation of a force of infection +force_of_infection = mp_join( + infection_flow = mp_subset(flow_rates, Epi = "lambda"), + infectious_state = mp_subset(state, Epi = "I"), + rate = mp_subset(flow_rates, Epi = "beta") +) -init_vecs = list( - state = mp_vector(state), - flow_rates = mp_vector(flow_rates) +## links between entries in the indexes ----------- +SIR_starter = function( + # indices for model quantities + state, + flow_rates, + # ledgers for calculation archetypes + infection, + recovery, + force_of_infection +){ + + link_data = list( + flows = mp_link_data(infection, recovery), + fois = mp_link_data(force_of_infection) + ) + + ## Initialize indexed vectors (to all zeros) -------------- + + init_vecs = list( + state = mp_vector(state), + flow_rates = mp_vector(flow_rates) + ) + + ## Set up expressions list -------------- + expr_list = mp_expr_list( + before = list( + ## aggregations + N ~ sum(state) + ), + during = list( + ## influences + flow_rates[infection_flow] ~ + state[infectious_state] * flow_rates[rate] / N + + ## flows + , flows_per_time ~ state[from] * flow_rates[rate] + + ## state update + , total_inflow ~ groupSums(flows_per_time, to, state) + , total_outflow ~ groupSums(flows_per_time, from, state) + , state ~ state + total_inflow - total_outflow + ) + ) + + ## Final output ----------------- + + DynamicModel( + expr_list = expr_list, + link_data = link_data, + init_vecs = init_vecs, + unstruc_mats = list() + ) +} + +model = SIR_starter( + state, + flow_rates, + infection, + recovery, + transmission ) -## Final output ----------------- -dynamic_model = DynamicModel( - expr_list = expr_list, - link_data = link_data, - init_vecs = init_vecs, - unstruc_mats = list() -) -sir_sim = mp_tmb_simulator(dynamic_model +sir_sim_edited = mp_tmb_simulator(model , vectors = list( state = c(S = 999, I = 1, R = 0), - flow_rates = c(lambda = NA, gamma = 0.1, beta = 0.25) + flow_rates = c(beta = 0.25, gamma = 0.1, lambda = NA) ) , time_steps = 100L ) -mp_report(sir_sim) +sim.edited = mp_report(sir_sim_edited) +``` + +```{r} +uniformize = function(sim){ + (sim + |> dplyr::filter(matrix == "state")) +} + +waldo::compare(uniformize(sim.working), + uniformize(sim.edit)) ``` + From 5e1566da12b81c4e022e666125d33a486d2006b0 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 7 Nov 2023 18:59:14 -0500 Subject: [PATCH 067/332] a (bugged) sketch of the vignette --- vignettes/quickstart.Rmd | 336 +++++++++++++++------------------------ 1 file changed, 125 insertions(+), 211 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index a8464cf0..f0bd1b94 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -1,5 +1,5 @@ --- -title: "Quickstart Guide: specifying and simulating a simple compartmental model" +title: "Quickstart part 1: understanding model specification in `macpan2`" output: rmarkdown::html_vignette: toc: true @@ -18,211 +18,65 @@ library(macpan2) ``` -# Motivation/nomenclature +# Motivation -- goal is to teach a user how models are specified in `macpan2` via a familiar and simple model +A common approach to modelling is to start with a simple model and then expand and/or stratify as further complexity is needed. In reality, this process can be full of friction as one implements and re-implements the model for numerical simulation. One of the goals of `macpan2` is to provide a grammar for model specification that streamlines model expansion. -`macpan2`'s model specification is extremely flexible, which is also what can make it difficult to learn to work with at first. The key is to understand that there is no built-in or prescribed method to perform simulations; for instance, when simulating a compartmental model, how each state variables is updated must be specified by the user. This can be difficult to +# Amuse bouche: a (simple) metapopulation SIR model -- unstructured model = a pure epidemic compartmental model, where each state is only denoted using a disease status (_e.g._ $S$ = susceptible, $I$ = infected, etc.) -- structured model = an epidemic model **crossed** with something else, like locations to generate a meta-population model or age groups to generate an age-stratified model +- define model used in slides (ignoring contact matrices???) +- point out that there is a lot of repetition +- introduce the idea of calculation archetypes (formulas? patterns?) that get repeated -As we explore the model specification for a simple, unstructured model in the [next section]{#SIR}, it will likely seem overly complicated at first. However, when we then expand this simple model to include more structure (stratification) [later](#SIR-age) may seem like overkill for a simple model, we will demonstrate later how the model specification can be used to quickly and efficiently generate a complicated, structured model by expanding a simple model +`macpan2`'s flexibility is derived from the ability to identify calculation archetypes that may or may not be repeated throughout the model. For instance, consider the following simple metapopulation SIR model, cast as a system of difference equations (as is often the case when numerically simulating ordinary differential equation models): -# A simple, unstructured model {#SIR} +[insert diagram with three cities and the same set of SIR difference equations within. note that we've simplified the usual metapop model by not considering movement between cities... does that make it an overly simple stratified model?? ugh, i dont know. maybe we would be better off with a model that has two forces of infection and explain calculation archetypes with that ??] -We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models. The SIR model is generally cast as a system of ordinary differential equations - -\begin{align} -\frac{dS}{dt} &= -\beta S(t) I(t)/N, \\ -\frac{dI}{dt} &= \beta S(t) I(t)/N - \gamma I(t), \\ -\frac{dR}{dt} &= \gamma I(t), -\label{eqn:SIR-ode} -\end{align} - -where +[define states and rates with this diagram - $S$, $I$, and $R$ are the number of susceptible, infected, and recovered individuals, respectively, - $N$ is the total population size ($S + I + R$), - $\beta$ is the transmission rate, - $\gamma$ is the recovery rate. +] -For a system of ordinary differential equations, we assume time is continuous, but it will be more useful to think of the SIR model in terms of a system of difference equations (in discrete time) as that is how we will simulate it numerically: +Each city has an infection flow, a recovery flow, and a force of infection that need to be computed for each time step in the numerical simulation of the model. But the **form** of each of these calculations is essentially the same! Instead of manually writing out each calculation, for instance as -\begin{align} -S_{t+1} &= -\beta S_t I_t \\ -I_{t+1} &= \beta S_t I_t - \gamma I_t \\ -R_{t+1} &= \gamma I_t -\label{eqn:SIR-difference} -\end{align} - -Most flows in SIR-type compartmental models are of the form $rX$ where $r$ is a flow and $X$ is the name of the "from" compartment, that is the compartment from which the flow originates. These flows translate to +``` +S.calgary[t+1] = - lambda.calgary * S.calgary[t] +S.hamilton[t+1] = - lambda.hamilton * S.hamilton[t] +S.quebec[t+1] = - lambda.quebec * S.quebec[t] +``` +we will specify a single calculation archetype for it, for instance something like, +``` +S[t+1] = -lambda * S[t] +``` +and then attach information to the model object about which specific calculations to do under this archetype (one per city). +# Appetizer: specifying the basic SIR model {#SIR} -We call the rates $r$ in such terms **flow rates**, and we could re-cast (\ref{eqn:SIR-difference}) to be purely in terms of flow rates, by setting $\lambda = \beta I_t$: +Let's start with the basic epidemic model being repeated across cities and learn how to specify that in `macpan2`. We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models: \begin{align} -S_{t+1} &= -\lambda S_t \\ -I_{t+1} &= \lambda S_t - \gamma I_t \\ -R_{t+1} &= \gamma I_t +S_{t+1} &= -\beta S_t I_t/N, \\ +I_{t+1} &= \beta S_t I_t/N - \gamma I_t, \\ +R_{t+1} &= \gamma I_t. \label{eqn:SIR-difference} \end{align} -Now the model is stated purely in terms of the **flow rates** $\lambda$ and $\gamma$.^[In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.] This framing enables us to start building our simulator. - -However, $\lambda$ is special from a simulation standpoint as it depends on a state variable ($I_t$), so it must be recomputed at every step of the simulation (unlike $\gamma$, which is generally fixed and chosen by the modeller when specifying the model). $\lambda$ also depends on $\beta$, which is what a modeller would actually want to specify. So we - -In order to specify a model, we must first decide on the labels we will use for model components. For the SIR model (and most compartmental models), these components will be: - -- **States**: Names of the compartments. -- **Flow rates**: Names of rates used in flow expressions of the form $rX$, where $r$ is the rate and $X$ is the "from" state---the state from which the flow originates. In the SIR model above, these would be $\gamma$ and $\lambda$. -- **Influence rates**: Names of rates used in flow expressions that additionally include a state other than the "from" state, such as (but not limited to) those of the form $rYX$, where $r$ is the influence rate, $X$ is the "from" state and $Y$ is the "to" state. In the SIR model above, $\beta$ would be an influence rate. - -The distinction between flow and influence rates is not always made when working with compartmental epidemic models (_i.e._ $\beta$ is sometimes called a flow rate), but flow and influence rates are processed differently in `macpan2` for the purpose of simulating the model, and so we must also make this distinction when specifying the model. +It will be helpful to set $\lambda = \beta I_t/N$ and recast the equations as: -For the SIR model, the labels (_indices_) are generated as follows: +\begin{align} +S_{t+1} &= -\lambda S_t, \\ +I_{t+1} &= \lambda S_t - \gamma I_t, \\ +R_{t+1} &= \gamma I_t. +\label{eqn:SIR-difference} +\end{align} -```{r} -state = mp_index(Epi = c("S", "I", "R")) -flow_rates = mp_index(Epi = c("lambda", "gamma")) -influence_rates = mp_index(Epi = "beta") -``` +(In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.) -We start by +The focus of this quickstart guide is to help a user begin to understand how models are specified using `macpan2`. The following function has been created to sweep some of the details of initializing an SIR model object under the rug (for now). Please ignore this function's definition on your first read and just understand that it will output a model object from which we can then build a simulator. In order to do that, we have to create each of the inputs of this function, which is what the rest of this section will be about. ```{r} -state -``` - -# Stratifying a simple model with age structure {#SIR-age} - -# Interlude: simplifying model spec - -This is just me trying to see if we can simplify the model specification further to lower the barrier to entry. - -```{r model-working} -expr_list = mp_expr_list( - before = list( - ## aggregations - N ~ sum(state) - ), - during = list( - ## influences - flow_rates[infection_flow] ~ - state[infectious_state] * influence_rates[transmission] / N - - ## flows - , flows_per_time ~ state[from] * flow_rates[edge] - - ## state update - , total_inflow ~ groupSums(flows_per_time, to, state) - , total_outflow ~ groupSums(flows_per_time, from, state) - , state ~ state + total_inflow - total_outflow - ) -) - -## basic model indexes ------------------------- - -state = mp_index(Epi = c("S", "I", "R")) -# flow_rates = mp_index(Epi = c("lambda", "gamma", "beta")) -flow_rates = mp_index(Epi = c("lambda", "gamma")) -influence_rates = mp_index(Epi = "beta") - -## subset indexes ----------------- - -S = mp_subset(state, Epi = "S") -I = mp_subset(state, Epi = "I") -R = mp_subset(state, Epi = "R") -lambda = mp_subset(flow_rates, Epi = "lambda") -gamma = mp_subset(flow_rates, Epi = "gamma") -beta = mp_subset(influence_rates, Epi = "beta") -# beta = mp_subset(flow_rates, Epi = "beta") - -## links between entries in the indexes ----------- - -# flows_per_time ~ state[from] * flow_rates[edge] -infection = mp_join(from = S, to = I, rate = lambda) -recovery = mp_join(from = I, to = R, rate = gamma) - -# flow_rates[infection_flow] ~ -# state[infectious_state] * influence_rates[transmission] / N -transmission = mp_join( - infectious_state = I, - infection_flow = lambda, - transmission = beta -) - -link_data = list( - influences = mp_link_data(transmission) - , flows = mp_link_data(infection, recovery) -) - -## Initialize indexed vectors (to all zeros) -------------- - -init_vecs = list( - state = mp_vector(state), - flow_rates = mp_vector(flow_rates), - influence_rates = mp_vector(influence_rates) -) - -## Final output ----------------- - -dynamic_model = DynamicModel( - expr_list = expr_list, - link_data = link_data, - init_vecs = init_vecs, - unstruc_mats = list() -) - -sir_sim = mp_tmb_simulator(dynamic_model - , vectors = list( - state = c(S = 999, I = 1, R = 0), - flow_rates = c(lambda = NA, gamma = 0.1), - influence_rates = c(beta = 0.25) - ) - , time_steps = 100L -) - -sim.working = mp_report(sir_sim) -``` - -```{r model-edited} -## indices (labels) for model quantities ------------------------- - -state = mp_index(Epi = c("S", "I", "R")) -flow_rates = mp_index(Epi = c("beta", "gamma", "lambda")) - -## connect simulation calculations to indices ----------- - -# there are three main calculation archetypes in simulating the SIR model -# that a modeller may want to have control over to expand later: -# 1. infection: the flow between susceptible and infected classes (if there are multiple) -# 2. recovery: the flow between infected and recovered classes -# 3. force of infection: the prevalence-dependent rate involved in calculating infection above - -# for each of these calculation archetypes, we need to write down a ledger cataloguing each specific calculation -# the SIR model is so simple that there will only be one infection flow calculation, one recovery flow calculation, and one force of infection calculation, but we will see with a structured model that these ledgers give us the flexbility to easily expand a model without being redundant - -# infection -infection = mp_join( - from = mp_subset(state, Epi = "S"), - to = mp_subset(state, Epi = "I"), - rate = mp_subset(flow_rates, Epi = "lambda") -) - -# recovery -recovery = mp_join( - from = mp_subset(state, Epi = "I"), - to = mp_subset(state, Epi = "R"), - rate = mp_subset(flow_rates, Epi = "gamma") -) - -# infection additionally involves the calculation of a force of infection -force_of_infection = mp_join( - infection_flow = mp_subset(flow_rates, Epi = "lambda"), - infectious_state = mp_subset(state, Epi = "I"), - rate = mp_subset(flow_rates, Epi = "beta") -) - ## links between entries in the indexes ----------- SIR_starter = function( # indices for model quantities @@ -234,30 +88,18 @@ SIR_starter = function( force_of_infection ){ - link_data = list( - flows = mp_link_data(infection, recovery), - fois = mp_link_data(force_of_infection) - ) - - ## Initialize indexed vectors (to all zeros) -------------- - - init_vecs = list( - state = mp_vector(state), - flow_rates = mp_vector(flow_rates) - ) - - ## Set up expressions list -------------- + ## Set up expressions list for each calculation archetype -------------- expr_list = mp_expr_list( before = list( ## aggregations N ~ sum(state) ), during = list( - ## influences + ## force of infections flow_rates[infection_flow] ~ state[infectious_state] * flow_rates[rate] / N - ## flows + ## unsigned individual flows , flows_per_time ~ state[from] * flow_rates[rate] ## state update @@ -267,8 +109,20 @@ SIR_starter = function( ) ) - ## Final output ----------------- + ## Ledgers for each specific calculation -------------- + link_data = list( + flows = mp_link_data(infection, recovery), + fois = mp_link_data(force_of_infection) + ) + + ## Initialize indexed vectors (to all zeros) -------------- + # used as placeholders for user input + init_vecs = list( + state = mp_vector(state), + flow_rates = mp_vector(flow_rates) + ) + ## Initialize model object ----------------- DynamicModel( expr_list = expr_list, link_data = link_data, @@ -276,18 +130,82 @@ SIR_starter = function( unstruc_mats = list() ) } +``` + +Now that we have our equations, we can start by naming the main quantities used in specifying and simulating the model. Quantities are named using "indices", which you can think of for now as labels for each quantity: + +```{r} +## indices (labels) for model quantities ------------------------- + +state = mp_index(Epi = c("S", "I", "R")) +flow_rates = mp_index(Epi = c("beta", "gamma", "lambda")) +``` + +You can think of the `mp_index()` fuction a setting up data frames with model quantity labels: + +```{r} +state +``` + +```{r} +flow_rates +``` +(Ignore the column name `Epi` for now.) + +In this model, we have three main calculation archetypes. In this case, we're thinking of calculation archetypes not as repeated calculations, but as calculations that a modeller may want to repeat down the line, as they expand the model. The archetypes are: + +1. infection: flows between susceptible and infected classes +2. recovery: flows between infected and recovered classes +3. force of infection: prevalence-dependent rates involved in calculating infection flows + +For each of these calculation archetypes, we need to write down a ledger cataloguing each specific calculation that uses each archetype. (Note that the SIR model is so simple that there will only be one infection flow calculation, one recovery flow calculation, and one force of infection calculation, but we will see later with a structured model that these ledgers give us the flexbility to easily expand a model without being redundant.) + +We start by creating the infection ledger: + +```{r} +# infection ledger +infection = mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + rate = mp_subset(flow_rates, Epi = "lambda") +) +``` + +- explain what the `mp_join()` function is doing (at a high level)... mention that the argument names are flexible an depend on how the calculation archetypes are specified, but we should ignore this for now. + +Then we create the remaining ledgers: + +```{r} +# recovery +recovery = mp_join( + from = mp_subset(state, Epi = "I"), + to = mp_subset(state, Epi = "R"), + rate = mp_subset(flow_rates, Epi = "gamma") +) + +# infection additionally involves the calculation of a force of infection +force_of_infection = mp_join( + infection_flow = mp_subset(flow_rates, Epi = "lambda"), + infectious_state = mp_subset(state, Epi = "I"), + rate = mp_subset(flow_rates, Epi = "beta") +) +``` + +Now we can use the `SIR_starter()` function to initialize our model object: + +```{r} model = SIR_starter( state, flow_rates, infection, recovery, - transmission + force_of_infection ) +``` - - -sir_sim_edited = mp_tmb_simulator(model +```{r} +sir_simulator = mp_tmb_simulator(model , vectors = list( state = c(S = 999, I = 1, R = 0), flow_rates = c(beta = 0.25, gamma = 0.1, lambda = NA) @@ -295,17 +213,13 @@ sir_sim_edited = mp_tmb_simulator(model , time_steps = 100L ) -sim.edited = mp_report(sir_sim_edited) +sim_report = mp_report(sir_simulator) ``` -```{r} -uniformize = function(sim){ - (sim - |> dplyr::filter(matrix == "state")) -} +# Main course: expanding the basic SIR into a metapopulation SIR -waldo::compare(uniformize(sim.working), - uniformize(sim.edit)) -``` +- here is where we see the power of macpan +# Desert: understanding model simulation in `macpan2` +- follow up vignette, tentative title: "Quickstart part 2: understanding model simulation in `macpan2`" From 1e44bc8a943d280737dbd78036d5331add6c6147 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 8 Nov 2023 03:57:17 -0500 Subject: [PATCH 068/332] late --- NAMESPACE | 1 + R/calibration.R | 315 ++++++++++++++++++++++++++++++++++++++ man/dot-known_dist.Rd | 20 +++ man/mk_calibrate.Rd | 95 ++++++++++++ vignettes/calibration.Rmd | 2 +- 5 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 man/dot-known_dist.Rd create mode 100644 man/mk_calibrate.Rd diff --git a/NAMESPACE b/NAMESPACE index 534c5816..633baa3e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -161,6 +161,7 @@ export(labelled_frame) export(labelled_zero_vector) export(labelling_column_names) export(make_expr_parser) +export(mk_calibrate) export(model_starter) export(mp_aggregate) export(mp_cartesian) diff --git a/R/calibration.R b/R/calibration.R index 97c4f694..fb31edc4 100644 --- a/R/calibration.R +++ b/R/calibration.R @@ -80,3 +80,318 @@ TMBOptimizationHistory = function(simulator) { self$save = function(opt_obj) self$.history = append(self$.history, list(opt_obj)) return_object(self, "TMBOptimizationHistory") } + + +add_slot <- function(sim, x, value = empty_matrix, save_x = FALSE, return_x = FALSE) { + args <- list() + argstr <- list() + if (save_x) { + args <- c(args, list(.mats_to_save = x)) + argstr <- append(argstr, sprintf(".mats_to_save = %s", x)) + } + if (return_x) { + args <- c(args, list(.mats_to_return = x)) + argstr <- append(argstr, sprintf(".mats_to_return = %s", x)) + } + args <- c(list(value), args) + argstr <- append(argstr, sprintf("%s = %s", x, deparse(substitute(value)))) + names(args)[1] <- x + do.call(sim$add$matrices, args) + argstr <- sprintf("sim$add$matrices(%s)", + do.call(paste, c(list(unlist(argstr)), list(collapse = ", ")))) + return(invisible(argstr)) +} + +## Note: example is failing with bf1de7f99 +## with: Error in valid$consistency_params_mats$check(self$model) : +## optimization parameters are not consistent with matrices +## but validity could not be checked because: +## Error in if (any(!valid_pars)) { : missing value where TRUE/FALSE needed + +##' Add calibration information to a simulator +##' +##' ## To do/FIXME +##' * see hacks for getting simulation variables, state variables +##' * modularize? +##' * switch for enabling a differenced/incidence class (add a flow/accumulator var to model; add a differencing step)? +##' * allow setting clamp tolerance? allow specified list of variables to clamp rather than all or nothing +##' * rename and move into macpan2 +##' * document that 'log-likelihood' means -1*(loss function) (e.g. for SSQ, chi-squared fits) +##' @param sim A \code{macpan2} simulator (i.e., a \code{TMBSimulator} object). +##' @param params a list of parameters with default/starting values. +##' @param transforms TODO. +##' @param data A data frame containing data to add (i.e., observed variables that will be compared with simulations). If the data frame contains a column called "time" or "date" (any capitalization), it will be used. +##' @param start_time A time or date, overriding first time in data; set to 1 otherwise. +##' @param end_time A time or date, overriding last time in data; set to number of time steps otherwise. +##' @param exprs A list of expressions to add. +##' @param debug (logical) Print debugging information? +##' @param clamp_vars (logical) Force state variables to be positive in likelihood expression? +##' @return This function modifies the simulator object **in place**. It also returns (invisibly) a character vector of the lower-level operations it performs. +##' @export +#' @examples +#' ## it's convenient to have a function that sets up a fresh simulation +#' ## (since adding already-existing components to a simulation object throws an error) +#' library(dplyr) +#' setup_sim <- function() { +#' m <- Compartmental(system.file("starter_models", "sir", package = "macpan2")) +#' sim <- m$simulators$tmb( +#' time_steps = 100, +#' state = c(S = 99, I = 1, R = 0), +#' flow = c(foi = NA, gamma = 0.1), +#' beta = 0.2, +#' N = empty_matrix +#' ) +#' } +#' sim <- setup_sim() +#' if (require(outbreaks)) { +#' I_obs <- influenza_england_1978_school[["in_bed"]] +#' } else { +#' set.seed(101) +#' I_obs <- (sim$report(.phases = "during") +#' |> filter(row == "I") +#' |> mutate(obs = rnbinom(100, mu = value, size = 2)) +#' |> pull(obs) +#' ) +#' } +#' m1 <- mk_calibrate(sim, +#' data = data.frame(I_obs), +#' params = list(beta = 0.2, I_sd = 1), +#' transforms = list(beta = "log", I_sd = "log"), +#' exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), +#' ) +#' cat(m1, sep = "\n") +#' sim$optimize$nlminb() +#' sim <- setup_sim() ## refresh +#' mk_calibrate(sim, +#' data = data.frame(I_obs), +#' params = list(beta = 0.2, gamma = 0.05), +#' transforms = list(beta = "log", gamma = "log"), +#' exprs = list(log_lik ~ dpois(I_obs, I)) +#' ) +#' sim$optimize$nlminb() +#' ## warning about NA/NaN function evaluation is probably harmless ... +mk_calibrate <- function(sim, + params = list(), + transforms = list(), + data = NULL, + start_time = NULL, + end_time = NULL, + exprs = list(), + debug = FALSE, + clamp_vars = FALSE) { + ## how do I get these programmatically from sim? + ## is there a better/easier way to get state names?? + ## these are present in 'Compartmental' objects; + ## easy to get with sim$labels$state(). Should they be carried along + ## somehow? + data_sub <- deparse(substitute(data)) + desc <- list() + + ## if only one data frame is passed, assume that it is + ## referencing state variables to be fitted. in the future + ## we will allow a list of data frames, with one per vector. + if (inherits(data, "data.frame")) data = list(state = data) + + logit <- plogis ## ugh; better way to handle transformations? + + ## for testing! + + cap <- function(s) paste0(toupper(substring(s, 1, 1)), substring(s, 2)) + + ## add log-likelihood slot + ## add comments ??? + desc <- append(desc, "# add log_lik matrix (empty)") + + desc <- append(desc, add_slot(sim, "log_lik")) + ## added_vars <- character(0) + + ## add data + if (!is.null(data)) { + for (vctr in names(data)) { + + if (!is.data.frame(data[[vctr]])) stop("'data' argument must be a data frame or list of data frames") + timecol <- grep("([Tt]ime|[Dd]ate)", names(data[[vctr]])) + if (length(timecol) > 1) stop("multiple time/date columns detected") + if (length(timecol) == 0 && (!is.null(start_time) || !is.null(end_time))) { + stop("if start_time or end_time are specified, data must include a time/date column") + } + if (length(timecol) > 0) { + timevec <- data[[vctr]][[timecol]] + if (length(unique(timevec)) < length(timevec)) stop("time steps in data must be unique") + if (any(diff(timevec) < 0)) stop("data must be sorted by time") + if (any(diff(timevec) < 1)) stop("time steps must be equal to 1") + if (is.null(start_time)) start_time <- timevec[1] + if (is.null(end_time)) end_time <- tail(timevec, 1) + ts <- as.numeric(end_time - start_time) + 1 ## check on +1/edge effect issues? + ## generate time index that matches up with data frame + timevec <- as.integer(timevec - start_time + 1) + data[[vctr]] <- data[[vctr]][, -timecol, drop = FALSE] + } else { + ts <- nrow(data[[vctr]]) + timevec <- seq(ts) + } + irreg_time <- any(is.na(data[[vctr]])) || any(diff(timevec) > 1) + cur_ts <- sim$tmb_model$time_steps$time_steps + if (ts != cur_ts) { + if (debug) cat(sprintf("resetting number of time steps (%d -> %d)\n", + cur_ts, nrow(data[[vctr]]))) + sim$replace$time_steps(nrow(data[[vctr]])) + desc <- append(desc, sprintf("sim$replace$time_steps(%d)", nrow(data[[vctr]]))) + } + for (nm in names(data[[vctr]])) { + if (debug) cat(sprintf("add data matrix: %s\n", nm)) + do.call(sim$add$matrices, data[[vctr]][nm]) + desc <- append(desc, sprintf("sim$add$matrices(%s[['%s']]", data_sub, nm)) + } + } + } + + ## ?? needs to go before expressions get added? + for (p in setdiff(names(params), sim$matrix_names())) { + ## add params if not already in model (e.g. dispersion parameter) + if (debug) cat("add param (scalar placeholder value): ", p, "\n") + desc <- append(desc, add_slot(sim, p, 1.0)) + } + + ## add _sim analogues for state variables referred to in expressions; + ## substitute rbind_time(*_sim) in expressions + for (i in seq_along(exprs)) { + ee <- exprs[[i]] + if (debug) cat("process expression: ", deparse(ee), "\n") + all_vars <- all.vars(ee) + ## create a placeholder + ## FIXME: change logic to allow time indices for derived variables as well as state variables? + ## should have a separate loop that checks for 'specials' (dnorm, dpois, etc.) and uses those arguments + ## to match sim vs obs for time-index-matching + ## right now this *won't* work for the combination of derived variables (e.g. sum of hospital classes, incidences) + irregular time + for (vctr in names(data)) { + for (v in intersect(all_vars, labels(sim)[[vctr]])) { + ph <- paste0(v, "_sim") + if (debug) cat("add (empty matrix): ", ph, "\n") + desc <- append(desc, add_slot(sim, ph, save_x = TRUE)) + newexpr <- reformulate(v, response = ph, env = emptyenv()) + if (debug) cat("add ", deparse(newexpr), "\n") + sim$insert$expressions( + newexpr, + .phase = "during", + .at = Inf) + desc <- append(desc, sprintf("sim$insert$expressions(%s, .phase = 'during', .at = Inf", deparse(newexpr))) + if (irreg_time) { + ## could skip indices for any data elements that are regular? + ## need to match observed var with sim var explicitly + ## for now let's hope there's a one-to-one match between expressions in the state vector + ## and observed data ... + data_var <- intersect(all_vars, names(data[[vctr]])) + t_ind <- timevec[!is.na(data[[vctr]][[data_var]])] + t_indnm <- paste0(v, "_tind") + do.call(sim$add$matrices, setNames(list(t_ind), t_indnm)) + if (debug) cat(sprintf("add data matrix: %s", t_indnm)) + desc <- append(desc, sprintf("%s <- %s", t_indnm, deparse(t_ind))) + desc <- append(desc, sprintf("sim$add$matrices(%s)", t_indnm)) + bind_var <- sprintf("rbind_time(%s, %s)", ph, t_indnm) + } else { + bind_var <- sprintf("rbind_time(%s)", ph) + } + ## convert to parsed expression, then get rid of expression() + newsym <- parse(text = bind_var)[[1]] + exprs[[i]] <- do.call(substitute, + list(ee, setNames(list(newsym), v))) + ## substitute clamp(*_sim) [INSIDE] rbind_time() + if (clamp_vars) { + clamp_var <- sprintf("clamp(%s)", ph) + newsym <- parse(text = clamp_var)[[1]] + exprs[[i]] <- do.call(substitute, + list(exprs[[i]], setNames(list(newsym), ph))) + } + } + } + if (debug) cat("add ", deparse(exprs[[i]]), "\n") + sim$insert$expressions(exprs[[i]], .phase = "after") + desc <- append(desc, sprintf("sim$insert$expressions(%s, .phase = 'after', .at = Inf", deparse(exprs[[i]]))) + } + + ## modify names for transforms, apply transform to specified values + pp = function(...) { + l = list(...) + l = l[!vapply(l, is.null, logical(1L))] + if (length(l) == 1L) return(l[[1L]]) + if (length(l) == 0L) return("") + do.call(paste, c(l, sep = "_")) + } + trans_params = character() + for (m in names(params)) { + tr_params = names(transforms[[m]]) + if (!is.null(tr_params)) { + trans_params = append(trans_params, + paste( + unname(transforms[[m]]), + names(params[[m]][tr_params]), + sep = "_" + ) + ) + } else { + trans_params = append(trans_params, + paste(transforms[[m]], names(params[m]), sep = "_") + ) + } + } + + # trpars <- transforms != "" + # trp <- params[trpars] + # names(trp) <- paste(transforms[trpars], names(trp), sep = "_") + ## trp <- Map(function(x, tr) get(tolower(tr))(x), trp, transforms[trpars]) + pframe <- data.frame(mat = trans_params, row = 0, col = 0, default = unlist(trp)) + rownames(pframe) <- NULL ## cosmetic + + desc <- append(desc, sprintf("pframe <- data.frame(mat = %s, row = 0, col = 0, default = %s)", + deparse(names(trp)), deparse(unname(unlist(trp))))) + if (debug) { + cat("param_frame:\n") + print(pframe) + } + + ## FIXME: handle no transformation case + if (debug) cat("adding transformations\n") + add_trans <- function(tr, nm) { + if (debug) cat("add transformation: ", cap(tr)," ", nm, "\n") + sim$add$transformations(get(cap(tr))(nm)) + ## does package checking complain about <<- ? could use assign(..., parent.frame()) + desc <<- append(desc, sprintf("sim$add$transformations(%s(\"%s\"))", cap(tr), nm)) + } + + ## add transformations + Map(add_trans, transforms[trpars], names(params)[trpars]) + + ## now add param frame (does order matter??) + ## + ## FIXME: refresh_param not found + sim$replace$params_frame(pframe) + desc <- append(desc, sprintf("sim$replace$params_frame(pframe)")) + + if (debug) cat("set obj_fn to -sum(log_lik)\n") + sim$replace$obj_fn(~ -sum(log_lik)) + desc <- append(desc, "sim$replace$obj_fn(~ -sum(log_lik))") + ## add transformations + ## add parameters + ## for now, assume all parameters are scalar? + + ## everything is done as a side effect (mutating state of sim) + return(invisible(unlist(desc))) + +} + + +#' Pick out obs/location pairs from terms involving probability distributions +#' find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) +#' find_obs_pairs(stuff ~ other_stuff + more_stuff) +.known_dist <- c("dnorm", "dpois", "dgamma", "dlnorm", "dnbinom") +find_obs_pairs <- function(form, specials = .known_dist, top = TRUE) { + if (is.symbol(form) || length(form) == 1) return(NULL) + if (deparse(form[[1]]) %in% specials) { + return(list(deparse(form[[2]]), deparse(form[[3]]))) + } + res <- lapply(form[-1], find_obs_pairs, top = FALSE) + ## drop NULL elements + res <- res[!vapply(res, is.null, logical(1))] + if (!top) res else res[[1]] +} diff --git a/man/dot-known_dist.Rd b/man/dot-known_dist.Rd new file mode 100644 index 00000000..4955b4a2 --- /dev/null +++ b/man/dot-known_dist.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/calibration.R +\docType{data} +\name{.known_dist} +\alias{.known_dist} +\title{Pick out obs/location pairs from terms involving probability distributions +find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) +find_obs_pairs(stuff ~ other_stuff + more_stuff)} +\format{ +An object of class \code{character} of length 5. +} +\usage{ +.known_dist +} +\description{ +Pick out obs/location pairs from terms involving probability distributions +find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) +find_obs_pairs(stuff ~ other_stuff + more_stuff) +} +\keyword{datasets} diff --git a/man/mk_calibrate.Rd b/man/mk_calibrate.Rd new file mode 100644 index 00000000..1b410072 --- /dev/null +++ b/man/mk_calibrate.Rd @@ -0,0 +1,95 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/calibration.R +\name{mk_calibrate} +\alias{mk_calibrate} +\title{Add calibration information to a simulator} +\usage{ +mk_calibrate( + sim, + params = list(), + transforms = list(), + data = NULL, + start_time = NULL, + end_time = NULL, + exprs = list(), + debug = FALSE, + clamp_vars = FALSE +) +} +\arguments{ +\item{sim}{A \code{macpan2} simulator (i.e., a \code{TMBSimulator} object).} + +\item{params}{a list of parameters with default/starting values.} + +\item{transforms}{TODO.} + +\item{data}{A data frame containing data to add (i.e., observed variables that will be compared with simulations). If the data frame contains a column called "time" or "date" (any capitalization), it will be used.} + +\item{start_time}{A time or date, overriding first time in data; set to 1 otherwise.} + +\item{end_time}{A time or date, overriding last time in data; set to number of time steps otherwise.} + +\item{exprs}{A list of expressions to add.} + +\item{debug}{(logical) Print debugging information?} + +\item{clamp_vars}{(logical) Force state variables to be positive in likelihood expression?} +} +\value{ +This function modifies the simulator object \strong{in place}. It also returns (invisibly) a character vector of the lower-level operations it performs. +} +\description{ +\subsection{To do/FIXME}{ +\itemize{ +\item see hacks for getting simulation variables, state variables +\item modularize? +\item switch for enabling a differenced/incidence class (add a flow/accumulator var to model; add a differencing step)? +\item allow setting clamp tolerance? allow specified list of variables to clamp rather than all or nothing +\item rename and move into macpan2 +\item document that 'log-likelihood' means -1*(loss function) (e.g. for SSQ, chi-squared fits) +} +} +} +\examples{ +## it's convenient to have a function that sets up a fresh simulation +## (since adding already-existing components to a simulation object throws an error) +library(dplyr) +setup_sim <- function() { + m <- Compartmental(system.file("starter_models", "sir", package = "macpan2")) + sim <- m$simulators$tmb( + time_steps = 100, + state = c(S = 99, I = 1, R = 0), + flow = c(foi = NA, gamma = 0.1), + beta = 0.2, + N = empty_matrix + ) +} +sim <- setup_sim() +if (require(outbreaks)) { + I_obs <- influenza_england_1978_school[["in_bed"]] +} else { + set.seed(101) + I_obs <- (sim$report(.phases = "during") + |> filter(row == "I") + |> mutate(obs = rnbinom(100, mu = value, size = 2)) + |> pull(obs) + ) +} +m1 <- mk_calibrate(sim, + data = data.frame(I_obs), + params = list(beta = 0.2, I_sd = 1), + transforms = list(beta = "log", I_sd = "log"), + exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), +) +cat(m1, sep = "\n") +sim$optimize$nlminb() +sim <- setup_sim() ## refresh +mk_calibrate(sim, + data = data.frame(I_obs), + params = list(beta = 0.2, gamma = 0.05), + transforms = list(beta = "log", gamma = "log"), + exprs = list(log_lik ~ dpois(I_obs, I)) +) +sim$optimize$nlminb() +## warning about NA/NaN function evaluation is probably harmless ... +} diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index f1432c9f..8f06fd58 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -85,7 +85,7 @@ print(gg0) Now we'll use the *experimental* `mk_calibrate()` function from `macpan2helpers` package ```{r mk_calibrate} -mk_calibrate(sir_simulator, +macpan2::mk_calibrate(sir_simulator, data = data.frame(I_obs = sir_prevalence$obs_val), params = list(trans_rates = c(beta = 1), I_sd = 1), transforms = list(trans_rates = c(beta = "log"), I_sd = "log"), From ae72191658a06798522c150e0e64c563d4c5dd57 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 8 Nov 2023 05:13:36 -0500 Subject: [PATCH 069/332] fix quickstart issue and provide rough explanation --- vignettes/quickstart.Rmd | 80 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index f0bd1b94..d6a270e7 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -15,6 +15,8 @@ editor_options: ```{r setup, echo = FALSE} library(macpan2) +library(ggplot2) +library(dplyr) ``` @@ -97,7 +99,7 @@ SIR_starter = function( during = list( ## force of infections flow_rates[infection_flow] ~ - state[infectious_state] * flow_rates[rate] / N + state[infectious_state] * flow_rates[trans_rate] / N ## unsigned individual flows , flows_per_time ~ state[from] * flow_rates[rate] @@ -126,8 +128,7 @@ SIR_starter = function( DynamicModel( expr_list = expr_list, link_data = link_data, - init_vecs = init_vecs, - unstruc_mats = list() + init_vecs = init_vecs ) } ``` @@ -188,10 +189,75 @@ recovery = mp_join( force_of_infection = mp_join( infection_flow = mp_subset(flow_rates, Epi = "lambda"), infectious_state = mp_subset(state, Epi = "I"), - rate = mp_subset(flow_rates, Epi = "beta") + trans_rate = mp_subset(flow_rates, Epi = "beta") ) ``` +Note that in the infection and recovery flow ledgers we used `rate` as the name of the third index, but in the force of infection we have used `trans_rate`. Each set of ledgers must use unique names because they are accessed globally during model simulation do extract elements from particular vectors, and if the same name is used for two vectors of different length the spec will not make sense. + +- TODO: Clean up this last paragraph +- TODO: Give a more informative error message when the same name is used !! + +Aside for Irena to explain what happened with naming. +```{r, eval=FALSE} +## copied from func above +ld = link_data = list( + flows = mp_link_data(infection, recovery), + fois = mp_link_data(force_of_infection) +) + +## get the position vectors that will be passed to c++ +## (but with one-based indexing for sanity in R +## -- c++ uses zero-based) +(flow_pos = ld$flows$positions_frame(zero_based = FALSE)) +(fois_pos = ld$fois$positions_frame(zero_based = FALSE)) + +## get the vectors from which we will extract +s = c(S = 999, I = 1, R = 0) +r = c(beta = 0.25, gamma = 0.1, lambda = NA) + +## we can reconstruct the edge matrix: +edge = data.frame( + from = s[flow_pos$from], + to = s[flow_pos$to], + rate = r[flow_pos$rate] +) +print(edge) + +## and the 'transmission-edge' matrix: +edge_trans = data.frame( + state = s[fois_pos$infectious_state], + flow = s[fois_pos$infection_flow], + trans = r[fois_pos$trans_rate] +) +print(edge_trans) + +## but if we tried to do this using `flow_pos$rate` instead +## of `fois_pos$trans_rate` we would get a warning +## (but a bad one!) because the vectors don't line up as +## they need to to form a data frame. +data.frame( + state = s[fois_pos$infectious_state], + flow = s[fois_pos$infection_flow], + trans = r[flow_pos$rate] +) +``` + +- On the R-side we have these lists and so we could clearly +distinguish between `flow_pos$rate` and `fois_pos$rate`, +but on the C-side we have to (for now at least!) names +and not names-in-lists. but more to the point, in the archetypes (awesome name) we have: +``` + ## force of infections + flow_rates[infection_flow] ~ + state[infectious_state] * flow_rates[trans_rate] / N + + ## unsigned individual flows + , flows_per_time ~ state[from] * flow_rates[rate] +``` +- This `trans_rate` and `rate`, as we saw, are just completely different integer vectors. If we gave them the same name, one would overwrite the other, and the specific formulas in the archetypes would just not line up correctly. + + Now we can use the `SIR_starter()` function to initialize our model object: ```{r} @@ -214,6 +280,12 @@ sir_simulator = mp_tmb_simulator(model ) sim_report = mp_report(sir_simulator) +(sim_report + |> filter(matrix == "state") + |> mutate(state = factor(row, levels = c("S", "I", "R"))) + |> ggplot(aes(time, value, colour = state)) + + geom_line() +) ``` # Main course: expanding the basic SIR into a metapopulation SIR From d970a4218a100a4bfa13f81a772fe4d79d7e0aed Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Wed, 8 Nov 2023 13:30:58 -0500 Subject: [PATCH 070/332] checkpoint quickstart draft for macpan tech --- vignettes/quickstart.Rmd | 387 +++++++++++++++++++++++++++------------ 1 file changed, 268 insertions(+), 119 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index d6a270e7..1847e01f 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -19,12 +19,13 @@ library(ggplot2) library(dplyr) ``` +[change "link_data" to "ledgers" in `mp_*()` API] # Motivation A common approach to modelling is to start with a simple model and then expand and/or stratify as further complexity is needed. In reality, this process can be full of friction as one implements and re-implements the model for numerical simulation. One of the goals of `macpan2` is to provide a grammar for model specification that streamlines model expansion. -# Amuse bouche: a (simple) metapopulation SIR model +# Amuse bouche: a structured SIR model {#amuse-bouche} - define model used in slides (ignoring contact matrices???) - point out that there is a lot of repetition @@ -32,17 +33,21 @@ A common approach to modelling is to start with a simple model and then expand a `macpan2`'s flexibility is derived from the ability to identify calculation archetypes that may or may not be repeated throughout the model. For instance, consider the following simple metapopulation SIR model, cast as a system of difference equations (as is often the case when numerically simulating ordinary differential equation models): -[insert diagram with three cities and the same set of SIR difference equations within. note that we've simplified the usual metapop model by not considering movement between cities... does that make it an overly simple stratified model?? ugh, i dont know. maybe we would be better off with a model that has two forces of infection and explain calculation archetypes with that ??] +[insert diagram of SIR with symptom strata, remark that we could be considering much more complicated structure] + +[define states and rates with this diagram] -[define states and rates with this diagram - $S$, $I$, and $R$ are the number of susceptible, infected, and recovered individuals, respectively, + - $N$ is the total population size ($S + I + R$), + - $\beta$ is the transmission rate, + - $\gamma$ is the recovery rate. -] -Each city has an infection flow, a recovery flow, and a force of infection that need to be computed for each time step in the numerical simulation of the model. But the **form** of each of these calculations is essentially the same! Instead of manually writing out each calculation, for instance as +Each infectious state is associated with a different force of infection (FOI) that need to be computed for each time step in the numerical simulation of the model. But the **form** of each FOI calculation is essentially the same! Instead of manually writing out each calculation, for instance as +[redo below] ``` S.calgary[t+1] = - lambda.calgary * S.calgary[t] S.hamilton[t+1] = - lambda.hamilton * S.hamilton[t] @@ -52,9 +57,11 @@ we will specify a single calculation archetype for it, for instance something li ``` S[t+1] = -lambda * S[t] ``` -and then attach information to the model object about which specific calculations to do under this archetype (one per city). +and then attach a "ledger" to the model object that tabulates which specific calculations to do under this archetype. In this case, we would have one infection calculation per city. -# Appetizer: specifying the basic SIR model {#SIR} +[not that impressive with this simple example with infection strata, but you can imagine if you have 10 age groups and you want to repeat the force of infection for each of them, you're actually calculating 100 forces of infection (one for each combination of age groups), this repetition gets pretty rote!] + +# Appetizer: specifying the basic SIR model Let's start with the basic epidemic model being repeated across cities and learn how to specify that in `macpan2`. We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models: @@ -62,7 +69,6 @@ Let's start with the basic epidemic model being repeated across cities and learn S_{t+1} &= -\beta S_t I_t/N, \\ I_{t+1} &= \beta S_t I_t/N - \gamma I_t, \\ R_{t+1} &= \gamma I_t. -\label{eqn:SIR-difference} \end{align} It will be helpful to set $\lambda = \beta I_t/N$ and recast the equations as: @@ -71,19 +77,18 @@ It will be helpful to set $\lambda = \beta I_t/N$ and recast the equations as: S_{t+1} &= -\lambda S_t, \\ I_{t+1} &= \lambda S_t - \gamma I_t, \\ R_{t+1} &= \gamma I_t. -\label{eqn:SIR-difference} \end{align} (In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.) -The focus of this quickstart guide is to help a user begin to understand how models are specified using `macpan2`. The following function has been created to sweep some of the details of initializing an SIR model object under the rug (for now). Please ignore this function's definition on your first read and just understand that it will output a model object from which we can then build a simulator. In order to do that, we have to create each of the inputs of this function, which is what the rest of this section will be about. +The focus of this quickstart guide is to help a user begin to understand how models are specified using `macpan2`. We have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model and it will output a model object from which we can build a simulator. Our focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. -```{r} -## links between entries in the indexes ----------- +```{r SIR-starter, echo = FALSE} +## helper function to simplify the exposition in this vigette ----------- SIR_starter = function( # indices for model quantities state, - flow_rates, + rate, # ledgers for calculation archetypes infection, recovery, @@ -98,30 +103,30 @@ SIR_starter = function( ), during = list( ## force of infections - flow_rates[infection_flow] ~ - state[infectious_state] * flow_rates[trans_rate] / N + rate[infection_flow_rates] ~ + state[infectious_states] * rate[transmission_rates] / N ## unsigned individual flows - , flows_per_time ~ state[from] * flow_rates[rate] + , flow_per_time ~ state[from_states] * rate[flow_rates] ## state update - , total_inflow ~ groupSums(flows_per_time, to, state) - , total_outflow ~ groupSums(flows_per_time, from, state) + , total_inflow ~ groupSums(flow_per_time, to_states, state) + , total_outflow ~ groupSums(flow_per_time, from_states, state) , state ~ state + total_inflow - total_outflow ) ) ## Ledgers for each specific calculation -------------- link_data = list( - flows = mp_link_data(infection, recovery), - fois = mp_link_data(force_of_infection) + flow = mp_link_data(infection, recovery), + force_of_infection = mp_link_data(force_of_infection) ) ## Initialize indexed vectors (to all zeros) -------------- # used as placeholders for user input init_vecs = list( state = mp_vector(state), - flow_rates = mp_vector(flow_rates) + rate = mp_vector(rate) ) ## Initialize model object ----------------- @@ -133,165 +138,309 @@ SIR_starter = function( } ``` -Now that we have our equations, we can start by naming the main quantities used in specifying and simulating the model. Quantities are named using "indices", which you can think of for now as labels for each quantity: +The inputs to `SIR_starter()` are of two types: +- **indices** (labels) for model quantities, +- **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included calculation archetypes). + +The **indices** we need to specify fall into two groups: +- `state`: state names, $S$, $I$, and $R$ from the model equations +- `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ + +We have identified three useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: + +- `infection`, the flow from susceptible classes to infectious classes at some rate +- `recovery`, the flow from infectious classes to recovered classes at some rate +- `force_of_infection`, the prevalence-dependent per capita rate of flow from susceptible classes to infectious classes, used in calculating the `infection` flow + +For each of these archetypes, we need to write down a ledger cataloging each specific calculation that uses each archetype. (The SIR model is so simple that there will only be one specific calculation per archetype, but we will see [later](#main-course) that these ledgers give us the flexibility to easily expand a model while avoiding cumbersome redundancy.) + +We start by creating the `state` and `rate` indices: ```{r} ## indices (labels) for model quantities ------------------------- - state = mp_index(Epi = c("S", "I", "R")) -flow_rates = mp_index(Epi = c("beta", "gamma", "lambda")) +rate = mp_index(Epi = c("beta", "gamma", "lambda")) ``` -You can think of the `mp_index()` fuction a setting up data frames with model quantity labels: +You can think of the `mp_index()` function a setting up data frames tabulating the model quantity labels: ```{r} state ``` ```{r} -flow_rates +rate ``` -(Ignore the column name `Epi` for now.) +The `Epi` column name is not really useful in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. -In this model, we have three main calculation archetypes. In this case, we're thinking of calculation archetypes not as repeated calculations, but as calculations that a modeller may want to repeat down the line, as they expand the model. The archetypes are: +For the ledgers, the `infection` and `recovery` ledgers are tied to the same calculation archetype, which requires one to specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). -1. infection: flows between susceptible and infected classes -2. recovery: flows between infected and recovered classes -3. force of infection: prevalence-dependent rates involved in calculating infection flows - -For each of these calculation archetypes, we need to write down a ledger cataloguing each specific calculation that uses each archetype. (Note that the SIR model is so simple that there will only be one infection flow calculation, one recovery flow calculation, and one force of infection calculation, but we will see later with a structured model that these ledgers give us the flexbility to easily expand a model without being redundant.) - -We start by creating the infection ledger: +We use the `mp_join()` function to create the `infection` ledger like so: ```{r} # infection ledger infection = mp_join( - from = mp_subset(state, Epi = "S"), - to = mp_subset(state, Epi = "I"), - rate = mp_subset(flow_rates, Epi = "lambda") + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") ) ``` -- explain what the `mp_join()` function is doing (at a high level)... mention that the argument names are flexible an depend on how the calculation archetypes are specified, but we should ignore this for now. +The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, namely + +```{r} +mp_subset(state, Epi = "S") +``` + +```{r} +mp_subset(state, Epi = "I") +``` + +and + +```{r} +mp_subset(rate, Epi = "lambda") +``` + +and by default creates one entry in the ledger for each combination of these values (_i.e._, "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger in this case! + +```{r} +infection +``` + +As a side note, the names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general they are completely up to the modeller and how they choose to specify their calculation archetypes.^[There is only one `mp_join()` argument name that is not available to the user, and that is `by`, which has a special role that we will see [later](#main-course).] -Then we create the remaining ledgers: +Since the `recovery` ledger uses the same calculation archetype as `infection`, we create it in much the same way: ```{r} # recovery recovery = mp_join( - from = mp_subset(state, Epi = "I"), - to = mp_subset(state, Epi = "R"), - rate = mp_subset(flow_rates, Epi = "gamma") + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") ) +recovery +``` + +Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different calculation archetype in `SIR_starter()` (so the `mp_join()` argument names are different): + +```{r} # infection additionally involves the calculation of a force of infection force_of_infection = mp_join( - infection_flow = mp_subset(flow_rates, Epi = "lambda"), - infectious_state = mp_subset(state, Epi = "I"), - trans_rate = mp_subset(flow_rates, Epi = "beta") + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta"), + infection_flow_rates = mp_subset(rate, Epi = "lambda") ) ``` -Note that in the infection and recovery flow ledgers we used `rate` as the name of the third index, but in the force of infection we have used `trans_rate`. Each set of ledgers must use unique names because they are accessed globally during model simulation do extract elements from particular vectors, and if the same name is used for two vectors of different length the spec will not make sense. +For this calculation archetype, we need to specify the `transmission_rates` and `infectious_states` involved in computing the force of infection, as well as the names where we want to store the results of this calculation (`infection_flow_rates`) for use in the `infection` flow calculations. -- TODO: Clean up this last paragraph -- TODO: Give a more informative error message when the same name is used !! +Now we can use the `SIR_starter()` function to initialize our model object: -Aside for Irena to explain what happened with naming. -```{r, eval=FALSE} -## copied from func above -ld = link_data = list( - flows = mp_link_data(infection, recovery), - fois = mp_link_data(force_of_infection) +```{r} +model = SIR_starter( + state, + rate, + infection, + recovery, + force_of_infection ) +``` -## get the position vectors that will be passed to c++ -## (but with one-based indexing for sanity in R -## -- c++ uses zero-based) -(flow_pos = ld$flows$positions_frame(zero_based = FALSE)) -(fois_pos = ld$fois$positions_frame(zero_based = FALSE)) - -## get the vectors from which we will extract -s = c(S = 999, I = 1, R = 0) -r = c(beta = 0.25, gamma = 0.1, lambda = NA) - -## we can reconstruct the edge matrix: -edge = data.frame( - from = s[flow_pos$from], - to = s[flow_pos$to], - rate = r[flow_pos$rate] +We can create a model simulator using `mp_tmb_simulator()`, provided we give it the model object (`model`), initial values for the states and rates (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): + +```{r} +sir_simulator = mp_tmb_simulator( + model, + vectors = list( + state = c(S = 999, I = 1, R = 0), + rate = c(beta = 0.25, gamma = 0.1, lambda = NA) + ), + time_steps = 100L ) -print(edge) +``` -## and the 'transmission-edge' matrix: -edge_trans = data.frame( - state = s[fois_pos$infectious_state], - flow = s[fois_pos$infection_flow], - trans = r[fois_pos$trans_rate] +Then we can actually simulate the model by passing our model simulator to `mp_report()`: + +```{r} +sir_results = mp_report(sir_simulator) +``` + +The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer) that can easily be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`: + +```{r} +(sir_results + |> filter(matrix == "state") # keep state variables at each point in time + |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in plot + |> ggplot(aes(time, value, colour = state)) + + geom_line() ) -print(edge_trans) - -## but if we tried to do this using `flow_pos$rate` instead -## of `fois_pos$trans_rate` we would get a warning -## (but a bad one!) because the vectors don't line up as -## they need to to form a data frame. -data.frame( - state = s[fois_pos$infectious_state], - flow = s[fois_pos$infection_flow], - trans = r[flow_pos$rate] +``` + +(Above, we used the [base R pipe operator](https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/#pipes), `|>`.) + +If you want to use base R for plotting, you can convert the long format data to wide format: + +```{r pivot_wider} +sir_results_wide <- (sir_results + ## drop 'matrix' (has only a single value) and 'col' (empty) + |> dplyr::select(-c(matrix, col)) + |> tidyr::pivot_wider(id_cols = time, names_from = row) ) + +head(sir_results_wide, n = 3) ``` -- On the R-side we have these lists and so we could clearly -distinguish between `flow_pos$rate` and `fois_pos$rate`, -but on the C-side we have to (for now at least!) names -and not names-in-lists. but more to the point, in the archetypes (awesome name) we have: +```{r base_plot_ex, fig.width = 6} +with(sir_results_wide, + plot(x = time, + y = I) +) ``` - ## force of infections - flow_rates[infection_flow] ~ - state[infectious_state] * flow_rates[trans_rate] / N - - ## unsigned individual flows - , flows_per_time ~ state[from] * flow_rates[rate] + +or + +```{r base_matplot_ex, fig.width = 6} +par(las = 1) ## horizontal y-axis ticks +matplot(sir_results_wide[, 1], + sir_results_wide[,-1], + type = "l", + log = "y", + xlab = "time", ylab = "") +# legend("bottom", col = 1:3, lty = 1:3, legend = model$labels$state()) ``` -- This `trans_rate` and `rate`, as we saw, are just completely different integer vectors. If we gave them the same name, one would overwrite the other, and the specific formulas in the archetypes would just not line up correctly. +# Main course: expanding the basic SIR into a metapopulation SIR {#main-course} -Now we can use the `SIR_starter()` function to initialize our model object: +- here is where we see the power of macpan +- re-write the equations with stratified symptom statuses and point out the repetition of the FOI term form. We already have a calculation archetype for this baked into `SIR_starter()`! Let's leverage that. + +- expand the states and rates to include infecteds stratified by symptom status ```{r} -model = SIR_starter( - state, - flow_rates, - infection, - recovery, - force_of_infection +# new indices for infection type +# (asymptomatic and symptomatic) +InfType_indices = c("asymp", "symp") +``` + +```{r} +state = mp_union( + # stratify I by infection type + # asymptomatic and symptomatic + mp_cartesian( + mp_subset(state, Epi = "I"), + mp_index(InfType = InfType_indices) + ), + # keep S and R as before + mp_subset(state, Epi = c("S", "R")) ) ``` +explain `mp_cartesian()`, mention other types of products? + ```{r} -sir_simulator = mp_tmb_simulator(model - , vectors = list( - state = c(S = 999, I = 1, R = 0), - flow_rates = c(beta = 0.25, gamma = 0.1, lambda = NA) - ) - , time_steps = 100L +rate = + mp_union( + # stratify rates involved in the infection + # process by infection type + mp_cartesian( + mp_subset(rate, Epi = c("beta", "lambda")), + mp_index(InfType = InfType_indices) + ), + # recovery rate will be the same across symptom types + mp_subset(rate, Epi = "gamma") ) +``` -sim_report = mp_report(sir_simulator) -(sim_report - |> filter(matrix == "state") - |> mutate(state = factor(row, levels = c("S", "I", "R"))) - |> ggplot(aes(time, value, colour = state)) - + geom_line() +- maybe overkill to do it this way, easy to write out states and rates here by hand, but you can imagine if you have many more states or many more rates or many more desired strata that this way is very powerful. + +- redo ledgers with stratification +- start with infection join +- if we just do the join as before... + +```{r} +# infection ledger from before +mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") ) ``` -# Main course: expanding the basic SIR into a metapopulation SIR +We get infection flows with all possible combinations between the `from_states`, the `to_states`, and the `flow_rates` (the full join). However, for this model, we only want two of these flows: -- here is where we see the power of macpan +- a flow between `S` and `I.asymp` with flow rate `lambda.asymp` +- a flow between `S` and `I.symp` with flow rate `lambda.symp` + +In other words, we want the `InfType` index on `I` to match with the `InfType` index on `lambda`. We can specify this within `mp_join()` when building the ledger like so: + +```{r} +# new infection ledger +infection = mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda"), + by = list( + to_states.flow_rates = "InfType" + ) +) -# Desert: understanding model simulation in `macpan2` +infection +``` + +- explain what the `mp_join()` function is doing (at a high level)... mention that the argument names are flexible an depend on how the calculation archetypes are specified, but we should ignore this for now. +- explain `by` syntax and how the dot concatenation needs to be pairwise + +Then we create the recovery ledger: + +```{r} +recovery = + mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) +recovery +``` + +This is exactly the code as before for the recovery ledger, and it's what we want (same recovery rate for both infectious classes). + +Finally, for the force of infection ledger, the previous version yields a full join again: + +```{r} +mp_join( + infection_flow_rates = mp_subset(rate, Epi = "lambda"), + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta") +) +``` + +For this model, we want the infectious states and the transmission rates to be matched up on the `InfType` index, but we want each `I` and `beta` pair to be used in all `lambda` expressions, so we do: + +```{r} +force_of_infection = mp_join( + infection_flow_rates = mp_subset(rate, Epi = "lambda"), + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta"), + by = list( + infectious_states.transmission_rates = "InfType" + ) +) +force_of_infection +``` + +# Dessert: understanding model simulation in `macpan2` {#desert} - follow up vignette, tentative title: "Quickstart part 2: understanding model simulation in `macpan2`" +- set of ledgers are associated with a set of archetypes, column names need to match across these paired sets of ledgers and archetypes, but between the paired sets, columns names to be unique + +explain that we will explain: + +```{r} +<> +``` + + + From c8fc3af1aa57aa91939b8ca8f31e3109214ca5f8 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Wed, 8 Nov 2023 13:33:06 -0500 Subject: [PATCH 071/332] upgrade to working draft --- vignettes/quickstart.Rmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 1847e01f..f16c6bf8 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -11,9 +11,9 @@ editor_options: chunk_output_type: console --- -[![status](https://img.shields.io/badge/status-stub-red)](https://canmod.github.io/macpan2/articles/vignette-status#stub) +[![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) -```{r setup, echo = FALSE} +```{r setup, echo = FALSE, message = FALSE} library(macpan2) library(ggplot2) library(dplyr) From d39e2c4562eadcf12c34777f84de99fb64f893c8 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Wed, 8 Nov 2023 18:10:30 -0500 Subject: [PATCH 072/332] checkpoint progress getting somewhere! - motivation, amuse bouche, and appetizer are pretty well fleshed out - need to update model def in main course and then flesh out the text a bit more - dessert needs a bit of text --- vignettes/quickstart.Rmd | 88 ++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index f16c6bf8..786377d9 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -19,51 +19,74 @@ library(ggplot2) library(dplyr) ``` -[change "link_data" to "ledgers" in `mp_*()` API] +[ + +- change "link_data" to "ledgers" in `mp_*()` API +- consolidate spec in examples with steve's presentation + +] # Motivation -A common approach to modelling is to start with a simple model and then expand and/or stratify as further complexity is needed. In reality, this process can be full of friction as one implements and re-implements the model for numerical simulation. One of the goals of `macpan2` is to provide a grammar for model specification that streamlines model expansion. +One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when build upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. + +There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, such as models including: + +- age-structure +- multiple locations (a metapopulation model) +- strain replacement +- testing processes to identify infections +- vaccination status +- infection types (asymptomatic, symptomatic) + +[add links to papers with these?] + +This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. The package also comes with a model library (see `vignette("example_models")`) to get users started with commonly used basic models, and to demonstrate some more complex cases. # Amuse bouche: a structured SIR model {#amuse-bouche} -- define model used in slides (ignoring contact matrices???) -- point out that there is a lot of repetition -- introduce the idea of calculation archetypes (formulas? patterns?) that get repeated +[i'm now worried that this model is _too_ simple (there are actually only two component FOIs because we're not crossing with different types of susceptibles, like when we have age structure or a metapopulation...) perhaps a two-age-group model would be better? but then you have to deal with subpopulation sizes and contact matrix terms, so more names...? i don't know, maybe this is OK. let's see how an updated [main course](#main-course) section looks] -`macpan2`'s flexibility is derived from the ability to identify calculation archetypes that may or may not be repeated throughout the model. For instance, consider the following simple metapopulation SIR model, cast as a system of difference equations (as is often the case when numerically simulating ordinary differential equation models): +A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculation across model structures. For instance, consider an SIR model that has two infection states: -[insert diagram of SIR with symptom strata, remark that we could be considering much more complicated structure] +[insert diagram of SIR with symptom strata] -[define states and rates with this diagram] +Here, -- $S$, $I$, and $R$ are the number of susceptible, infected, and recovered individuals, respectively, +- $S$, $I_a$, $I_s$, and $R$ are the number of susceptible, asymptomatic infectious, symptomatic infectious, and recovered individuals, respectively, +- $\beta_a$ and $\beta_s$ are the transmission rates for asymptomatic and symptomatic individuals, respectively, reflecting that symptom status can affect the infectiousness of individuals, +- $\gamma$ is the recovery rate for all infecteds. -- $N$ is the total population size ($S + I + R$), +We can cast this model as a system of difference equations: -- $\beta$ is the transmission rate, +\begin{align} +S_{t+1} &= - [\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t, \\ +(I_a)_{t+1} &= (1-\sigma)[\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t - \gamma (I_a)_t, \\ +(I_s)_{t+1} &= \sigma[\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t - \gamma (I_s)_t, \\ +R_{t+1} &= \gamma (I_a)_t + \gamma (I_s)_t. +\end{align} -- $\gamma$ is the recovery rate. +where $\sigma$ is severity (proportion of symptomatic infections) and $N$ is the total population size ($S + I_a + I_s + R$). We notice that each force of infection, $\lambda_a = \beta_a (I_a)/N$ and $\lambda_s = \beta_s (I_s)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. -Each infectious state is associated with a different force of infection (FOI) that need to be computed for each time step in the numerical simulation of the model. But the **form** of each FOI calculation is essentially the same! Instead of manually writing out each calculation, for instance as +When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: -[redo below] ``` -S.calgary[t+1] = - lambda.calgary * S.calgary[t] -S.hamilton[t+1] = - lambda.hamilton * S.hamilton[t] -S.quebec[t+1] = - lambda.quebec * S.quebec[t] +lambda.a = beta.a I.a/N +lambda.s = beta.s I.s/N ``` -we will specify a single calculation archetype for it, for instance something like, + +However, in `macpan2`, we can specify a single calculation archetype for it, for instance something like, ``` -S[t+1] = -lambda * S[t] +lambda = beta * I / N ``` -and then attach a "ledger" to the model object that tabulates which specific calculations to do under this archetype. In this case, we would have one infection calculation per city. -[not that impressive with this simple example with infection strata, but you can imagine if you have 10 age groups and you want to repeat the force of infection for each of them, you're actually calculating 100 forces of infection (one for each combination of age groups), this repetition gets pretty rote!] +and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. Here, there would only be two calculations in the force of infection ledger (one calculation per infectious class), but one can easily imagine a more complicated case. For instance, we could consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms with the exact same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the 11 patches). + +By using calculation **archetypes** and **ledgers**, the modeller can focus on the modelling (deciding on an expression for all forces of infection in the model), while `macpan2` will handle the bookkeeping (matching stratified variables with each other each time the expression must be calculated). This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger. # Appetizer: specifying the basic SIR model -Let's start with the basic epidemic model being repeated across cities and learn how to specify that in `macpan2`. We will start with the SIR model, which is the simplest epidemic model that is routinely used as a basis for much more complicated epidemic models: +Let's start with specifying the basic SIR model in `macpan2`, which is the foundation of the infection-stratified model above: \begin{align} S_{t+1} &= -\beta S_t I_t/N, \\ @@ -71,7 +94,7 @@ I_{t+1} &= \beta S_t I_t/N - \gamma I_t, \\ R_{t+1} &= \gamma I_t. \end{align} -It will be helpful to set $\lambda = \beta I_t/N$ and recast the equations as: +It will be helpful to set $\lambda = \beta I/N$ and recast the equations as: \begin{align} S_{t+1} &= -\lambda S_t, \\ @@ -79,9 +102,7 @@ I_{t+1} &= \lambda S_t - \gamma I_t, \\ R_{t+1} &= \gamma I_t. \end{align} -(In epidemic modelling, $\lambda$ is chosen suggestively as it models the [force of infection](https://en.wikipedia.org/wiki/Force_of_infection) denoted by the same character.) - -The focus of this quickstart guide is to help a user begin to understand how models are specified using `macpan2`. We have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model and it will output a model object from which we can build a simulator. Our focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. +Since the focus of this quickstart guide is `macpan2`'s model specification grammar, we have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model using the model grammar and it will output a model object from which we can build a simulator. Our focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. ```{r SIR-starter, echo = FALSE} ## helper function to simplify the exposition in this vigette ----------- @@ -139,10 +160,12 @@ SIR_starter = function( ``` The inputs to `SIR_starter()` are of two types: + - **indices** (labels) for model quantities, - **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included calculation archetypes). The **indices** we need to specify fall into two groups: + - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ @@ -152,6 +175,8 @@ We have identified three useful calculation archetypes that we have baked into ` - `recovery`, the flow from infectious classes to recovered classes at some rate - `force_of_infection`, the prevalence-dependent per capita rate of flow from susceptible classes to infectious classes, used in calculating the `infection` flow +[these's some repeition below that we can cut down on given the latest version of the [amuse bouche](#amuse-bouche)] + For each of these archetypes, we need to write down a ledger cataloging each specific calculation that uses each archetype. (The SIR model is so simple that there will only be one specific calculation per archetype, but we will see [later](#main-course) that these ledgers give us the flexibility to easily expand a model while avoiding cumbersome redundancy.) We start by creating the `state` and `rate` indices: @@ -203,7 +228,7 @@ and mp_subset(rate, Epi = "lambda") ``` -and by default creates one entry in the ledger for each combination of these values (_i.e._, "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger in this case! +and by default creates one entry in the ledger for each combination of these values (_i.e._, a "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger in this case! ```{r} infection @@ -285,6 +310,7 @@ If you want to use base R for plotting, you can convert the long format data to ```{r pivot_wider} sir_results_wide <- (sir_results + |> dplyr::filter(matrix == "state") # keep state variables at each point in time ## drop 'matrix' (has only a single value) and 'col' (empty) |> dplyr::select(-c(matrix, col)) |> tidyr::pivot_wider(id_cols = time, names_from = row) @@ -307,16 +333,17 @@ par(las = 1) ## horizontal y-axis ticks matplot(sir_results_wide[, 1], sir_results_wide[,-1], type = "l", - log = "y", xlab = "time", ylab = "") -# legend("bottom", col = 1:3, lty = 1:3, legend = model$labels$state()) +legend("bottom", col = 1:3, lty = 1:3, legend = state$labels()) ``` -# Main course: expanding the basic SIR into a metapopulation SIR {#main-course} +# Main course: expanding the basic SIR into an SIR with infection types {#main-course} - here is where we see the power of macpan - re-write the equations with stratified symptom statuses and point out the repetition of the FOI term form. We already have a calculation archetype for this baked into `SIR_starter()`! Let's leverage that. +[TODO: update model defintion to what's currently in [amuse bouche](#amuse-bouche)] + - expand the states and rates to include infecteds stratified by symptom status ```{r} @@ -425,6 +452,7 @@ force_of_infection = mp_join( infectious_states = mp_subset(state, Epi = "I"), transmission_rates = mp_subset(rate, Epi = "beta"), by = list( + infection_flow_rates.infectious_states = "InfType", infectious_states.transmission_rates = "InfType" ) ) From e9156a6063530575232998819e178c9b9bea8989 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 11:03:41 -0500 Subject: [PATCH 073/332] small changes --- vignettes/quickstart.Rmd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 786377d9..c6e5476d 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -60,8 +60,8 @@ Here, We can cast this model as a system of difference equations: \begin{align} -S_{t+1} &= - [\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t, \\ -(I_a)_{t+1} &= (1-\sigma)[\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t - \gamma (I_a)_t, \\ +S_{t+1} &= - [\beta_a (I_a)_t/N + \beta_s (I_s)_t/N] S_t, \\ +(I_a)_{t+1} &= (1-\sigma)[\beta_a (I_a)_t/N + \beta_s (I_s)_t/N] S_t - \gamma (I_a)_t, \\ (I_s)_{t+1} &= \sigma[\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t - \gamma (I_s)_t, \\ R_{t+1} &= \gamma (I_a)_t + \gamma (I_s)_t. \end{align} @@ -80,9 +80,9 @@ However, in `macpan2`, we can specify a single calculation archetype for it, for lambda = beta * I / N ``` -and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. Here, there would only be two calculations in the force of infection ledger (one calculation per infectious class), but one can easily imagine a more complicated case. For instance, we could consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms with the exact same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the 11 patches). +and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. Here, there would only be two calculations in the force of infection ledger (one calculation per infectious class), but one can easily imagine a more complicated case. For instance, we could consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms with the exact same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 11 patches). -By using calculation **archetypes** and **ledgers**, the modeller can focus on the modelling (deciding on an expression for all forces of infection in the model), while `macpan2` will handle the bookkeeping (matching stratified variables with each other each time the expression must be calculated). This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger. +By using calculation **archetypes** and **ledgers**, the modeller can focus on the modelling (designing model structure and choosing on an expression for the forces of infection), while `macpan2` will handle the bookkeeping (matching stratified variables with each other each time the expression must be calculated). This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. # Appetizer: specifying the basic SIR model From b068a302671b26230c46265daee708a46df118de Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 16:51:27 -0500 Subject: [PATCH 074/332] `mp_union` doesnt work on Link class tried exporting existing `mp_union.Link()` method and re-installing but that didn't work @stevencarlislewalker, is this a quick fix? if not, don't worry, i can keep the infection and recovery ledgers separate in the new quickstart --- R/mp.R | 1 + tests/mp_union-reprex.R | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/mp_union-reprex.R diff --git a/R/mp.R b/R/mp.R index e84871bb..44ddb6d9 100644 --- a/R/mp.R +++ b/R/mp.R @@ -196,6 +196,7 @@ mp_union.Index = function(...) { } ## not used anymore? +#' @export mp_union.Link = function(...) { l = list(...) column_map = lapply(l, getElement, "column_map") |> unique() diff --git a/tests/mp_union-reprex.R b/tests/mp_union-reprex.R new file mode 100644 index 00000000..f50d9295 --- /dev/null +++ b/tests/mp_union-reprex.R @@ -0,0 +1,21 @@ +library(macpan2) + +state = mp_index(Epi = c("S", "I", "R")) +rate = mp_index(Epi = c("beta", "gamma", "lambda")) + +# infection ledger +infection = mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") +) + +# recovery ledger +recovery = mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) + +# unite into a single flows ledger +mp_union(infection, recovery) From a204ba61cc32757f47252ac34c68d47870ca5775 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 16:52:42 -0500 Subject: [PATCH 075/332] switch to two strain model in amuse bouche --- vignettes/quickstart.Rmd | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c6e5476d..06230828 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -45,34 +45,34 @@ This vignette seeks to explain `macpan2`'s model specification grammar and in pa # Amuse bouche: a structured SIR model {#amuse-bouche} -[i'm now worried that this model is _too_ simple (there are actually only two component FOIs because we're not crossing with different types of susceptibles, like when we have age structure or a metapopulation...) perhaps a two-age-group model would be better? but then you have to deal with subpopulation sizes and contact matrix terms, so more names...? i don't know, maybe this is OK. let's see how an updated [main course](#main-course) section looks] +[update this spec to the two-strain model] -A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculation across model structures. For instance, consider an SIR model that has two infection states: +A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculation across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): -[insert diagram of SIR with symptom strata] +[insert diagram of SIR with two strains] Here, -- $S$, $I_a$, $I_s$, and $R$ are the number of susceptible, asymptomatic infectious, symptomatic infectious, and recovered individuals, respectively, -- $\beta_a$ and $\beta_s$ are the transmission rates for asymptomatic and symptomatic individuals, respectively, reflecting that symptom status can affect the infectiousness of individuals, +- $S$, $I_A$, $I_B$, and $R$ are the number of individuals that are susceptible, infectious with strain A, infectious with strain B, and recovered, respectively, +- $\beta_A$ and $\beta_B$ are the transmission rates for strains A and B, respectively, - $\gamma$ is the recovery rate for all infecteds. We can cast this model as a system of difference equations: \begin{align} -S_{t+1} &= - [\beta_a (I_a)_t/N + \beta_s (I_s)_t/N] S_t, \\ -(I_a)_{t+1} &= (1-\sigma)[\beta_a (I_a)_t/N + \beta_s (I_s)_t/N] S_t - \gamma (I_a)_t, \\ -(I_s)_{t+1} &= \sigma[\beta_a (I_a)_t/N - \beta_s (I_s)_t/N] S_t - \gamma (I_s)_t, \\ -R_{t+1} &= \gamma (I_a)_t + \gamma (I_s)_t. +S_{t+1} &= - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ +(I_A)_{t+1} &= \beta_A (I_A)_t/N - \gamma (I_A)_t, \\ +(I_B)_{t+1} &= \beta_B (I_B)_t/N - \gamma (I_B)_t, \\ +R_{t+1} &= \gamma (I_A)_t + \gamma (I_B)_t. \end{align} -where $\sigma$ is severity (proportion of symptomatic infections) and $N$ is the total population size ($S + I_a + I_s + R$). We notice that each force of infection, $\lambda_a = \beta_a (I_a)/N$ and $\lambda_s = \beta_s (I_s)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. +where $N$ is the total population size ($S + I_A + I_B + R$). We notice that each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: ``` -lambda.a = beta.a I.a/N -lambda.s = beta.s I.s/N +lambda.A = beta.A I.A/N +lambda.B = beta.B I.B/N ``` However, in `macpan2`, we can specify a single calculation archetype for it, for instance something like, @@ -80,13 +80,15 @@ However, in `macpan2`, we can specify a single calculation archetype for it, for lambda = beta * I / N ``` -and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. Here, there would only be two calculations in the force of infection ledger (one calculation per infectious class), but one can easily imagine a more complicated case. For instance, we could consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms with the exact same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 11 patches). +and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. -By using calculation **archetypes** and **ledgers**, the modeller can focus on the modelling (designing model structure and choosing on an expression for the forces of infection), while `macpan2` will handle the bookkeeping (matching stratified variables with each other each time the expression must be calculated). This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. +Here, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 11 patches). + +By using calculation **archetypes** and **ledgers**, the modeller can focus on the modelling (designing model structure, choosing an expression for the forces of infection), while `macpan2` will handle the bookkeeping (matching stratified variables with each other each time the expression must be calculated). This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. # Appetizer: specifying the basic SIR model -Let's start with specifying the basic SIR model in `macpan2`, which is the foundation of the infection-stratified model above: +Let's start with specifying the basic SIR model in `macpan2`, which is the foundation of the two-strain model above: \begin{align} S_{t+1} &= -\beta S_t I_t/N, \\ From 14895e64c18d7c071b4a1e3930c4744e8bace06b Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 16:53:58 -0500 Subject: [PATCH 076/332] attempt at using single `flow` ledger in appetizer (bugged, see b068a30) --- vignettes/quickstart.Rmd | 80 +++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 06230828..59df4fdf 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -113,8 +113,7 @@ SIR_starter = function( state, rate, # ledgers for calculation archetypes - infection, - recovery, + flow, force_of_infection ){ @@ -141,7 +140,7 @@ SIR_starter = function( ## Ledgers for each specific calculation -------------- link_data = list( - flow = mp_link_data(infection, recovery), + flow = mp_link_data(flow), force_of_infection = mp_link_data(force_of_infection) ) @@ -171,15 +170,13 @@ The **indices** we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ -We have identified three useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: +There are two useful calculation archetypes that we have baked into `SIR_starter()`. The archetypes are: -- `infection`, the flow from susceptible classes to infectious classes at some rate -- `recovery`, the flow from infectious classes to recovered classes at some rate -- `force_of_infection`, the prevalence-dependent per capita rate of flow from susceptible classes to infectious classes, used in calculating the `infection` flow +- `flow`: Flows from one class to another, where the flow takes the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. -[these's some repeition below that we can cut down on given the latest version of the [amuse bouche](#amuse-bouche)] +- `force_of_infection`: The prevalence-dependent per capita rate of flow from susceptible classes to infectious classes with form $\beta I / N$, used in calculating the $\lambda$ `flow` rates. -For each of these archetypes, we need to write down a ledger cataloging each specific calculation that uses each archetype. (The SIR model is so simple that there will only be one specific calculation per archetype, but we will see [later](#main-course) that these ledgers give us the flexibility to easily expand a model while avoiding cumbersome redundancy.) +In this case, the `flow` archetype is repeated within the model equations but the `force_of_infection` archetype is only used once. However, we've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). In either case, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation **ledger** for each of these archetypes. We start by creating the `state` and `rate` indices: @@ -201,9 +198,9 @@ rate The `Epi` column name is not really useful in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. -For the ledgers, the `infection` and `recovery` ledgers are tied to the same calculation archetype, which requires one to specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). +The `flow` ledger requires one to specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). -We use the `mp_join()` function to create the `infection` ledger like so: +We use the `mp_join()` function to create the `flow` ledger by first specifying infection and recovery flows separately: ```{r} # infection ledger @@ -212,9 +209,16 @@ infection = mp_join( to_states = mp_subset(state, Epi = "I"), flow_rates = mp_subset(rate, Epi = "lambda") ) + +# recovery ledger +recovery = mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) ``` -The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, namely +The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, like ```{r} mp_subset(state, Epi = "S") @@ -238,17 +242,10 @@ infection As a side note, the names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general they are completely up to the modeller and how they choose to specify their calculation archetypes.^[There is only one `mp_join()` argument name that is not available to the user, and that is `by`, which has a special role that we will see [later](#main-course).] -Since the `recovery` ledger uses the same calculation archetype as `infection`, we create it in much the same way: +We combine these two ledgers into one `flow` ledger with `mp_union()`: ```{r} -# recovery -recovery = mp_join( - from_states = mp_subset(state, Epi = "I"), - to_states = mp_subset(state, Epi = "R"), - flow_rates = mp_subset(rate, Epi = "gamma") -) - -recovery +flow = mp_union(infection, recovery) ``` Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different calculation archetype in `SIR_starter()` (so the `mp_join()` argument names are different): @@ -270,8 +267,7 @@ Now we can use the `SIR_starter()` function to initialize our model object: model = SIR_starter( state, rate, - infection, - recovery, + flow, force_of_infection ) ``` @@ -339,28 +335,20 @@ matplot(sir_results_wide[, 1], legend("bottom", col = 1:3, lty = 1:3, legend = state$labels()) ``` -# Main course: expanding the basic SIR into an SIR with infection types {#main-course} - -- here is where we see the power of macpan -- re-write the equations with stratified symptom statuses and point out the repetition of the FOI term form. We already have a calculation archetype for this baked into `SIR_starter()`! Let's leverage that. - -[TODO: update model defintion to what's currently in [amuse bouche](#amuse-bouche)] - -- expand the states and rates to include infecteds stratified by symptom status +# Main course: expanding the basic SIR into an SIR with two pathogen strains {#main-course} ```{r} # new indices for infection type # (asymptomatic and symptomatic) -InfType_indices = c("asymp", "symp") +Strain_indices = c("A", "B") ``` ```{r} state = mp_union( - # stratify I by infection type - # asymptomatic and symptomatic + # stratify I by strain mp_cartesian( mp_subset(state, Epi = "I"), - mp_index(InfType = InfType_indices) + mp_index(Strain = Strain_indices) ), # keep S and R as before mp_subset(state, Epi = c("S", "R")) @@ -372,11 +360,10 @@ explain `mp_cartesian()`, mention other types of products? ```{r} rate = mp_union( - # stratify rates involved in the infection - # process by infection type + # stratify rates involved in the infection process by strain mp_cartesian( mp_subset(rate, Epi = c("beta", "lambda")), - mp_index(InfType = InfType_indices) + mp_index(Strain = InfType_indices) ), # recovery rate will be the same across symptom types mp_subset(rate, Epi = "gamma") @@ -461,6 +448,23 @@ force_of_infection = mp_join( force_of_infection ``` +## Prototyping... + +```{r} +trans_rates_sir = mp_index(Epi = "beta") +infection_type = mp_index(InfType = InfType_indices) +infection_type +trans_rates = mp_cartesian( +trans_rates_sir, +mp_cartesian( +mp_rename(infection_type, InfTypeInfectious = "InfType"), +mp_rename(infection_type, InfTypeSusceptible = "InfType") +) +) +trans_rates +``` + + # Dessert: understanding model simulation in `macpan2` {#desert} - follow up vignette, tentative title: "Quickstart part 2: understanding model simulation in `macpan2`" From 165ba6c580c704c989c8ce88d501c4e202f2098e Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 16:54:19 -0500 Subject: [PATCH 077/332] Revert "attempt at using single `flow` ledger in appetizer (bugged, see b068a30)" This reverts commit 14895e64c18d7c071b4a1e3930c4744e8bace06b. --- vignettes/quickstart.Rmd | 80 +++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 59df4fdf..06230828 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -113,7 +113,8 @@ SIR_starter = function( state, rate, # ledgers for calculation archetypes - flow, + infection, + recovery, force_of_infection ){ @@ -140,7 +141,7 @@ SIR_starter = function( ## Ledgers for each specific calculation -------------- link_data = list( - flow = mp_link_data(flow), + flow = mp_link_data(infection, recovery), force_of_infection = mp_link_data(force_of_infection) ) @@ -170,13 +171,15 @@ The **indices** we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ -There are two useful calculation archetypes that we have baked into `SIR_starter()`. The archetypes are: +We have identified three useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: -- `flow`: Flows from one class to another, where the flow takes the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. +- `infection`, the flow from susceptible classes to infectious classes at some rate +- `recovery`, the flow from infectious classes to recovered classes at some rate +- `force_of_infection`, the prevalence-dependent per capita rate of flow from susceptible classes to infectious classes, used in calculating the `infection` flow -- `force_of_infection`: The prevalence-dependent per capita rate of flow from susceptible classes to infectious classes with form $\beta I / N$, used in calculating the $\lambda$ `flow` rates. +[these's some repeition below that we can cut down on given the latest version of the [amuse bouche](#amuse-bouche)] -In this case, the `flow` archetype is repeated within the model equations but the `force_of_infection` archetype is only used once. However, we've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). In either case, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation **ledger** for each of these archetypes. +For each of these archetypes, we need to write down a ledger cataloging each specific calculation that uses each archetype. (The SIR model is so simple that there will only be one specific calculation per archetype, but we will see [later](#main-course) that these ledgers give us the flexibility to easily expand a model while avoiding cumbersome redundancy.) We start by creating the `state` and `rate` indices: @@ -198,9 +201,9 @@ rate The `Epi` column name is not really useful in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. -The `flow` ledger requires one to specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). +For the ledgers, the `infection` and `recovery` ledgers are tied to the same calculation archetype, which requires one to specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). -We use the `mp_join()` function to create the `flow` ledger by first specifying infection and recovery flows separately: +We use the `mp_join()` function to create the `infection` ledger like so: ```{r} # infection ledger @@ -209,16 +212,9 @@ infection = mp_join( to_states = mp_subset(state, Epi = "I"), flow_rates = mp_subset(rate, Epi = "lambda") ) - -# recovery ledger -recovery = mp_join( - from_states = mp_subset(state, Epi = "I"), - to_states = mp_subset(state, Epi = "R"), - flow_rates = mp_subset(rate, Epi = "gamma") -) ``` -The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, like +The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, namely ```{r} mp_subset(state, Epi = "S") @@ -242,10 +238,17 @@ infection As a side note, the names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general they are completely up to the modeller and how they choose to specify their calculation archetypes.^[There is only one `mp_join()` argument name that is not available to the user, and that is `by`, which has a special role that we will see [later](#main-course).] -We combine these two ledgers into one `flow` ledger with `mp_union()`: +Since the `recovery` ledger uses the same calculation archetype as `infection`, we create it in much the same way: ```{r} -flow = mp_union(infection, recovery) +# recovery +recovery = mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) + +recovery ``` Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different calculation archetype in `SIR_starter()` (so the `mp_join()` argument names are different): @@ -267,7 +270,8 @@ Now we can use the `SIR_starter()` function to initialize our model object: model = SIR_starter( state, rate, - flow, + infection, + recovery, force_of_infection ) ``` @@ -335,20 +339,28 @@ matplot(sir_results_wide[, 1], legend("bottom", col = 1:3, lty = 1:3, legend = state$labels()) ``` -# Main course: expanding the basic SIR into an SIR with two pathogen strains {#main-course} +# Main course: expanding the basic SIR into an SIR with infection types {#main-course} + +- here is where we see the power of macpan +- re-write the equations with stratified symptom statuses and point out the repetition of the FOI term form. We already have a calculation archetype for this baked into `SIR_starter()`! Let's leverage that. + +[TODO: update model defintion to what's currently in [amuse bouche](#amuse-bouche)] + +- expand the states and rates to include infecteds stratified by symptom status ```{r} # new indices for infection type # (asymptomatic and symptomatic) -Strain_indices = c("A", "B") +InfType_indices = c("asymp", "symp") ``` ```{r} state = mp_union( - # stratify I by strain + # stratify I by infection type + # asymptomatic and symptomatic mp_cartesian( mp_subset(state, Epi = "I"), - mp_index(Strain = Strain_indices) + mp_index(InfType = InfType_indices) ), # keep S and R as before mp_subset(state, Epi = c("S", "R")) @@ -360,10 +372,11 @@ explain `mp_cartesian()`, mention other types of products? ```{r} rate = mp_union( - # stratify rates involved in the infection process by strain + # stratify rates involved in the infection + # process by infection type mp_cartesian( mp_subset(rate, Epi = c("beta", "lambda")), - mp_index(Strain = InfType_indices) + mp_index(InfType = InfType_indices) ), # recovery rate will be the same across symptom types mp_subset(rate, Epi = "gamma") @@ -448,23 +461,6 @@ force_of_infection = mp_join( force_of_infection ``` -## Prototyping... - -```{r} -trans_rates_sir = mp_index(Epi = "beta") -infection_type = mp_index(InfType = InfType_indices) -infection_type -trans_rates = mp_cartesian( -trans_rates_sir, -mp_cartesian( -mp_rename(infection_type, InfTypeInfectious = "InfType"), -mp_rename(infection_type, InfTypeSusceptible = "InfType") -) -) -trans_rates -``` - - # Dessert: understanding model simulation in `macpan2` {#desert} - follow up vignette, tentative title: "Quickstart part 2: understanding model simulation in `macpan2`" From 84c3d3b0543ee08dd9f0a0ca948b9dbeab49f5fe Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 17:05:51 -0500 Subject: [PATCH 078/332] workaround in lieu of `flow = mp_union(infection, recovery)` --- vignettes/quickstart.Rmd | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 06230828..7806bbf3 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -113,8 +113,7 @@ SIR_starter = function( state, rate, # ledgers for calculation archetypes - infection, - recovery, + flow, # list of individual ledgers force_of_infection ){ @@ -141,7 +140,7 @@ SIR_starter = function( ## Ledgers for each specific calculation -------------- link_data = list( - flow = mp_link_data(infection, recovery), + flow = mp_link_data(flow[[1]], flow[[2]]), force_of_infection = mp_link_data(force_of_infection) ) @@ -171,15 +170,12 @@ The **indices** we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ -We have identified three useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: +We have identified two useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: -- `infection`, the flow from susceptible classes to infectious classes at some rate -- `recovery`, the flow from infectious classes to recovered classes at some rate +- `flow`: Flows from one class to another, where the flow takes the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. - `force_of_infection`, the prevalence-dependent per capita rate of flow from susceptible classes to infectious classes, used in calculating the `infection` flow -[these's some repeition below that we can cut down on given the latest version of the [amuse bouche](#amuse-bouche)] - -For each of these archetypes, we need to write down a ledger cataloging each specific calculation that uses each archetype. (The SIR model is so simple that there will only be one specific calculation per archetype, but we will see [later](#main-course) that these ledgers give us the flexibility to easily expand a model while avoiding cumbersome redundancy.) +In this case, the `flow` archetype is repeated within the model equations but the `force_of_infection` archetype is only used once. However, we've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). In either case, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation **ledger** for each of these archetypes. We start by creating the `state` and `rate` indices: @@ -201,7 +197,7 @@ rate The `Epi` column name is not really useful in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. -For the ledgers, the `infection` and `recovery` ledgers are tied to the same calculation archetype, which requires one to specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). +For the `flow` archetype, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$. We specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). We use the `mp_join()` function to create the `infection` ledger like so: @@ -238,7 +234,7 @@ infection As a side note, the names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general they are completely up to the modeller and how they choose to specify their calculation archetypes.^[There is only one `mp_join()` argument name that is not available to the user, and that is `by`, which has a special role that we will see [later](#main-course).] -Since the `recovery` ledger uses the same calculation archetype as `infection`, we create it in much the same way: +We create the `recovery` ledger in a similar way: ```{r} # recovery @@ -270,13 +266,15 @@ Now we can use the `SIR_starter()` function to initialize our model object: model = SIR_starter( state, rate, - infection, - recovery, + flow = list( + infection, + recovery + ), force_of_infection ) ``` -We can create a model simulator using `mp_tmb_simulator()`, provided we give it the model object (`model`), initial values for the states and rates (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): +We can create a model simulator using `mp_tmb_simulator()`, provided we give it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): ```{r} sir_simulator = mp_tmb_simulator( @@ -289,6 +287,8 @@ sir_simulator = mp_tmb_simulator( ) ``` +Note that we've specified `NA` for `lambda` as it will be calculated internally using the `force_of_infection` archetype. + Then we can actually simulate the model by passing our model simulator to `mp_report()`: ```{r} From 0568f72e0af57173e29eb5cfc77f16cd67c0782e Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 18:01:41 -0500 Subject: [PATCH 079/332] finished draft of main course --- vignettes/quickstart.Rmd | 158 ++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 51 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 7806bbf3..a28d3504 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -45,8 +45,6 @@ This vignette seeks to explain `macpan2`'s model specification grammar and in pa # Amuse bouche: a structured SIR model {#amuse-bouche} -[update this spec to the two-strain model] - A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculation across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): [insert diagram of SIR with two strains] @@ -263,14 +261,16 @@ For this calculation archetype, we need to specify the `transmission_rates` and Now we can use the `SIR_starter()` function to initialize our model object: ```{r} -model = SIR_starter( - state, - rate, +sir = SIR_starter( + # indices + state = state, + rate = rate, + # ledgers flow = list( infection, recovery ), - force_of_infection + force_of_infection = force_of_infection ) ``` @@ -278,7 +278,7 @@ We can create a model simulator using `mp_tmb_simulator()`, provided we give it ```{r} sir_simulator = mp_tmb_simulator( - model, + sir, vectors = list( state = c(S = 999, I = 1, R = 0), rate = c(beta = 0.25, gamma = 0.1, lambda = NA) @@ -339,55 +339,80 @@ matplot(sir_results_wide[, 1], legend("bottom", col = 1:3, lty = 1:3, legend = state$labels()) ``` -# Main course: expanding the basic SIR into an SIR with infection types {#main-course} +# Main course: expanding the basic SIR with additional structure {#main-course} -- here is where we see the power of macpan -- re-write the equations with stratified symptom statuses and point out the repetition of the FOI term form. We already have a calculation archetype for this baked into `SIR_starter()`! Let's leverage that. +As previously noted, we created a `force_of_infection` archetype of the form $\beta I / N$ despite it only being used once to define the SIR model. However, if we consider the two-strain model from [before](#amuse-bouche), we see this calculation is repeated for each strain: + +\begin{align} +\lambda_A &= \beta_A I_A/N \\ +\lambda_B &= \beta_B I_B/N +\end{align} -[TODO: update model defintion to what's currently in [amuse bouche](#amuse-bouche)] +Since we already have an archetype for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. -- expand the states and rates to include infecteds stratified by symptom status +We start by creating a new index for the strains: ```{r} -# new indices for infection type -# (asymptomatic and symptomatic) -InfType_indices = c("asymp", "symp") +Strain_indices = c("A", "B") +``` + +Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so easily using the `mp_cartesian()` function, which takes the Cartesian product of indices (all possible combinations across sets): + +```{r} +I_index = mp_cartesian( + mp_subset(state, Epi = "I"), + mp_index(Strain = Strain_indices) +) + +I_index ``` +We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function: + ```{r} state = mp_union( - # stratify I by infection type - # asymptomatic and symptomatic - mp_cartesian( - mp_subset(state, Epi = "I"), - mp_index(InfType = InfType_indices) - ), - # keep S and R as before - mp_subset(state, Epi = c("S", "R")) + mp_subset(state, Epi = "S"), + I_index, + mp_subset(state, Epi = "R") ) + +state ``` -explain `mp_cartesian()`, mention other types of products? +We update the rates similarly: ```{r} rate = mp_union( - # stratify rates involved in the infection - # process by infection type + # stratify rates involved in the infection process by strain mp_cartesian( mp_subset(rate, Epi = c("beta", "lambda")), - mp_index(InfType = InfType_indices) + mp_index(Strain = Strain_indices) ), - # recovery rate will be the same across symptom types + # recovery rate will be the same across strains mp_subset(rate, Epi = "gamma") ) + +rate +``` + +It may seem like overkill to use `mp_subset()`, `mp_cartesian()`, and `mp_union()` here to define the indices using the original ones from the SIR when they could be written out by hand quickly as + +```{r eval = FALSE} +state = mp_index( + Epi = c("S", rep("I", 2), "R"), + Strain = c("", Strain_indices, "") +) + +rate = mp_index( + Epi = c(rep(c("beta", "lambda"), 2), "gamma"), + Strain = c(rep(c("A", "B"), each = 2), "") +) ``` -- maybe overkill to do it this way, easy to write out states and rates here by hand, but you can imagine if you have many more states or many more rates or many more desired strata that this way is very powerful. +but you can imagine if you're building off of an existing, working model and you want to stratify many states into many new categories, the first way of building the indices might be preferred, which is why we demonstrated it above. -- redo ledgers with stratification -- start with infection join -- if we just do the join as before... +For the `infection` ledger, let's see what our previous code for generating it yields now that we have some stratification by `Strain`: ```{r} # infection ledger from before @@ -398,12 +423,12 @@ mp_join( ) ``` -We get infection flows with all possible combinations between the `from_states`, the `to_states`, and the `flow_rates` (the full join). However, for this model, we only want two of these flows: +As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join). However, for this model, we want only two of these flows: -- a flow between `S` and `I.asymp` with flow rate `lambda.asymp` -- a flow between `S` and `I.symp` with flow rate `lambda.symp` +- a flow between `S` and `I.A` with flow rate `lambda.A` +- a flow between `S` and `I.B` with flow rate `lambda.B` -In other words, we want the `InfType` index on `I` to match with the `InfType` index on `lambda`. We can specify this within `mp_join()` when building the ledger like so: +In other words, we want the `Strain` index on `I` to match with the `Strain` index on `lambda`. We can specify this within `mp_join()` when building the ledger like so: ```{r} # new infection ledger @@ -412,31 +437,27 @@ infection = mp_join( to_states = mp_subset(state, Epi = "I"), flow_rates = mp_subset(rate, Epi = "lambda"), by = list( - to_states.flow_rates = "InfType" + to_states.flow_rates = "Strain" ) ) infection ``` -- explain what the `mp_join()` function is doing (at a high level)... mention that the argument names are flexible an depend on how the calculation archetypes are specified, but we should ignore this for now. -- explain `by` syntax and how the dot concatenation needs to be pairwise +Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list name (`to_states.flow_rates`), with the names coming from argument names in `mp_join()` (`to_states`, `flow_rates`). The value should be a character name of the index type upon which to match. In this case, the value is `"Strain"` because we want the to state labels and the flow rate labels to match based on the strain label (`I.A` with `lambda.A` and `I.B` with `lambda.B`. -Then we create the recovery ledger: +For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: ```{r} -recovery = - mp_join( - from_states = mp_subset(state, Epi = "I"), - to_states = mp_subset(state, Epi = "R"), - flow_rates = mp_subset(rate, Epi = "gamma") +recovery = mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") ) recovery ``` -This is exactly the code as before for the recovery ledger, and it's what we want (same recovery rate for both infectious classes). - -Finally, for the force of infection ledger, the previous version yields a full join again: +For the force of infection ledger, the full join yields all sorts of combinations that we don't want: ```{r} mp_join( @@ -446,7 +467,7 @@ mp_join( ) ``` -For this model, we want the infectious states and the transmission rates to be matched up on the `InfType` index, but we want each `I` and `beta` pair to be used in all `lambda` expressions, so we do: +We want the `lambda`, `I`, and `beta` labels all matched on the stain index. But recall that `mp_join()` only performs pairwise joins, so we cannot specify a three-way join. Instead, we will specify two pairwise joins: ```{r} force_of_infection = mp_join( @@ -454,13 +475,48 @@ force_of_infection = mp_join( infectious_states = mp_subset(state, Epi = "I"), transmission_rates = mp_subset(rate, Epi = "beta"), by = list( - infection_flow_rates.infectious_states = "InfType", - infectious_states.transmission_rates = "InfType" + infection_flow_rates.infectious_states = "Strain", + infectious_states.transmission_rates = "Strain" ) ) + force_of_infection ``` +Now we're ready to build the two-strain model object and simulate it: + +```{r} +two_strain_model = SIR_starter( + # indices + state = state, + rate = rate, + # ledgers + flow = list( + infection, + recovery + ), + force_of_infection = force_of_infection +) + +two_strain_simulator = mp_tmb_simulator( + two_strain_model, + vectors = list( + state = c(S = 998, I.A = 1, I.B = 1, R = 0), + rate = c(beta.A = 0.25, lambda.A = NA, beta.B = 0.2, lambda.B = NA, gamma = 0.1) + ), + time_steps = 100L +) + +two_strain_results = mp_report(two_strain_simulator) + +(two_strain_results + |> filter(matrix == "state") # keep state variables at each point in time + |> mutate(state = factor(row, levels = c("S", "I.A", "I.B", "R"))) # to enforce logical state ordering in plot + |> ggplot(aes(time, value, colour = state)) + + geom_line() +) +``` + # Dessert: understanding model simulation in `macpan2` {#desert} - follow up vignette, tentative title: "Quickstart part 2: understanding model simulation in `macpan2`" From 5bcfb01494cb0afeada6ccd4acd709514b2ba87b Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Thu, 9 Nov 2023 18:31:12 -0500 Subject: [PATCH 080/332] full draft?! --- vignettes/quickstart.Rmd | 97 +++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index a28d3504..d01bae86 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -1,5 +1,5 @@ --- -title: "Quickstart part 1: understanding model specification in `macpan2`" +title: "Quickstart 1: understanding `macpan2`'s model specification grammar" output: rmarkdown::html_vignette: toc: true @@ -26,8 +26,6 @@ library(dplyr) ] -# Motivation - One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when build upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, such as models including: @@ -82,7 +80,7 @@ and then attach a **ledger** to the model object that tabulates which specific c Here, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 11 patches). -By using calculation **archetypes** and **ledgers**, the modeller can focus on the modelling (designing model structure, choosing an expression for the forces of infection), while `macpan2` will handle the bookkeeping (matching stratified variables with each other each time the expression must be calculated). This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. +**By using calculation archetypes and ledgers, the modeller can focus on the modelling**, like designing model structure and choosing an expression for the forces of infection, **while `macpan2` will handle the bookkeeping**, matching stratified variables with each other each time an expression must be calculated. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. # Appetizer: specifying the basic SIR model @@ -116,6 +114,8 @@ SIR_starter = function( ){ ## Set up expressions list for each calculation archetype -------------- + ## names are when the calculation gets performed relative to + ## the simulation loop (before, during, ...) expr_list = mp_expr_list( before = list( ## aggregations @@ -163,21 +163,21 @@ The inputs to `SIR_starter()` are of two types: - **indices** (labels) for model quantities, - **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included calculation archetypes). -The **indices** we need to specify fall into two groups: +The indices we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ We have identified two useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: -- `flow`: Flows from one class to another, where the flow takes the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. -- `force_of_infection`, the prevalence-dependent per capita rate of flow from susceptible classes to infectious classes, used in calculating the `infection` flow +- `flow`: Unsigned flows from one class to another of the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. +- `force_of_infection`: The prevalence-dependent per capita rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows -In this case, the `flow` archetype is repeated within the model equations but the `force_of_infection` archetype is only used once. However, we've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). In either case, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation **ledger** for each of these archetypes. +In this case, the `flow` archetype is repeated within the model equations but the `force_of_infection` archetype is only used once. However, we've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). In either case, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. We start by creating the `state` and `rate` indices: -```{r} +```{r sir-indices} ## indices (labels) for model quantities ------------------------- state = mp_index(Epi = c("S", "I", "R")) rate = mp_index(Epi = c("beta", "gamma", "lambda")) @@ -185,11 +185,8 @@ rate = mp_index(Epi = c("beta", "gamma", "lambda")) You can think of the `mp_index()` function a setting up data frames tabulating the model quantity labels: -```{r} +```{r sir-state-and-rate} state -``` - -```{r} rate ``` @@ -199,8 +196,8 @@ For the `flow` archetype, we will create two ledgers: `infection` for the flow f We use the `mp_join()` function to create the `infection` ledger like so: -```{r} -# infection ledger +```{r sir-infection-ledger} +## infection ledger ------------------------- infection = mp_join( from_states = mp_subset(state, Epi = "S"), to_states = mp_subset(state, Epi = "I"), @@ -210,23 +207,15 @@ infection = mp_join( The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, namely -```{r} +```{r sir-infection-ledger-inputs} mp_subset(state, Epi = "S") -``` - -```{r} mp_subset(state, Epi = "I") -``` - -and - -```{r} mp_subset(rate, Epi = "lambda") ``` and by default creates one entry in the ledger for each combination of these values (_i.e._, a "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger in this case! -```{r} +```{r sir-infection-ledger-2} infection ``` @@ -234,8 +223,8 @@ As a side note, the names of the arguments in the `mp_join()` function are tied We create the `recovery` ledger in a similar way: -```{r} -# recovery +```{r sir-recovery-ledger} +## recovery ledger ------------------------- recovery = mp_join( from_states = mp_subset(state, Epi = "I"), to_states = mp_subset(state, Epi = "R"), @@ -247,7 +236,8 @@ recovery Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different calculation archetype in `SIR_starter()` (so the `mp_join()` argument names are different): -```{r} +```{r sir-foi-ledger} +## force of infection ledger ------------------------- # infection additionally involves the calculation of a force of infection force_of_infection = mp_join( infectious_states = mp_subset(state, Epi = "I"), @@ -260,7 +250,8 @@ For this calculation archetype, we need to specify the `transmission_rates` and Now we can use the `SIR_starter()` function to initialize our model object: -```{r} +```{r sir} +## SIR model object ------------------------- sir = SIR_starter( # indices state = state, @@ -276,7 +267,8 @@ sir = SIR_starter( We can create a model simulator using `mp_tmb_simulator()`, provided we give it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): -```{r} +```{r sir-simulator} +## SIR model simulator ------------------------- sir_simulator = mp_tmb_simulator( sir, vectors = list( @@ -291,13 +283,14 @@ Note that we've specified `NA` for `lambda` as it will be calculated internally Then we can actually simulate the model by passing our model simulator to `mp_report()`: -```{r} +```{r sir-results} +## SIR model simulation results ------------------------- sir_results = mp_report(sir_simulator) ``` The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer) that can easily be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`: -```{r} +```{r sir-ggplot-example, fig.width = 6} (sir_results |> filter(matrix == "state") # keep state variables at each point in time |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in plot @@ -321,7 +314,7 @@ sir_results_wide <- (sir_results head(sir_results_wide, n = 3) ``` -```{r base_plot_ex, fig.width = 6} +```{r sir-base-plot-ex, fig.width = 6} with(sir_results_wide, plot(x = time, y = I) @@ -330,7 +323,7 @@ with(sir_results_wide, or -```{r base_matplot_ex, fig.width = 6} +```{r sir-base-matplot-ex, fig.width = 6} par(las = 1) ## horizontal y-axis ticks matplot(sir_results_wide[, 1], sir_results_wide[,-1], @@ -352,13 +345,13 @@ Since we already have an archetype for the force of infection, we can easily exp We start by creating a new index for the strains: -```{r} +```{r strain-indices} Strain_indices = c("A", "B") ``` Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so easily using the `mp_cartesian()` function, which takes the Cartesian product of indices (all possible combinations across sets): -```{r} +```{r strain-expand-I} I_index = mp_cartesian( mp_subset(state, Epi = "I"), mp_index(Strain = Strain_indices) @@ -369,7 +362,7 @@ I_index We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function: -```{r} +```{r two-strain-state} state = mp_union( mp_subset(state, Epi = "S"), I_index, @@ -381,7 +374,7 @@ state We update the rates similarly: -```{r} +```{r two-strain-rate} rate = mp_union( # stratify rates involved in the infection process by strain @@ -398,7 +391,7 @@ rate It may seem like overkill to use `mp_subset()`, `mp_cartesian()`, and `mp_union()` here to define the indices using the original ones from the SIR when they could be written out by hand quickly as -```{r eval = FALSE} +```{r two-strain-state-rate-manual, eval = FALSE} state = mp_index( Epi = c("S", rep("I", 2), "R"), Strain = c("", Strain_indices, "") @@ -414,7 +407,7 @@ but you can imagine if you're building off of an existing, working model and you For the `infection` ledger, let's see what our previous code for generating it yields now that we have some stratification by `Strain`: -```{r} +```{r two-strain-infection-default} # infection ledger from before mp_join( from_states = mp_subset(state, Epi = "S"), @@ -430,8 +423,8 @@ As before, the default in `mp_join()` is to give all possible combinations for t In other words, we want the `Strain` index on `I` to match with the `Strain` index on `lambda`. We can specify this within `mp_join()` when building the ledger like so: -```{r} -# new infection ledger +```{r two-strain-infection-ledger} +## new infection ledger ------------------------- infection = mp_join( from_states = mp_subset(state, Epi = "S"), to_states = mp_subset(state, Epi = "I"), @@ -448,7 +441,7 @@ Note the syntax of the `by` argument here. Each `by` list element will correspon For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: -```{r} +```{r two-strain-recovery-ledger} recovery = mp_join( from_states = mp_subset(state, Epi = "I"), to_states = mp_subset(state, Epi = "R"), @@ -459,7 +452,7 @@ recovery For the force of infection ledger, the full join yields all sorts of combinations that we don't want: -```{r} +```{r two-strain-foi-default} mp_join( infection_flow_rates = mp_subset(rate, Epi = "lambda"), infectious_states = mp_subset(state, Epi = "I"), @@ -469,7 +462,8 @@ mp_join( We want the `lambda`, `I`, and `beta` labels all matched on the stain index. But recall that `mp_join()` only performs pairwise joins, so we cannot specify a three-way join. Instead, we will specify two pairwise joins: -```{r} +```{r two-strain-foi-ledger} +## new force of infection ledger ------------------------- force_of_infection = mp_join( infection_flow_rates = mp_subset(rate, Epi = "lambda"), infectious_states = mp_subset(state, Epi = "I"), @@ -485,7 +479,7 @@ force_of_infection Now we're ready to build the two-strain model object and simulate it: -```{r} +```{r two-strain-results, fig.width = 6} two_strain_model = SIR_starter( # indices state = state, @@ -517,16 +511,15 @@ two_strain_results = mp_report(two_strain_simulator) ) ``` -# Dessert: understanding model simulation in `macpan2` {#desert} - -- follow up vignette, tentative title: "Quickstart part 2: understanding model simulation in `macpan2`" -- set of ledgers are associated with a set of archetypes, column names need to match across these paired sets of ledgers and archetypes, but between the paired sets, columns names to be unique +# Dessert: understanding model simulation in `macpan2` {#dessert} -explain that we will explain: +As mentioned, we've hidden some of the details of initializing a model object within the `SIR_starter()` function: -```{r} +```{r sir-starter-print} <> ``` +You may begin to see from this function definition how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `link_data` and `init_vecs` are simply set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. +These topics will be discussed fully in a future vignette. From 5d602f7a7a93f84edcdb5d76db0699764c321804 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 10 Nov 2023 06:27:05 -0500 Subject: [PATCH 081/332] birs --- DESCRIPTION | 6 +- R/link.R | 29 +++-- R/mp.R | 85 ++++++++++++--- inst/model_library/sir_age/model_structure.R | 12 +++ man/macpan2-package.Rd | 4 +- man/mp_join.Rd | 35 ++++++ misc/experiments/refactorcpp.R | 107 +++++++++++++++++-- vignettes/quickstart.Rmd | 2 +- 8 files changed, 245 insertions(+), 35 deletions(-) create mode 100644 man/mp_join.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 5703f1fa..afcc6dfe 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,11 +3,11 @@ Title: Fast and Flexible Compartmental Modelling Version: 1.0.0 Authors@R: c( person("Steve Walker", email="swalk@mcmaster.ca", role=c("cre", "aut")), - person("Darren Flynn-Primrose", role="aut"), + person("Irena Papst", role="ctb"), + person("Michael Li", role="ctb"), person("Weiguang Guan", role="aut"), person("Ben Bolker", role="aut"), - person("Michael Li", role="ctb"), - person("Irena Papst", role="ctb") + person("Darren Flynn-Primrose", role="aut") ) Description: Fast and flexible compartmental modelling with Template Model Builder. License: GPL-3 diff --git a/R/link.R b/R/link.R index f35744af..6e99e7e5 100644 --- a/R/link.R +++ b/R/link.R @@ -163,7 +163,7 @@ initial_labelling_column_names_list = function(labelling_column_names, dimension ## take two Merge objects and merge their frames ## and update the provenance-preserving column maps -merge_util = function(x, y, by.x, by.y) { +merge_util = function(x, y, by.x, by.y, table_names_order) { ## ---- ## resolve non-unique column names in the output, with @@ -241,9 +241,9 @@ merge_util = function(x, y, by.x, by.y) { ## ---- Link( z, - z_column_map, - z_reference_index_list, - z_lab_names_list + z_column_map[table_names_order], + z_reference_index_list[table_names_order], + z_lab_names_list[table_names_order] ) } @@ -338,7 +338,7 @@ explicit_provenance = function(x, col_nm) { } -merge_generic_by_util = function(x, y, ...) { +merge_generic_by_util = function(x, y, table_names_order, ...) { by = filter_by_list( names(x$column_map), names(y$column_map), @@ -366,14 +366,23 @@ merge_generic_by_util = function(x, y, ...) { by = data.frame(x = by.x, y = by.y) |> unique() dup.x = duplicated(by$x) - if (any(dup.x)) { - cols_to_fix = by$x[dup.x] - for (col_nm in cols_to_fix) { - x = explicit_provenance(x, col_nm) + dup.y = duplicated(by$y) + if (any(dup.x) | any(dup.y)) { + if (any(dup.x)) { + cols_to_fix = by$x[dup.x] + for (col_nm in cols_to_fix) { + x = explicit_provenance(x, col_nm) + } + } + if (any(dup.y)) { + cols_to_fix = by$y[dup.y] + for (col_nm in cols_to_fix) { + y = explicit_provenance(y, col_nm) + } } merge_generic_by_util(x, y, ...) } else { - merge_util(x, y, by$x, by$y) + merge_util(x, y, by$x, by$y, table_names_order) } } diff --git a/R/mp.R b/R/mp.R index e84871bb..f2e8fa33 100644 --- a/R/mp.R +++ b/R/mp.R @@ -175,6 +175,31 @@ mp_setdiff = function(x, ...) { Index(partition, x$labelling_column_names, x) } +# mp_aggregate = function(x, by = "Group", ) { +# mp_join( +# alive = x, +# group = mp_group(x, by), +# by = by +# ) +# } + +#' @export +mp_aggregate = function(index, by = "Group", ledger_column = "group") { + index_columns = to_names(by) + if (length(index_columns) == 1L & !index_columns %in% names(index)) { + partition = index$partition$constant(by, "a") + index = Index(partition) + } else { + partition = index$partition + } + Link( + partition$frame(), + macpan2:::initial_column_map(names(partition), ledger_column), + macpan2:::initial_reference_index_list(index, ledger_column), + setNames(list(index_columns), ledger_column) + ) +} + #' Union of Indexes #' #' @param ... Indexes. @@ -235,8 +260,6 @@ mp_rbind = function(...) { ) } - - #' @export mp_choose = function(x, subset_name, ...) { l = list(...) @@ -266,11 +289,35 @@ mp_choose_out = function(x, subset_name, ...) { init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) } - +#' Join Indexes +#' +#' @param ... Named arguments giving indexes created by +#' \code{\link{mp_index}} or another function that manipulates indexes. +#' Each argument will become a position vector used to subset +#' or expand numeric vectors in archetype formulas. +#' @param by What columns to use to join the indexes. If there are +#' only two indexes in \code{...} the `by` argument can be either +#' (1) a string giving the dot-concatenation of columns to join on +#' that are common among the tables or (2) a two-sided formula with +#' strings on either side. In the formula case, the left-hand-side +#' is a dot-concatenation of columns in the first index and the +#' right-hand-side is a dot-concatenation of the columns in the second +#' index. This formula notation is useful if the name of a column in +#' one index is different from a column in the other index that +#' should be joined on. If there are more than two indexes in \code{...} +#' the `by` argument is a named list of strings and/or formulas. +#' Each item in the list corresponds to a pair of indexes and how +#' their columns are to be matched. The name of each item is a dot +#' contactenation of the names of the corresponding pairs of arguments +#' in \code{...}. The value of each item follows the same rules as the +#' case given above with two indexes. TODO: create examples and point +#' to them. +#' #' @export mp_join = function(..., by = empty_named_list()) { table_list = valid$named_list$assert(list(...)) table_nms = names(table_list) + if (length(table_nms) < 2L) stop("cannot join fewer than two index objects.") if (is.character(by)) { if (length(table_nms) != 2L) { @@ -278,7 +325,16 @@ mp_join = function(..., by = empty_named_list()) { } by = setNames(list(by), to_name(table_nms)) } + by_list = valid$named_list$assert(by) + table_order = (by_list + |> names() + |> lapply(to_names) + |> unlist() + |> unique() + ) + ordered_table_list = table_list[table_order] + by_nms = names(by_list) |> strsplit(".", fixed = TRUE) good_by_nms = (by_nms |> lapply(`%in%`, table_nms) @@ -301,13 +357,13 @@ mp_join = function(..., by = empty_named_list()) { ) |> stop() } if (!is.null(table_nms)) { - for (nm in names(table_list)) { - if (nm != "" & inherits(table_list[[nm]], "Index")) { - table_list[[nm]] = mp_choose(table_list[[nm]], nm) + for (nm in names(ordered_table_list)) { + if (nm != "" & inherits(ordered_table_list[[nm]], "Index")) { + ordered_table_list[[nm]] = mp_choose(ordered_table_list[[nm]], nm) } } } - orig_tab_nms = (table_list + orig_tab_nms = (ordered_table_list |> method_apply("table_names") |> unname() |> unlist(recursive = FALSE) @@ -317,6 +373,7 @@ mp_join = function(..., by = empty_named_list()) { |> lapply(as.integer) |> vapply(diff, integer(1L)) ) + bad_by_args = table_nm_diffs < 1L ## table names in the wrong order if (any(bad_by_args)) { fixed_by_nms = (by_nms[bad_by_args] @@ -329,10 +386,15 @@ mp_join = function(..., by = empty_named_list()) { by_list[bad_by_args] = fixed_by_args names(by_list)[bad_by_args] = fixed_by_nms } - z = table_list[[1L]] - for (i in 2:length(table_list)) { + + z = ordered_table_list[[1L]] + for (i in 2:length(ordered_table_list)) { args = c( - list(x = z, y = table_list[[i]]), + list( + x = z, + y = ordered_table_list[[i]], + table_names_order = names(table_list) + ), by_list ) z = do.call(merge_generic_by_util, args) @@ -340,8 +402,7 @@ mp_join = function(..., by = empty_named_list()) { z } -#' @export -mp_aggregate = function(formula +mp_aggregate_old = function(formula , group_by , index , ... diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index de036e64..234e3092 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -48,6 +48,18 @@ age_contact = mp_cartesian( mp_rename(age, AgeSusceptible = "Age") ) + + +trans_rates_sir = mp_index(Epi = "beta") +symptoms = mp_index(Symp = c("mild", "severe")) +trans_rates = mp_cartesian( + trans_rates_sir, + mp_cartesian( + mp_rename(symptoms, SympInfectious = "Symp"), + mp_rename(symptoms, SympSusceptible = "Symp") + ) +) + ## structured model -------- state = (state_sir diff --git a/man/macpan2-package.Rd b/man/macpan2-package.Rd index 5e05e1c3..29ed036d 100644 --- a/man/macpan2-package.Rd +++ b/man/macpan2-package.Rd @@ -24,15 +24,15 @@ Useful links: Authors: \itemize{ - \item Darren Flynn-Primrose \item Weiguang Guan \item Ben Bolker + \item Darren Flynn-Primrose } Other contributors: \itemize{ - \item Michael Li [contributor] \item Irena Papst [contributor] + \item Michael Li [contributor] } } diff --git a/man/mp_join.Rd b/man/mp_join.Rd new file mode 100644 index 00000000..7c3da07b --- /dev/null +++ b/man/mp_join.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_join} +\alias{mp_join} +\title{Join Indexes} +\usage{ +mp_join(..., by = empty_named_list()) +} +\arguments{ +\item{...}{Named arguments giving indexes created by +\code{\link{mp_index}} or another function that manipulates indexes. +Each argument will become a position vector used to subset +or expand numeric vectors in archetype formulas.} + +\item{by}{What columns to use to join the indexes. If there are +only two indexes in \code{...} the \code{by} argument can be either +(1) a string giving the dot-concatenation of columns to join on +that are common among the tables or (2) a two-sided formula with +strings on either side. In the formula case, the left-hand-side +is a dot-concatenation of columns in the first index and the +right-hand-side is a dot-concatenation of the columns in the second +index. This formula notation is useful if the name of a column in +one index is different from a column in the other index that +should be joined on. If there are more than two indexes in \code{...} +the \code{by} argument is a named list of strings and/or formulas. +Each item in the list corresponds to a pair of indexes and how +their columns are to be matched. The name of each item is a dot +contactenation of the names of the corresponding pairs of arguments +in \code{...}. The value of each item follows the same rules as the +case given above with two indexes. TODO: create examples and point +to them.} +} +\description{ +Join Indexes +} diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 836516df..c62d959b 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -13,11 +13,11 @@ sir_two_strains = (mp_rename(sir, A = "Epi") ## replacement model -n_strains = 10L -strain_nms = letters[seq_len(n_strains)] +n_strains = 2L +strain_nms = letters[seq_len(n_strains + 1L)] strains = mp_index( - Strain = strain_nms[-n_strains], + Strain = strain_nms[-(n_strains + 1L)], Replace = strain_nms[-1L], labelling_column_names = "Strain" ) @@ -35,6 +35,16 @@ replacement_state = mp_union( strains ) ) +infection = mp_join( + from = mp_subset(replacement_state, Epi = c("S", "R")), + to = mp_subset(replacement_state, Epi = "E"), + by = list(from.to = "Replace" ~ "Strain") +) +progression = mp_join( + from = mp_subset(replacement_state, Epi = "E"), + to = mp_subset(replacement_state, Epi = "I"), + by = list(from.to = "Strain") +) xx = macpan2:::LinkData( @@ -98,18 +108,101 @@ xx$labels() ## atomic model indices --------------- -state_sir = mp_index( - Epi = c("S", "I", "R", "D"), - Vital = c("alive", "alive", "alive", "dead"), - labelling_column_names = "Epi" +state = mp_index( + Epi = c("S", "I", "R") + #Vital = c("alive", "alive", "alive", "dead"), + #labelling_column_names = "Epi" ) flow_rates_sir = mp_index(Epi = c("lambda", "gamma", "mu")) trans_rates_sir = mp_index(Epi = "beta") cities = mp_index(Loc = c("cal", "ham", "que")) + movement = mp_linear(cities, "Move") +mp_square(cities, c("Loc", "Move")) + age = mp_index(Age = c("young", "old")) contact = mp_square(age, c("Infectious", "Infection")) + +library(macpan2) + +sir = mp_index(Epi = c("S", "I", "R", "lambda", "gamma")) +strain = mp_index(Strain = c("a", "b")) +state = mp_union( + mp_cartesian(mp_subset(sir, Epi = "I"), strain), + mp_subset(sir, Epi = c("S", "R")) +) + +flow_rates = mp_union( + mp_cartesian(mp_subset(sir, Epi = "lambda"), strain), + mp_subset(sir, Epi = "gamma") +) +trans_rates = mp_cartesian( + mp_index(Epi = "beta"), + strain +) + +state +strain +flow_rates +trans_rates + +ll = mp_join( + state = mp_subset(state, Epi = "I"), + flow = mp_subset(flow_rates, Epi = "lambda"), + trans = trans_rates, + by = list( + state.flow = "Strain", + flow.trans = "Strain" + ) +) +list(group = list()) +hh = function(index, by = "Group", ledger_column = "group") { + index_columns = to_names(by) + if (length(index_columns) == 1L & !index_columns %in% names(index)) { + partition = index$partition$constant(by, "a") + index = Index(partition) + } else { + partition = index$partition + } + Link( + partition$frame(), + macpan2:::initial_column_map(names(partition), ledger_column), + macpan2:::initial_reference_index_list(index, ledger_column), + setNames(list(index_columns), ledger_column) + ) +} +hh(movement, by = "Move", ledger_column = "move") +hh(movement) +mp_link_data(hh(state))$positions_frame(TRUE) +hh = function(len) { + data.frame(group = rep("group", len)) + +} +Link( + , + column_map = macpan2:::initial_column_map() +) + +mp_join( + state = mp_subset(state, Epi = "I"), + rate = trans_rates, + by = list( + state.rate = "Strain" + ) +) + +mp_join( + flow = mp_subset(flow_rates, Epi = "lambda"), + rate = trans_rates, + by = list( + flow.rate = "Strain" + ) +) + + + + ## structured model indices ------------- state = (state_sir diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c6e5476d..aad6381f 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -120,7 +120,7 @@ SIR_starter = function( expr_list = mp_expr_list( before = list( ## aggregations - N ~ sum(state) + N ~ groupSums(state, alive, state) ), during = list( ## force of infections From b7e34024f9b193d05dfca7554775e3b4fce79319 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 10 Nov 2023 09:39:02 -0500 Subject: [PATCH 082/332] steve's comments and edits on quickstart --- NAMESPACE | 27 ++++---- R/formula_data.R | 24 +++---- R/index_to_tmb.R | 4 +- R/link.R | 86 ++++++++++++++------------ R/model_def_run.R | 26 ++++---- R/mp.R | 38 +++++++----- man/IndexedExpressions.Rd | 4 +- man/{Link.Rd => Ledger.Rd} | 12 ++-- man/{mp_link_data.Rd => mp_ledgers.Rd} | 10 +-- vignettes/quickstart.Rmd | 48 +++++++++----- 10 files changed, 154 insertions(+), 125 deletions(-) rename man/{Link.Rd => Ledger.Rd} (65%) rename man/{mp_link_data.Rd => mp_ledgers.Rd} (62%) diff --git a/NAMESPACE b/NAMESPACE index 3cc72708..da55844c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,22 +7,23 @@ S3method(Vector,Index) S3method(Vector,data.frame) S3method(Vector,numeric) S3method(as.data.frame,Index) -S3method(as.data.frame,Link) +S3method(as.data.frame,Ledger) S3method(as.matrix,Vector) S3method(c,String) S3method(c,StringData) -S3method(head,Link) +S3method(head,Ledger) S3method(labelling_column_names,Index) -S3method(labelling_column_names,Link) +S3method(labelling_column_names,Ledger) S3method(labels,Index) S3method(length,Vector) S3method(mp_index,character) S3method(mp_index,data.frame) S3method(mp_labels,Index) -S3method(mp_labels,Link) +S3method(mp_labels,Ledger) S3method(mp_tmb_simulator,DynamicModel) S3method(mp_tmb_simulator,ModelDefRun) S3method(mp_union,Index) +S3method(mp_union,Ledger) S3method(mp_vector,Index) S3method(mp_vector,Link) S3method(mp_vector,character) @@ -30,27 +31,27 @@ S3method(mp_vector,data.frame) S3method(mp_vector,numeric) S3method(names,Index) S3method(names,IntVecs) -S3method(names,Link) +S3method(names,Ledger) S3method(names,MatsList) S3method(names,MethList) S3method(names,Partition) S3method(print,ExprList) S3method(print,Index) -S3method(print,Link) +S3method(print,Ledger) S3method(print,MathExpression) S3method(print,Partition) S3method(print,Quantities) S3method(print,String) S3method(print,StringData) S3method(print,Vector) -S3method(print,summary.Link) +S3method(print,summary.Ledger) S3method(split_by,character) S3method(split_by,formula) -S3method(str,Link) -S3method(summary,Link) +S3method(str,Ledger) +S3method(summary,Ledger) S3method(swap_sides,character) S3method(swap_sides,formula) -S3method(tail,Link) +S3method(tail,Ledger) S3method(to_labels,Index) S3method(to_labels,Labels) S3method(to_labels,Partition) @@ -94,8 +95,8 @@ export(IndexedExpressions) export(Infection) export(IntVecs) export(JSONReader) -export(Link) -export(LinkList) +export(Ledger) +export(LedgerList) export(Log) export(LogFile) export(Logit) @@ -165,8 +166,8 @@ export(mp_indicator) export(mp_indices) export(mp_join) export(mp_labels) +export(mp_ledgers) export(mp_linear) -export(mp_link_data) export(mp_rename) export(mp_report) export(mp_set_numbers) diff --git a/R/formula_data.R b/R/formula_data.R index 19c6c8fe..1927a345 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -1,8 +1,8 @@ -LinkData = function(...) { +LedgerData = function(...) { self = Base() - self$link_list = list(...) + self$ledger_list = list(...) - labelling_column_names_list = (self$link_list + labelling_column_names_list = (self$ledger_list |> lapply(getElement, "labelling_column_names_list") |> unname() |> unique() @@ -10,7 +10,7 @@ LinkData = function(...) { stopifnot(length(labelling_column_names_list) == 1L) self$labelling_column_names_list = labelling_column_names_list[[1L]] - reference_index_list = (self$link_list + reference_index_list = (self$ledger_list |> lapply(getElement, "reference_index_list") |> unname() |> unique() @@ -18,7 +18,7 @@ LinkData = function(...) { stopifnot(length(reference_index_list) == 1L) self$reference_index_list = reference_index_list[[1L]] - table_names = (self$link_list + table_names = (self$ledger_list |> method_apply("table_names") |> unname() |> unique() @@ -27,7 +27,7 @@ LinkData = function(...) { self$table_names = table_names[[1L]] self$labels_frame = function() { - (self$link_list + (self$ledger_list |> method_apply("labels_frame") |> bind_rows() ) @@ -35,34 +35,34 @@ LinkData = function(...) { self$positions_frame = function(zero_based = FALSE) { positions_list = list() - for (i in seq_along(self$link_list)) { + for (i in seq_along(self$ledger_list)) { positions_list[[i]] = list() for (d in self$table_names) { - positions_list[[i]][[d]] = self$link_list[[i]]$positions_for[[d]](zero_based) + positions_list[[i]][[d]] = self$ledger_list[[i]]$positions_for[[d]](zero_based) } positions_list[[i]] = as.data.frame(positions_list[[i]]) } bind_rows(positions_list) } - return_object(self, "LinkData") + return_object(self, "LedgerData") } #' #' @export -#' print.LinkData = function(x, ...) { +#' print.LedgerData = function(x, ...) { #' print(x$frame, row.names = FALSE) #' } #' @export -mp_link_data = function(...) LinkData(...) +mp_ledgers = function(...) LedgerData(...) #' Indexed Expressions #' #' @param ... Formula objects that reference the columns in the #' \code{index_data}, the vectors in \code{vector_list} and the matrices #' in \code{unstructured_matrix_list}. -#' @param index_data An object produced using \code{\link{mp_link_data}}. +#' @param ledgers An object produced using \code{\link{mp_ledgers}}. #' @param vector_list Named list of objected produced using #' \code{\link{mp_vector}}. #' @param unstructured_matrix_list Named list of objects that can be coerced diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 0c802b7a..ca9d415b 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -34,9 +34,9 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model , initialize_ad_fun = TRUE , ... ) { - link_data = dynamic_model$link_data + ledgers = dynamic_model$ledgers expr_list = dynamic_model$expr_list - int_vecs = (link_data + int_vecs = (ledgers |> method_apply("positions_frame", zero_based = TRUE) |> lapply(as.list) |> unname() diff --git a/R/link.R b/R/link.R index 6e99e7e5..8fc16b13 100644 --- a/R/link.R +++ b/R/link.R @@ -1,7 +1,7 @@ -#' Link +#' Ledger #' -#' Make an object to describe links between the entities in an -#' \code{\link{Index}}. \code{Link} object are created by operating on existing +#' Make an object to describe ledgers between the entities in an +#' \code{\link{Index}}. \code{Ledger} object are created by operating on existing #' \code{\link{Index}} objects. For example, here the \code{\link{mp_join}} #' combines two #' ```{r} @@ -18,7 +18,7 @@ #' ``` #' #' @export -Link = function(frame, column_map, reference_index_list, labelling_column_names_list) { +Ledger = function(frame, column_map, reference_index_list, labelling_column_names_list) { self = Base() self$frame = frame self$column_map = column_map @@ -69,7 +69,7 @@ Link = function(frame, column_map, reference_index_list, labelling_column_names_ self$filter = function(condition) { condition = substitute(condition) i = eval(condition, envir = c(self$column_by_dim, self$frame)) - Link(self$frame[i, , drop = FALSE] + Ledger(self$frame[i, , drop = FALSE] , column_map = self$column_map , reference_index_list = self$reference_index_list , labelling_column_names_list = self$labelling_column_names_list @@ -79,8 +79,18 @@ Link = function(frame, column_map, reference_index_list, labelling_column_names_ substitute(condition) eval(condition, envir = c(self$column_by_dim, self$frame)) } - self$partition = self$partition_for[[1L]]() ## hack! should probably have a method and then change the partition field in Index to a method as well - return_object(self, "Link") + self$reorder = function(table_names_order) { + ## NB: in-place! + self$labelling_column_names_list = self$labelling_column_names_list[table_names_order] + self$column_map = self$column_map[table_names_order] + self$reference_index_list = self$reference_index_list[table_names_order] + self + } + + ## hack! should probably have a method and then change the partition + ## field in Index to a method as well + self$partition = self$partition_for[[1L]]() + return_object(self, "Ledger") } ColumnGetter = function(link, dimension_name, column_name) { @@ -163,7 +173,7 @@ initial_labelling_column_names_list = function(labelling_column_names, dimension ## take two Merge objects and merge their frames ## and update the provenance-preserving column maps -merge_util = function(x, y, by.x, by.y, table_names_order) { +merge_util = function(x, y, by.x, by.y) { ## ---- ## resolve non-unique column names in the output, with @@ -239,11 +249,11 @@ merge_util = function(x, y, by.x, by.y, table_names_order) { ## ---- ## wrap up the result with provenance-preserving column map ## ---- - Link( + Ledger( z, - z_column_map[table_names_order], - z_reference_index_list[table_names_order], - z_lab_names_list[table_names_order] + z_column_map, + z_reference_index_list, + z_lab_names_list ) } @@ -262,7 +272,7 @@ filter_by_list = function(x_orig, y_orig, by_list) { #' @export init_merge = function(frame, dimension_name, reference_index, labelling_column_names) { - Link(frame + Ledger(frame , initial_column_map(names(frame), dimension_name) , initial_reference_index_list(reference_index, dimension_name) , initial_labelling_column_names_list(labelling_column_names, dimension_name) @@ -294,7 +304,7 @@ apply_col_map = function(map, orig_table_nm, by) { map[[orig_table_nm]][by] |> unlist(use.names = FALSE) } -## @param x Link object +## @param x Ledger object ## @param col_nm Name of a column to check for implicit provenance is_provenance_implicit = function(x, col_nm) { (x$column_map @@ -304,7 +314,7 @@ is_provenance_implicit = function(x, col_nm) { ) } -## @param x Link object +## @param x Ledger object explicit_provenance = function(x, col_nm) { m = x$column_map implicit = is_provenance_implicit(x, col_nm) @@ -332,13 +342,13 @@ explicit_provenance = function(x, col_nm) { m[[tab_nm]][[col_nm]] = new_col_nm } f[[col_nm]] = NULL - ## TODO: update with four-arg form of Link + ## frame, column_map, reference_index_list, labelling_column_names_list - Link(f, m, ii, l) + Ledger(f, m, ii, l) } -merge_generic_by_util = function(x, y, table_names_order, ...) { +merge_generic_by_util = function(x, y, ...) { by = filter_by_list( names(x$column_map), names(y$column_map), @@ -382,29 +392,29 @@ merge_generic_by_util = function(x, y, table_names_order, ...) { } merge_generic_by_util(x, y, ...) } else { - merge_util(x, y, by$x, by$y, table_names_order) + merge_util(x, y, by$x, by$y) } } #' @export -as.data.frame.Link = function(x, row.names = NULL, optional = FALSE, ...) { +as.data.frame.Ledger = function(x, row.names = NULL, optional = FALSE, ...) { x$labels_frame() } #' @export -summary.Link = function(object, ...) { +summary.Ledger = function(object, ...) { formats = c("name", "combined") structure( sapply(formats, link_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), - class = "summary.Link" + class = "summary.Ledger" ) } #' @export -print.summary.Link = function(x, ...) { +print.summary.Ledger = function(x, ...) { msg_hline() |> message() msg( - "Link object from macpan2 describing", + "Ledger object from macpan2 describing", "an aspect of model shape" ) |> message() msg_hline() |> message() @@ -413,10 +423,10 @@ print.summary.Link = function(x, ...) { } #' @export -names.Link = function(x) names(x$frame) +names.Ledger = function(x) names(x$frame) #' @export -labelling_column_names.Link = function(x) x$labelling_column_names_list +labelling_column_names.Ledger = function(x) x$labelling_column_names_list link_format_picker = function(x @@ -431,7 +441,7 @@ link_format_picker = function(x } #' @export -print.Link = function(x +print.Ledger = function(x , format = c("labels", "link", "combined", "separate") , ... ) { @@ -440,7 +450,7 @@ print.Link = function(x } #' @export -head.Link = function(x +head.Ledger = function(x , n = 6L , format = c("labels", "link", "combined", "separate") , ... @@ -454,7 +464,7 @@ head.Link = function(x } #' @export -tail.Link = function(x +tail.Ledger = function(x , n = 6L , format = c("labels", "link", "combined", "separate") , ... @@ -468,7 +478,7 @@ tail.Link = function(x } #' @export -str.Link = function(x +str.Ledger = function(x , format = c("labels", "link", "combined", "separate") , ... ) { @@ -478,29 +488,29 @@ str.Link = function(x #' @export -LinkList = function() { +LedgerList = function() { self = Base() self$list = list() |> setNames(as.character()) self$add = function(new_link, ...) { - new_links = list(...) + new_ledgers = list(...) if (missing(new_link)) { - macpan2:::valid$named_list$check(new_links) - } else if(length(new_links) == 0L) { + macpan2:::valid$named_list$check(new_ledgers) + } else if(length(new_ledgers) == 0L) { new_nm = deparse1(substitute(new_link)) - new_links = setNames(list(new_link), new_nm) + new_ledgers = setNames(list(new_link), new_nm) } else { stop("If supplying more than one join result, please name them with {name} = {join_result}") } - for (nm in names(new_links)) { + for (nm in names(new_ledgers)) { if (nm %in% names(self$list)) { msg( "Join result", nm, "is already in the list.", "Overwriting the existing one." ) |> message() } - self$list[[nm]] = new_links[[nm]] + self$list[[nm]] = new_ledgers[[nm]] } } - return_object(self, "LinkList") + return_object(self, "LedgerList") } diff --git a/R/model_def_run.R b/R/model_def_run.R index 4dd4cc15..7ac4e386 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -13,23 +13,23 @@ Compartmental2 = function(model_directory) { ) self$dynamic_model = self$def_env[[self$settings()[["model_object"]]]] - self$link_data_type = function(type) { - self$dynamic_model$link_data[[self$settings()[[type]]]] - } self$index_data_type = function(type) { x = self$def_env[[self$settings()[[type]]]] stopifnot(inherits(x, "Index")) x } - self$flow_links = function() self$dynamic_model$link_data$flows - self$influence_links = function() self$dynamic_model$link_data$influences - #self$normalization_links = function() self$link_data_type("normalization") - #self$aggregation_links = function() self$link_data_type("aggregation") + self$flow_ledgers = function() self$dynamic_model$ledgers$flows + self$influence_ledgers = function() self$dynamic_model$ledgers$influences + #self$ledgers_type = function(type) { + # self$dynamic_model$ledgers[[self$settings()[[type]]]] + #} + #self$normalization_ledgers = function() self$ledgers_type("normalization") + #self$aggregation_ledgers = function() self$ledgers_type("aggregation") - self$flows = function() self$flow_links()$labels_frame() - self$influences = function() self$influence_links()$labels_frame() - #self$normalization = function() self$normalization_links()$labels_frame() - #self$aggregation = function() self$aggregation_links()$labels_frame() + self$flows = function() self$flow_ledgers()$labels_frame() + self$influences = function() self$influence_ledgers()$labels_frame() + #self$normalization = function() self$normalization_ledgers()$labels_frame() + #self$aggregation = function() self$aggregation_ledgers()$labels_frame() self$expr_list = function() self$def_env[[self$settings()[["expr_list"]]]] ## back-compatibility @@ -63,10 +63,10 @@ LabelsScripts = function(model) { } #' @export -DynamicModel = function(expr_list = ExprList(), link_data = list(), init_vecs = list(), unstruc_mats = list()) { +DynamicModel = function(expr_list = ExprList(), ledgers = list(), init_vecs = list(), unstruc_mats = list()) { self = Base() self$expr_list = expr_list - self$link_data = link_data + self$ledgers = ledgers self$init_vecs = init_vecs self$unstruc_mats = unstruc_mats return_object(self, "DynamicModel") diff --git a/R/mp.R b/R/mp.R index 9d835434..a9feff4d 100644 --- a/R/mp.R +++ b/R/mp.R @@ -192,7 +192,7 @@ mp_aggregate = function(index, by = "Group", ledger_column = "group") { } else { partition = index$partition } - Link( + Ledger( partition$frame(), macpan2:::initial_column_map(names(partition), ledger_column), macpan2:::initial_reference_index_list(index, ledger_column), @@ -222,14 +222,14 @@ mp_union.Index = function(...) { ## not used anymore? #' @export -mp_union.Link = function(...) { +mp_union.Ledger = function(...) { l = list(...) column_map = lapply(l, getElement, "column_map") |> unique() if (length(column_map) != 1L) { msg_colon( msg( - "Union of inconsistent Link objects.", - "All Link objects must have the same", + "Union of inconsistent Ledger objects.", + "All Ledger objects must have the same", "column_map, but the following distinct", "maps were found:" ), @@ -241,8 +241,8 @@ mp_union.Link = function(...) { if (length(labelling_column_names_list) != 1L) { msg_colon( msg( - "Union of inconsistent Link objects.", - "All Link objects must have the same", + "Union of inconsistent Ledger objects.", + "All Ledger objects must have the same", "labelling_column_names_list, but the following", "distinct maps were found:" ), @@ -250,7 +250,7 @@ mp_union.Link = function(...) { ) |> stop() } frame = mp_rbind(...) - LinkData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) + LedgerData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) } ## not used anymore? @@ -328,12 +328,17 @@ mp_join = function(..., by = empty_named_list()) { } by_list = valid$named_list$assert(by) - table_order = (by_list - |> names() - |> lapply(to_names) - |> unlist() - |> unique() - ) + if (length(by_list) > 1L) { + table_order = (by_list + |> names() + |> lapply(to_names) + |> unlist() + |> unique() + |> union(names(table_list)) + ) + } else { + table_order = names(table_list) + } ordered_table_list = table_list[table_order] by_nms = names(by_list) |> strsplit(".", fixed = TRUE) @@ -393,14 +398,13 @@ mp_join = function(..., by = empty_named_list()) { args = c( list( x = z, - y = ordered_table_list[[i]], - table_names_order = names(table_list) + y = ordered_table_list[[i]] ), by_list ) z = do.call(merge_generic_by_util, args) } - z + z$reorder(names(table_list)) } mp_aggregate_old = function(formula @@ -573,7 +577,7 @@ mp_zero_vector = function(x, labelling_column_names, ...) { } #' @export -mp_labels.Link = function(x, labelling_column_names) { +mp_labels.Ledger = function(x, labelling_column_names) { x$labels_for[[labelling_column_names]]() } diff --git a/man/IndexedExpressions.Rd b/man/IndexedExpressions.Rd index 052a8c91..768b53f5 100644 --- a/man/IndexedExpressions.Rd +++ b/man/IndexedExpressions.Rd @@ -16,13 +16,13 @@ IndexedExpressions( \code{index_data}, the vectors in \code{vector_list} and the matrices in \code{unstructured_matrix_list}.} -\item{index_data}{An object produced using \code{\link{mp_link_data}}.} - \item{vector_list}{Named list of objected produced using \code{\link{mp_vector}}.} \item{unstructured_matrix_list}{Named list of objects that can be coerced to a matrix.} + +\item{ledgers}{An object produced using \code{\link{mp_ledgers}}.} } \description{ Indexed Expressions diff --git a/man/Link.Rd b/man/Ledger.Rd similarity index 65% rename from man/Link.Rd rename to man/Ledger.Rd index dc218387..ea3d8808 100644 --- a/man/Link.Rd +++ b/man/Ledger.Rd @@ -1,14 +1,14 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/link.R -\name{Link} -\alias{Link} -\title{Link} +\name{Ledger} +\alias{Ledger} +\title{Ledger} \usage{ -Link(frame, column_map, reference_index_list, labelling_column_names_list) +Ledger(frame, column_map, reference_index_list, labelling_column_names_list) } \description{ -Make an object to describe links between the entities in an -\code{\link{Index}}. \code{Link} object are created by operating on existing +Make an object to describe ledgers between the entities in an +\code{\link{Index}}. \code{Ledger} object are created by operating on existing \code{\link{Index}} objects. For example, here the \code{\link{mp_join}} combines two diff --git a/man/mp_link_data.Rd b/man/mp_ledgers.Rd similarity index 62% rename from man/mp_link_data.Rd rename to man/mp_ledgers.Rd index e62e8bfc..d7593d9a 100644 --- a/man/mp_link_data.Rd +++ b/man/mp_ledgers.Rd @@ -1,17 +1,17 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/formula_data.R -\name{mp_link_data} -\alias{mp_link_data} +\name{mp_ledgers} +\alias{mp_ledgers} \title{#' @export -print.LinkData = function(x, ...) { +print.LedgerData = function(x, ...) { print(x$frame, row.names = FALSE) }} \usage{ -mp_link_data(...) +mp_ledgers(...) } \description{ #' @export -print.LinkData = function(x, ...) { +print.LedgerData = function(x, ...) { print(x$frame, row.names = FALSE) } } diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c190c635..966d400b 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -21,14 +21,15 @@ library(dplyr) [ -- change "link_data" to "ledgers" in `mp_*()` API +- make sure all functions used have good help pages +- get model library ready - consolidate spec in examples with steve's presentation ] -One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when build upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. +One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when building upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. -There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, such as models including: +There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying [SW: extending? expanding?] **structured** models, such as models including: - age-structure - multiple locations (a metapopulation model) @@ -37,9 +38,9 @@ There is a trade-off between the flexibility and the simplicity of the model gra - vaccination status - infection types (asymptomatic, symptomatic) -[add links to papers with these?] +[add links to papers with these?] [SW: how about this one? https://idpjournal.biomedcentral.com/articles/10.1186/s40249-022-01001-y] -This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. The package also comes with a model library (see `vignette("example_models")`) to get users started with commonly used basic models, and to demonstrate some more complex cases. +This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. The package also comes with a model library (see `vignette("example_models")`) to get users started with commonly used basic models, and to demonstrate some more complex cases. [SW: I'm currently concerned about the condition of the model library. Please be patient while I make a decision about how much of the library to include and whether or not to reference it in this vignette.] # Amuse bouche: a structured SIR model {#amuse-bouche} @@ -51,7 +52,7 @@ Here, - $S$, $I_A$, $I_B$, and $R$ are the number of individuals that are susceptible, infectious with strain A, infectious with strain B, and recovered, respectively, - $\beta_A$ and $\beta_B$ are the transmission rates for strains A and B, respectively, -- $\gamma$ is the recovery rate for all infecteds. +- $\gamma$ is the recovery rate for all infecteds. [SW: is this a word? should we say 'infected individuals'?] We can cast this model as a system of difference equations: @@ -78,10 +79,12 @@ lambda = beta * I / N and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. -Here, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider an age-structured metapopulation model with 10 age groups within each of the 11 patches: there would be 10x10x11 = `r 10*10*11` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 11 patches). +Here, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider an age-structured metapopulation model with 10 age groups within each of 10 patches: there would be 10x10x10 = `r 10*10*10` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 10 patches). [SW: I changed 11 to 10 to be simpler. I know I first said 11, but that was because I was thinking of the specific case of NL that has 11 public health districts. Here we should be more abstract I think.] **By using calculation archetypes and ledgers, the modeller can focus on the modelling**, like designing model structure and choosing an expression for the forces of infection, **while `macpan2` will handle the bookkeeping**, matching stratified variables with each other each time an expression must be calculated. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. +[SW: I really like this paragraph. But it personally makes me think something like 'why do I need yet another syntax to learn when I can write my own code to cut down on rote repetition?' I think the answer has something to do with the division of labour of modellers and software developers, but I don't know if this is the right time to bring this up.] + # Appetizer: specifying the basic SIR model Let's start with specifying the basic SIR model in `macpan2`, which is the foundation of the two-strain model above: @@ -119,7 +122,7 @@ SIR_starter = function( expr_list = mp_expr_list( before = list( ## aggregations - N ~ groupSums(state, alive, state) + N ~ sum(state) ), during = list( ## force of infections @@ -137,9 +140,9 @@ SIR_starter = function( ) ## Ledgers for each specific calculation -------------- - link_data = list( - flow = mp_link_data(flow[[1]], flow[[2]]), - force_of_infection = mp_link_data(force_of_infection) + ledgers = list( + flow = mp_ledgers(flow[[1]], flow[[2]]), + force_of_infection = mp_ledgers(force_of_infection) ) ## Initialize indexed vectors (to all zeros) -------------- @@ -152,7 +155,7 @@ SIR_starter = function( ## Initialize model object ----------------- DynamicModel( expr_list = expr_list, - link_data = link_data, + ledgers = ledgers, init_vecs = init_vecs ) } @@ -183,7 +186,7 @@ state = mp_index(Epi = c("S", "I", "R")) rate = mp_index(Epi = c("beta", "gamma", "lambda")) ``` -You can think of the `mp_index()` function a setting up data frames tabulating the model quantity labels: +You can think of the `mp_index()` function as setting up data frames tabulating the model quantity labels: ```{r sir-state-and-rate} state @@ -194,6 +197,8 @@ The `Epi` column name is not really useful in this simple model, but it will be For the `flow` archetype, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$. We specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). +[SW: Should we be writing 'from-state' and 'flow-rate' instead of 'to state' and 'flow rate'? I only ask because 'We specify flows using a from state ... ' is an awkward phrase. ] + We use the `mp_join()` function to create the `infection` ledger like so: ```{r sir-infection-ledger} @@ -349,7 +354,7 @@ We start by creating a new index for the strains: Strain_indices = c("A", "B") ``` -Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so easily using the `mp_cartesian()` function, which takes the Cartesian product of indices (all possible combinations across sets): +Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so easily using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets): ```{r strain-expand-I} I_index = mp_cartesian( @@ -416,6 +421,8 @@ mp_join( ) ``` +[SW: This is the first time dot-concatenation has appeared. Should we introduce that concept here or before? Or maybe it is intuitive enough to introduce later as a note? I'm not sure. ] + As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join). However, for this model, we want only two of these flows: - a flow between `S` and `I.A` with flow rate `lambda.A` @@ -460,7 +467,7 @@ mp_join( ) ``` -We want the `lambda`, `I`, and `beta` labels all matched on the stain index. But recall that `mp_join()` only performs pairwise joins, so we cannot specify a three-way join. Instead, we will specify two pairwise joins: +We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective indices. But recall that internally `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins: ```{r two-strain-foi-ledger} ## new force of infection ledger ------------------------- @@ -469,8 +476,15 @@ force_of_infection = mp_join( infectious_states = mp_subset(state, Epi = "I"), transmission_rates = mp_subset(rate, Epi = "beta"), by = list( - infection_flow_rates.infectious_states = "Strain", + infectious_states.infection_flow_rates = "Strain", infectious_states.transmission_rates = "Strain" + + ## SW: for IP -- these two pairs of by arguments also work now + #infection_flow_rates.infectious_states = "Strain", + #infectious_states.transmission_rates = "Strain" + + #infection_flow_rates.transmission_rates = "Strain", + #infectious_states.transmission_rates = "Strain" ) ) @@ -519,7 +533,7 @@ As mentioned, we've hidden some of the details of initializing a model object wi <> ``` -You may begin to see from this function definition how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `link_data` and `init_vecs` are simply set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. +You may begin to see from this function definition how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `ledgers` and `init_vecs` are simply set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. These topics will be discussed fully in a future vignette. From cf48c1ef8eab1cf5033e81ef326bfd0283ab2b08 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 10 Nov 2023 10:15:37 -0500 Subject: [PATCH 083/332] more convenient ledger list construction --- R/formula_data.R | 13 ++++++++++++- vignettes/quickstart.Rmd | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/R/formula_data.R b/R/formula_data.R index 1927a345..770d88ee 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -55,7 +55,18 @@ LedgerData = function(...) { #' @export -mp_ledgers = function(...) LedgerData(...) +mp_ledgers = function(...) { + wrap_ledgers_in_one_element_lists = function(x) { + if (inherits(x, "Ledger")) return(list(x)) + if (inherits(x, "list")) return(x) + stop("You can only pass ledgers and/or lists of ledgers.") + } + args = (list(...) + |> lapply(wrap_ledgers_in_one_element_lists) + |> unlist(recursive = FALSE, use.names = FALSE) + ) + do.call(LedgerData, args) +} #' Indexed Expressions #' diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 966d400b..28cb229c 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -141,7 +141,7 @@ SIR_starter = function( ## Ledgers for each specific calculation -------------- ledgers = list( - flow = mp_ledgers(flow[[1]], flow[[2]]), + flow = mp_ledgers(flow), force_of_infection = mp_ledgers(force_of_infection) ) From 394ac2328e093c43890f6d075c74145cd813cb89 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 10 Nov 2023 10:34:32 -0500 Subject: [PATCH 084/332] sir graphviz --- vignettes/quickstart.Rmd | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 28cb229c..0dcdc8b8 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -11,6 +11,25 @@ editor_options: chunk_output_type: console --- +```{r dot-ex, engine = "dot", fig.cap = "SIR Model", cache=TRUE, echo = FALSE} +digraph G { + layout=dot + rankdir="LR" + + node [fontcolor=blue fontname="Courier New"] + edge [fontcolor=blue fontname="Courier New"] + + SI [label=<λ*S> shape=plaintext] + IR [label=<γ*I> shape=plaintext] + + S:e->SI:w [arrowhead=none] + SI:e->I:w + I:e->IR:w [arrowhead=none] + IR:e->R:w + I:s->SI:s [label=<λ=β*I/N> style=dotted] +} +``` + [![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) ```{r setup, echo = FALSE, message = FALSE} From 383962c75062d414a93c22a677918d2ee59d9652 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Fri, 10 Nov 2023 10:41:03 -0500 Subject: [PATCH 085/332] Revert "sir graphviz" This reverts commit 394ac2328e093c43890f6d075c74145cd813cb89. --- vignettes/quickstart.Rmd | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 0dcdc8b8..28cb229c 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -11,25 +11,6 @@ editor_options: chunk_output_type: console --- -```{r dot-ex, engine = "dot", fig.cap = "SIR Model", cache=TRUE, echo = FALSE} -digraph G { - layout=dot - rankdir="LR" - - node [fontcolor=blue fontname="Courier New"] - edge [fontcolor=blue fontname="Courier New"] - - SI [label=<λ*S> shape=plaintext] - IR [label=<γ*I> shape=plaintext] - - S:e->SI:w [arrowhead=none] - SI:e->I:w - I:e->IR:w [arrowhead=none] - IR:e->R:w - I:s->SI:s [label=<λ=β*I/N> style=dotted] -} -``` - [![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) ```{r setup, echo = FALSE, message = FALSE} From 689a8bb0b21b664c4435b1a27a12fb4d2dbec89f Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Fri, 10 Nov 2023 12:23:41 -0500 Subject: [PATCH 086/332] another pass at edits + resolving SW's comments --- vignettes/quickstart.Rmd | 115 ++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 28cb229c..e6b946ac 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -4,7 +4,7 @@ output: rmarkdown::html_vignette: toc: true vignette: > - %\VignetteIndexEntry{Quickstart Guide: specifying and simulating a simple compartmental model} + %\VignetteIndexEntry{Quickstart 1: understanding `macpan2`'s model specification grammar} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: @@ -21,40 +21,40 @@ library(dplyr) [ +TO DO: + +- make diagram for two-strain model and include it below - make sure all functions used have good help pages -- get model library ready -- consolidate spec in examples with steve's presentation +- get model library ready (or remove text below referencing it) ] One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when building upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. -There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying [SW: extending? expanding?] **structured** models, such as models including: +There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, especially when they are cast as expansions of simple models. Such [structured models](https://idpjournal.biomedcentral.com/articles/10.1186/s40249-022-01001-y) can include: +- multiple pathogen strains +- multiple infection types (_e.g._, asymptomatic and symptomatic or mild and severe) - age-structure - multiple locations (a metapopulation model) -- strain replacement - testing processes to identify infections - vaccination status -- infection types (asymptomatic, symptomatic) - -[add links to papers with these?] [SW: how about this one? https://idpjournal.biomedcentral.com/articles/10.1186/s40249-022-01001-y] -This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. The package also comes with a model library (see `vignette("example_models")`) to get users started with commonly used basic models, and to demonstrate some more complex cases. [SW: I'm currently concerned about the condition of the model library. Please be patient while I make a decision about how much of the library to include and whether or not to reference it in this vignette.] +This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. The package also comes with a model library (see `vignette("example_models")`) to get users started with commonly used basic models, and to demonstrate some more complex cases. # Amuse bouche: a structured SIR model {#amuse-bouche} -A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculation across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): +A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): [insert diagram of SIR with two strains] Here, -- $S$, $I_A$, $I_B$, and $R$ are the number of individuals that are susceptible, infectious with strain A, infectious with strain B, and recovered, respectively, +- $S$, $I_A$, $I_B$, and $R$ are the numbers of individuals that are susceptible, infected with strain A, infected with strain B, and recovered, respectively, - $\beta_A$ and $\beta_B$ are the transmission rates for strains A and B, respectively, -- $\gamma$ is the recovery rate for all infecteds. [SW: is this a word? should we say 'infected individuals'?] +- $\gamma$ is the recovery rate for all infected individuals. -We can cast this model as a system of difference equations: +We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: \begin{align} S_{t+1} &= - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ @@ -63,13 +63,13 @@ S_{t+1} &= - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ R_{t+1} &= \gamma (I_A)_t + \gamma (I_B)_t. \end{align} -where $N$ is the total population size ($S + I_A + I_B + R$). We notice that each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. +where $N = S + I_A + I_B + R$ is the total population size. Note that each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: ``` -lambda.A = beta.A I.A/N -lambda.B = beta.B I.B/N +lambda.A = beta.A * I.A / N +lambda.B = beta.B * I.B / N ``` However, in `macpan2`, we can specify a single calculation archetype for it, for instance something like, @@ -77,13 +77,13 @@ However, in `macpan2`, we can specify a single calculation archetype for it, for lambda = beta * I / N ``` -and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a record of which specific `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. +and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a table of which specific subscripted `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. -Here, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider an age-structured metapopulation model with 10 age groups within each of 10 patches: there would be 10x10x10 = `r 10*10*10` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated in each of the 10 patches). [SW: I changed 11 to 10 to be simpler. I know I first said 11, but that was because I was thinking of the specific case of NL that has 11 public health districts. Here we should be more abstract I think.] +In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each its two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). **By using calculation archetypes and ledgers, the modeller can focus on the modelling**, like designing model structure and choosing an expression for the forces of infection, **while `macpan2` will handle the bookkeeping**, matching stratified variables with each other each time an expression must be calculated. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. -[SW: I really like this paragraph. But it personally makes me think something like 'why do I need yet another syntax to learn when I can write my own code to cut down on rote repetition?' I think the answer has something to do with the division of labour of modellers and software developers, but I don't know if this is the right time to bring this up.] +While a modeller could certainly write their own code to cut down on repetition when expanding a simple model (and many do), the idea behind `macpan2` is to give individuals a ready-made model specification grammar that enables easy model extension, especially when building [product models](https://arxiv.org/abs/2307.10308), and that can readily interface with blazing fast simulation engines, like [TMB](https://cran.r-project.org/web/packages/TMB/index.html). # Appetizer: specifying the basic SIR model @@ -103,7 +103,7 @@ I_{t+1} &= \lambda S_t - \gamma I_t, \\ R_{t+1} &= \gamma I_t. \end{align} -Since the focus of this quickstart guide is `macpan2`'s model specification grammar, we have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model using the model grammar and it will output a model object from which we can build a simulator. Our focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. +Since the focus of this quickstart guide is `macpan2`'s model specification grammar, we have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model using the model grammar and it will output a model object from which we can build a simulator. Our primary focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. ```{r SIR-starter, echo = FALSE} ## helper function to simplify the exposition in this vigette ----------- @@ -171,12 +171,12 @@ The indices we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ -We have identified two useful calculation archetypes that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: +We have identified two useful **calculation archetypes** that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: - `flow`: Unsigned flows from one class to another of the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. -- `force_of_infection`: The prevalence-dependent per capita rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows +- `force_of_infection`: The prevalence-dependent per capita rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. -In this case, the `flow` archetype is repeated within the model equations but the `force_of_infection` archetype is only used once. However, we've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). In either case, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. +In this case, the `flow` archetype is actually repeated within these model equations but the `force_of_infection` archetype is only used once. We've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. We start by creating the `state` and `rate` indices: @@ -193,11 +193,9 @@ state rate ``` -The `Epi` column name is not really useful in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. - -For the `flow` archetype, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$. We specify flows using a from state (`from_states`), a to state (`to_states`), and a flow rate name (`flow_rates`). +The `Epi` column name is not really important in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. -[SW: Should we be writing 'from-state' and 'flow-rate' instead of 'to state' and 'flow rate'? I only ask because 'We specify flows using a from state ... ' is an awkward phrase. ] +For the `flow` archetype, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$. We specify flows using the name of the state from which it originates (`from_states`), the state to which it goes (`to_states`), and a flow rate name (`flow_rates`). We use the `mp_join()` function to create the `infection` ledger like so: @@ -218,7 +216,7 @@ mp_subset(state, Epi = "I") mp_subset(rate, Epi = "lambda") ``` -and by default creates one entry in the ledger for each combination of these values (_i.e._, a "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger in this case! +and by default creates one entry in the ledger for each combination of these values (_i.e._, a "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger: ```{r sir-infection-ledger-2} infection @@ -284,7 +282,7 @@ sir_simulator = mp_tmb_simulator( ) ``` -Note that we've specified `NA` for `lambda` as it will be calculated internally using the `force_of_infection` archetype. +Note that we've specified `NA` for `lambda` as it will be calculated for us using the `force_of_infection` archetype. Then we can actually simulate the model by passing our model simulator to `mp_report()`: @@ -293,11 +291,19 @@ Then we can actually simulate the model by passing our model simulator to `mp_re sir_results = mp_report(sir_simulator) ``` -The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer) that can easily be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`: +The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer): + +```{r} +head(sir_results) +``` + +[IP: small point, but i assume the row index starts at 4 because the first three entries (where `time = 0`) are the initial conditions. it would be a little cleaner if those row names were removed before output from `mp_report()`.] + +This output can be manipulated and plotted easily with standard tools, like `dplyr` and `ggplot2`: ```{r sir-ggplot-example, fig.width = 6} (sir_results - |> filter(matrix == "state") # keep state variables at each point in time + |> filter(matrix == "state") # keep just the state variables at each point in time |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in plot |> ggplot(aes(time, value, colour = state)) + geom_line() @@ -311,7 +317,7 @@ If you want to use base R for plotting, you can convert the long format data to ```{r pivot_wider} sir_results_wide <- (sir_results |> dplyr::filter(matrix == "state") # keep state variables at each point in time - ## drop 'matrix' (has only a single value) and 'col' (empty) + ## drop unneeded columns before pivoting |> dplyr::select(-c(matrix, col)) |> tidyr::pivot_wider(id_cols = time, names_from = row) ) @@ -319,14 +325,17 @@ sir_results_wide <- (sir_results head(sir_results_wide, n = 3) ``` +We can plot one state like so + ```{r sir-base-plot-ex, fig.width = 6} with(sir_results_wide, plot(x = time, - y = I) + y = I, + type = "l") ) ``` -or +or multiple states on the same plot with ```{r sir-base-matplot-ex, fig.width = 6} par(las = 1) ## horizontal y-axis ticks @@ -334,7 +343,7 @@ matplot(sir_results_wide[, 1], sir_results_wide[,-1], type = "l", xlab = "time", ylab = "") -legend("bottom", col = 1:3, lty = 1:3, legend = state$labels()) +legend("left", col = 1:3, lty = 1:3, legend = state$labels()) ``` # Main course: expanding the basic SIR with additional structure {#main-course} @@ -354,7 +363,7 @@ We start by creating a new index for the strains: Strain_indices = c("A", "B") ``` -Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so easily using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets): +Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets): ```{r strain-expand-I} I_index = mp_cartesian( @@ -394,7 +403,7 @@ rate = rate ``` -It may seem like overkill to use `mp_subset()`, `mp_cartesian()`, and `mp_union()` here to define the indices using the original ones from the SIR when they could be written out by hand quickly as +It may seem like overkill to use `mp_subset()`, `mp_cartesian()`, and `mp_union()` here to define the indices using the original ones from the SIR model when they could be written out by hand quickly as ```{r two-strain-state-rate-manual, eval = FALSE} state = mp_index( @@ -421,9 +430,9 @@ mp_join( ) ``` -[SW: This is the first time dot-concatenation has appeared. Should we introduce that concept here or before? Or maybe it is intuitive enough to introduce later as a note? I'm not sure. ] +As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join), where the individual indices, denoted by values in the `Epi` and `Strain` columns, are dot concatenated for the full quantity labels. -As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join). However, for this model, we want only two of these flows: +For this model, we want only two of these flows: - a flow between `S` and `I.A` with flow rate `lambda.A` - a flow between `S` and `I.B` with flow rate `lambda.B` @@ -444,7 +453,7 @@ infection = mp_join( infection ``` -Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list name (`to_states.flow_rates`), with the names coming from argument names in `mp_join()` (`to_states`, `flow_rates`). The value should be a character name of the index type upon which to match. In this case, the value is `"Strain"` because we want the to state labels and the flow rate labels to match based on the strain label (`I.A` with `lambda.A` and `I.B` with `lambda.B`. +Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two of the index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list element name (`to_states.flow_rates`), with the names coming from `mp_join()`'s argument names (`to_states`, `flow_rates`). The list element value should be a character name of the index type upon which to match. In this case, the value is `"Strain"` because we want the "to state" labels and the "flow rate" labels to match based on the strain index (`I.A` with `lambda.A` and `I.B` with `lambda.B`). For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: @@ -467,7 +476,7 @@ mp_join( ) ``` -We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective indices. But recall that internally `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins: +We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective indices. Internally, `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins to the same effect: ```{r two-strain-foi-ledger} ## new force of infection ledger ------------------------- @@ -476,15 +485,8 @@ force_of_infection = mp_join( infectious_states = mp_subset(state, Epi = "I"), transmission_rates = mp_subset(rate, Epi = "beta"), by = list( - infectious_states.infection_flow_rates = "Strain", - infectious_states.transmission_rates = "Strain" - - ## SW: for IP -- these two pairs of by arguments also work now - #infection_flow_rates.infectious_states = "Strain", - #infectious_states.transmission_rates = "Strain" - - #infection_flow_rates.transmission_rates = "Strain", - #infectious_states.transmission_rates = "Strain" + infection_flow_rates.infectious_states = "Strain", # first pairwise join + infectious_states.transmission_rates = "Strain" # second pairwise join ) ) @@ -515,11 +517,14 @@ two_strain_simulator = mp_tmb_simulator( time_steps = 100L ) -two_strain_results = mp_report(two_strain_simulator) +two_strain_results = (mp_report(two_strain_simulator) + |> filter(matrix == "state") +) + +levels = unique(two_strain_results$row) # get state variables in the desired order -(two_strain_results - |> filter(matrix == "state") # keep state variables at each point in time - |> mutate(state = factor(row, levels = c("S", "I.A", "I.B", "R"))) # to enforce logical state ordering in plot +(two_strain_results # keep state variables at each point in time + |> mutate(state = factor(row, levels = levels)) # to enforce logical state ordering in plot |> ggplot(aes(time, value, colour = state)) + geom_line() ) @@ -533,7 +538,7 @@ As mentioned, we've hidden some of the details of initializing a model object wi <> ``` -You may begin to see from this function definition how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `ledgers` and `init_vecs` are simply set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. +You may begin to see from this function definition how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `ledgers` and `init_vecs` are simply set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. These topics will be discussed fully in a future vignette. From 68ce2c153c4b57d3b210b93244ecd4e2734e5c2c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 10 Nov 2023 12:38:17 -0500 Subject: [PATCH 087/332] quickstart --- vignettes/quickstart.Rmd | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 0dcdc8b8..b3648829 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -1,8 +1,9 @@ --- title: "Quickstart 1: understanding `macpan2`'s model specification grammar" output: - rmarkdown::html_vignette: + html_document: toc: true + keep_md: yes vignette: > %\VignetteIndexEntry{Quickstart Guide: specifying and simulating a simple compartmental model} %\VignetteEncoding{UTF-8} @@ -11,24 +12,6 @@ editor_options: chunk_output_type: console --- -```{r dot-ex, engine = "dot", fig.cap = "SIR Model", cache=TRUE, echo = FALSE} -digraph G { - layout=dot - rankdir="LR" - - node [fontcolor=blue fontname="Courier New"] - edge [fontcolor=blue fontname="Courier New"] - - SI [label=<λ*S> shape=plaintext] - IR [label=<γ*I> shape=plaintext] - - S:e->SI:w [arrowhead=none] - SI:e->I:w - I:e->IR:w [arrowhead=none] - IR:e->R:w - I:s->SI:s [label=<λ=β*I/N> style=dotted] -} -``` [![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) @@ -67,6 +50,8 @@ A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repe [insert diagram of SIR with two strains] +![](../misc/images/sir.png) + Here, - $S$, $I_A$, $I_B$, and $R$ are the number of individuals that are susceptible, infectious with strain A, infectious with strain B, and recovered, respectively, From 7e548e2e83128eb7e38ecd5c12e90629e3034da0 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 10 Nov 2023 13:06:50 -0500 Subject: [PATCH 088/332] fix diagram arrows --- misc/diagrams/quickstart/two-strain.png | Bin 26067 -> 25833 bytes vignettes/quickstart.Rmd | 1 - 2 files changed, 1 deletion(-) diff --git a/misc/diagrams/quickstart/two-strain.png b/misc/diagrams/quickstart/two-strain.png index 62ac53ade40fdeaeb331177a0eb62e4decf69db6..fb7122f3353dd14b0c9102f809ca445e02ef278e 100644 GIT binary patch delta 14100 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�t-HO9&K%Iv`$(qWM(Gr9mRil+_RXNCT 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 lz?K0Mrd5>K=J$`;kRTLS{AkX4x z8e6mH-6c96@E#@f!rhFYiV=Xwi@@;v5MSCSGC{*EQ{pV~gaxwi^ijWz=%RDHe4>ia z6`F>C0WQc-!T-i39YmG$B(b`dBZxNAO%h>H5@+ejpx!$Xf>^U7hrO}A$Y0eiHTTbX2} zvwrrpI`mrcjie96>eU%|``54^k}~jktJxquT&U0C6%#l(Y&dENrN_Rezq1G@^hc+@ zJguhk>T_c{c>EaSffnbnj$Y!?T@zIn?Q+P;r_k(6wu;7BW2o5gg$*Qk<|-VNsw)3% z3fw$9e33mjS9dhod7$8Q_pGgKv!%GMWvq4l%5<}B+hM1ruga!BH9doZ;yRqI6H*F= z4xlOyCA`?#aG9<{;exo}gJ|4KjL3nyA`avlI~(*n1NPDb2nHh7hKu_W+z5q#0KfPj zyA1U|`?&w#r;QMPdV{)jC@HOqJkzGW?vA3QpzKx_L7*ge5;@hK|NbzjlNdvC&}91V zEqy*5l-rpgzk6=(GF|KC5$@8TDp);_x|+Q_UM9bBqkCbfX3U6x>ygd4)YG{cGWkD` znpP6k`vQ-qpSHZ{be^L)Tj z7UOo!xM{VC&+Lzji(c+G9|MkN$1k!_G9Mb&G7bmCh7gS?dP6T4SqchD{pxJC_AKwI z-dNcU9!6=moH|BInQ=oq<`)Y=1f^DAVh1y(?A`=yZB-A|a>PnQZ#p9FiD?BY?#D0~ zX32UnDJdaJKGYf&xd%s`{rR=L(y$cCpITkqFqCE%=)nEPze+Pp_J>gOE}T&mWsPaV z&qMcA!p%?@UOtfC53#r?HE${~2BXPRyKKJ#`M=+FV=2bKz#UU_Hm2D*U_WY{44Qt| zP8$=b=9@Hmdw1Wmlcpk{p7UKMwaZe<=TM0ynQqxSRg~;;PBZZ~v|tkWeR8la=kNZj zvV;BE6j(F67)B{?*XNI^Vo?J1vd?2)Vk#gTe!J}iSt(x2F;Y_wg<1Nzg!+N~4;4)xn!>@r)bCx(ZU^zU%Lf!C<(Of$IMg9y_RrFReyDUv4g6Mk(!&pXv5c#AHT4?D9w5{qGnN; zv#HK>jnX3FX7mf+K^fHg#6Y@O42ivspc@|Q;;#XnK}l%>&#T*3_f4xECad0&GKd$A zI8E0*eHzZ}{mP~0hrSSP-Eyp)QKqB|NlENg3C6ARyPF(8)LL{%!fEUoEVAFou(r0i z0ZOdTrbe|?y86WYw*@71c{R<<_2TqzH5kbN=L5$d#jBPW)CsEJY`;T|&V~RCiSk*D z9@;B-({Cw)Th^eginc!4d7fHnE{^Jmh3Y@W%56)lU><}Fml!q%KkTHEPU%8QkiEbD#fgL%- zso}O-@v5(m6?Ohi)@x(swE1``iYD>ia3K_GJo%lE1MaxT_Q|*^=qH9e@OUxYdr1DM zR1w{Ek?p%gc!WAw`}$<@s(0J6Qs1_ho%^WsH>Uz>vcy1G#eQ|Voq*MO+O_EeDXW5i z=+)aRd`9Pm3TW%C=*eokhjzX21@oBcaxUCHZ2KsXxJC%~&yw@0mD_0^{annW+5S9T z+#%8H=dgv08rvIn;g;_W`A9M6OIksr$9yh2WqV-ztYSj)Rfl`3B<+Vk*+f-tczn<> z_C`TSoT3{ZaV2GTz07f|W@lHe5Wc907p$A;2qk8wUC;8F^YHwilv6+PcXQlluTzCJ;A023W8mp8?#?NWK-Hd?qL~(*VS-Y6 zm_(85?`}6BqDhz-7L*$vKKhuYzO9D9O4@&~ZWjj>n*Azw+wWSxh_6Dgq#=YEb*bv*inV4-6EbO>_*MAE^pQh@@aMjp?4x9|KiNX^ z52u`j4N~Fzu(|~0uunaFy3X!MdPR`ga1=_(@V z`n_h9cpKDCWj_%CnhvI2Q51IvwcoR9{fYDL(TwM8KtyihtPtzHE+ek)2fQ&$A$TV6 z_sqr!Bx$h~ht=9k|4t^#GU z5n&JSUVKkEdHm-l#FMD%6S8jLMqXDUELX|uPMH;%ap#-p6gdJ%K0jw+RaD-eo%wk; zyF$%OeQy&RSK%Xbe~d8OT}kMb&xQ=OvS_vKt)I*WJDpBI1Ayf`zeJQb{y;+W14#c7ryqh;sd62(c(7 zL=;!GenJ2ArV0l?a~EAeqI!S9M{u~MrYfj=NFqbk?t~!|7f|*(5o~Nw4cz;rP+YWP z2AF4o;fq8Q2qXvrF6O%a7M~Izps!$*qowk_Lf{%G_)Hz79RdJSye%#Qo$V0!ym7AO z#q^X@`EM4lCeRbI(C0$FCTW3JsB*y)5`v+}eC|3gUt)&EIQMjJn6}|j7Kf^^9kCJ4 zKS9Y?MPh&P_1??oKE6RIlzgU6P2`Qh=G^-PYBe3%r3U zL8mp-?zhD3!5%+l#lFn~nnc*$E7Ww02(0jZ6j!!8q z^d-#l4#t#p@*)LyKO)e0_{dJ;YwQbm!3qyOgGF_Vg{r>R01Nm><7=e*XI%&Z;r#8j zf#fs ztU9tpDm~|*LQbfF$R`F<2;|z;zkzZXif8j8maMO1=jNy%&7TdIRz3am%c`Wj^?7s9 zcR5|~M&xy+5cT%ope`$z=ef?d0KIhxLjw(ebAh29RM{`_;zEt9tRp_uPC4%3>+EO% za66FPsE$HaF~Pu@PD7d6T1=glixJV5mUsnlt^-05@iTpFh2cd3MMM%I_C3EhY`^p+ zN^oK#Z!0~d1uN)>cec!nu1W`oklyVMUlH4~I`Aq}`tu?RDXKoJrwP^Q8JI}SDQ;x( zg8S2Te11Jl4aqla3xsxz7F#HjO?Q)i4q69|0T$pyY+^-HSg-0cXGK^ihRcSy1)#?CB)I z%jLMc&MA8OYH`DfAAf@JM3r6lWMyQKeOAQLHdW&^9gIiJ#Cy0kIa=1Z!AB0$h=bI3 zUz{D$Q5LA!n5yzY|0t3Y(+Pbfm7Mt0BEf0j9J`tMwAH<_e*(My&i9B$uMaP=t$)he$?CfX~c^0R;ks$!an&ut#beMa@ROhU& ztn)xMSVns|5;?p1niqnJjcE-9?_7teU7jC2{0!;5m-dSV3Xl69r{@a*#MFnnImgYi zrZSbpcPckAZj)6uzbKK3&#_BOEMaYc5l?QwAi?_&h=iz{eK8#x_8}hhIce5+jYLyZ z7N{D&<0;-jEI0YTtLKWm%}r@>`_Z_3KgDjAy?@$6b>3Uxh4wBKX+xn4a^H@H%n%bl zKK$cQ5qbapiFS$sT#Ak|HC}X7nr1$t+vjAY$dlEEmh!YHg`{`o-(6v;s0T`gd{6`;N6)SbpnA-V1PETT zzG)U2MvwCrUk06@+$SubU3zFlMZdm8r`Ia}i!Gi;BZN)^mw^9DYdRw`xCJ9Z1nRx3q>=4B%NBZwYph6FjhQ3!DEj=pu+K`dO~ z_z##?&HQ}?(!8hbbc27|Z#@fpI(w!XzsCCu4y&nHIn=iO(A}%% z-+&uwdCWRsRe-1;Kn|MHHXe)Ss8l zXd_x-)2+Gaxdut(pzM5TOumx8w6w@ql6lZ2Cip_+C=`Y9#V+-g8GuzW7zso?^v0Wp){>K zhH*OX?xp>kz*C3zH5R>AScSSH$x84~Ib zaCg2?QtIdCEp~=C?T`@=ubv+ak!xb=-RnPgRk}9*D&U)yqIIc|zOi@A>_NhEXW}AK zxe);Ksl{+s)V8J7YWb1Ps$Qts<2-h4G_gugP$&C1T+3rydsSU0Oxo&ey4aVp)>Fr$ z6N06_8~7v@UnQpp8rr$Do+Vmb8!OW8oDKGB1jMVpt?g*;MY?Jt?nETy0BQ4g<3}}X z;BmW2Z(dMx2L+Uvk!m3?Tf9-FU}+RYC<>x=X+laCPJOq zk+iTfHCGdJb50LZ^f3V8GMgSNJqfNFeb4x+hdfLedx1x-q`E^TCgiK|2;y41+|KR+enTJ)Ej#MTOl@Hvr^zt**5C()eos zaQdm4q<^$@yevQ0)pkY`z6QXY-t+X{-pji#}jT|dvzm>}!iui;_Oq-l+ z*m8NgIej#bVx59ocUEb?INEjrYnt-}t(hrV1Ded+j#p!wjBtdOCZy0%&@QnkKDCB1U$KoQiT&uQTy%( zbY;OG^m>v`*pVV0@uj@`5~2z0{eDPj&4Y7>a=e|wswpKA`L$P8Te;yZ9)@Q?^pFzB zuPhV3*#^@>;~bBo9^OqMp~Sx}M5|Aq_R0z1RhP7{@5Vj^UYvTL{wkC!*`2?-tSiAA zaCtq(__Vol7OX|tvujJFs^r8KUYldFFi+wz_WP1jhSrI<6>5ujqu}Ci0uIxwtuf?@ zFN?P#7>wJ%fjefSs3t^7O+1~~LwCsX!|wu|**^swTC-4DPHNSc0F>~r%TjxOm3Q-8 zwPH?ubW7A4Bll9qzPOqu(hPytvrl-%R>;Q4t1;&LEl+t+r=2r|#GO<|xzbuXM$KbI z(ipKM+GUD4N+2oHD@kp#UK>c?J_S3uzVtkd`t@}e)oY@vQ+le7?gpV|H}l%_Y{g7p z{H__RwnGgODQDe9waURh#gfJ;GryaVjr3QuFu=G=(<9B!(;3A`o1;7jWxjki=J)Wb z?5boSB&c~huOM3BzLvrVwWj92DAHxxUHbE(n5-UW& zXu`EkRA8<=(BL10E)mZXmp-9guG;R!YU4B z!j5>^bl*Z^Os&F?(3G0r%EIG8H6&t(;>s#vDfSMK`tHj^^*MfAHEmTzRYL*>Y1czc z=<{;FhtTW0-gra6;%nZZ86_9{4x+ZS5k#p*-9cp$a%!>CZ`b;{H4OSpv<3Ff-dx2w zqccPzl&&)$=ACA%I=t`e4@; z(zkxGs|}oOx)y91F%PXm!e+lKSSE{qqu^U$W0#@AJ&%aMs(;(uTO7V>{RSG$3sbIu zxDI9%;7iQkZv$1_Aez7bs>-$G;DZXiQRqTiFcKX~A#V2W-7i^Mx_82;H~#w({Dhz6 zZAUHHmuRWuyJLh_Q(@m04z)!zAVDM0jddR8^gDi40T+MA2j4AUb;esH?$bPpLp!TZ z1sr^p^81p!XO4NY98+P+`#Cbofr&}y9=hHeMwEB6y!#qYOhD!3aqen2Wk^OlyczLH z>UAP41Lt8kNd<>S!i%^H)TXG~No0)tB5mZJyWPR(pc;%age<6pWUxVbH^t@^@olq1 z!t=yG^3-*ZYxF+C4obw=in|3AuD4Rh?CghrGkiiN23}?M{FO?zM@JTM^`sRPZn(j0y6m6EMHH?C}Qo_;A5;6`-x+OA%tvde! zVf=qrm3eOpGp$kTm{w5k66?|2nS}30HBQwFxP?&Do+Dm8W#}bNjkYg?J*8Vi9VcypG@exvH zl#S@SvI^waziGxdf=BXt>(0EQ0;L72(janM8214^kYwO}f0=vG1@A4; zifYh>v&$4^WMaQo64muUK-M%=SzB+U4~t7gQ}C$a`p>sjLDW4a9dzKIgaH&M{MG5h zMx9X*MtPkx;^oen0j-6akeucN$Z;sJGuQ);$*kd{-$md#Fv{J4t3IeeuT7yE;k7mj z6yW&z>2L{HqV07#fDXcJgR(0F)S;tjPAN2oGs#_pZ~O9YA)_& z&}ZVDmOtyfSi*$9wp4Vi|vJ>DZv?mAc5mfB+RQBKULbE*jcmXmz61DJa@ostk zx748AG{K9SuBS&zUj;BMOOE)i%hgd-&%ir?O7VVcgDlZCb_(Jk z*r70@9*gH}GhJ+{xiok@Ll+rKJ5mLoOpQLg%iOE1l?<7GzA5N5k{|KA@(W9B13<1O zw#7?dR4h;%I2W*&(eKo}XaNfry{@)3FXOp}>^*M!;=|vc-#YQhnCRsQ5+G_?kTWy!1&Qh zQ!6V}F@l4_=W74rEtbGcR9cxZ?It}`s01KnoB@NjAs@3&=0_^AGhvfQG!b)>N71?DrFS2 z)qtK;WkSjU&`slEk6{)!UiJPx)u6GTqZDGvD=idX=~^}j=GO&h+rA6{7!C~Ab`X!{ zwk0|#bmWde2eZ%2H^3uqP72zVA}rCbK42w(j+P`X+4m$Bbf&)T2@6=zLB->4WX!fa z^CTejB?TrNr8sk;?m%MWM`3F29B_HEUi+&clc6{9+70~rVZ-l*<5cW$fxD&ceg|ul z$-KJ8Y@WbzC@U)jR?qztjmOve&vy5|=gxtF-MkT0guo;y&jmT}M&A)Cv3C*0~ncizk3hFguM(&ii}5jXwpPC^qZ%poB?O zE@D;}sBpdz1SgydR^O4l`mN%$0tYw|J5^ZO!N+h#YTkd8Rsd;^)d^rq)a-C{NP~rC zY3BkSvC^s!thdiAlE|9sXpA1aZui&*FGHUkvrih$U4%MaZ(Y24JI2S2f}{7?`pSKn z*bbUP%BZfxa~?sCTT#mB(2?eD8t`8dY-5UQ|G(=l|9f*WGemPXY>D%-FVF$!2mJ@N zC>KwtsoMz&N<@I}!XH%uOUnYi<{bDBui_Q_2}E-p9O<#;14d;-nYvOnnvZ^I2IK+i zk?86`&aGT4t;Ck zn3?ZDFpI;t9ln05F;VJuW=oU<4qM~%A7Z??BbK*e5@s>f%t62)oeb#R9((e%22ggX zQ$?CT8m}RKYEWSfpjZ_bCNgUvkX6N+{+7+T8F8PpfM&DZxveHiX_08}SUL>GcJE$l zFBvk#eA4}l4Z2$V-D<;4{UpGr%fY95I)R+9f%U4c1u=z7&5fRXbKCetiUdOK;$<{l@ej~y%ZKh+43$u@$3;e+7wX40?$0XruBUWm>#%T z_Z_Otk?s)qd6oYa$!6ArP6GF^N4RAu1@x*+w@}Nufr=ZBR#v67aGtfc#G$m{`p)E+ z*cndlGq1)fZXaKbb0hGUm~FI)|K4Xw^G`T*!qL-2aP;v$q?8M{80#a^z)!D?agS|f z&l5l9rbtsle=dEFQbrrK`LUyb3`xvo^5+V26ASc>ER~hP4Udm?eGVQ|E`laa5>!3s z``jHcufgbtQ_0-y$}L>d2c4sL)aKvH{^5p}k|nFJvsgzhrsdx|{mco&!J)f>%@RC~ zc!GvJC{RJzdjt1n()|nbdT?W!>GSvMCtqAB7Jk>wyMj}~17#>S1&%E{;6KEjDtlUXJ>GmwThE> z21L0GRg~J@HeKuD1jLExQ1_ET?G^RMFC7_3Gyr??4gWE{Pb z`A3}^4W$m@_9(B8M004<5)dM)fJZfXIO}891gy!|c+@okf4-^I0xCOoFm(4RpgL)c zVR$pS_Xt#QlW*mJ_$PL7XZv=Lp%jkIn}C_~yR>@HV_RoL_zd&E#2uCwTes`7**;^r zaEQcUh9(2So$Q<(+1^TkMTt4>6O+iq#z4hd6=r!Gt&CqPK3;$A87sfv!vTy)=MvDK zUrnc(^h;w2YYdD0RJQ+q@2at)@y`EO?Lh&#rw9=uHvoZ>ink$VpgZ3&v zM&yUc&;Pp2WaGM8GVd6+>9=V^m(qeM7J1 zh_Uy&g>D&FS0(Hn095vUW~>yD^V#0ga@5-5)h(D%INMr&M`KD8h|-S0-+Tv<$aZB3 z(7HtSCd_m4i}sauJc49{CO{6?7)fDF#=uK*g!+|>yO&Tq_m>?oUX*A#1$ZPGFEA&a%49mWT6uUNM0V* zSD+`Xvvhqy5pnURJfp3AjBy)$EwlYWuz4VP_gc6EfH|)j2h_C;)|sVDhv`cI zO_#LlCOHC`gsHyT84vZNCUAyNN={lQRiT_Bq7nqsC0#PG@`_E{)j|llGYA)`M>bKh z+Swcf75O>YP({oxj=RAUav^#8Q^f5MXAcpDu}8>`=m*~|)^#zPYR}_$CjiUuv+T#3 z3?yPoUZi z)Ooxt3mbKCU)!)TE0s2m$VnjSV0de7V-utBx*OR3wFIpG!zry4m_ZloK%ZGJ1C>;4 z4kTU7uF?4*h=Aqil0?H8%d)$C#C2NHBfI}41~mZLGBVb|<~WA+u7Wsa@U@{d;K1f} z9JB3|g`AtB0#9tFK?9hbX{YYbw$trIuCy9Wo$w^r^X9&Kxy9r3wh^bJQ}oWS{3IqN ze3yv|3o?V!_IqS9)(uZ)uv!Q!?o^J_jiag%tmk+*k3xkzc>Sn=Z_!^5qRAvDJ26g@ zrd;zJ{b*(TWUaqneiFQUV`P7fZvvrMqL^+Jo7&l-I^%jd|ph)spP+NMN#Jpq8;w~va2ksP6X{OKw297`*r{m zCNb9<@FLuSzC`2$ExS`#CsijbEguAlmWrt$N)DzsSa1w=kHCUegE4p_T2qyo9gX29zJ^OGRqixi=Bit5Us&cpjdnQ1?gP#dx*bR68s{tXQ!24)me*QC;Y-pdB1<~6MH{m28&t!69 zTzV~H0q5D|jPzI7U`NIOI^8Vy*^FEQ~@spDNRt+~iZELOLaLyms%tK;{JamAH^UlQVn~f7s1!u@g z;gQG_pGS@dBkIC3@BpBKM>t0bT6JkviP=~Zj_Ko zk&(VbC|tykMa`CWw0zPjJKaHDO=nHvHmePY2$eCDXHl=`GIgvwyDJQbu`^&usq!rZlWJgHivHpgADIN4 zA6cT2XmOFbz_Z`KUs=TxW=Ruqm?<0x9!enHyWWsXFW_(!&Ec}sjd6veaA{bP-|rN^ zisIdKfm8ajUn?k;$B4NlM9J4l18c2YNfE@a$C_tq8TP)1^dlUMnv;+&)eke8QnM>ndls|x`Ku+gkN6u12|6UF zotLvYZNIKg^B})$yg1)OePJ#rRq0DD()V3Uw}V?{>;#k?$WmH(<(X`oBBPV}Du$7- zU^p1NMX2s|9TQGMW%Q>8L+uR+f8s44a-UlUac(z`vD%R90ktmfa>c5MTAzW&>jo)y zMzRPXsC&pfw6XCB`UT9S-=zpgqi;0fKJf{0!48$~ML#d09Vn<{D0K#HvlbskC@GV7 zC#^d}rE3=K8?Nt~(Qvy|ZKqLidwycFFzqA4XI!O7u=T3})X-2?lqAT)WUFVghs3dUGt%#0X8nF@* zYX78Q=wlxTRHJ0=0mAj4+4jKt*3AG0Xh&WN+w}GS=rzB8B+YCoqkAphCqUW$BckB; z{CIaAKv*MM<^L)*d3@z)S0ShXyG?;R-#>loo%Xsyp8)iaHvg@}-hXNiiC{{wmL3B6 z$XCKw4~Ri1pavPxw4Pp_1FwE9JgJP2;y=q`%Sc&nyw2Itp@-t1V0c98EW8i~h{fy813l{NCt%eE=-xXv3bAzv__vo)#4j-% ztV)3W&ps?Tds`Wu{Y&y#q#3yGpH*Kc-UkqTWaGyQ2vqc7gJ}vpzcRkFv5iYh6z@NH zM44-_A(G_8^pO2QYFDRlNej5!AuvW_%q{dsB;|eG=tRwLZvD@NL0Eww2Ws5xTgm@c zn>g;Jz{c*Y_u=UvW1;uy{#pZgQFf6DC}~b?`Y``TNiQMDlaPa{`9B(wc(0^b=I;1- zH6;fMFaN%WGPG*w{+5zwt@~`NYDO0X6At}02LL;`$q`*d$C(Mep0?)-5*f5Oh#p3V ziDAb@%6MgF7O}sN#wMC1z_(qXq41r@96>;50|d0xSW^>FYi!A(ur>F9@Oo8|ZXVB2 z*G-UWpm167CDM+TU;*g~L2Pa;3g>0@|N zhd9JRsfuiUEkr|6tRlQL!5U{NPNJN`a20DlQ4zx7dK+3&-M=5Yq#?Wlf zy)Y7YSJ9ZS^1i#~M?M#UtJ1y&IuS|O9>Du35pfgjj0)_|h$_EN2ed3aEWgZwMezK; z=?AkIY8kIJ9107Jn{{#eD8L)Yyo7fyuP$JCo87`2{)?~}8`uM&PXb&Z- zfFw$8BnUV@e1c7|w4QF%b~8BTTT%;~Hc!mG*pWZGJlpvIGRj(q6{I7l0A>9KmHzX7!K>}&y)s}`g)8c}U`Pgas@ zcUsRH+r+q4chl{%Bc5G}RBcQxfh{W_beS^8Ox2Yrvr+WcdErygoGW6**e=U!3jp2< zpuoyTkwxvk_8fG-A{(u^^yPH{ieM?w*J5*yGv*yjKG52}s&XoP_-5PGQaih+{(lP~ zAE7>yjaitdc+7(&=`1tcqlBaVO|cdf{_w|VTls?lvET0tvpvui+l`ygxp_t5Iv{Uc z18keGz5hbq>bqe1Yf$IQthqLU0H(e6@&+htx+j6--VwR1qJ4eS)#-TKRu}a{V~|32 zF&)Hd-Dy6Eb)Pq?actPM9m;k=fe_A{i=nIYht^cFAd5_k2dYuqd}yZt>zP&;yEgCL zmjiEafAevS0hXfF0{2^*E*qa0{<3QsNNP0GZk?p})WsaX^K5>58}6xHDezWu#D_{i!|4uC7oc-SP`7s(IG`;H!5ZZ(;qk308^&MKk>YRX4}*_i!Uk6jU(_fAoKI z$O`AXoe@ax==AcEFJAnf>h5xOUo&i;I9G-O7BRQ+qh#F@O`$AXRBZtq@#!KwIsaRZG7&mdTWhfWno$ebm|U8K z)D!jVzrf6WtSo;mDu}J4fH7{~uz8HCYDES;ZTXuTRh|0Ox{guG)i_n6^?bko1Bh!K z0;8}2K`68j645V<9E~8`4dUsHjY!R$*^R6qmv))*O-zV#D zd|*9HY+Ey7DaRWnJkVKfqqcb-D!tC>!<6BOC_cAB+8x^)WQpBr~-S9KNX7TsVpW%CwAg-Dw z!j6hcSC^$AQp20E+gP;+v=Y#&Nt=|^^2Z1?e^QGm6c*+)W@y*Y1y`i+2@ya(2SU-q zTZBr{jLsE7Uo)kK3_}KXFdVTJARyKxnwoP(V_>`psfMb@Dj>V#8DZ+3g7YqQvs-WD z6siE1Zggg-AAjQ7U~}u+>LB?oxo4=K=J6K8;q$j8Q?ZKiP6d~3Q7SVi#_V4iq$m*m zFi>{mcpB6L8e(lL)Jb zr`acBs03Q`@27)({TK54B#e>+DaGb~OH1%=AT%~{+%r;q6ib2QX3e9vULUlm1pDkE&=#K3f)&NxIj~DxxJC&*_jF!t_e*( zQ%r7G!c-_ltb5jM zb*t0eY20JT<+dtldJbMPg6_~iY97xvzW18o6Nb!#=3PI-;dRcs_ZT%}mMzNO z(E0Xdbb@*+a;}TXbM|j=P*G3|DA}J0vK85E;gT*>r3+(l1qzmYCEHXdcG@0-k$i2T zDvx00#PQuyV}Wkdmgfx)0ROpS^V(kvj?-&FR-}93>K5a^mGgF%6U{#A9O0+c_z3AV zv!MhKp}aKrb2@=r0>Vp%H!t9d$Oq>XQk@SXPmx9n!HIYUuNde8ZAh2ey+Z#~RADZ( z-M&&Y2AuwJFALOjmGunO;7w|8={~gGElUDml8C*Z zhsz_$d$#EsqO@c|fis$>{*P3n@cTDrJcnw)4w{XJ)V4u`BB&k04 zMaIMj?lOriFTlwG*8Z&Q*-3!xN&d83m_Y#v0rn?}QDf}@-sh!1?+1l^yBokX-)lii zfw7T0rh@8ad&y|h6R+5d^Dd$kWRxF8!2uQ-0C~)Tb1DiP;GAF}HTYO0d@~ diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 1881b50c..de55bec3 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -24,7 +24,6 @@ library(dplyr) TO DO: -- make diagram for two-strain model and include it below - make sure all functions used have good help pages - get model library ready (or remove text below referencing it) From 7d1c96d46e98049ff6ff3c28203079bd5430be14 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Fri, 10 Nov 2023 13:41:20 -0500 Subject: [PATCH 089/332] a little html for diagram centering --- vignettes/quickstart.Rmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index de55bec3..30f508f4 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -46,7 +46,9 @@ This vignette seeks to explain `macpan2`'s model specification grammar and in pa A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): +
    ![](../misc/diagrams/quickstart/two-strain.png) +
    Here, From 44aaf5c6236c805dce56eddeed5559afc0664429 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Fri, 10 Nov 2023 13:43:25 -0500 Subject: [PATCH 090/332] small tweaks --- vignettes/quickstart.Rmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 30f508f4..c8489948 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -295,7 +295,7 @@ sir_results = mp_report(sir_simulator) The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer): -```{r} +```{r sir-results-head} head(sir_results) ``` @@ -303,7 +303,7 @@ head(sir_results) This output can be manipulated and plotted easily with standard tools, like `dplyr` and `ggplot2`: -```{r sir-ggplot-example, fig.width = 6} +```{r sir-ggplot-example, fig.width = 6, fig.height = 4} (sir_results |> filter(matrix == "state") # keep just the state variables at each point in time |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in plot @@ -497,7 +497,7 @@ force_of_infection Now we're ready to build the two-strain model object and simulate it: -```{r two-strain-results, fig.width = 6} +```{r two-strain-results, fig.width = 6, fig.height = 4} two_strain_model = SIR_starter( # indices state = state, From f5ab22b8b64ffea88778959d82dd7061a3e1f5b4 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Fri, 10 Nov 2023 13:49:13 -0500 Subject: [PATCH 091/332] convert comment to todo item --- vignettes/quickstart.Rmd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c8489948..9ea22acc 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -26,6 +26,7 @@ TO DO: - make sure all functions used have good help pages - get model library ready (or remove text below referencing it) +- update `mp_report()` so that row names are reset before outputting the results df ] @@ -299,8 +300,6 @@ The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy- head(sir_results) ``` -[IP: small point, but i assume the row index starts at 4 because the first three entries (where `time = 0`) are the initial conditions. it would be a little cleaner if those row names were removed before output from `mp_report()`.] - This output can be manipulated and plotted easily with standard tools, like `dplyr` and `ggplot2`: ```{r sir-ggplot-example, fig.width = 6, fig.height = 4} From bafae17beaaf8fc58b9f667eaa21fe8e40669e93 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Fri, 10 Nov 2023 13:59:28 -0500 Subject: [PATCH 092/332] more small tweaks --- vignettes/quickstart.Rmd | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 9ea22acc..35f1d362 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -53,9 +53,11 @@ A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repe Here, -- $S$, $I_A$, $I_B$, and $R$ are the numbers of individuals that are susceptible, infected with strain A, infected with strain B, and recovered, respectively, -- $\beta_A$ and $\beta_B$ are the transmission rates for strains A and B, respectively, -- $\gamma$ is the recovery rate for all infected individuals. +- $S$, $I_x$, and $R$ are the numbers of individuals that are susceptible, infected with strain $x$ ($A$ or $B$), and recovered, respectively, +- $N= S + I_A + I_B + R$ is the total population size, +- $\beta_x$ is the transmission rate for strain $x$, +- $\gamma$ is the recovery rate for infected individuals, +- $\lambda_x = \beta_x (I_x)/N$ is the force of infection for strain $x$. We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: @@ -66,9 +68,7 @@ S_{t+1} &= - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ R_{t+1} &= \gamma (I_A)_t + \gamma (I_B)_t. \end{align} -where $N = S + I_A + I_B + R$ is the total population size. Note that each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. - -When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: +Each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: ``` lambda.A = beta.A * I.A / N @@ -76,6 +76,7 @@ lambda.B = beta.B * I.B / N ``` However, in `macpan2`, we can specify a single calculation archetype for it, for instance something like, + ``` lambda = beta * I / N ``` From b87e1d7fe6d29e8cb36b897e2054f5c34db1f51f Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Sat, 11 Nov 2023 20:34:55 -0500 Subject: [PATCH 093/332] quickstart tweaks --- vignettes/quickstart.Rmd | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 35f1d362..006b07b0 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -56,16 +56,16 @@ Here, - $S$, $I_x$, and $R$ are the numbers of individuals that are susceptible, infected with strain $x$ ($A$ or $B$), and recovered, respectively, - $N= S + I_A + I_B + R$ is the total population size, - $\beta_x$ is the transmission rate for strain $x$, -- $\gamma$ is the recovery rate for infected individuals, +- $\gamma$ is the recovery rate for infected individuals,^[in this model we are choosing to make the recovery rate (and hence the infectious period) the same for both strains, but it would be easy to relax this assumption] - $\lambda_x = \beta_x (I_x)/N$ is the force of infection for strain $x$. We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: \begin{align} -S_{t+1} &= - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ -(I_A)_{t+1} &= \beta_A (I_A)_t/N - \gamma (I_A)_t, \\ -(I_B)_{t+1} &= \beta_B (I_B)_t/N - \gamma (I_B)_t, \\ -R_{t+1} &= \gamma (I_A)_t + \gamma (I_B)_t. +S_{t+1} & = - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ +(I_A)_{t+1} &= \phantom{-[} \beta_A (I_A)_t/N - \gamma (I_A)_t, \\ +(I_B)_{t+1} &= \phantom{-[} \beta_B (I_B)_t/N - \gamma (I_B)_t, \\ +R_{t+1} &= \phantom{-[} \gamma (I_A)_t + \gamma (I_B)_t. \end{align} Each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: @@ -75,7 +75,7 @@ lambda.A = beta.A * I.A / N lambda.B = beta.B * I.B / N ``` -However, in `macpan2`, we can specify a single calculation archetype for it, for instance something like, +However, in `macpan2`, we can specify a single calculation archetype for it, for instance ``` lambda = beta * I / N @@ -85,26 +85,26 @@ and then attach a **ledger** to the model object that tabulates which specific c In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each its two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). -**By using calculation archetypes and ledgers, the modeller can focus on the modelling**, like designing model structure and choosing an expression for the forces of infection, **while `macpan2` will handle the bookkeeping**, matching stratified variables with each other each time an expression must be calculated. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, as opposed to directly editing specific calculations in the simulation code. +**Using calculation archetypes and ledgers allows the modeller to focus on modelling questions**, like the design of the model structure and the choice of expressions for the forces of infection, while **`macpan2` handles the bookkeeping**, matching stratified variables with each other when calculating expressions. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, rather than error-prone editing of calculations in the simulation code. -While a modeller could certainly write their own code to cut down on repetition when expanding a simple model (and many do), the idea behind `macpan2` is to give individuals a ready-made model specification grammar that enables easy model extension, especially when building [product models](https://arxiv.org/abs/2307.10308), and that can readily interface with fast simulation engines, like [TMB](https://cran.r-project.org/web/packages/TMB/index.html). +While a modeller could write their own code to cut down on repetition when expanding a simple model (and many do), `macpan2` provides a ready-made model specification grammar that enables easy model extension, especially when building [product models](https://arxiv.org/abs/2307.10308), and that can readily interface with fast simulation and calibration engines, like [TMB](https://cran.r-project.org/web/packages/TMB/index.html). # Appetizer: specifying the basic SIR model -Let's start with specifying the basic SIR model in `macpan2`, which is the foundation of the two-strain model above: +Let's start with specifying the basic SIR model, the foundation of the two-strain model above, in `macpan2`: \begin{align} S_{t+1} &= -\beta S_t I_t/N, \\ -I_{t+1} &= \beta S_t I_t/N - \gamma I_t, \\ -R_{t+1} &= \gamma I_t. +I_{t+1} &= \phantom{-} \beta S_t I_t/N - \gamma I_t, \\ +R_{t+1} &= \phantom{-} \gamma I_t. \end{align} It will be helpful to set $\lambda = \beta I/N$ and recast the equations as: \begin{align} S_{t+1} &= -\lambda S_t, \\ -I_{t+1} &= \lambda S_t - \gamma I_t, \\ -R_{t+1} &= \gamma I_t. +I_{t+1} &= \phantom{-} \lambda S_t - \gamma I_t, \\ +R_{t+1} &= \phantom{-} \gamma I_t. \end{align} Since the focus of this quickstart guide is `macpan2`'s model specification grammar, we have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model using the model grammar and it will output a model object from which we can build a simulator. Our primary focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. @@ -226,7 +226,7 @@ and by default creates one entry in the ledger for each combination of these val infection ``` -As a side note, the names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general they are completely up to the modeller and how they choose to specify their calculation archetypes.^[There is only one `mp_join()` argument name that is not available to the user, and that is `by`, which has a special role that we will see [later](#main-course).] +The names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general modellers can choose their calculation archetypes and the corresponding argument names however they like.^[There is only one `mp_join()` argument name that is not available to the user — `by`, which has a special role that we will see [later](#main-course).] We create the `recovery` ledger in a similar way: @@ -272,7 +272,7 @@ sir = SIR_starter( ) ``` -We can create a model simulator using `mp_tmb_simulator()`, provided we give it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): +We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], provided we give it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): ```{r sir-simulator} ## SIR model simulator ------------------------- From 1e9caedc2536fd3e3580489a872125ecf4b1839a Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Sun, 12 Nov 2023 10:42:52 -0500 Subject: [PATCH 094/332] more tweaks to quickstart --- vignettes/quickstart.Rmd | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 006b07b0..5b2535f0 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -177,8 +177,8 @@ The indices we need to specify fall into two groups: We have identified two useful **calculation archetypes** that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: -- `flow`: Unsigned flows from one class to another of the form $rX$, with $r$ being the flow rate and $X$ being the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. -- `force_of_infection`: The prevalence-dependent per capita rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. +- `flow`: Unsigned flows from one class to another of the form $rX$, with $r$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. +- `force_of_infection`: The prevalence-dependent *per capita* rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. In this case, the `flow` archetype is actually repeated within these model equations but the `force_of_infection` archetype is only used once. We've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. @@ -190,14 +190,15 @@ state = mp_index(Epi = c("S", "I", "R")) rate = mp_index(Epi = c("beta", "gamma", "lambda")) ``` -You can think of the `mp_index()` function as setting up data frames tabulating the model quantity labels: +The `mp_index()` function sets structures like data frames that tabulate the model quantity labels: +[BMB: tabulate → enumerate? list?] ```{r sir-state-and-rate} state rate ``` -The `Epi` column name is not really important in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. +The `Epi` column name is unimportant in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. For the `flow` archetype, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$. We specify flows using the name of the state from which it originates (`from_states`), the state to which it goes (`to_states`), and a flow rate name (`flow_rates`). @@ -212,7 +213,7 @@ infection = mp_join( ) ``` -The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, namely +The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, e.g. ```{r sir-infection-ledger-inputs} mp_subset(state, Epi = "S") @@ -220,7 +221,7 @@ mp_subset(state, Epi = "I") mp_subset(rate, Epi = "lambda") ``` -and by default creates one entry in the ledger for each combination of these values (_i.e._, a "[full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)"). However, since there is only one value in each column, there is only one entry in the resulting ledger: +and by default creates one entry in the ledger for each combination of these values (_i.e._, a [full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)). However, since there is only one value in each column, there is only one entry in the resulting ledger: ```{r sir-infection-ledger-2} infection @@ -272,7 +273,7 @@ sir = SIR_starter( ) ``` -We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], provided we give it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): +We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): ```{r sir-simulator} ## SIR model simulator ------------------------- @@ -285,8 +286,9 @@ sir_simulator = mp_tmb_simulator( time_steps = 100L ) ``` +[BMB: the `L` syntax for specifying an integer quantity might be unfamiliar. Is it important here or can we leave it out?] -Note that we've specified `NA` for `lambda` as it will be calculated for us using the `force_of_infection` archetype. +Note that we've specified `NA` for `lambda` as it will be calculated for us using the `force_of_infection` archetype. [BMB: is this required, or could `macpan2` assume that unspecified values are set to `NA` automatically? (There could be an implicit/explicit design tradeoff here - i.e. it would be convenient to do this automatically but might be better to force users to be explicit ...)] Then we can actually simulate the model by passing our model simulator to `mp_report()`: @@ -301,12 +303,12 @@ The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy- head(sir_results) ``` -This output can be manipulated and plotted easily with standard tools, like `dplyr` and `ggplot2`: +This output can be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`, e.g.: ```{r sir-ggplot-example, fig.width = 6, fig.height = 4} (sir_results |> filter(matrix == "state") # keep just the state variables at each point in time - |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in plot + |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in legend |> ggplot(aes(time, value, colour = state)) + geom_line() ) @@ -314,7 +316,7 @@ This output can be manipulated and plotted easily with standard tools, like `dpl (Above, we used the [base R pipe operator](https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/#pipes), `|>`.) -If you want to use base R for plotting, you can convert the long format data to wide format: +If you prefer to make plots in base R, you can convert the long format data to wide format: ```{r pivot_wider} sir_results_wide <- (sir_results @@ -365,7 +367,7 @@ We start by creating a new index for the strains: Strain_indices = c("A", "B") ``` -Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets): +Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: ```{r strain-expand-I} I_index = mp_cartesian( @@ -419,9 +421,9 @@ rate = mp_index( ) ``` -but you can imagine if you're building off of an existing, working model and you want to stratify many states into many new categories, the first way of building the indices might be preferred, which is why we demonstrated it above. +but the more implicit approach with `mp_subset()` etc. is much easier if you are building a model with many strata or crossing several different strata, or adding strata to an existing model. -For the `infection` ledger, let's see what our previous code for generating it yields now that we have some stratification by `Strain`: +For the `infection` ledger, let's see what our previous code for generating it yields now that we are (partially) stratifying by `Strain`: ```{r two-strain-infection-default} # infection ledger from before @@ -432,7 +434,7 @@ mp_join( ) ``` -As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join), where the individual indices, denoted by values in the `Epi` and `Strain` columns, are dot concatenated for the full quantity labels. +As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join), where the individual indices, denoted by values in the `Epi` and `Strain` columns, are dot-concatenated for the full quantity labels. For this model, we want only two of these flows: @@ -468,7 +470,7 @@ recovery = mp_join( recovery ``` -For the force of infection ledger, the full join yields all sorts of combinations that we don't want: +For the force of infection ledger, the full join yields many combinations that we don't want: ```{r two-strain-foi-default} mp_join( @@ -540,7 +542,7 @@ As mentioned, we've hidden some of the details of initializing a model object wi <> ``` -You may begin to see from this function definition how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `ledgers` and `init_vecs` are simply set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. +This function definition shows how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `ledgers` and `init_vecs` are set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. These topics will be discussed fully in a future vignette. From a44e99f264fcff7bb2aa834fcb956d08a183354a Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Sun, 12 Nov 2023 15:46:39 -0700 Subject: [PATCH 095/332] updates based on DE's comments --- vignettes/quickstart.Rmd | 113 ++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 50 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 5b2535f0..d6ef7ded 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -14,7 +14,7 @@ editor_options: [![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) -```{r setup, echo = FALSE, message = FALSE} +```{r setup, echo = FALSE, message = FALSE, warning = FALSE} library(macpan2) library(ggplot2) library(dplyr) @@ -27,6 +27,7 @@ TO DO: - make sure all functions used have good help pages - get model library ready (or remove text below referencing it) - update `mp_report()` so that row names are reset before outputting the results df +- be sure `DynamicModel()` is exported ] @@ -56,7 +57,7 @@ Here, - $S$, $I_x$, and $R$ are the numbers of individuals that are susceptible, infected with strain $x$ ($A$ or $B$), and recovered, respectively, - $N= S + I_A + I_B + R$ is the total population size, - $\beta_x$ is the transmission rate for strain $x$, -- $\gamma$ is the recovery rate for infected individuals,^[in this model we are choosing to make the recovery rate (and hence the infectious period) the same for both strains, but it would be easy to relax this assumption] +- $\gamma$ is the recovery rate for infected individuals,^[In this model we are choosing to make the recovery rate (and hence the infectious period) the same for both strains, but it would be easy to relax this assumption in the model specification by following the way in which we stratify the state variable $I$ by strain below.] - $\lambda_x = \beta_x (I_x)/N$ is the force of infection for strain $x$. We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: @@ -111,7 +112,7 @@ Since the focus of this quickstart guide is `macpan2`'s model specification gram ```{r SIR-starter, echo = FALSE} ## helper function to simplify the exposition in this vigette ----------- -SIR_starter = function( +SIR_starter <- function( # indices for model quantities state, rate, @@ -121,9 +122,9 @@ SIR_starter = function( ){ ## Set up expressions list for each calculation archetype -------------- - ## names are when the calculation gets performed relative to - ## the simulation loop (before, during, ...) - expr_list = mp_expr_list( + ## names refer to when the calculation gets performed relative to + ## the simulation time-step loop (before, during, ...) + expr_list <- mp_expr_list( before = list( ## aggregations N ~ sum(state) @@ -144,14 +145,14 @@ SIR_starter = function( ) ## Ledgers for each specific calculation -------------- - ledgers = list( + ledgers <- list( flow = mp_ledgers(flow), force_of_infection = mp_ledgers(force_of_infection) ) ## Initialize indexed vectors (to all zeros) -------------- # used as placeholders for user input - init_vecs = list( + init_vecs <- list( state = mp_vector(state), rate = mp_vector(rate) ) @@ -177,17 +178,17 @@ The indices we need to specify fall into two groups: We have identified two useful **calculation archetypes** that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: -- `flow`: Unsigned flows from one class to another of the form $rX$, with $r$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. +- `flow`: Unsigned flows from one class to another of the form $rX$, with $r>0$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. - `force_of_infection`: The prevalence-dependent *per capita* rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. -In this case, the `flow` archetype is actually repeated within these model equations but the `force_of_infection` archetype is only used once. We've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. +In this case, the `flow` archetype is actually repeated within these model equations but the `force_of_infection` archetype is used only once. We've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. We start by creating the `state` and `rate` indices: ```{r sir-indices} ## indices (labels) for model quantities ------------------------- -state = mp_index(Epi = c("S", "I", "R")) -rate = mp_index(Epi = c("beta", "gamma", "lambda")) +state <- mp_index(Epi = c("S", "I", "R")) +rate <- mp_index(Epi = c("beta", "gamma", "lambda")) ``` The `mp_index()` function sets structures like data frames that tabulate the model quantity labels: @@ -206,7 +207,7 @@ We use the `mp_join()` function to create the `infection` ledger like so: ```{r sir-infection-ledger} ## infection ledger ------------------------- -infection = mp_join( +infection <- mp_join( from_states = mp_subset(state, Epi = "S"), to_states = mp_subset(state, Epi = "I"), flow_rates = mp_subset(rate, Epi = "lambda") @@ -233,7 +234,7 @@ We create the `recovery` ledger in a similar way: ```{r sir-recovery-ledger} ## recovery ledger ------------------------- -recovery = mp_join( +recovery <- mp_join( from_states = mp_subset(state, Epi = "I"), to_states = mp_subset(state, Epi = "R"), flow_rates = mp_subset(rate, Epi = "gamma") @@ -247,7 +248,7 @@ Finally, the `force_of_infection` ledger is slightly different as it corresponds ```{r sir-foi-ledger} ## force of infection ledger ------------------------- # infection additionally involves the calculation of a force of infection -force_of_infection = mp_join( +force_of_infection <- mp_join( infectious_states = mp_subset(state, Epi = "I"), transmission_rates = mp_subset(rate, Epi = "beta"), infection_flow_rates = mp_subset(rate, Epi = "lambda") @@ -260,7 +261,7 @@ Now we can use the `SIR_starter()` function to initialize our model object: ```{r sir} ## SIR model object ------------------------- -sir = SIR_starter( +sir <- SIR_starter( # indices state = state, rate = rate, @@ -277,8 +278,8 @@ We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "te ```{r sir-simulator} ## SIR model simulator ------------------------- -sir_simulator = mp_tmb_simulator( - sir, +sir_simulator <- mp_tmb_simulator( + dynamic_model = sir, vectors = list( state = c(S = 999, I = 1, R = 0), rate = c(beta = 0.25, gamma = 0.1, lambda = NA) @@ -294,7 +295,7 @@ Then we can actually simulate the model by passing our model simulator to `mp_re ```{r sir-results} ## SIR model simulation results ------------------------- -sir_results = mp_report(sir_simulator) +sir_results <- mp_report(sir_simulator) ``` The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer): @@ -303,6 +304,14 @@ The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy- head(sir_results) ``` +The simulation output has several columns: + +- `matrix`: The matrix storing our values internally, corresponding to our two indices, `state` and `rate`. +- `time`: An internal time index, where `time = 0` corresponds to the user-provided values (initial conditions). +- `row`: The primary label for the `value` (the row name in the corresponding `matrix`). +- `column`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the states and rates in this model are not stratified by a secondary property (like age or location), this column is empty for all entries. +- `value`: The numerical value. + This output can be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`, e.g.: ```{r sir-ggplot-example, fig.width = 6, fig.height = 4} @@ -361,29 +370,49 @@ As previously noted, we created a `force_of_infection` archetype of the form $\b Since we already have an archetype for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. +To define the two-strain model, we again must specify our `state` and `rate` indices, as well as our `infection`, `recovery`, and `force_of_infection` ledgers. + We start by creating a new index for the strains: ```{r strain-indices} -Strain_indices = c("A", "B") +Strain_indices <- c("A", "B") +``` + +A simple approach would be to define the new state and rate names directly using the `mp_index()` function, as we did above: + +```{r two-strain-state-rate-manual, eval = FALSE} +state <- mp_index( + Epi = c("S", rep("I", 2), "R"), + Strain = c("", Strain_indices, "") +) + +rate <- mp_index( + Epi = c(rep(c("beta", "lambda"), 2), "gamma"), + Strain = c(rep(c("A", "B"), each = 2), "") +) ``` -Now we expand the states and rates associated with different strains. For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: +However, this approach is less flexible if we want to build a complex model or if we already have a simpler, working model (like the SIR above) and want expand it with many strata and/or several different types of strata. We present an alternative approach below that is more verbose but far more flexible. + +For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: ```{r strain-expand-I} -I_index = mp_cartesian( +I_indices <- mp_cartesian( mp_subset(state, Epi = "I"), mp_index(Strain = Strain_indices) ) -I_index +I_indices ``` +This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names are be treated like any other component used to label a particular compartment (like location or age group).] + We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function: ```{r two-strain-state} -state = mp_union( +state <- mp_union( mp_subset(state, Epi = "S"), - I_index, + I_indices, mp_subset(state, Epi = "R") ) @@ -393,7 +422,7 @@ state We update the rates similarly: ```{r two-strain-rate} -rate = +rate <- mp_union( # stratify rates involved in the infection process by strain mp_cartesian( @@ -407,22 +436,6 @@ rate = rate ``` -It may seem like overkill to use `mp_subset()`, `mp_cartesian()`, and `mp_union()` here to define the indices using the original ones from the SIR model when they could be written out by hand quickly as - -```{r two-strain-state-rate-manual, eval = FALSE} -state = mp_index( - Epi = c("S", rep("I", 2), "R"), - Strain = c("", Strain_indices, "") -) - -rate = mp_index( - Epi = c(rep(c("beta", "lambda"), 2), "gamma"), - Strain = c(rep(c("A", "B"), each = 2), "") -) -``` - -but the more implicit approach with `mp_subset()` etc. is much easier if you are building a model with many strata or crossing several different strata, or adding strata to an existing model. - For the `infection` ledger, let's see what our previous code for generating it yields now that we are (partially) stratifying by `Strain`: ```{r two-strain-infection-default} @@ -445,7 +458,7 @@ In other words, we want the `Strain` index on `I` to match with the `Strain` ind ```{r two-strain-infection-ledger} ## new infection ledger ------------------------- -infection = mp_join( +infection <- mp_join( from_states = mp_subset(state, Epi = "S"), to_states = mp_subset(state, Epi = "I"), flow_rates = mp_subset(rate, Epi = "lambda"), @@ -462,7 +475,7 @@ Note the syntax of the `by` argument here. Each `by` list element will correspon For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: ```{r two-strain-recovery-ledger} -recovery = mp_join( +recovery <- mp_join( from_states = mp_subset(state, Epi = "I"), to_states = mp_subset(state, Epi = "R"), flow_rates = mp_subset(rate, Epi = "gamma") @@ -484,7 +497,7 @@ We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column ```{r two-strain-foi-ledger} ## new force of infection ledger ------------------------- -force_of_infection = mp_join( +force_of_infection <- mp_join( infection_flow_rates = mp_subset(rate, Epi = "lambda"), infectious_states = mp_subset(state, Epi = "I"), transmission_rates = mp_subset(rate, Epi = "beta"), @@ -500,7 +513,7 @@ force_of_infection Now we're ready to build the two-strain model object and simulate it: ```{r two-strain-results, fig.width = 6, fig.height = 4} -two_strain_model = SIR_starter( +two_strain_model <- SIR_starter( # indices state = state, rate = rate, @@ -512,8 +525,8 @@ two_strain_model = SIR_starter( force_of_infection = force_of_infection ) -two_strain_simulator = mp_tmb_simulator( - two_strain_model, +two_strain_simulator <- mp_tmb_simulator( + dynamic_model = two_strain_model, vectors = list( state = c(S = 998, I.A = 1, I.B = 1, R = 0), rate = c(beta.A = 0.25, lambda.A = NA, beta.B = 0.2, lambda.B = NA, gamma = 0.1) @@ -521,11 +534,11 @@ two_strain_simulator = mp_tmb_simulator( time_steps = 100L ) -two_strain_results = (mp_report(two_strain_simulator) +two_strain_results <- (mp_report(two_strain_simulator) |> filter(matrix == "state") ) -levels = unique(two_strain_results$row) # get state variables in the desired order +levels <- unique(two_strain_results$row) # get state variables in the desired order (two_strain_results # keep state variables at each point in time |> mutate(state = factor(row, levels = levels)) # to enforce logical state ordering in plot From 6a860e3c0ffd80658bea88e61e97f66015e9d5af Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 08:53:55 -0500 Subject: [PATCH 096/332] :pencil: update specs [no ci] --- vignettes/calibration.Rmd | 6 +++--- vignettes/quickstart.Rmd | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index 8f06fd58..fc9c0ee7 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -48,8 +48,8 @@ First set up the model from the quickstart guide. For convenience (since we will ```{r sir_setup} mk_sim <- function(init_state = c(S = 99, I = 1, R = 0)) { - #sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) - sir = Compartmental2(system.file("model_library", "sir", package = "macpan2")) + sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) + #sir = Compartmental2(system.file("model_library", "sir", package = "macpan2")) sim <- mp_tmb_simulator(sir, time_steps = 100, vectors = list( @@ -85,7 +85,7 @@ print(gg0) Now we'll use the *experimental* `mk_calibrate()` function from `macpan2helpers` package ```{r mk_calibrate} -macpan2::mk_calibrate(sir_simulator, +macpan2helpers::mk_calibrate(sir_simulator, data = data.frame(I_obs = sir_prevalence$obs_val), params = list(trans_rates = c(beta = 1), I_sd = 1), transforms = list(trans_rates = c(beta = "log"), I_sd = "log"), diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index d6ef7ded..0f3dfc57 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -49,7 +49,7 @@ This vignette seeks to explain `macpan2`'s model specification grammar and in pa A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections):
    -![](../misc/diagrams/quickstart/two-strain.png) +![](../misc/diagrams/quickstart/two-strain.svg)
    Here, @@ -63,7 +63,7 @@ Here, We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: \begin{align} -S_{t+1} & = - [\beta_a (I_A)_t/N + \beta_s (I_B)_t/N] S_t, \\ +S_{t+1} & = - [\beta_A (I_A)_t/N + \beta_B (I_B)_t/N] S_t, \\ (I_A)_{t+1} &= \phantom{-[} \beta_A (I_A)_t/N - \gamma (I_A)_t, \\ (I_B)_{t+1} &= \phantom{-[} \beta_B (I_B)_t/N - \gamma (I_B)_t, \\ R_{t+1} &= \phantom{-[} \gamma (I_A)_t + \gamma (I_B)_t. @@ -84,7 +84,7 @@ lambda = beta * I / N and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a table of which specific subscripted `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. -In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each its two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). +In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each of two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). **Using calculation archetypes and ledgers allows the modeller to focus on modelling questions**, like the design of the model structure and the choice of expressions for the forces of infection, while **`macpan2` handles the bookkeeping**, matching stratified variables with each other when calculating expressions. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, rather than error-prone editing of calculations in the simulation code. From 77f69104b7698bbbca709813bc748751a3988e30 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 09:09:28 -0500 Subject: [PATCH 097/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 0f3dfc57..e135d236 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -28,6 +28,7 @@ TO DO: - get model library ready (or remove text below referencing it) - update `mp_report()` so that row names are reset before outputting the results df - be sure `DynamicModel()` is exported +- harmonize presentation with quickstart ] @@ -284,10 +285,9 @@ sir_simulator <- mp_tmb_simulator( state = c(S = 999, I = 1, R = 0), rate = c(beta = 0.25, gamma = 0.1, lambda = NA) ), - time_steps = 100L + time_steps = 100 ) ``` -[BMB: the `L` syntax for specifying an integer quantity might be unfamiliar. Is it important here or can we leave it out?] Note that we've specified `NA` for `lambda` as it will be calculated for us using the `force_of_infection` archetype. [BMB: is this required, or could `macpan2` assume that unspecified values are set to `NA` automatically? (There could be an implicit/explicit design tradeoff here - i.e. it would be convenient to do this automatically but might be better to force users to be explicit ...)] @@ -531,7 +531,7 @@ two_strain_simulator <- mp_tmb_simulator( state = c(S = 998, I.A = 1, I.B = 1, R = 0), rate = c(beta.A = 0.25, lambda.A = NA, beta.B = 0.2, lambda.B = NA, gamma = 0.1) ), - time_steps = 100L + time_steps = 100 ) two_strain_results <- (mp_report(two_strain_simulator) From 76995916ce289c96b9704c83d20665c642228e74 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:35:20 -0500 Subject: [PATCH 098/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index e135d236..44334bcf 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -307,7 +307,7 @@ head(sir_results) The simulation output has several columns: - `matrix`: The matrix storing our values internally, corresponding to our two indices, `state` and `rate`. -- `time`: An internal time index, where `time = 0` corresponds to the user-provided values (initial conditions). +- `time`: An internal time index, where `time = 0` corresponds to the user-provided values (initial conditions). [SW: note that the default now more intuitively starts at time = 1] - `row`: The primary label for the `value` (the row name in the corresponding `matrix`). - `column`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the states and rates in this model are not stratified by a secondary property (like age or location), this column is empty for all entries. - `value`: The numerical value. From cd3028784f85fbba0d2774a2040b334ab3fa2191 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:35:40 -0500 Subject: [PATCH 099/332] mp_report fix --- NAMESPACE | 2 ++ R/calibration.R | 12 +++++++++++- R/index_to_tmb.R | 7 ++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index da05a877..8841ed40 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,6 +14,8 @@ S3method(c,StringData) S3method(head,Ledger) S3method(labelling_column_names,Index) S3method(labelling_column_names,Ledger) +S3method(labels,Compartmental) +S3method(labels,DynamicModel) S3method(labels,Index) S3method(labels,LabelsDynamic) S3method(labels,LabelsScripts) diff --git a/R/calibration.R b/R/calibration.R index fb31edc4..a9cbe87d 100644 --- a/R/calibration.R +++ b/R/calibration.R @@ -58,8 +58,18 @@ TMBCurrentParams = function(simulator) { ## TMBSimulator self = Base() self$simulator = simulator - self$params_vector = function() self$simulator$ad_fun()$env$parList()$params + self$n_params = function() { + self$simulator$tmb_model$params$data_frame() |> nrow() + } + self$n_random = function() { + self$simulator$tmb_model$random$data_frame() |> nrow() + } + self$params_vector = function() { + if (self$n_params() == 0L) return(numeric()) + self$simulator$ad_fun()$env$parList()$params + } self$random_vector = function() { + if (self$n_random() == 0L) return(numeric()) self$simulator$objective(self$params_vector()) self$simulator$ad_fun()$env$parList()$random } diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index d01b4818..5f4cda62 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -109,5 +109,10 @@ mp_tmb_simulator.ModelDefRun = function(dynamic_model do.call(mp_tmb_simulator, args) } +#' @param parameter_vector Numeric vector equal in length to +#' @param simulator Object produced by \code{\link{tmb_simulator}}. #' @export -mp_report = function(simulator, ...) simulator$report() +mp_report = function(simulator, parameter_vector = numeric(), phases = "during") { + if (length(parameter_vector) == 0L) parameter_vector = 0 + do.call(simulator$report, c(as.list(parameter_vector), list(.phases = phases))) +} From 343f1b9a3ae4b5c3cdd22e8d1f4d435c95ffff38 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:39:37 -0500 Subject: [PATCH 100/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 44334bcf..1bf68d57 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -309,7 +309,7 @@ The simulation output has several columns: - `matrix`: The matrix storing our values internally, corresponding to our two indices, `state` and `rate`. - `time`: An internal time index, where `time = 0` corresponds to the user-provided values (initial conditions). [SW: note that the default now more intuitively starts at time = 1] - `row`: The primary label for the `value` (the row name in the corresponding `matrix`). -- `column`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the states and rates in this model are not stratified by a secondary property (like age or location), this column is empty for all entries. +- `col`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the outputs of this model (i.e. states and rates) are specified as vectors and not matrices, this column is empty for all entries. TODO: When would this be useful - `value`: The numerical value. This output can be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`, e.g.: From a2a5957525f927a81ce52c6b75e11b7cdfdc0490 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:40:19 -0500 Subject: [PATCH 101/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 1bf68d57..669a00d3 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -309,7 +309,7 @@ The simulation output has several columns: - `matrix`: The matrix storing our values internally, corresponding to our two indices, `state` and `rate`. - `time`: An internal time index, where `time = 0` corresponds to the user-provided values (initial conditions). [SW: note that the default now more intuitively starts at time = 1] - `row`: The primary label for the `value` (the row name in the corresponding `matrix`). -- `col`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the outputs of this model (i.e. states and rates) are specified as vectors and not matrices, this column is empty for all entries. TODO: When would this be useful +- `col`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the outputs of this model (i.e. states and rates) are specified as vectors and not matrices, this column is empty for all entries. TODO: When would this be useful? - `value`: The numerical value. This output can be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`, e.g.: From 2e225128bb6760e79af7dbc03cbc3b5327d76142 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:43:36 -0500 Subject: [PATCH 102/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 1 - 1 file changed, 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 669a00d3..7d6fcf89 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -26,7 +26,6 @@ TO DO: - make sure all functions used have good help pages - get model library ready (or remove text below referencing it) -- update `mp_report()` so that row names are reset before outputting the results df - be sure `DynamicModel()` is exported - harmonize presentation with quickstart From a6f6352b699b3b497c61094308c391d9bd20334a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:44:04 -0500 Subject: [PATCH 103/332] birs --- R/index_to_tmb.R | 10 +++++++++- inst/model_library/sir/model_structure.R | 8 ++++---- inst/model_library/sir_age/model_structure.R | 14 +++++++------- misc/experiments/refactorcpp.R | 8 ++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 5f4cda62..4b376028 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -114,5 +114,13 @@ mp_tmb_simulator.ModelDefRun = function(dynamic_model #' @export mp_report = function(simulator, parameter_vector = numeric(), phases = "during") { if (length(parameter_vector) == 0L) parameter_vector = 0 - do.call(simulator$report, c(as.list(parameter_vector), list(.phases = phases))) + r = do.call( + simulator$report, + c( + as.list(parameter_vector), + list(.phases = phases) + ) + ) + rownames(r) = NULL + r } diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index 5d30141b..31172821 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -62,9 +62,9 @@ transmission = mp_join( transmission = beta ) -link_data = list( - influences = mp_link_data(transmission) - , flows = mp_link_data(infection, recovery) +ledgers = list( + influences = mp_ledgers(transmission) + , flows = mp_ledgers(infection, recovery) ) ## Initialize indexed vectors (to all zeros) -------------- @@ -79,7 +79,7 @@ init_vecs = list( dynamic_model = mp_dynamic_model( expr_list = expr_list, - link_data = link_data, + ledgers = ledgers, init_vecs = init_vecs, unstruc_mats = list() ) diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index cff25af4..30add462 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -162,12 +162,12 @@ recovery = mp_join( ## wrapping up the links defined by the joins above ------------ -link_data = list( - decompositions = mp_link_data(trans_decomposition) - , aggregations = mp_link_data(aggregation) - , normalizations = mp_link_data(normalization) - , influences = mp_link_data(transmission) - , flows = mp_link_data(infection, recovery) +ledgers = list( + decompositions = mp_ledgers(trans_decomposition) + , aggregations = mp_ledgers(aggregation) + , normalizations = mp_ledgers(normalization) + , influences = mp_ledgers(transmission) + , flows = mp_ledgers(infection, recovery) ) ## instantiate numeric vectors labelled with particular indexes ----------- @@ -187,7 +187,7 @@ init_vecs = list( dynamic_model = mp_dynamic_model( expr_list = expr_list, - link_data = link_data, + ledgers = ledgers, init_vecs = init_vecs, unstruc_mats = list() ) diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index c62d959b..f7b31aee 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -174,7 +174,7 @@ hh = function(index, by = "Group", ledger_column = "group") { } hh(movement, by = "Move", ledger_column = "move") hh(movement) -mp_link_data(hh(state))$positions_frame(TRUE) +mp_ledgers(hh(state))$positions_frame(TRUE) hh = function(len) { data.frame(group = rep("group", len)) @@ -282,7 +282,7 @@ death_flows = mp_join( ) ) -flows = mp_link_data( +flows = mp_ledgers( movement_flows , infection_flows , recovery_flows @@ -309,7 +309,7 @@ state_aggregation = mp_join( alive = alive, group_by = strata, by = list(alive.group_by = "Loc.Age") -) |> mp_link_data() +) |> mp_ledgers() ## N ~ groupSums(state[alive], group_by, N) state_vector = Vector(state) state_vector$set_numbers(Epi = c(I = 1))$set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) @@ -370,7 +370,7 @@ self$reference_index_list$N$labels() groupSums() mp_labels xx$reference_index_list$N$labelling_column_names -yy = mp_link_data(xx) +yy = mp_ledgers(xx) yy$reference_index_list yy = Vector(state) From e496b0cbe3f698c7773fdee2f42d7e966e5ff683 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 10:56:36 -0500 Subject: [PATCH 104/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 7d6fcf89..ff89c048 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -158,7 +158,7 @@ SIR_starter <- function( ) ## Initialize model object ----------------- - DynamicModel( + mp_dynamic_model( expr_list = expr_list, ledgers = ledgers, init_vecs = init_vecs From 99f14e7f15b77ef4b1bff64095db108edc8f4fa8 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 12:25:03 -0500 Subject: [PATCH 105/332] mp_join help --- R/link.R | 6 ++- R/mp.R | 88 +++++++++++++++++++++++++++++-------- man/Ledger.Rd | 4 +- man/mp_join.Rd | 93 +++++++++++++++++++++++++++++++++++++--- vignettes/quickstart.Rmd | 2 +- 5 files changed, 165 insertions(+), 28 deletions(-) diff --git a/R/link.R b/R/link.R index 8fc16b13..81a15280 100644 --- a/R/link.R +++ b/R/link.R @@ -11,8 +11,8 @@ #' age #' ) #' mp_join( -#' mp_choose(state, "from", Epi = "S"), -#' mp_choose(state, "to", Epi = "I"), +#' from = mp_subset(state, Epi = "S"), +#' to = mp_subset(state, Epi = "I"), #' from.to = "Age" #' ) #' ``` @@ -194,6 +194,8 @@ merge_util = function(x, y, by.x, by.y) { sort = FALSE ) + if (nrow(z) == 0L) stop("Nothing matched in a join resulting in a ledger with zero rows, which is not allowed") + ## ---- ## update the column name map to preserve provenance ## ---- diff --git a/R/mp.R b/R/mp.R index 0bd536be..9c9214d5 100644 --- a/R/mp.R +++ b/R/mp.R @@ -292,28 +292,80 @@ mp_choose_out = function(x, subset_name, ...) { #' Join Indexes #' +#' Join two or more index tables (see \code{\link{mp_index}}) to produce a +#' ledger (see \code{\link{mp_ledgers}}). +#' +#' When two index tables are passed to `...`, `mp_join` behaves very much like +#' an ordinary [inner join](https://en.wikipedia.org/wiki/Join_(SQL)). +#' When more than two tables are passed to `...`, `mp_join` iteratively joins +#' pairs of tables to produce a final ledger. For example, if index tables `A` +#' `B`, and `C` are passed to `mp_join`, an inner join of `A` and `B` is +#' performed and the result is joined with `C`. In each of these successive +#' internal joins. The properties of inner +#' joins ensures that the order of tables does not affect the set of rows in +#' the final table (SW states without proof!). +#' +#' When two index tables are passed to `...`, the `by` argument is just a +#' character vector of column names on which to join (as in standard R functions +#' for joining data frames), or the dot-concatenation of these column names. +#' For example, +#' ```{r, echo = TRUE, eval = TRUE} +#' state = mp_index( +#' Epi = c("S", "I", "S", "I"), +#' Age = c("young", "young", "old", "old") +#' ) +#' mp_join( +#' from = mp_subset(state, Epi = "S"), +#' to = mp_subset(state, Epi = "I"), +#' by = "Age" +#' ) +#' ``` +#' If there are more than two tables then the `by` argument must be a named +#' list of character vectors, each describing how to join the columns of +#' a pair of tables in `...`. The names of this list are dot-concatenations +#' of the names of pairs of tables in `...`. For example, +#' ```{r, echo = TRUE, eval = TRUE} +#' rates = mp_index( +#' Epi = c("lambda", "lambda"), +#' Age = c("young", "old") +#' ) +#' mp_join( +#' from = mp_subset(state, Epi = "S"), +#' to = mp_subset(state, Epi = "I"), +#' rate = mp_subset(rates, Epi = "lambda"), +#' by = list( +#' from.to = "Age", +#' from.rate = "Age" +#' ) +#' ) +#' ``` +#' If the `by` columns have different names in two tables, then you can +#' specify these using formula notation where the left-hand-side +#' is a dot-concatenation of columns in the first table and the +#' right-hand-side is a dot-concatenation of the columns in the second +#' table. For example, +#' ```{r} +#' contact = mp_index( +#' AgeSusceptible = c("young", "young", "old", "old"), +#' AgeInfectious = c("young", "old", "young", "old") +#' ) +#' mp_join( +#' sus = mp_subset(state, Epi = "S"), +#' inf = mp_subset(state, Epi = "I"), +#' con = contact, +#' by = list( +#' sus.con = "Age" ~ "AgeSusceptible", +#' inf.con = "Age" ~ "AgeInfectious" +#' ) +#' ) +#' ``` +#' #' @param ... Named arguments giving indexes created by #' \code{\link{mp_index}} or another function that manipulates indexes. #' Each argument will become a position vector used to subset #' or expand numeric vectors in archetype formulas. -#' @param by What columns to use to join the indexes. If there are -#' only two indexes in \code{...} the `by` argument can be either -#' (1) a string giving the dot-concatenation of columns to join on -#' that are common among the tables or (2) a two-sided formula with -#' strings on either side. In the formula case, the left-hand-side -#' is a dot-concatenation of columns in the first index and the -#' right-hand-side is a dot-concatenation of the columns in the second -#' index. This formula notation is useful if the name of a column in -#' one index is different from a column in the other index that -#' should be joined on. If there are more than two indexes in \code{...} -#' the `by` argument is a named list of strings and/or formulas. -#' Each item in the list corresponds to a pair of indexes and how -#' their columns are to be matched. The name of each item is a dot -#' contactenation of the names of the corresponding pairs of arguments -#' in \code{...}. The value of each item follows the same rules as the -#' case given above with two indexes. TODO: create examples and point -#' to them. -#' +#' @param by What columns to use to join the indexes. See below on +#' how to specify this argument. #' @export mp_join = function(..., by = empty_named_list()) { table_list = valid$named_list$assert(list(...)) diff --git a/man/Ledger.Rd b/man/Ledger.Rd index ea3d8808..59b1eea2 100644 --- a/man/Ledger.Rd +++ b/man/Ledger.Rd @@ -18,8 +18,8 @@ state = mp_cartesian( age ) mp_join( - mp_choose(state, "from", Epi = "S"), - mp_choose(state, "to", Epi = "I"), + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), from.to = "Age" ) }\if{html}{\out{
    }} diff --git a/man/mp_join.Rd b/man/mp_join.Rd index 7c3da07b..29d8f159 100644 --- a/man/mp_join.Rd +++ b/man/mp_join.Rd @@ -12,7 +12,93 @@ mp_join(..., by = empty_named_list()) Each argument will become a position vector used to subset or expand numeric vectors in archetype formulas.} -\item{by}{What columns to use to join the indexes. If there are +\item{by}{What columns to use to join the indexes. See below on +how to specify this argument.} +} +\description{ +Join two or more index tables (see \code{\link{mp_index}}) to produce a +ledger (see \code{\link{mp_ledgers}}). +} +\details{ +When two index tables are passed to \code{...}, \code{mp_join} behaves very much like +an ordinary \href{https://en.wikipedia.org/wiki/Join_(SQL)}{inner join}. +When more than two tables are passed to \code{...}, \code{mp_join} iteratively joins +pairs of tables to produce a final ledger. For example, if index tables \code{A} +\code{B}, and \code{C} are passed to \code{mp_join}, an inner join of \code{A} and \code{B} is +performed and the result is joined with \code{C}. In each of these successive +internal joins. The properties of inner +joins ensures that the order of tables does not affect the set of rows in +the final table (SW states without proof!). + +When two index tables are passed to \code{...}, the \code{by} argument is just a +character vector of column names on which to join (as in standard R functions +for joining data frames), or the dot-concatenation of these column names. +For example, + +\if{html}{\out{
    }}\preformatted{state = mp_index( + Epi = c("S", "I", "S", "I"), + Age = c("young", "young", "old", "old") +) +mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + by = "Age" +) +#> from to +#> S.young I.young +#> S.old I.old +}\if{html}{\out{
    }} + +If there are more than two tables then the \code{by} argument must be a named +list of character vectors, each describing how to join the columns of +a pair of tables in \code{...}. The names of this list are dot-concatenations +of the names of pairs of tables in \code{...}. For example, + +\if{html}{\out{
    }}\preformatted{rates = mp_index( + Epi = c("lambda", "lambda"), + Age = c("young", "old") +) +mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + rate = mp_subset(rates, Epi = "lambda"), + by = list( + from.to = "Age", + from.rate = "Age" + ) +) +#> from to rate +#> S.young I.young lambda.young +#> S.old I.old lambda.old +}\if{html}{\out{
    }} + +If the \code{by} columns have different names in two tables, then you can +specify these using formula notation where the left-hand-side +is a dot-concatenation of columns in the first table and the +right-hand-side is a dot-concatenation of the columns in the second +table. For example, + +\if{html}{\out{
    }}\preformatted{contact = mp_index( + AgeSusceptible = c("young", "young", "old", "old"), + AgeInfectious = c("young", "old", "young", "old") +) +mp_join( + sus = mp_subset(state, Epi = "S"), + inf = mp_subset(state, Epi = "I"), + con = contact, + by = list( + sus.con = "Age" ~ "AgeSusceptible", + inf.con = "Age" ~ "AgeInfectious" + ) +) +#> sus inf con +#> S.young I.young young.young +#> S.old I.young old.young +#> S.young I.old young.old +#> S.old I.old old.old +}\if{html}{\out{
    }} + +If there are only two indexes in \code{...} the \code{by} argument can be either (1) a string giving the dot-concatenation of columns to join on that are common among the tables or (2) a two-sided formula with @@ -28,8 +114,5 @@ their columns are to be matched. The name of each item is a dot contactenation of the names of the corresponding pairs of arguments in \code{...}. The value of each item follows the same rules as the case given above with two indexes. TODO: create examples and point -to them.} -} -\description{ -Join Indexes +to them. } diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index ff89c048..0413027a 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -404,7 +404,7 @@ I_indices <- mp_cartesian( I_indices ``` -This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names are be treated like any other component used to label a particular compartment (like location or age group).] +This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names are to be treated like any other component used to label a particular compartment (like location or age group).] We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function: From c930aec8184c675b2ce10f70841cc9d886da437f Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 10:50:32 -0700 Subject: [PATCH 106/332] migrating some documentation --- R/vector.R | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/R/vector.R b/R/vector.R index ef9f2ee6..94528c82 100644 --- a/R/vector.R +++ b/R/vector.R @@ -135,6 +135,32 @@ as.matrix.Vector = function(x, ...) x$numbers() |> as.matrix() zero_vector = function(labels) setNames(rep(0, length(labels)), labels) +#' Stub +#' +#' This documentation was originally in [mp_index()] and should be cleaned up +#' See issue #131 +#' +#' #' These labels can be used to create 'multidimensional' names for the elements +#' of vectors. Here is the above example expressed in vector form. +#' ```{r, echo = FALSE} +#' v = Vector(prod) +#' v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") +#' ``` +#' This example vector could be stored as a 3-by-2 matrix. But other examples +#' cannot, making this indexing approach more general. For example, consider the +#' following index. +#' ```{r, echo = FALSE} +# symp = mp_index( +# Epi = c("S", "I", "I", "R"), +# Symptoms = c("", "mild", "severe", "") +# ) +# symp +#' ``` +#' This index has an associated indexed vector that cannot be expressed as a +#' matrix. +#' ```{r, echo = FALSE} +#' mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") +#' ``` #' @export mp_vector = function(x, ...) UseMethod("mp_vector") From fae10e045ddbe43dc358cd2b2a77792e3106cae8 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 11:14:57 -0700 Subject: [PATCH 107/332] move an example --- R/index.R | 6 ------ R/vector.R | 13 +++++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/R/index.R b/R/index.R index 09edb12d..b08df3c9 100644 --- a/R/index.R +++ b/R/index.R @@ -78,12 +78,6 @@ #' ) #' print(state) #' labels(state) -#' state_vector = (state -#' |> mp_vector() -#' |> mp_set_numbers(Epi = c(S = 1000)) -#' |> mp_set_numbers(Epi = c(I = 1), Age = "old") -#' ) -#' print(state_vector) #' mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) #' @family indexes #' @export diff --git a/R/vector.R b/R/vector.R index 94528c82..87103013 100644 --- a/R/vector.R +++ b/R/vector.R @@ -161,6 +161,19 @@ zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' ```{r, echo = FALSE} #' mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") #' ``` +#' +#' @examples +#' state = mp_index( +#' Epi = c("S", "I", "S", "I"), +#' Age = c("young", "young", "old", "old") +#' ) +#' state_vector = (state +#' |> mp_vector() +#' |> mp_set_numbers(Epi = c(S = 1000)) +#' |> mp_set_numbers(Epi = c(I = 1), Age = "old") +#' ) +#' print(state_vector) +#' #' @export mp_vector = function(x, ...) UseMethod("mp_vector") From 86240f4176a511cda724a6eb3b52d9d0b14e1235 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 11:15:26 -0700 Subject: [PATCH 108/332] updated documentation for `mp_index()` --- R/index.R | 125 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/R/index.R b/R/index.R index b08df3c9..fda316cb 100644 --- a/R/index.R +++ b/R/index.R @@ -1,75 +1,75 @@ -#' Model Component Index +#' Model Component Index Table #' -#' Make an index object to represent descriptions of model components and their -#' dimensions of variation. These objects generalize and wrap -#' \code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like -#' object is an entry in the index, and each column provides a description of -#' the entries. Each of these entries describes an element of a model component. -#' For example, the following index describes the -#' state variables of an age-structured SIR model. Each row corresponds to a -#' state variable and each state variable is described by columns `Epi` and -#' `Age`. -#' ```{r, echo = FALSE} +#' Make an index table to tabulate model quantity labels by category. These +#' objects generalize and wrap \code{\link{data.frame}}s, where each column is a +#' label category and each row is an entry in the index. Indices must contain +#' only letters, numbers, and underscores. Blank empty string entries are +#' allowed, but missing values (`NA`s) are not. +#' +#' For example, the following index table describes the state variables of the +#' model: +#' ```{r} +#' sir = mp_index(Epi = c("S", "I", "R")) +#' print(sir) +#' ``` +#' Here, the column `Epi` denotes that the category of these labels is +#' epidemiological. There is nothing special about this specific choice of +#' category name; we could have also used another name like `Compartment`. +#' +#' However, in more complicated models, it is good to think carefully about +#' choosing descriptive category names. For example, in an age-structured SIR +#' model, we could add an `Age` column to generate an index table as follows: +#' ```{r} +#' sir_age = mp_index( +#' Epi = rep(c("S", "I", "R"), 2), +#' Age = rep(c("young", "old"), each = 3) +#' ) +#' print(sir_age) +#' ``` +#' Here, having the first column in the index table labeled `Compartment` would +#' be somewhat misleading, as the compartments aren't actually just "S", "I", +#' and "R", they are each of the epidemiological states stratified by the age +#' groups "young" and "old". +#' +#' This index table could also be generated by first specifying individual index +#' tables for the `Epi` and `Age` columns, and then using a `macpan2` product +#' function that combines the tables into a single index table: +#' ```{r} #' sir = mp_index(Epi = c("S", "I", "R")) #' age = mp_index(Age = c("young", "old")) #' prod = mp_cartesian(sir, age) #' prod #' ``` -#' Each index can produce labels for the elements by dot-concatenating the -#' values in each row. The labels of the state variables in this -#' age-structured SIR example model are as follows. +#' The [mp_cartesian()] function will produce a table with entries that are all +#' possible combinations of the individual index tables. The "See Also" section +#' of the [mp_cartesian()] help page catalogues all available product functions. +#' +#' We can produce the full labels of model quantities, which are simply +#' dot-concatenated indices, one for each entry in the index table, using the +#' `labels()` function: #' ```{r, echo = FALSE} #' labels(prod) #' ``` -#' These labels can be used to create 'multidimensional' names for the elements -#' of vectors. Here is the above example expressed in vector form. -#' ```{r, echo = FALSE} -#' v = Vector(prod) -#' v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") -#' ``` -#' This example vector could be stored as a 3-by-2 matrix. But other examples -#' cannot, making this indexing approach more general. For example, consider -#' the following index. -#' ```{r, echo = FALSE} -#' symp = mp_index( -#' Epi = c("S", "I", "I", "R"), -#' Symptoms = c("", "mild", "severe", "") -#' ) -#' symp -#' ``` -#' This index has an associated indexed vector that cannot be expressed -#' as a matrix. -#' ```{r, echo = FALSE} -#' mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") -#' ``` -#' Dots are not allowed in the index so that the labels can be inverted to -#' reproduce the original index (provided that the column names can be -#' retrieved). See the details below for more restrictions. -#' -#' This function is analogous to the -#' \code{\link{data.frame}} function for creating data frames. -#' -#' All values in the index must contain only letters, numbers, and underscores, -#' and blank empty string entries are also allowed. No value can be missing. -#' These restrictions ensure that the dot-concatenation of each row can be -#' unambiguously inverted and that these dot-concatenations produce -#' syntactically valid names (see \code{\link{make.names}}). These -#' dot-concatenations are used to provide labels for the model components. -#' -#' By convention, it is recommended to use CamalCase for the columns of -#' indexes and either snake_case (aging_rate) or single uppercase -#' letters (e.g. S, I). This helps when reading code that contains -#' references to both column names and values in an index. +#' +#' Dots are not allowed in indices so that the labels can be inverted to +#' reproduce the original index table (provided that the column names can be +#' retrieved). +#' +#' It is recommended to use UpperCamelCase for the columns of index tables +#' and single uppercase characters ("S", "I"), all lowercase character +#' strings ("gamma"), and/or snake_case strings ("aging_rate") for indices. This +#' convention helps when reading code that contains references to both column +#' names and indices. #' #' @param ... Character vectors to combine to produce an index. Alternatively, -#' any number of data frames of character-valued columns. If data frames are -#' supplied, their rows will be binded and the result converted to an index -#' if possible. +#' any number of data frames of character-valued columns. If data frames are +#' supplied, their rows will be bound and the result converted to an index if +#' possible. #' @param labelling_column_names A \code{\link{character}} vector of the names -#' of the index that will be used to label the model components (i.e. rows) -#' being described. The \code{labelling_column_names} cannot have duplicates -#' and must contain at least one name. The index given by the -#' \code{labelling_column_names} must uniquely identify each row. +#' of the index that will be used to label the model components (i.e. rows) +#' being described. The \code{labelling_column_names} cannot have duplicates +#' and must contain at least one name. The index given by the +#' \code{labelling_column_names} must uniquely identify each row. #' #' @examples #' state = mp_index( @@ -79,7 +79,12 @@ #' print(state) #' labels(state) #' mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) +#' #' @family indexes +#' +#' @seealso [mp_vector()] +#' @seealso [mp_set_numbers()] +#' #' @export mp_index = function(..., labelling_column_names) UseMethod("mp_index") From 2d82dc641614e26c001a0d7acbbf979340bcbc5b Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 13:17:06 -0500 Subject: [PATCH 109/332] birs --- NAMESPACE | 1 - R/formula_data.R | 6 +++- R/link.R | 17 ++++++----- R/mp.R | 55 +++++++++++++++++++++++++++------- man/Ledger.Rd | 26 ---------------- man/LedgerDefinition.Rd | 29 ++++++++++++++++++ man/mp_aggregate.Rd | 17 +++++++++++ man/mp_cartesian.Rd | 21 ++++++++----- man/mp_index.Rd | 2 +- man/mp_join.Rd | 24 ++++----------- man/mp_linear.Rd | 22 ++++++++++++++ man/mp_rename.Rd | 2 +- man/mp_square.Rd | 25 ++++++++++++++++ man/mp_subset.Rd | 6 ++-- man/mp_symmetric.Rd | 26 ++++++++++++++++ man/mp_triangle.Rd | 34 +++++++++++++++++++++ man/mp_union.Rd | 2 +- man/roxygen/meta.R | 6 +++- misc/experiments/refactorcpp.R | 5 ++++ 19 files changed, 250 insertions(+), 76 deletions(-) delete mode 100644 man/Ledger.Rd create mode 100644 man/LedgerDefinition.Rd create mode 100644 man/mp_aggregate.Rd create mode 100644 man/mp_linear.Rd create mode 100644 man/mp_square.Rd create mode 100644 man/mp_symmetric.Rd create mode 100644 man/mp_triangle.Rd diff --git a/NAMESPACE b/NAMESPACE index 8841ed40..d40576fa 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -108,7 +108,6 @@ export(IndexedExpressions) export(Infection) export(IntVecs) export(JSONReader) -export(Ledger) export(LedgerList) export(Log) export(LogFile) diff --git a/R/formula_data.R b/R/formula_data.R index 7c01e84a..68aee849 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -53,7 +53,11 @@ LedgerData = function(...) { #' print(x$frame, row.names = FALSE) #' } - +#' Bundle up Ledgers +#' +#' Bundle up several ledgers (see \code{\link{LedgerDefinition}}) to pass +#' to \code{\link{mp_dynamic_model}}. +#' #' @export mp_ledgers = function(...) { wrap_ledgers_in_one_element_lists = function(x) { diff --git a/R/link.R b/R/link.R index 81a15280..a541a9f7 100644 --- a/R/link.R +++ b/R/link.R @@ -1,9 +1,10 @@ -#' Ledger +#' Ledgers #' -#' Make an object to describe ledgers between the entities in an -#' \code{\link{Index}}. \code{Ledger} object are created by operating on existing -#' \code{\link{Index}} objects. For example, here the \code{\link{mp_join}} -#' combines two +#' A ledger is a table with rows that identify specific instances of a +#' functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers +#' are most commonly +#' created using the \code{\link{mp_join}} function as in the following +#' example. #' ```{r} #' age = mp_index(Age = c("young", "old")) #' state = mp_cartesian( @@ -16,8 +17,10 @@ #' from.to = "Age" #' ) #' ``` -#' -#' @export +#' @name LedgerDefinition +NULL + + Ledger = function(frame, column_map, reference_index_list, labelling_column_names_list) { self = Base() self$frame = frame diff --git a/R/mp.R b/R/mp.R index 9c9214d5..8663abbd 100644 --- a/R/mp.R +++ b/R/mp.R @@ -12,15 +12,14 @@ mp = function(mp_func) { } -#' Cartesian Product of Indexes +#' Cartesian Product of Index Tables #' -#' Produce a new index by taking all possible pairwise combinations -#' of the input indexes. This is useful for producing product models +#' Produce a new index table by taking all possible pairwise combinations +#' of the input tables. This is useful for producing product models #' that expand model components through stratification. #' -#' @param x,y Objects produced by \code{\link{mp_index}} or derived -#' from such an object using one of (TODO: list the functions that -#' will preserve indexness). +#' @param x Index table (see \code{\link{mp_index}}). +#' @param y Index table (see \code{\link{mp_index}}). #' #' @examples #' mp_cartesian( @@ -48,6 +47,7 @@ mp = function(mp_func) { #' ) #' #' @family indexes +#' @family products #' @export mp_cartesian = function(x, y) { shared_columns = intersect(names(x), names(y)) @@ -69,6 +69,12 @@ mp_cartesian = function(x, y) { Index(f, labelling_column_names = labelling_column_names) } +#' Self Cartesian Product +#' +#' @param suffixes Length-2 character vector giving suffixes that +#' disambiguate the column names in the output. +#' @inheritParams cartesian +#' @family products #' @export mp_square = function(x, suffixes = c("A", "B")) { l1 = sprintf("%s%s", x$labelling_column_names, suffixes[1L]) @@ -86,6 +92,15 @@ mp_square = function(x, suffixes = c("A", "B")) { mp_cartesian(x, y) } +#' Self Cartesian Product Excluding One Off-Diagonal Side +#' +#' @inheritParams cartesian +#' @param y_labelling_column_names TODO +#' @param exclude_diag Should 'diagonal' commponents be excluded from the output. +#' @param lower_tri Should the lower triangular components be include from the +#' output. If \code{FALSE} the result is upper triangular. +#' +#' @family products #' @export mp_triangle = function(x, y_labelling_column_names, exclude_diag = TRUE, lower_tri = FALSE) { f = x$partition$frame() @@ -112,6 +127,10 @@ mp_triangle = function(x, y_labelling_column_names, exclude_diag = TRUE, lower_t Index(f, names(f)) } +#' Symmetric Self Cartesian Product +#' +#' @inheritParams mp_triangle +#' @family products #' @export mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { f = x$partition$frame() @@ -134,6 +153,12 @@ mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { Index(f, names(f)) } +#' Linear Chain Product +#' +#' TODO: what does this mean? +#' +#' @inheritParams mp_square +#' @family products #' @export mp_linear = function(x, y_labelling_column_names) { f = x$partition$frame() @@ -153,7 +178,9 @@ mp_linear = function(x, y_labelling_column_names) { #' Subset of Indexes #' -#' Take a subset of the rows of an index to produce another index. +#' Take a subset of the rows of an index table (see \code{\link{mp_index}}) +#' to produce another index table. The `mp_subset` function gives rows that +#' match a certain criterion and `mp_setdiff` gives rows that do not match. #' #' @param x Model index. #' @param ... Name-value pairs. The names are columns (or sets of columns @@ -183,6 +210,12 @@ mp_setdiff = function(x, ...) { # ) # } +#' Aggregate an Index +#' +#' Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows +#' identifying instances of an aggregation. +#' +#' @family ledgers #' @export mp_aggregate = function(index, by = "Group", ledger_column = "group") { index_columns = to_names(by) @@ -194,8 +227,8 @@ mp_aggregate = function(index, by = "Group", ledger_column = "group") { } Ledger( partition$frame(), - macpan2:::initial_column_map(names(partition), ledger_column), - macpan2:::initial_reference_index_list(index, ledger_column), + initial_column_map(names(partition), ledger_column), + initial_reference_index_list(index, ledger_column), setNames(list(index_columns), ledger_column) ) } @@ -293,7 +326,7 @@ mp_choose_out = function(x, subset_name, ...) { #' Join Indexes #' #' Join two or more index tables (see \code{\link{mp_index}}) to produce a -#' ledger (see \code{\link{mp_ledgers}}). +#' ledger (see \code{\link{LedgerDefinition}}). #' #' When two index tables are passed to `...`, `mp_join` behaves very much like #' an ordinary [inner join](https://en.wikipedia.org/wiki/Join_(SQL)). @@ -366,6 +399,8 @@ mp_choose_out = function(x, subset_name, ...) { #' or expand numeric vectors in archetype formulas. #' @param by What columns to use to join the indexes. See below on #' how to specify this argument. +#' +#' @family ledgers #' @export mp_join = function(..., by = empty_named_list()) { table_list = valid$named_list$assert(list(...)) diff --git a/man/Ledger.Rd b/man/Ledger.Rd deleted file mode 100644 index 59b1eea2..00000000 --- a/man/Ledger.Rd +++ /dev/null @@ -1,26 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/link.R -\name{Ledger} -\alias{Ledger} -\title{Ledger} -\usage{ -Ledger(frame, column_map, reference_index_list, labelling_column_names_list) -} -\description{ -Make an object to describe ledgers between the entities in an -\code{\link{Index}}. \code{Ledger} object are created by operating on existing -\code{\link{Index}} objects. For example, here the \code{\link{mp_join}} -combines two - -\if{html}{\out{
    }}\preformatted{age = mp_index(Age = c("young", "old")) -state = mp_cartesian( - mp_index(Epi = c("S", "I", "R")), - age -) -mp_join( - from = mp_subset(state, Epi = "S"), - to = mp_subset(state, Epi = "I"), - from.to = "Age" -) -}\if{html}{\out{
    }} -} diff --git a/man/LedgerDefinition.Rd b/man/LedgerDefinition.Rd new file mode 100644 index 00000000..764b7ab0 --- /dev/null +++ b/man/LedgerDefinition.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/link.R +\name{LedgerDefinition} +\alias{LedgerDefinition} +\title{Ledgers} +\description{ +A table with rows that identify specific instances of a functional form used +to define a \code{\link{mp_dynamic_model}}. Ledgers are most commonly +created using the \code{\link{mp_join}} function as in the following +example. + +\if{html}{\out{
    }}\preformatted{age = mp_index(Age = c("young", "old")) +state = mp_cartesian( + mp_index(Epi = c("S", "I", "R")), + age +) +mp_join( + from = mp_subset(state, Epi = "S"), + to = mp_subset(state, Epi = "I"), + from.to = "Age" +) +}\if{html}{\out{
    }} +} +\seealso{ +Other functions that return ledgers +\code{\link{mp_aggregate}()}, +\code{\link{mp_join}()} +} +\concept{ledgers} diff --git a/man/mp_aggregate.Rd b/man/mp_aggregate.Rd new file mode 100644 index 00000000..dd192e0d --- /dev/null +++ b/man/mp_aggregate.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_aggregate} +\alias{mp_aggregate} +\title{Aggregate an Index} +\usage{ +mp_aggregate(index, by = "Group", ledger_column = "group") +} +\description{ +Create +} +\seealso{ +Other functions that return ledgers +\code{\link{LedgerDefinition}}, +\code{\link{mp_join}()} +} +\concept{ledgers} diff --git a/man/mp_cartesian.Rd b/man/mp_cartesian.Rd index 166861b3..2021f549 100644 --- a/man/mp_cartesian.Rd +++ b/man/mp_cartesian.Rd @@ -2,18 +2,18 @@ % Please edit documentation in R/mp.R \name{mp_cartesian} \alias{mp_cartesian} -\title{Cartesian Product of Indexes} +\title{Cartesian Product of Index Tables} \usage{ mp_cartesian(x, y) } \arguments{ -\item{x, y}{Objects produced by \code{\link{mp_index}} or derived -from such an object using one of (TODO: list the functions that -will preserve indexness).} +\item{x}{Index table (see \code{\link{mp_index}}).} + +\item{y}{Index table (see \code{\link{mp_index}}).} } \description{ -Produce a new index by taking all possible pairwise combinations -of the input indexes. This is useful for producing product models +Produce a new index table by taking all possible pairwise combinations +of the input tables. This is useful for producing product models that expand model components through stratification. } \examples{ @@ -43,10 +43,17 @@ mp_union( } \seealso{ -Other functions that return indexes +Other functions that return index tables \code{\link{mp_index}()}, \code{\link{mp_rename}()}, \code{\link{mp_subset}()}, \code{\link{mp_union}()} + +Other functions that take products of index tables and return one index tables +\code{\link{mp_linear}()}, +\code{\link{mp_square}()}, +\code{\link{mp_symmetric}()}, +\code{\link{mp_triangle}()} } \concept{indexes} +\concept{products} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index 3be3cf9e..85366655 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -140,7 +140,7 @@ print(state_vector) mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) } \seealso{ -Other functions that return indexes +Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_rename}()}, \code{\link{mp_subset}()}, diff --git a/man/mp_join.Rd b/man/mp_join.Rd index 29d8f159..31ba5574 100644 --- a/man/mp_join.Rd +++ b/man/mp_join.Rd @@ -97,22 +97,10 @@ mp_join( #> S.young I.old young.old #> S.old I.old old.old }\if{html}{\out{}} - -If there are -only two indexes in \code{...} the \code{by} argument can be either -(1) a string giving the dot-concatenation of columns to join on -that are common among the tables or (2) a two-sided formula with -strings on either side. In the formula case, the left-hand-side -is a dot-concatenation of columns in the first index and the -right-hand-side is a dot-concatenation of the columns in the second -index. This formula notation is useful if the name of a column in -one index is different from a column in the other index that -should be joined on. If there are more than two indexes in \code{...} -the \code{by} argument is a named list of strings and/or formulas. -Each item in the list corresponds to a pair of indexes and how -their columns are to be matched. The name of each item is a dot -contactenation of the names of the corresponding pairs of arguments -in \code{...}. The value of each item follows the same rules as the -case given above with two indexes. TODO: create examples and point -to them. } +\seealso{ +Other functions that return ledgers +\code{\link{LedgerDefinition}}, +\code{\link{mp_aggregate}()} +} +\concept{ledgers} diff --git a/man/mp_linear.Rd b/man/mp_linear.Rd new file mode 100644 index 00000000..af47f082 --- /dev/null +++ b/man/mp_linear.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_linear} +\alias{mp_linear} +\title{Linear Chain Product} +\usage{ +mp_linear(x, y_labelling_column_names) +} +\arguments{ +\item{x}{A \code{\link{Partition}} object.} +} +\description{ +TODO: what does this mean? +} +\seealso{ +Other functions that take products of index tables and return one index tables +\code{\link{mp_cartesian}()}, +\code{\link{mp_square}()}, +\code{\link{mp_symmetric}()}, +\code{\link{mp_triangle}()} +} +\concept{products} diff --git a/man/mp_rename.Rd b/man/mp_rename.Rd index 4cc8b4b5..f8afe281 100644 --- a/man/mp_rename.Rd +++ b/man/mp_rename.Rd @@ -14,7 +14,7 @@ is a character vector giving the old name.} Rename Index Columns } \seealso{ -Other functions that return indexes +Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, \code{\link{mp_subset}()}, diff --git a/man/mp_square.Rd b/man/mp_square.Rd new file mode 100644 index 00000000..ed4c565c --- /dev/null +++ b/man/mp_square.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_square} +\alias{mp_square} +\title{Self Cartesian Product} +\usage{ +mp_square(x, suffixes = c("A", "B")) +} +\arguments{ +\item{x}{A \code{\link{Partition}} object.} + +\item{suffixes}{Length-2 character vector giving suffixes that +disambiguate the column names in the output.} +} +\description{ +Self Cartesian Product +} +\seealso{ +Other functions that take products of index tables and return one index tables +\code{\link{mp_cartesian}()}, +\code{\link{mp_linear}()}, +\code{\link{mp_symmetric}()}, +\code{\link{mp_triangle}()} +} +\concept{products} diff --git a/man/mp_subset.Rd b/man/mp_subset.Rd index 7d1613de..ffe1881c 100644 --- a/man/mp_subset.Rd +++ b/man/mp_subset.Rd @@ -18,10 +18,12 @@ that refer to labels with respect to those columns. These values determine the resulting subset.} } \description{ -Take a subset of the rows of an index to produce another index. +Take a subset of the rows of an index table (see \code{\link{mp_index}}) +to produce another index table. The \code{mp_subset} function gives rows that +match a certain criterion and \code{mp_setdiff} gives rows that do not match. } \seealso{ -Other functions that return indexes +Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, \code{\link{mp_rename}()}, diff --git a/man/mp_symmetric.Rd b/man/mp_symmetric.Rd new file mode 100644 index 00000000..0cb0fe9b --- /dev/null +++ b/man/mp_symmetric.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_symmetric} +\alias{mp_symmetric} +\title{Symmetric Self Cartesian Product} +\usage{ +mp_symmetric(x, y_labelling_column_names, exclude_diag = TRUE) +} +\arguments{ +\item{x}{A \code{\link{Partition}} object.} + +\item{y_labelling_column_names}{TODO} + +\item{exclude_diag}{Should 'diagonal' commponents be excluded from the output.} +} +\description{ +Symmetric Self Cartesian Product +} +\seealso{ +Other functions that take products of index tables and return one index tables +\code{\link{mp_cartesian}()}, +\code{\link{mp_linear}()}, +\code{\link{mp_square}()}, +\code{\link{mp_triangle}()} +} +\concept{products} diff --git a/man/mp_triangle.Rd b/man/mp_triangle.Rd new file mode 100644 index 00000000..af34c094 --- /dev/null +++ b/man/mp_triangle.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_triangle} +\alias{mp_triangle} +\title{Self Cartesian Product Excluding One Off-Diagonal Side} +\usage{ +mp_triangle( + x, + y_labelling_column_names, + exclude_diag = TRUE, + lower_tri = FALSE +) +} +\arguments{ +\item{x}{A \code{\link{Partition}} object.} + +\item{y_labelling_column_names}{TODO} + +\item{exclude_diag}{Should 'diagonal' commponents be excluded from the output.} + +\item{lower_tri}{Should the lower triangular components be include from the +output. If \code{FALSE} the result is upper triangular.} +} +\description{ +Self Cartesian Product Excluding One Off-Diagonal Side +} +\seealso{ +Other functions that take products of index tables and return one index tables +\code{\link{mp_cartesian}()}, +\code{\link{mp_linear}()}, +\code{\link{mp_square}()}, +\code{\link{mp_symmetric}()} +} +\concept{products} diff --git a/man/mp_union.Rd b/man/mp_union.Rd index ab3746b6..f8dfaf9d 100644 --- a/man/mp_union.Rd +++ b/man/mp_union.Rd @@ -13,7 +13,7 @@ mp_union(...) Union of Indexes } \seealso{ -Other functions that return indexes +Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, \code{\link{mp_rename}()}, diff --git a/man/roxygen/meta.R b/man/roxygen/meta.R index 63290ef2..9b1b42dd 100644 --- a/man/roxygen/meta.R +++ b/man/roxygen/meta.R @@ -1,3 +1,7 @@ list( - rd_family_title = list(indexes = "Other functions that return indexes") + rd_family_title = list( + indexes = "Other functions that return index tables", + products = "Other functions that take products of index tables and return one index tables", + ledgers = "Other functions that return ledgers" + ) ) diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index f7b31aee..985cecca 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -11,6 +11,11 @@ sir_two_strains = (mp_rename(sir, A = "Epi") |> mp_setdiff(A.B = "I.I", A.C = "I.I", B.C = "I.I") ) + +gg = mp_aggregate(sir_two_strains, by = "A", ledger_column = "a_strain") +gg$positions_for$a_strain() + + ## replacement model n_strains = 2L From f258279d1793654a202b9581fb4483db3518c472 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 11:47:38 -0700 Subject: [PATCH 110/332] index table = output of `mp_index()`, index = entry in an index table --- vignettes/quickstart.Rmd | 43 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 0413027a..bfb2d04e 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -113,7 +113,7 @@ Since the focus of this quickstart guide is `macpan2`'s model specification gram ```{r SIR-starter, echo = FALSE} ## helper function to simplify the exposition in this vigette ----------- SIR_starter <- function( - # indices for model quantities + # index tables for model quantities state, rate, # ledgers for calculation archetypes @@ -150,7 +150,7 @@ SIR_starter <- function( force_of_infection = mp_ledgers(force_of_infection) ) - ## Initialize indexed vectors (to all zeros) -------------- + ## Initialize vectors from index tables (with all zeros for values) -------------- # used as placeholders for user input init_vecs <- list( state = mp_vector(state), @@ -168,10 +168,10 @@ SIR_starter <- function( The inputs to `SIR_starter()` are of two types: -- **indices** (labels) for model quantities, +- **index tables** containing indices (labels) of model quantities, - **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included calculation archetypes). -The indices we need to specify fall into two groups: +The index tables we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ @@ -183,16 +183,15 @@ We have identified two useful **calculation archetypes** that we have baked into In this case, the `flow` archetype is actually repeated within these model equations but the `force_of_infection` archetype is used only once. We've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. -We start by creating the `state` and `rate` indices: +We start by creating the `state` and `rate` index tables: -```{r sir-indices} -## indices (labels) for model quantities ------------------------- +```{r sir-index-tables} +## index tables to label model quantities ------------------------- state <- mp_index(Epi = c("S", "I", "R")) rate <- mp_index(Epi = c("beta", "gamma", "lambda")) ``` The `mp_index()` function sets structures like data frames that tabulate the model quantity labels: -[BMB: tabulate → enumerate? list?] ```{r sir-state-and-rate} state @@ -262,7 +261,7 @@ Now we can use the `SIR_starter()` function to initialize our model object: ```{r sir} ## SIR model object ------------------------- sir <- SIR_starter( - # indices + # index tables state = state, rate = rate, # ledgers @@ -274,7 +273,7 @@ sir <- SIR_starter( ) ``` -We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for the indices (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): +We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for each index (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): ```{r sir-simulator} ## SIR model simulator ------------------------- @@ -305,8 +304,8 @@ head(sir_results) The simulation output has several columns: -- `matrix`: The matrix storing our values internally, corresponding to our two indices, `state` and `rate`. -- `time`: An internal time index, where `time = 0` corresponds to the user-provided values (initial conditions). [SW: note that the default now more intuitively starts at time = 1] +- `matrix`: The matrix storing our values internally, corresponding to our two index tables, `state` and `rate`. +- `time`: An internal time index, where `time = 1` is the result after the first step through the simulation loop. - `row`: The primary label for the `value` (the row name in the corresponding `matrix`). - `col`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the outputs of this model (i.e. states and rates) are specified as vectors and not matrices, this column is empty for all entries. TODO: When would this be useful? - `value`: The numerical value. @@ -369,15 +368,15 @@ As previously noted, we created a `force_of_infection` archetype of the form $\b Since we already have an archetype for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. -To define the two-strain model, we again must specify our `state` and `rate` indices, as well as our `infection`, `recovery`, and `force_of_infection` ledgers. +To define the two-strain model, we again must specify our `state` and `rate` index tables, as well as our `infection`, `recovery`, and `force_of_infection` ledgers. -We start by creating a new index for the strains: +We start by creating a new set of indices for the strains: ```{r strain-indices} Strain_indices <- c("A", "B") ``` -A simple approach would be to define the new state and rate names directly using the `mp_index()` function, as we did above: +A simple approach would be to define a table of the new state and rate indices directly using the `mp_index()` function, as we did above: ```{r two-strain-state-rate-manual, eval = FALSE} state <- mp_index( @@ -393,7 +392,7 @@ rate <- mp_index( However, this approach is less flexible if we want to build a complex model or if we already have a simpler, working model (like the SIR above) and want expand it with many strata and/or several different types of strata. We present an alternative approach below that is more verbose but far more flexible. -For the state, we want to cross $I$ with the different strains to create one $I$ compartment per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: +For the state, we want to cross $I$ with the different strains to create one $I$ compartment name per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: ```{r strain-expand-I} I_indices <- mp_cartesian( @@ -404,9 +403,9 @@ I_indices <- mp_cartesian( I_indices ``` -This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names are to be treated like any other component used to label a particular compartment (like location or age group).] +This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names get treated like any other component used to label a particular compartment (like location or age group).] -We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function: +We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function to make a `state` index table: ```{r two-strain-state} state <- mp_union( @@ -418,7 +417,7 @@ state <- mp_union( state ``` -We update the rates similarly: +We update the `rate` index table similarly: ```{r two-strain-rate} rate <- @@ -469,7 +468,7 @@ infection <- mp_join( infection ``` -Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two of the index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list element name (`to_states.flow_rates`), with the names coming from `mp_join()`'s argument names (`to_states`, `flow_rates`). The list element value should be a character name of the index type upon which to match. In this case, the value is `"Strain"` because we want the "to state" labels and the "flow rate" labels to match based on the strain index (`I.A` with `lambda.A` and `I.B` with `lambda.B`). +Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two of the index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list element name (`to_states.flow_rates`), with the names coming from `mp_join()`'s argument names (`to_states`, `flow_rates`). The list element value should be a character string corresponding to the index table column name upon which to perform matches. In this case, the value is `"Strain"` because we want the "to state" labels and the "flow rate" labels to match based on the `Strain` index table column (`I.A` with `lambda.A` and `I.B` with `lambda.B`). For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: @@ -492,7 +491,7 @@ mp_join( ) ``` -We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective indices. Internally, `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins to the same effect: +We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective index tables. Internally, `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins to the same effect: ```{r two-strain-foi-ledger} ## new force of infection ledger ------------------------- @@ -513,7 +512,7 @@ Now we're ready to build the two-strain model object and simulate it: ```{r two-strain-results, fig.width = 6, fig.height = 4} two_strain_model <- SIR_starter( - # indices + # index tables state = state, rate = rate, # ledgers From d267d9c23f6b220d46bce6569d7f1e9a39545769 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 12:05:25 -0700 Subject: [PATCH 111/332] "calculation archetype" -> "functional form" --- vignettes/quickstart.Rmd | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index bfb2d04e..506e7cdd 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -46,7 +46,7 @@ This vignette seeks to explain `macpan2`'s model specification grammar and in pa # Amuse bouche: a structured SIR model {#amuse-bouche} -A key to `macpan2`'s flexible model grammar is the use of **archetypes** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): +A key to `macpan2`'s flexible model grammar is the use of **functional forms** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections):
    ![](../misc/diagrams/quickstart/two-strain.svg) @@ -69,24 +69,24 @@ S_{t+1} & = - [\beta_A (I_A)_t/N + \beta_B (I_B)_t/N] S_t, \\ R_{t+1} &= \phantom{-[} \gamma (I_A)_t + \gamma (I_B)_t. \end{align} -Each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: +Each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **functional form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: ``` lambda.A = beta.A * I.A / N lambda.B = beta.B * I.B / N ``` -However, in `macpan2`, we can specify a single calculation archetype for it, for instance +However, in `macpan2`, we can specify a single functional form for it, for instance ``` lambda = beta * I / N ``` -and then attach a **ledger** to the model object that tabulates which specific calculations to do under this archetype, that is, a table of which specific subscripted `lambda`, `beta`, and `I` to use each time we invoke this archetype during the simulation. +and then attach a **ledger** to the model object that tabulates specific instances of when this functional form is used to define a component of the model. In other words, this ledger should enumerate which specific subscripted `lambda`, `beta`, and `I` to use each time we invoke the associated functional form during the simulation. In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each of two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). -**Using calculation archetypes and ledgers allows the modeller to focus on modelling questions**, like the design of the model structure and the choice of expressions for the forces of infection, while **`macpan2` handles the bookkeeping**, matching stratified variables with each other when calculating expressions. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, rather than error-prone editing of calculations in the simulation code. +**Using functional forms and ledgers allows the modeller to focus on modelling questions**, like the design of the model structure and the choice of expressions for the forces of infection, while **`macpan2` handles the bookkeeping**, matching stratified variables with each other when calculating expressions. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, rather than error-prone editing of calculations in the simulation code. While a modeller could write their own code to cut down on repetition when expanding a simple model (and many do), `macpan2` provides a ready-made model specification grammar that enables easy model extension, especially when building [product models](https://arxiv.org/abs/2307.10308), and that can readily interface with fast simulation and calibration engines, like [TMB](https://cran.r-project.org/web/packages/TMB/index.html). @@ -116,12 +116,12 @@ SIR_starter <- function( # index tables for model quantities state, rate, - # ledgers for calculation archetypes + # ledgers tabulating the use of different functional forms flow, # list of individual ledgers force_of_infection ){ - ## Set up expressions list for each calculation archetype -------------- + ## Set up expressions list for each functional form -------------- ## names refer to when the calculation gets performed relative to ## the simulation time-step loop (before, during, ...) expr_list <- mp_expr_list( @@ -169,19 +169,19 @@ SIR_starter <- function( The inputs to `SIR_starter()` are of two types: - **index tables** containing indices (labels) of model quantities, -- **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included calculation archetypes). +- **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included functional forms). The index tables we need to specify fall into two groups: - `state`: state names, $S$, $I$, and $R$ from the model equations - `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ -We have identified two useful **calculation archetypes** that we have baked into `SIR_starter()`. In this case, we're thinking of calculation archetypes not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The archetypes are: +We have identified two useful **functional forms** that we have baked into `SIR_starter()`. In this case, we're thinking of these forms not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The forms are: -- `flow`: Unsigned flows from one class to another of the form $rX$, with $r>0$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. -- `force_of_infection`: The prevalence-dependent *per capita* rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. +- **flow**: Unsigned flows from one class to another of the form $rX$, with $r>0$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. +- **force of infection**: The prevalence-dependent *per capita* rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. -In this case, the `flow` archetype is actually repeated within these model equations but the `force_of_infection` archetype is used only once. We've identified the force of infection as an archetype since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these archetypes are already baked into `SIR_starter()`, so our task will be creating a calculation ledger for each of these archetypes. +In this case, the flow form is repeated within these model equations, while the force of infection form is used only once. We've identified the force of infection as a functional form since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these forms are already baked into `SIR_starter()`, so our task will be creating a ledger for each of these forms to input into the function. We start by creating the `state` and `rate` index tables: @@ -200,7 +200,7 @@ rate The `Epi` column name is unimportant in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. -For the `flow` archetype, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$. We specify flows using the name of the state from which it originates (`from_states`), the state to which it goes (`to_states`), and a flow rate name (`flow_rates`). +For the flow form, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$ and then pass these as a list to the `flow` argument of `SIR_starter()`. We specify flows using the name of the state from which it originates (`from_states`), the state to which it goes (`to_states`), and a flow rate name (`flow_rates`). We use the `mp_join()` function to create the `infection` ledger like so: @@ -227,7 +227,7 @@ and by default creates one entry in the ledger for each combination of these val infection ``` -The names of the arguments in the `mp_join()` function are tied to the calculation archetypes baked into `SIR_starter()`, but in general modellers can choose their calculation archetypes and the corresponding argument names however they like.^[There is only one `mp_join()` argument name that is not available to the user — `by`, which has a special role that we will see [later](#main-course).] +The names of the arguments in the `mp_join()` function are tied to how the functional form baked into `SIR_starter()` is specified, but in general modellers can define their functional forms and the corresponding `mp_join()` argument names however they like.^[There is only one `mp_join()` argument name that is not available to the user — `by`, which has a special role that we will see [later](#main-course).] We create the `recovery` ledger in a similar way: @@ -242,7 +242,7 @@ recovery <- mp_join( recovery ``` -Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different calculation archetype in `SIR_starter()` (so the `mp_join()` argument names are different): +Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different functional form in `SIR_starter()` (so the `mp_join()` argument names are different): ```{r sir-foi-ledger} ## force of infection ledger ------------------------- @@ -254,7 +254,7 @@ force_of_infection <- mp_join( ) ``` -For this calculation archetype, we need to specify the `transmission_rates` and `infectious_states` involved in computing the force of infection, as well as the names where we want to store the results of this calculation (`infection_flow_rates`) for use in the `infection` flow calculations. +For this functional form, we need to specify the `transmission_rates` and `infectious_states` involved in computing the force of infection, as well as the names where we want to store the results of this calculation (`infection_flow_rates`) for use in the `infection` flow calculations. Now we can use the `SIR_starter()` function to initialize our model object: @@ -287,7 +287,7 @@ sir_simulator <- mp_tmb_simulator( ) ``` -Note that we've specified `NA` for `lambda` as it will be calculated for us using the `force_of_infection` archetype. [BMB: is this required, or could `macpan2` assume that unspecified values are set to `NA` automatically? (There could be an implicit/explicit design tradeoff here - i.e. it would be convenient to do this automatically but might be better to force users to be explicit ...)] +Note that we've specified `NA` for `lambda` as it will be calculated for us using the force of infection functional form. Then we can actually simulate the model by passing our model simulator to `mp_report()`: @@ -359,14 +359,14 @@ legend("left", col = 1:3, lty = 1:3, legend = state$labels()) # Main course: expanding the basic SIR with additional structure {#main-course} -As previously noted, we created a `force_of_infection` archetype of the form $\beta I / N$ despite it only being used once to define the SIR model. However, if we consider the two-strain model from [before](#amuse-bouche), we see this calculation is repeated for each strain: +As previously noted, we created a force of infection functional form ($\beta I / N$) despite it only being used once to define the SIR model. However, if we consider the two-strain model from [before](#amuse-bouche), we see this calculation is repeated for each strain: \begin{align} \lambda_A &= \beta_A I_A/N \\ \lambda_B &= \beta_B I_B/N \end{align} -Since we already have an archetype for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. +Since we already have a form for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. To define the two-strain model, we again must specify our `state` and `rate` index tables, as well as our `infection`, `recovery`, and `force_of_infection` ledgers. @@ -553,7 +553,7 @@ As mentioned, we've hidden some of the details of initializing a model object wi <> ``` -This function definition shows how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the calculation archetypes used to simulate the model, including some we explored above (for the forces of infection and the unsigned flows), as well as some that we didn't discuss, like the archetypes for total inflow, total outflow, and state update. The `ledgers` and `init_vecs` are set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. +This function definition shows how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the functional forms used to simulate the model, including some we explored above (unsigned flows, force of infection), as well as some that we didn't discuss (total inflow, total outflow, state update). The `ledgers` and `init_vecs` are just set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. These topics will be discussed fully in a future vignette. From 357dd73710f0c07e376155a3535197ee49b3522c Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 12:08:11 -0700 Subject: [PATCH 112/332] remove mention of the model library (not ready) --- vignettes/quickstart.Rmd | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 506e7cdd..c0296390 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -24,8 +24,6 @@ library(dplyr) TO DO: -- make sure all functions used have good help pages -- get model library ready (or remove text below referencing it) - be sure `DynamicModel()` is exported - harmonize presentation with quickstart @@ -42,7 +40,7 @@ There is a trade-off between the flexibility and the simplicity of the model gra - testing processes to identify infections - vaccination status -This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. The package also comes with a model library (see `vignette("example_models")`) to get users started with commonly used basic models, and to demonstrate some more complex cases. +This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. # Amuse bouche: a structured SIR model {#amuse-bouche} From 05529883d754058f04fe38d315ed4a142fe92ae2 Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 12:18:47 -0700 Subject: [PATCH 113/332] slight update to index documentation --- R/index.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/index.R b/R/index.R index fda316cb..e4c8980a 100644 --- a/R/index.R +++ b/R/index.R @@ -1,8 +1,8 @@ -#' Model Component Index Table +#' Model Quantity Index Table #' -#' Make an index table to tabulate model quantity labels by category. These +#' Make an index table to enumerate model quantity labels by category. These #' objects generalize and wrap \code{\link{data.frame}}s, where each column is a -#' label category and each row is an entry in the index. Indices must contain +#' label category and each row is an index. Indices must contain #' only letters, numbers, and underscores. Blank empty string entries are #' allowed, but missing values (`NA`s) are not. #' From 895beb34c0ae7d7933da307d0b1bde41d3886d3e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 14 Nov 2023 14:59:10 -0500 Subject: [PATCH 114/332] :pencil: update specs [no ci] --- vignettes/quickstart.Rmd | 9 --------- 1 file changed, 9 deletions(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c0296390..c517ddaf 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -20,15 +20,6 @@ library(ggplot2) library(dplyr) ``` -[ - -TO DO: - -- be sure `DynamicModel()` is exported -- harmonize presentation with quickstart - -] - One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when building upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, especially when they are cast as expansions of simple models. Such [structured models](https://idpjournal.biomedcentral.com/articles/10.1186/s40249-022-01001-y) can include: From 432985c124d1d677c81bf1fa1e2ea1af76ca9042 Mon Sep 17 00:00:00 2001 From: Mikael Jagan Date: Tue, 14 Nov 2023 17:13:47 -0700 Subject: [PATCH 115/332] don't try to build incompatible vignettes --- .Rbuildignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index c88d1bdb..a698a372 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -19,3 +19,13 @@ LICENSE ^doc$ ^Meta$ drat/ + +^vignettes/calibration[.]Rmd$ +^vignettes/example_models[.]Rmd$ +^vignettes/flow_types[.]Rmd$ +^vignettes/hello_products[.]Rmd$ +^vignettes/model_definitions[.]Rmd$ +^vignettes/quickstart2[.]Rmd$ +^vignettes/quickstart_products[.]Rmd$ +^vignettes/sir_radial_basis_transmission[.]Rmd$ +^vignettes/time_varying_parameters[.]Rmd$ From 6edcabc01498c54db1291c949aab68287e7f5eeb Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 14 Nov 2023 19:34:21 -0700 Subject: [PATCH 116/332] basic code chunks for code that shouldn't be executed --- vignettes/quickstart.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c517ddaf..78c287ba 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -367,7 +367,7 @@ Strain_indices <- c("A", "B") A simple approach would be to define a table of the new state and rate indices directly using the `mp_index()` function, as we did above: -```{r two-strain-state-rate-manual, eval = FALSE} +``` state <- mp_index( Epi = c("S", rep("I", 2), "R"), Strain = c("", Strain_indices, "") From 72776ec02f8dd2ba3b151ba41447052bacebe6f5 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 15 Nov 2023 09:07:41 -0500 Subject: [PATCH 117/332] docs and fix #133 --- NAMESPACE | 1 + R/index_to_tmb.R | 16 +++++ R/model_def_run.R | 4 ++ R/vector.R | 18 +++-- man/LedgerDefinition.Rd | 11 +--- man/mp_aggregate.Rd | 4 +- man/mp_dynamic_model.Rd | 16 +++++ man/mp_index.Rd | 137 +++++++++++++++++++++------------------ man/mp_join.Rd | 3 +- man/mp_ledgers.Rd | 9 ++- man/mp_tmb_simulator.Rd | 48 ++++++++++++++ man/mp_vector.Rd | 45 +++++++++++++ vignettes/quickstart.Rmd | 6 +- 13 files changed, 230 insertions(+), 88 deletions(-) create mode 100644 man/mp_dynamic_model.Rd create mode 100644 man/mp_tmb_simulator.Rd create mode 100644 man/mp_vector.Rd diff --git a/NAMESPACE b/NAMESPACE index d40576fa..a2aa9caf 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ S3method(Index,Index) S3method(Index,Partition) S3method(Index,data.frame) S3method(Vector,Index) +S3method(Vector,Vector) S3method(Vector,data.frame) S3method(Vector,numeric) S3method(as.data.frame,Index) diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 4b376028..9b5af227 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -1,3 +1,13 @@ +#' TMB Simulator from Dynamic Model +#' +#' @param dynamic_model Object product by \code{\link{dynamic_model}}. +#' @param vectors Named list of named vectors as initial values for the +#' simulations that are referenced in the expression list in the dynamic model. +#' @param unstruc_mats = Named list of objects that can be coerced to +#' numerical matrices that are used in the expression list of the +#' dynamic model. +#' @inheritParams TMBModel +#' #' @importFrom oor method_apply #' @export mp_tmb_simulator = function(dynamic_model @@ -46,6 +56,12 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model indexed_mats = dynamic_model$init_vecs mats_to_save = names(indexed_mats) } else { + for (v in names(vectors)) { + vectors[[v]] = Vector( + vectors[[v]], + dynamic_model$init_vecs[[v]]$index + ) + } indexed_mats = lapply(vectors, as.matrix) } if (is.null(unstruc_mats)) { diff --git a/R/model_def_run.R b/R/model_def_run.R index 63fdf65b..f65a0fe1 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -111,6 +111,10 @@ DynamicModel = function(expr_list = ExprList(), ledgers = list(), init_vecs = li } +#' Dynamic Model +#' +#' +#' #' @export mp_dynamic_model = DynamicModel diff --git a/R/vector.R b/R/vector.R index 87103013..376a60be 100644 --- a/R/vector.R +++ b/R/vector.R @@ -58,6 +58,11 @@ Vector.numeric = function(x, index, ...) { do.call(v$set_numbers, args) } +#' @export +Vector.Vector = function(x, index, ...) { + Vector(x$numbers(), index, ...) +} + #' @export Vector.Index = function(x, ...) { self = Base() @@ -65,6 +70,7 @@ Vector.Index = function(x, ...) { self$.numbers = zero_vector(self$index$labels()) self$numbers = function(...) { l = list(...) + ## check if all numbers if (length(l) == 0L) return(self$.numbers) i = mp_subset(self$index, ...)$labels() self$.numbers[i] @@ -131,15 +137,17 @@ print.Vector = function(x, ...) print(x$numbers()) names.Vector = function(x) x$numbers() |> names() #' @export -as.matrix.Vector = function(x, ...) x$numbers() |> as.matrix() +as.matrix.Vector = function(x, ...) { + x$numbers() |> as.matrix() +} zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' Stub -#' +#' #' This documentation was originally in [mp_index()] and should be cleaned up #' See issue #131 -#' +#' #' #' These labels can be used to create 'multidimensional' names for the elements #' of vectors. Here is the above example expressed in vector form. #' ```{r, echo = FALSE} @@ -161,7 +169,7 @@ zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' ```{r, echo = FALSE} #' mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") #' ``` -#' +#' #' @examples #' state = mp_index( #' Epi = c("S", "I", "S", "I"), @@ -173,7 +181,7 @@ zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' |> mp_set_numbers(Epi = c(I = 1), Age = "old") #' ) #' print(state_vector) -#' +#' #' @export mp_vector = function(x, ...) UseMethod("mp_vector") diff --git a/man/LedgerDefinition.Rd b/man/LedgerDefinition.Rd index 764b7ab0..73f87631 100644 --- a/man/LedgerDefinition.Rd +++ b/man/LedgerDefinition.Rd @@ -4,8 +4,9 @@ \alias{LedgerDefinition} \title{Ledgers} \description{ -A table with rows that identify specific instances of a functional form used -to define a \code{\link{mp_dynamic_model}}. Ledgers are most commonly +A ledger is a table with rows that identify specific instances of a +functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers +are most commonly created using the \code{\link{mp_join}} function as in the following example. @@ -21,9 +22,3 @@ mp_join( ) }\if{html}{\out{}} } -\seealso{ -Other functions that return ledgers -\code{\link{mp_aggregate}()}, -\code{\link{mp_join}()} -} -\concept{ledgers} diff --git a/man/mp_aggregate.Rd b/man/mp_aggregate.Rd index dd192e0d..f48d7669 100644 --- a/man/mp_aggregate.Rd +++ b/man/mp_aggregate.Rd @@ -7,11 +7,11 @@ mp_aggregate(index, by = "Group", ledger_column = "group") } \description{ -Create +Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows +identifying instances of an aggregation. } \seealso{ Other functions that return ledgers -\code{\link{LedgerDefinition}}, \code{\link{mp_join}()} } \concept{ledgers} diff --git a/man/mp_dynamic_model.Rd b/man/mp_dynamic_model.Rd new file mode 100644 index 00000000..d83b13d2 --- /dev/null +++ b/man/mp_dynamic_model.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model_def_run.R +\name{mp_dynamic_model} +\alias{mp_dynamic_model} +\title{Dynamic Model} +\usage{ +mp_dynamic_model( + expr_list = ExprList(), + ledgers = list(), + init_vecs = list(), + unstruc_mats = list() +) +} +\description{ +Dynamic Model +} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index 85366655..e4c60910 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -7,7 +7,7 @@ \alias{labelling_column_names.Index} \alias{to_labels.Index} \alias{labels.Index} -\title{Model Component Index} +\title{Model Quantity Index Table} \usage{ mp_index(..., labelling_column_names) @@ -24,8 +24,8 @@ mp_index(..., labelling_column_names) \arguments{ \item{...}{Character vectors to combine to produce an index. Alternatively, any number of data frames of character-valued columns. If data frames are -supplied, their rows will be binded and the result converted to an index -if possible.} +supplied, their rows will be bound and the result converted to an index if +possible.} \item{labelling_column_names}{A \code{\link{character}} vector of the names of the index that will be used to label the model components (i.e. rows) @@ -34,17 +34,38 @@ and must contain at least one name. The index given by the \code{labelling_column_names} must uniquely identify each row.} } \description{ -Make an index object to represent descriptions of model components and their -dimensions of variation. These objects generalize and wrap -\code{\link{data.frame}}s. Each row of this \code{\link{data.frame}}-like -object is an entry in the index, and each column provides a description of -the entries. Each of these entries describes an element of a model component. -For example, the following index describes the -state variables of an age-structured SIR model. Each row corresponds to a -state variable and each state variable is described by columns \code{Epi} and -\code{Age}. - -\if{html}{\out{
    }}\preformatted{#> Epi Age +Make an index table to enumerate model quantity labels by category. These +objects generalize and wrap \code{\link{data.frame}}s, where each column is a +label category and each row is an index. Indices must contain +only letters, numbers, and underscores. Blank empty string entries are +allowed, but missing values (\code{NA}s) are not. +} +\details{ +For example, the following index table describes the state variables of the +model: + +\if{html}{\out{
    }}\preformatted{sir = mp_index(Epi = c("S", "I", "R")) +print(sir) +#> Epi +#> S +#> I +#> R +}\if{html}{\out{
    }} + +Here, the column \code{Epi} denotes that the category of these labels is +epidemiological. There is nothing special about this specific choice of +category name; we could have also used another name like \code{Compartment}. + +However, in more complicated models, it is good to think carefully about +choosing descriptive category names. For example, in an age-structured SIR +model, we could add an \code{Age} column to generate an index table as follows: + +\if{html}{\out{
    }}\preformatted{sir_age = mp_index( + Epi = rep(c("S", "I", "R"), 2), + Age = rep(c("young", "old"), each = 3) +) +print(sir_age) +#> Epi Age #> S young #> I young #> R young @@ -53,57 +74,48 @@ state variable and each state variable is described by columns \code{Epi} and #> R old }\if{html}{\out{
    }} -Each index can produce labels for the elements by dot-concatenating the -values in each row. The labels of the state variables in this -age-structured SIR example model are as follows. - -\if{html}{\out{
    }}\preformatted{#> [1] "S.young" "I.young" "R.young" "S.old" "I.old" "R.old" -}\if{html}{\out{
    }} +Here, having the first column in the index table labeled \code{Compartment} would +be somewhat misleading, as the compartments aren't actually just "S", "I", +and "R", they are each of the epidemiological states stratified by the age +groups "young" and "old". -These labels can be used to create 'multidimensional' names for the elements -of vectors. Here is the above example expressed in vector form. +This index table could also be generated by first specifying individual index +tables for the \code{Epi} and \code{Age} columns, and then using a \code{macpan2} product +function that combines the tables into a single index table: -\if{html}{\out{
    }}\preformatted{#> S.young I.young R.young S.old I.old R.old -#> 1000 0 0 1000 1 0 +\if{html}{\out{
    }}\preformatted{sir = mp_index(Epi = c("S", "I", "R")) +age = mp_index(Age = c("young", "old")) +prod = mp_cartesian(sir, age) +prod +#> Epi Age +#> S young +#> I young +#> R young +#> S old +#> I old +#> R old }\if{html}{\out{
    }} -This example vector could be stored as a 3-by-2 matrix. But other examples -cannot, making this indexing approach more general. For example, consider -the following index. +The \code{\link[=mp_cartesian]{mp_cartesian()}} function will produce a table with entries that are all +possible combinations of the individual index tables. The "See Also" section +of the \code{\link[=mp_cartesian]{mp_cartesian()}} help page catalogues all available product functions. -\if{html}{\out{
    }}\preformatted{#> Epi Symptoms -#> S -#> I mild -#> I severe -#> R -}\if{html}{\out{
    }} - -This index has an associated indexed vector that cannot be expressed -as a matrix. +We can produce the full labels of model quantities, which are simply +dot-concatenated indices, one for each entry in the index table, using the +\code{labels()} function: -\if{html}{\out{
    }}\preformatted{#> S. I.mild I.severe R. -#> 1000 0 1 0 +\if{html}{\out{
    }}\preformatted{#> [1] "S.young" "I.young" "R.young" "S.old" "I.old" "R.old" }\if{html}{\out{
    }} -Dots are not allowed in the index so that the labels can be inverted to -reproduce the original index (provided that the column names can be -retrieved). See the details below for more restrictions. -} -\details{ -This function is analogous to the -\code{\link{data.frame}} function for creating data frames. - -All values in the index must contain only letters, numbers, and underscores, -and blank empty string entries are also allowed. No value can be missing. -These restrictions ensure that the dot-concatenation of each row can be -unambiguously inverted and that these dot-concatenations produce -syntactically valid names (see \code{\link{make.names}}). These -dot-concatenations are used to provide labels for the model components. - -By convention, it is recommended to use CamalCase for the columns of -indexes and either snake_case (aging_rate) or single uppercase -letters (e.g. S, I). This helps when reading code that contains -references to both column names and values in an index. +Dots are not allowed in indices so that the labels can be inverted to +reproduce the original index table (provided that the column names can be +retrieved). + +It is recommended to use UpperCamelCase for the columns of index tables +and single uppercase characters ("S", "I"), all lowercase character +strings ("gamma"), and/or snake_case strings ("aging_rate") for indices. This +convention helps when reading code that contains references to both column +names and indices. } \section{Functions}{ \itemize{ @@ -131,15 +143,14 @@ state = mp_index( ) print(state) labels(state) -state_vector = (state - |> mp_vector() - |> mp_set_numbers(Epi = c(S = 1000)) - |> mp_set_numbers(Epi = c(I = 1), Age = "old") -) -print(state_vector) mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) + } \seealso{ +\code{\link[=mp_vector]{mp_vector()}} + +\code{\link[=mp_set_numbers]{mp_set_numbers()}} + Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_rename}()}, diff --git a/man/mp_join.Rd b/man/mp_join.Rd index 31ba5574..4fed5260 100644 --- a/man/mp_join.Rd +++ b/man/mp_join.Rd @@ -17,7 +17,7 @@ how to specify this argument.} } \description{ Join two or more index tables (see \code{\link{mp_index}}) to produce a -ledger (see \code{\link{mp_ledgers}}). +ledger (see \code{\link{LedgerDefinition}}). } \details{ When two index tables are passed to \code{...}, \code{mp_join} behaves very much like @@ -100,7 +100,6 @@ mp_join( } \seealso{ Other functions that return ledgers -\code{\link{LedgerDefinition}}, \code{\link{mp_aggregate}()} } \concept{ledgers} diff --git a/man/mp_ledgers.Rd b/man/mp_ledgers.Rd index d7593d9a..bdf78593 100644 --- a/man/mp_ledgers.Rd +++ b/man/mp_ledgers.Rd @@ -5,13 +5,12 @@ \title{#' @export print.LedgerData = function(x, ...) { print(x$frame, row.names = FALSE) -}} +} +Bundle up Ledgers} \usage{ mp_ledgers(...) } \description{ -#' @export -print.LedgerData = function(x, ...) { -print(x$frame, row.names = FALSE) -} +Bundle up several ledgers (see \code{\link{LedgerDefinition}}) to pass +to \code{\link{mp_dynamic_model}}. } diff --git a/man/mp_tmb_simulator.Rd b/man/mp_tmb_simulator.Rd new file mode 100644 index 00000000..f56b0b94 --- /dev/null +++ b/man/mp_tmb_simulator.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/index_to_tmb.R +\name{mp_tmb_simulator} +\alias{mp_tmb_simulator} +\title{TMB Simulator from Dynamic Model} +\usage{ +mp_tmb_simulator( + dynamic_model, + time_steps = 0L, + vectors = NULL, + unstruc_mats = NULL, + mats_to_save = names(vectors), + mats_to_return = mats_to_save, + params = OptParamsList(0), + random = OptParamsList(), + obj_fn = ObjectiveFunction(~0), + log_file = LogFile(), + do_pred_sdreport = TRUE, + tmb_cpp = "macpan2", + initialize_ad_fun = TRUE, + ... +) +} +\arguments{ +\item{dynamic_model}{Object product by \code{\link{dynamic_model}}.} + +\item{time_steps}{An object of class \code{\link{Time}}.} + +\item{vectors}{Named list of named vectors as initial values for the +simulations that are referenced in the expression list in the dynamic model.} + +\item{unstruc_mats}{= Named list of objects that can be coerced to +numerical matrices that are used in the expression list of the +dynamic model.} + +\item{params}{An object of class \code{\link{OptParamsList}}.} + +\item{random}{An object of class \code{\link{OptParamsList}}.} + +\item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} + +\item{log_file}{An object of class \code{\link{LogFile}}.} + +\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} +} +\description{ +TMB Simulator from Dynamic Model +} diff --git a/man/mp_vector.Rd b/man/mp_vector.Rd new file mode 100644 index 00000000..bf6b4173 --- /dev/null +++ b/man/mp_vector.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vector.R +\name{mp_vector} +\alias{mp_vector} +\title{Stub} +\usage{ +mp_vector(x, ...) +} +\description{ +This documentation was originally in \code{\link[=mp_index]{mp_index()}} and should be cleaned up +See issue #131 +} +\details{ +#' These labels can be used to create 'multidimensional' names for the elements +of vectors. Here is the above example expressed in vector form. + +\if{html}{\out{
    }}\preformatted{v = Vector(prod) +v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") +}\if{html}{\out{
    }} + +This example vector could be stored as a 3-by-2 matrix. But other examples +cannot, making this indexing approach more general. For example, consider the +following index. + +\if{html}{\out{
    }}\preformatted{}\if{html}{\out{
    }} + +This index has an associated indexed vector that cannot be expressed as a +matrix. + +\if{html}{\out{
    }}\preformatted{mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") +}\if{html}{\out{
    }} +} +\examples{ +state = mp_index( + Epi = c("S", "I", "S", "I"), + Age = c("young", "young", "old", "old") +) +state_vector = (state + |> mp_vector() + |> mp_set_numbers(Epi = c(S = 1000)) + |> mp_set_numbers(Epi = c(I = 1), Age = "old") +) +print(state_vector) + +} diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index c517ddaf..5b6edb08 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -270,7 +270,7 @@ sir_simulator <- mp_tmb_simulator( dynamic_model = sir, vectors = list( state = c(S = 999, I = 1, R = 0), - rate = c(beta = 0.25, gamma = 0.1, lambda = NA) + rate = c(beta = 0.25, gamma = 0.1) ), time_steps = 100 ) @@ -515,8 +515,8 @@ two_strain_model <- SIR_starter( two_strain_simulator <- mp_tmb_simulator( dynamic_model = two_strain_model, vectors = list( - state = c(S = 998, I.A = 1, I.B = 1, R = 0), - rate = c(beta.A = 0.25, lambda.A = NA, beta.B = 0.2, lambda.B = NA, gamma = 0.1) + state = c(S. = 998, I.A = 1, I.B = 1, R. = 0), + rate = c(beta.A = 0.25, gamma. = 0.1, beta.B = 0.2) ), time_steps = 100 ) From be215dbbd1551093ac3b40fc3b46c7c319542f7d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 15 Nov 2023 15:00:44 -0500 Subject: [PATCH 118/332] fix #137 --- R/name_utils.R | 7 +++++++ R/vector.R | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/R/name_utils.R b/R/name_utils.R index 69866c3d..7d74ca19 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -167,3 +167,10 @@ wrap_colon_terms = function(x) { x[i] = sprintf("(%s)", x[i]) x } + +n_dots = function(x) nchar(x) - nchar(gsub(".", "", x, fixed = TRUE)) +make_n_dots = function(n) lapply(n, rep, x = ".") |> vapply(paste0, character(1L), collapse = "") +extrapolate_dots = function(x, string_with_all_dots) { + required_n_dots = n_dots(string_with_all_dots[[1L]]) - n_dots(x) + sprintf("%s%s", x, make_n_dots(required_n_dots)) +} diff --git a/R/vector.R b/R/vector.R index 376a60be..513cd516 100644 --- a/R/vector.R +++ b/R/vector.R @@ -54,7 +54,9 @@ Vector.data.frame = function(x, index = NULL, values_name = "values", ...) { #' @export Vector.numeric = function(x, index, ...) { v = Vector(index) - args = setNames(list(x), to_name(index$labelling_column_names)) + index_name = to_name(index$labelling_column_names) + names(x) = extrapolate_dots(names(x), index_name) + args = setNames(list(x), index_name) do.call(v$set_numbers, args) } From 8a74b36718e3912b3d14a8e9463803ccb1296a51 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 20 Nov 2023 11:11:33 -0500 Subject: [PATCH 119/332] reactions to birs --- NAMESPACE | 6 +++++- R/engine_functions.R | 20 ++++++++--------- R/enum.R | 6 +++--- R/files_to_models.R | 48 ++++++++++++++++++++--------------------- R/formula_data.R | 1 - R/index.R | 38 +++++++++++++++++++------------- R/math.R | 6 +++--- R/model_def_run.R | 29 +++++++++++++++++++++++++ R/mp.R | 29 ++++++++++++++----------- R/simulation_methods.R | 16 +++++++------- R/tmb_model.R | 4 ++-- man/TMBModel.Rd | 2 +- man/engine_functions.Rd | 20 ++++++++--------- 13 files changed, 135 insertions(+), 90 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index a2aa9caf..88e59d4b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -26,12 +26,13 @@ S3method(labels,TMBDynamicSimulator) S3method(labels,VariableLabels) S3method(length,Vector) S3method(mp_extract,DynamicModel) -S3method(mp_extract,Link) +S3method(mp_extract,Ledger) S3method(mp_extract,ModelDefRun) S3method(mp_index,character) S3method(mp_index,data.frame) S3method(mp_labels,Index) S3method(mp_labels,Ledger) +S3method(mp_reference,Ledger) S3method(mp_tmb_simulator,DynamicModel) S3method(mp_tmb_simulator,ModelDefRun) S3method(mp_union,Index) @@ -166,6 +167,7 @@ export(mk_calibrate) export(model_starter) export(mp_aggregate) export(mp_cartesian) +export(mp_catalogue) export(mp_choose) export(mp_choose_out) export(mp_decompose) @@ -183,6 +185,7 @@ export(mp_join) export(mp_labels) export(mp_ledgers) export(mp_linear) +export(mp_reference) export(mp_rename) export(mp_report) export(mp_set_numbers) @@ -190,6 +193,7 @@ export(mp_setdiff) export(mp_square) export(mp_subset) export(mp_symmetric) +export(mp_test_tmb) export(mp_tmb_simulator) export(mp_triangle) export(mp_union) diff --git a/R/engine_functions.R b/R/engine_functions.R index c67fb6d7..33d856a7 100644 --- a/R/engine_functions.R +++ b/R/engine_functions.R @@ -313,11 +313,11 @@ #' #' * `sum(...)` -- Sum all of the elements of all of the #' matrices passed to `...`. -#' * `colSums(x)` -- Row vector containing the sums +#' * `col_sums(x)` -- Row vector containing the sums #' of each column. -#' * `rowSums(x)` -- Column vector containing the sums +#' * `row_sums(x)` -- Column vector containing the sums #' of each row. -#' * `groupSums(x, f, n)` -- Column vector containing the +#' * `group_sums(x, f, n)` -- Column vector containing the #' sums of groups of elements in `x`. The groups are #' determined by the integers in `f` and the order of #' the sums in the output is determined by these @@ -327,7 +327,7 @@ #' #' * `...` -- Any number of matrices of any shape. #' * `x` -- A matrix of any dimensions, except for -#' `groupSums` that expects `x` to be a column vector. +#' `group_sums` that expects `x` to be a column vector. #' * `f` -- A column vector the same length as `x` #' containing integers between `0` and `n-`. #' * `n` -- Length of the output column vector. @@ -356,9 +356,9 @@ #' A = matrix(1:12, 4, 3) #' engine_eval(~ sum(y), y = y) #' engine_eval(~sum(x, y, A), x = x, y = y, z = z) -#' engine_eval(~ colSums(A), A = A) -#' engine_eval(~ rowSums(A), A = A) -#' engine_eval(~ groupSums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) +#' engine_eval(~ col_sums(A), A = A) +#' engine_eval(~ row_sums(A), A = A) +#' engine_eval(~ group_sums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) #' ``` #' #' ## Extracting Matrix Elements @@ -744,9 +744,9 @@ #' @aliases `%*%` #' @aliases sum #' @aliases rep -#' @aliases rowSums -#' @aliases colSums -#' @aliases groupSums +#' @aliases row_sums +#' @aliases col_sums +#' @aliases group_sums #' @aliases `[` #' @aliases block #' @aliases t diff --git a/R/enum.R b/R/enum.R index 5d456797..2684f6f7 100644 --- a/R/enum.R +++ b/R/enum.R @@ -13,9 +13,9 @@ valid_func_sigs = c( , "binop,null: `%*%`(x, y)" , "null,null: sum(...)" , "fwrap,null: rep(x, times)" - , "fwrap,null: rowSums(x)" - , "fwrap,null: colSums(x)" - , "fwrap,null: groupSums(x, f, n)" + , "fwrap,null: row_sums(x)" + , "fwrap,null: col_sums(x)" + , "fwrap,null: group_sums(x, f, n)" , "null,null: `[`(x, i, j)" , "fwrap,fail: block(x, i, j, n, m)" , "fwrap,null: t(x)" diff --git a/R/files_to_models.R b/R/files_to_models.R index 1c2bda61..57e07687 100644 --- a/R/files_to_models.R +++ b/R/files_to_models.R @@ -37,14 +37,14 @@ # , get_per_capita_outflow = ~ state[per_capita_outflow_from] * flow[per_capita_outflow_flow] # , get_absolute_inflow = ~ flow[absolute_inflow_flow] # , get_absolute_outflow = ~ flow[absolute_outflow_flow] -# , get_per_capita_state_in = ~ groupSums(per_capita, per_capita_to, state_length) -# , get_per_capita_state_in = ~ groupSums(absolute, absolute_to, state_length) -# , get_per_capita_inflow_state_in = ~ groupSums(per_capita_inflow, per_capita_inflow_to, state_length) -# , get_absolute_inflow_state_in = ~ groupSums(absolute_inflow, absolute_inflow_to, state_length) -# , get_per_capita_state_out = ~ groupSums(per_capita, per_capita_from, state_length) -# , get_absolute_state_out = ~ groupSums(absolute, absolute_from, state_length) -# , get_per_capita_outflow_state_out = ~ groupSums(per_capita_outflow, per_capita_outflow_from, state_length) -# , get_absolute_outflow_state_out = ~ groupSums(absolute_outflow, absolute_outflow_from, state_length) +# , get_per_capita_state_in = ~ group_sums(per_capita, per_capita_to, state_length) +# , get_per_capita_state_in = ~ group_sums(absolute, absolute_to, state_length) +# , get_per_capita_inflow_state_in = ~ group_sums(per_capita_inflow, per_capita_inflow_to, state_length) +# , get_absolute_inflow_state_in = ~ group_sums(absolute_inflow, absolute_inflow_to, state_length) +# , get_per_capita_state_out = ~ group_sums(per_capita, per_capita_from, state_length) +# , get_absolute_state_out = ~ group_sums(absolute, absolute_from, state_length) +# , get_per_capita_outflow_state_out = ~ group_sums(per_capita_outflow, per_capita_outflow_from, state_length) +# , get_absolute_outflow_state_out = ~ group_sums(absolute_outflow, absolute_outflow_from, state_length) # ), # int_vecs = IntVecs( # state_length = length(model$labels$state()) @@ -551,10 +551,10 @@ StandardExpr = function(model){ present_outflows = unlist(lapply(self$.outflow_flow_types, self$.flow_tester), use.names = FALSE) total_inflow_expression_vct = c( - "groupSums(per_capita, per_capita_to, state_length)", - "groupSums(absolute, absolute_to, state_length)", - "groupSums(per_capita_inflow, per_capita_inflow_to, state_length)", - "groupSums(absolute_inflow, absolute_inflow_to, state_length)" + "group_sums(per_capita, per_capita_to, state_length)", + "group_sums(absolute, absolute_to, state_length)", + "group_sums(per_capita_inflow, per_capita_inflow_to, state_length)", + "group_sums(absolute_inflow, absolute_inflow_to, state_length)" ) total_inflow_argument_list = list( "per_capita", "per_capita_to", "absolute", "absolute_to", "per_capita_inflow", @@ -562,10 +562,10 @@ StandardExpr = function(model){ ) total_outflow_expression_vct = c( - "groupSums(per_capita, per_capita_from, state_length)", - "groupSums(absolute, absolute_from, state_length)", - "groupSums(per_capita_outflow, per_capita_outflow_from, state_length)", - "groupSums(absolute_outflow, absolute_outflow_from, state_length)" + "group_sums(per_capita, per_capita_from, state_length)", + "group_sums(absolute, absolute_from, state_length)", + "group_sums(per_capita_outflow, per_capita_outflow_from, state_length)", + "group_sums(absolute_outflow, absolute_outflow_from, state_length)" ) total_outflow_argument_list = list( "per_capita", "per_capita_from", "absolute", "absolute_from", "per_capita_outflow", @@ -722,10 +722,10 @@ StandardExprHazard = function(model){ present_outflows = unlist(lapply(self$.outflow_flow_types, self$.flow_tester), use.names = FALSE) total_inflow_expression_vct = c( - "groupSums(per_capita, per_capita_to, state_length)", - "groupSums(absolute, absolute_to, state_length)", - "groupSums(per_capita_inflow, per_capita_inflow_to, state_length)", - "groupSums(absolute_inflow, absolute_inflow_to, state_length)" + "group_sums(per_capita, per_capita_to, state_length)", + "group_sums(absolute, absolute_to, state_length)", + "group_sums(per_capita_inflow, per_capita_inflow_to, state_length)", + "group_sums(absolute_inflow, absolute_inflow_to, state_length)" ) total_inflow_argument_list = list( "per_capita", "per_capita_to", "absolute", "absolute_to", "per_capita_inflow", @@ -733,10 +733,10 @@ StandardExprHazard = function(model){ ) total_outflow_expression_vct = c( - "groupSums(per_capita, per_capita_from, state_length)", - "groupSums(absolute, absolute_from, state_length)", - "groupSums(per_capita_outflow, per_capita_outflow_from, state_length)", - "groupSums(absolute_outflow, absolute_outflow_from, state_length)" + "group_sums(per_capita, per_capita_from, state_length)", + "group_sums(absolute, absolute_from, state_length)", + "group_sums(per_capita_outflow, per_capita_outflow_from, state_length)", + "group_sums(absolute_outflow, absolute_outflow_from, state_length)" ) total_outflow_argument_list = list( "per_capita", "per_capita_from", "absolute", "absolute_from", "per_capita_outflow", diff --git a/R/formula_data.R b/R/formula_data.R index 68aee849..2aae6d4c 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -1,7 +1,6 @@ LedgerData = function(...) { self = Base() self$ledger_list = list(...) - labelling_column_names_list = (self$ledger_list |> lapply(getElement, "labelling_column_names_list") |> unname() diff --git a/R/index.R b/R/index.R index e4c8980a..70244072 100644 --- a/R/index.R +++ b/R/index.R @@ -2,8 +2,8 @@ #' #' Make an index table to enumerate model quantity labels by category. These #' objects generalize and wrap \code{\link{data.frame}}s, where each column is a -#' label category and each row is an index. Indices must contain -#' only letters, numbers, and underscores. Blank empty string entries are +#' label category and each row is an index. Indices must contain +#' only letters, numbers, and underscores. Blank empty string entries are #' allowed, but missing values (`NA`s) are not. #' #' For example, the following index table describes the state variables of the @@ -32,7 +32,7 @@ #' groups "young" and "old". #' #' This index table could also be generated by first specifying individual index -#' tables for the `Epi` and `Age` columns, and then using a `macpan2` product +#' tables for the `Epi` and `Age` columns, and then using a `macpan2` product #' function that combines the tables into a single index table: #' ```{r} #' sir = mp_index(Epi = c("S", "I", "R")) @@ -40,25 +40,25 @@ #' prod = mp_cartesian(sir, age) #' prod #' ``` -#' The [mp_cartesian()] function will produce a table with entries that are all +#' The [mp_cartesian()] function will produce a table with entries that are all #' possible combinations of the individual index tables. The "See Also" section #' of the [mp_cartesian()] help page catalogues all available product functions. -#' -#' We can produce the full labels of model quantities, which are simply +#' +#' We can produce the full labels of model quantities, which are simply #' dot-concatenated indices, one for each entry in the index table, using the #' `labels()` function: #' ```{r, echo = FALSE} #' labels(prod) #' ``` -#' +#' #' Dots are not allowed in indices so that the labels can be inverted to #' reproduce the original index table (provided that the column names can be -#' retrieved). -#' +#' retrieved). +#' #' It is recommended to use UpperCamelCase for the columns of index tables -#' and single uppercase characters ("S", "I"), all lowercase character -#' strings ("gamma"), and/or snake_case strings ("aging_rate") for indices. This -#' convention helps when reading code that contains references to both column +#' and single uppercase characters ("S", "I"), all lowercase character +#' strings ("gamma"), and/or snake_case strings ("aging_rate") for indices. This +#' convention helps when reading code that contains references to both column #' names and indices. #' #' @param ... Character vectors to combine to produce an index. Alternatively, @@ -79,12 +79,12 @@ #' print(state) #' labels(state) #' mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) -#' +#' #' @family indexes -#' +#' #' @seealso [mp_vector()] #' @seealso [mp_set_numbers()] -#' +#' #' @export mp_index = function(..., labelling_column_names) UseMethod("mp_index") @@ -214,3 +214,11 @@ mp_index.data.frame = function(..., labelling_column_names) { if (missing(labelling_column_names)) labelling_column_names = names(f) Index(f, to_names(labelling_column_names)) } + + +#' @export +mp_catalogue = function(name, ..., labelling_column_names) { + l = list(...) + for (i in seq_along(l)) l[[i]] = do.call(mp_index, setNames(l[i], name)) + l +} diff --git a/R/math.R b/R/math.R index fd709171..f7b5fb03 100644 --- a/R/math.R +++ b/R/math.R @@ -87,9 +87,9 @@ SymbolicMath = function() { self$`%*%` = function(x, y) self$binop(" %*% ", x, y) self$`sum` = function(...) self$fwrap("sum", self$csv(...)) self$`rep` = function(x, n) self$fwrap("rep", self$csv(x, n)) - self$`rowSums` = function(x) self$fwrap("rowSums", x) - self$`colSums` = function(x) self$fwrap("colSums", x) - self$`groupSums` = function(x, f, n) self$fwrap("groupSums", self$csv(x, f, n)) + self$`row_sums` = function(x) self$fwrap("row_sums", x) + self$`col_sums` = function(x) self$fwrap("col_sums", x) + self$`group_sums` = function(x, f, n) self$fwrap("group_sums", self$csv(x, f, n)) self$`[` = function(x, i, ...) self$bwrap(x, self$csv(i, ...)) ## optional: j self$`block` = function(x, i, j, n, m) self$fwrap("block", self$csv(x, i, j, n, m)) self$`t` = function(x) self$fwrap("t", x) diff --git a/R/model_def_run.R b/R/model_def_run.R index f65a0fe1..c4460787 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -106,6 +106,18 @@ DynamicModel = function(expr_list = ExprList(), ledgers = list(), init_vecs = li self$ledgers = ledgers self$init_vecs = init_vecs self$unstruc_mats = unstruc_mats + self$int_vec_names = function() { + lapply(self$ledgers, getElement, "table_names") |> unlist(use.names = TRUE) |> unique() + } + self$derived_matrix_names = function() { + setdiff(self$expr_list$all_formula_vars() + , c( + names(self$init_vecs) + , self$int_vec_names() + , names(self$unstruc_mats) + ) + ) + } self$labels = LabelsDynamic(self) return_object(self, "DynamicModel") } @@ -118,6 +130,23 @@ DynamicModel = function(expr_list = ExprList(), ledgers = list(), init_vecs = li #' @export mp_dynamic_model = DynamicModel +#' @export +mp_test_tmb = function(..., ledgers, vectors, unstruc_mats) { + m = mp_dynamic_model( + expr_list = mp_expr_list(before = list(...)) + , ledgers = ledgers + , init_vecs = vectors + , unstruc_mats = unstruc_mats + ) + mp_tmb_simulator(m + , time_steps = 0L + , vectors = method_apply(vectors, "numbers") + , unstruc_mats = unstruc_mats + , mats_to_return = m$derived_matrix_names() + , mats_to_save = m$derived_matrix_names() + ) |> mp_report(phases = "before") +} + #' @export print.DynamicModel = function(x, ...) { print(x$expr_list) diff --git a/R/mp.R b/R/mp.R index 8663abbd..c158591b 100644 --- a/R/mp.R +++ b/R/mp.R @@ -202,14 +202,6 @@ mp_setdiff = function(x, ...) { Index(partition, x$labelling_column_names, x) } -# mp_aggregate = function(x, by = "Group", ) { -# mp_join( -# alive = x, -# group = mp_group(x, by), -# by = by -# ) -# } - #' Aggregate an Index #' #' Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows @@ -221,10 +213,11 @@ mp_aggregate = function(index, by = "Group", ledger_column = "group") { index_columns = to_names(by) if (length(index_columns) == 1L & !index_columns %in% names(index)) { partition = index$partition$constant(by, "a") - index = Index(partition) } else { partition = index$partition } + index = Index(partition) |> mp_group(by) + Ledger( partition$frame(), initial_column_map(names(partition), ledger_column), @@ -500,7 +493,7 @@ mp_aggregate_old = function(formula , ... ) { prototypes = list( - group_sums = macpan2:::MethodPrototype(y ~ groupSums(x), c("x", "y"), character()) + group_sums = macpan2:::MethodPrototype(y ~ group_sums(x), c("x", "y"), character()) ) consistent_agg_meths = (prototypes |> method_apply("consistent", formula) @@ -583,13 +576,25 @@ mp_decompose = function(formula, index, decomp_name, ...) { ) } +#' @export +mp_reference = function(x, dimension_name) { + UseMethod("mp_reference") +} + +#' @export +mp_reference.Ledger = function(x, dimension_name) { + ii = x$reference_index_list[[dimension_name]] + ii$reset_reference_index() + ii +} + #' @export mp_extract = function(x, dimension_name) { UseMethod("mp_extract") } #' @export -mp_extract.Link = function(x, dimension_name) { +mp_extract.Ledger = function(x, dimension_name) { ii = x$index_for[[dimension_name]]() ii$reset_reference_index() ii @@ -712,7 +717,7 @@ mp_expr_group_sum = function(x ) length_int = strata$labels() |> length() e_lhs = output_name - e_rhs = sprintf("groupSums(%s[%s], %s, %s)" + e_rhs = sprintf("group_sums(%s[%s], %s, %s)" , vector_name , subset_name , grouping_name diff --git a/R/simulation_methods.R b/R/simulation_methods.R index 62b7e86c..a6576fde 100644 --- a/R/simulation_methods.R +++ b/R/simulation_methods.R @@ -83,10 +83,10 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ ) total_inflow_expression_vct = c( - "groupSums(per_capita, per_capita_to, state_length)", - "groupSums(absolute, absolute_to, state_length)", - "groupSums(per_capita_inflow, per_capita_inflow_to, state_length)", - "groupSums(absolute_inflow, absolute_inflow_to, state_length)" + "group_sums(per_capita, per_capita_to, state_length)", + "group_sums(absolute, absolute_to, state_length)", + "group_sums(per_capita_inflow, per_capita_inflow_to, state_length)", + "group_sums(absolute_inflow, absolute_inflow_to, state_length)" ) if(!any(present_inflows)){ @@ -108,10 +108,10 @@ InsertTotalFLowExpressions = function(model_simulator, expanded_flows){ } total_outflow_expression_vct = c( - "groupSums(per_capita, per_capita_from, state_length)", - "groupSums(absolute, absolute_from, state_length)", - "groupSums(per_capita_outflow, per_capita_outflow_from, state_length)", - "groupSums(absolute_outflow, absolute_outflow_from, state_length)" + "group_sums(per_capita, per_capita_from, state_length)", + "group_sums(absolute, absolute_from, state_length)", + "group_sums(per_capita_outflow, per_capita_outflow_from, state_length)", + "group_sums(absolute_outflow, absolute_outflow_from, state_length)" ) if(!any(present_outflows)){ diff --git a/R/tmb_model.R b/R/tmb_model.R index 5262e3e7..b310bf5b 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -48,7 +48,7 @@ #' foi, 0, 0, #' 0, gamma, 0), 3, 3), #' flowmat ~ ratemat * state, -#' state ~ state - rowSums(flowmat) + t(colSums(flowmat)) +#' state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) #' ) #' ), #' params = OptParamsList(0.3 @@ -95,7 +95,7 @@ TMBModel = function( ## Standard Methods self$data_arg = function() { - existing_literals = self$expr_list$.literals() + # sexisting_literals = self$expr_list$.literals() c( self$init_mats$data_arg(), self$expr_list$data_arg(), diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd index 9c48ec22..8c8123f0 100644 --- a/man/TMBModel.Rd +++ b/man/TMBModel.Rd @@ -76,7 +76,7 @@ sir = TMBModel( foi, 0, 0, 0, gamma, 0), 3, 3), flowmat ~ ratemat * state, - state ~ state - rowSums(flowmat) + t(colSums(flowmat)) + state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) ) ), params = OptParamsList(0.3 diff --git a/man/engine_functions.Rd b/man/engine_functions.Rd index 01191d51..93f5c827 100644 --- a/man/engine_functions.Rd +++ b/man/engine_functions.Rd @@ -15,9 +15,9 @@ \alias{`\%*\%`} \alias{sum} \alias{rep} -\alias{rowSums} -\alias{colSums} -\alias{groupSums} +\alias{row_sums} +\alias{col_sums} +\alias{group_sums} \alias{`[`} \alias{block} \alias{t} @@ -406,11 +406,11 @@ engine_eval(~from_diag(to_diag(from_diag(matrix(1:9, 3, 3))))) \itemize{ \item \code{sum(...)} -- Sum all of the elements of all of the matrices passed to \code{...}. -\item \code{colSums(x)} -- Row vector containing the sums +\item \code{col_sums(x)} -- Row vector containing the sums of each column. -\item \code{rowSums(x)} -- Column vector containing the sums +\item \code{row_sums(x)} -- Column vector containing the sums of each row. -\item \code{groupSums(x, f, n)} -- Column vector containing the +\item \code{group_sums(x, f, n)} -- Column vector containing the sums of groups of elements in \code{x}. The groups are determined by the integers in \code{f} and the order of the sums in the output is determined by these @@ -422,7 +422,7 @@ integers. \itemize{ \item \code{...} -- Any number of matrices of any shape. \item \code{x} -- A matrix of any dimensions, except for -\code{groupSums} that expects \code{x} to be a column vector. +\code{group_sums} that expects \code{x} to be a column vector. \item \code{f} -- A column vector the same length as \code{x} containing integers between \code{0} and \verb{n-}. \item \code{n} -- Length of the output column vector. @@ -455,9 +455,9 @@ y = 1:3 A = matrix(1:12, 4, 3) engine_eval(~ sum(y), y = y) engine_eval(~sum(x, y, A), x = x, y = y, z = z) -engine_eval(~ colSums(A), A = A) -engine_eval(~ rowSums(A), A = A) -engine_eval(~ groupSums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) +engine_eval(~ col_sums(A), A = A) +engine_eval(~ row_sums(A), A = A) +engine_eval(~ group_sums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) }\if{html}{\out{
    }} } From 9f2d7c7664baa7d51f2753ff9d42a99bea60609d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 20 Nov 2023 11:12:51 -0500 Subject: [PATCH 120/332] group sum rename and more from birs --- inst/model_library/sir/model_structure.R | 4 +- inst/model_library/sir_age/model_structure.R | 14 ++--- inst/tmb_examples/sir.R | 4 +- misc/dev/dev-crbind.R | 18 +++---- misc/dev/dev-dimnames.R | 2 +- misc/dev/dev-group-sum.R | 2 +- misc/dev/dev-macpan-base-w-group-sums.R | 2 +- misc/dev/dev-seir-vax.R | 2 +- misc/dev/dev-seir.R | 2 +- misc/dev/dev-sir-log-lin.R | 2 +- misc/dev/dev-square-bracket-arguments.R | 6 +-- misc/dev/dev-square-bracket-in-obj-fn.R | 2 +- misc/dev/dev.cpp | 51 +++++++++++------- misc/experiments/derivations/tmp_scrap.R | 4 +- misc/experiments/refactorcpp.R | 54 +++++++++---------- misc/experiments/structured_expressions/age.R | 6 +-- .../structured_expressions/macpan_base.R | 6 +-- .../structured_expressions/rbind_lag_test.R | 4 +- .../structured_expressions/sequencing_colon.R | 4 +- .../structured_expressions/sequencing_seq.R | 4 +- misc/experiments/structured_expressions/sir.R | 6 +-- .../structured_expressions/sir_alt.R | 2 +- .../sir_from_model_def.R | 4 +- .../sir_objective_function.R | 6 +-- .../sir_segfault_example.R | 6 +-- .../sir_with_convolution.R | 2 +- .../structured_expressions/testing_status.R | 6 +-- misc/old-r-source/group_sums.R | 2 +- misc/old-r-source/symbolizer.R | 4 +- src/macpan2.cpp | 51 +++++++++++------- tests/testthat/test-sums.R | 6 +-- vignettes/quickstart.Rmd | 4 +- 32 files changed, 157 insertions(+), 135 deletions(-) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index 31172821..a663f3b4 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -16,8 +16,8 @@ expr_list = mp_expr_list( , flows_per_time ~ state[from] * flow_rates[edge] ## state update - , total_inflow ~ groupSums(flows_per_time, to, state) - , total_outflow ~ groupSums(flows_per_time, from, state) + , total_inflow ~ group_sums(flows_per_time, to, state) + , total_outflow ~ group_sums(flows_per_time, from, state) , state ~ state + total_inflow - total_outflow ) ) diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index 30add462..1bd45a8a 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -6,14 +6,14 @@ expr_list = mp_expr_list( before = list( sub_population_sizes = - N ~ groupSums(state[alive], groups, N) + N ~ group_sums(state[alive], groups, N) , decompose_transmission_rate = trans_rates[decomp] ~ infectivity[i] * contact[si] * susceptibility[s] ), during = list( - normalize_infectious_states = + normalize_infectious_states = norm_state ~ state[norm] / N[denominator] , update_force_of_infection = @@ -24,10 +24,10 @@ expr_list = mp_expr_list( flows_per_time ~ state[from] * flow_rates[edge] , update_inflows_per_time = - total_inflow ~ groupSums(flows_per_time, to, state) + total_inflow ~ group_sums(flows_per_time, to, state) , update_outflows_per_time = - total_outflow ~ groupSums(flows_per_time, from, state) + total_outflow ~ group_sums(flows_per_time, from, state) , update_state = state ~ state + total_inflow - total_outflow @@ -110,7 +110,7 @@ gamma = mp_subset(flow_rates, Epi = "gamma") ## aggregating states --------------- -### N ~ groupSums(state[alive], groups, N) +### N ~ group_sums(state[alive], groups, N) aggregation = mp_join( alive = mp_subset(state, Epi = c("S", "I", "R")) ## all states are alive in this model , groups = mp_group(state, "Age") @@ -146,9 +146,9 @@ transmission = mp_join( ### update_flows_per_time = ### flows_per_time ~ state[from] * flow_rates[edge] ### update_inflows_per_time = -### total_inflow ~ groupSums(flows_per_time, to, state) +### total_inflow ~ group_sums(flows_per_time, to, state) ### update_outflows_per_time = -### total_outflow ~ groupSums(flows_per_time, from, state) +### total_outflow ~ group_sums(flows_per_time, from, state) ### update_state = ### state ~ state + total_inflow - total_outflow infection = mp_join( diff --git a/inst/tmb_examples/sir.R b/inst/tmb_examples/sir.R index 444988d5..fa0276f7 100644 --- a/inst/tmb_examples/sir.R +++ b/inst/tmb_examples/sir.R @@ -47,8 +47,8 @@ sir_for_fake_data = TMBModel( , flow_pars[beta_index] * state_vector[I_index] / N ) , flows ~ flow_vector * state_vector[from_indices] - , incidence ~ groupSums(flows, to_indices, n_states) - , outflow ~ groupSums(flows, from_indices, n_states) + , incidence ~ group_sums(flows, to_indices, n_states) + , outflow ~ group_sums(flows, from_indices, n_states) , state_vector ~ state_vector + incidence - outflow , noisy_state ~ rnbinom(state_vector, 10) ) diff --git a/misc/dev/dev-crbind.R b/misc/dev/dev-crbind.R index 6b01cb2e..a51783a8 100644 --- a/misc/dev/dev-crbind.R +++ b/misc/dev/dev-crbind.R @@ -19,7 +19,7 @@ correct_answer = function(den = 89){ flowmat_hist = list(as.matrix(flowmat)) for (i in 1:2){ flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + t(colSums(flowmat)) + state = state - row_sums(flowmat) + t(col_sums(flowmat)) state_hist = c(state_hist, list(as.matrix(state))) flowmat_hist = c(flowmat_hist, list(as.matrix(flowmat))) } @@ -72,13 +72,13 @@ correct_answer = function(den = 89){ # 32 3 2 32 # 33 9 0 0 # 34 0 0 0 -## state ~ state - rowSums(flowmat) + t(colSums(flowmat)) -# 35 1 2 35 `+`(`-`(state,rowSums(flowmat)),t(colSums(flowmat))) -# 36 2 2 37 `-`(state,rowSums(flowmat)) -# 37 15 1 39 t(colSums(flowmat)) +## state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) +# 35 1 2 35 `+`(`-`(state,row_sums(flowmat)),t(col_sums(flowmat))) +# 36 2 2 37 `-`(state,row_sums(flowmat)) +# 37 15 1 39 t(col_sums(flowmat)) # 38 0 0 0 matrix = state -# 39 12 1 40 rowSums(flowmat) -# 40 13 1 41 colSums(flowmat) +# 39 12 1 40 row_sums(flowmat) +# 40 13 1 41 col_sums(flowmat) # 41 10 0 0 matrix = flowmat # 42 10 0 0 matrix = flowmat @@ -112,7 +112,7 @@ bindingModel = TMBModel( ), during = list( flowmat ~ ratemat*state, - state ~ state - rowSums(flowmat) + t(colSums(flowmat)) + state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) ) ), params = OptParamsList(89 @@ -122,7 +122,7 @@ bindingModel = TMBModel( , col_id = 0L ), random = OptParamsList(), - obj_fn = ObjectiveFunction(~ sum(rowSums(flowmat))), + obj_fn = ObjectiveFunction(~ sum(row_sums(flowmat))), time_steps = Time(time_steps = 2L) ) diff --git a/misc/dev/dev-dimnames.R b/misc/dev/dev-dimnames.R index 88e01822..524a8f38 100644 --- a/misc/dev/dev-dimnames.R +++ b/misc/dev/dev-dimnames.R @@ -45,7 +45,7 @@ exprs = ExprList( during = list( dummy ~ assign(rate, foi, 0, state[I, 0] * beta / sum(state)), flow ~ rate * state[from, 0], - state ~ state + groupSums(flow, to, 2) - groupSums(flow, from, 2) + state ~ state + group_sums(flow, to, 2) - group_sums(flow, from, 2) ) ) diff --git a/misc/dev/dev-group-sum.R b/misc/dev/dev-group-sum.R index 9de158d2..c2af884b 100644 --- a/misc/dev/dev-group-sum.R +++ b/misc/dev/dev-group-sum.R @@ -12,7 +12,7 @@ m = TMBModel( , .mats_to_return = c("x", "y", "z") ), expr_list = ExprList( - before = list(y ~ groupSums(x, z, 4)) + before = list(y ~ group_sums(x, z, 4)) ), params = OptParamsList(0), random = OptParamsList(), diff --git a/misc/dev/dev-macpan-base-w-group-sums.R b/misc/dev/dev-macpan-base-w-group-sums.R index 6d78b222..15431bb0 100644 --- a/misc/dev/dev-macpan-base-w-group-sums.R +++ b/misc/dev/dev-macpan-base-w-group-sums.R @@ -85,7 +85,7 @@ m = TMBModel( Is * beta0 * Cs / N * (1 - iso_s) , dummy ~ assign(rate, 0, 0, foi) , flow ~ state[from, 0]*rate - , state ~ state - groupSums(flow, from, 12) + groupSums(flow, to, 12) + , state ~ state - group_sums(flow, from, 12) + group_sums(flow, to, 12) , dummy ~ unpack(state, S, E, Ia, Ip, Im, Is, ICUs, ICUd, H, H2, R, D) ) ) diff --git a/misc/dev/dev-seir-vax.R b/misc/dev/dev-seir-vax.R index 32af1609..de48bfc9 100644 --- a/misc/dev/dev-seir-vax.R +++ b/misc/dev/dev-seir-vax.R @@ -46,7 +46,7 @@ m = TMBModel( , dummy ~ assign(rate, 0, 0, foi.unvax) , dummy ~ assign(rate, 7, 0, foi.vax) , flow ~ rate*state[from] - , state ~ state +groupSums(flow, to, 8) - groupSums(flow, from, 8) + , state ~ state +group_sums(flow, to, 8) - group_sums(flow, from, 8) ), ), params = OptParamsList(0.25, 0.07, diff --git a/misc/dev/dev-seir.R b/misc/dev/dev-seir.R index c6827831..6d3ca3e2 100644 --- a/misc/dev/dev-seir.R +++ b/misc/dev/dev-seir.R @@ -35,7 +35,7 @@ m = TMBModel( foi ~ beta*I/N, rate ~ c(foi, alpha, gamma), flow ~ state[from, 0]*rate, - state ~ state - groupSums(flow, from, 4) + groupSums(flow, to, 4), + state ~ state - group_sums(flow, from, 4) + group_sums(flow, to, 4), S~state[0,0], E~state[1,0], I~state[2,0], diff --git a/misc/dev/dev-sir-log-lin.R b/misc/dev/dev-sir-log-lin.R index cccef5fb..9d422028 100644 --- a/misc/dev/dev-sir-log-lin.R +++ b/misc/dev/dev-sir-log-lin.R @@ -37,7 +37,7 @@ m = TMBModel(init_mats = MatsList( , foi ~ beta_curr * state[I,0] / N , rate ~ c(foi, gamma) , flow ~ state[from, 0] * rate - , state ~ state - groupSums(flow, from, 3) + groupSums(flow, to, 3) + , state ~ state - group_sums(flow, from, 3) + group_sums(flow, to, 3) ) ) , params = OptParamsList(-1.6, -0.04 diff --git a/misc/dev/dev-square-bracket-arguments.R b/misc/dev/dev-square-bracket-arguments.R index 7ff54cd8..7fc83759 100644 --- a/misc/dev/dev-square-bracket-arguments.R +++ b/misc/dev/dev-square-bracket-arguments.R @@ -25,7 +25,7 @@ m_two_arg_sqr_brckt = TMBModel( foi ~ beta*state[1,0]/N, rate ~ c(foi, gamma), flow ~ state[from, 0]*rate, - state ~ state - groupSums(flow, from, 3) + groupSums(flow, to, 3) + state ~ state - group_sums(flow, from, 3) + group_sums(flow, to, 3) ) ), params = OptParamsList(0.05, 0.12, @@ -59,7 +59,7 @@ m_one_arg_sqr_brckt = TMBModel( foi ~ beta*state[1]/N, rate ~ c(foi, gamma), flow ~ state[from]*rate, - state ~ state - groupSums(flow, from, 3) + groupSums(flow, to, 3) + state ~ state - group_sums(flow, from, 3) + group_sums(flow, to, 3) ) ), params = OptParamsList(0.05, 0.12, @@ -93,7 +93,7 @@ m_three_arg_sqr_brckt = TMBModel( foi ~ beta*state[1, 0, 0]/N, rate ~ c(foi, gamma), flow ~ state[from, 0, 0]*rate, - state ~ state - groupSums(flow, from, 3) + groupSums(flow, to, 3) + state ~ state - group_sums(flow, from, 3) + group_sums(flow, to, 3) ) ), params = OptParamsList(0.05, 0.12, diff --git a/misc/dev/dev-square-bracket-in-obj-fn.R b/misc/dev/dev-square-bracket-in-obj-fn.R index a18b15e6..07dfe2d2 100644 --- a/misc/dev/dev-square-bracket-in-obj-fn.R +++ b/misc/dev/dev-square-bracket-in-obj-fn.R @@ -23,7 +23,7 @@ error_model = TMBModel( )), during = list( flowmat ~ ratemat*state, - state ~ state - rowSums(flowmat) + t(colSums(flowmat)) + state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) ) ), OptParamsList(0.3, par_id = 0L, mat = "const1", row_id = 0L, col_id = 0L), diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index a9205640..114a3972 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -74,9 +74,9 @@ enum macpan2_func { , MP2_MATRIX_MULTIPLY = 11 // binop,null: `%*%`(x, y) , MP2_SUM = 12 // null,null: sum(...) , MP2_REPLICATE = 13 // fwrap,null: rep(x, times) - , MP2_ROWSUMS = 14 // fwrap,null: rowSums(x) - , MP2_COLSUMS = 15 // fwrap,null: colSums(x) - , MP2_GROUPSUMS = 16 // fwrap,null: groupSums(x, f, n) + , MP2_ROWSUMS = 14 // fwrap,null: row_sums(x) + , MP2_COLSUMS = 15 // fwrap,null: col_sums(x) + , MP2_GROUPSUMS = 16 // fwrap,null: group_sums(x, f, n) , MP2_SQUARE_BRACKET = 17 // null,null: `[`(x, i, j) , MP2_BLOCK = 18 // fwrap,fail: block(x, i, j, n, m) , MP2_TRANSPOSE = 19 // fwrap,null: t(x) @@ -421,10 +421,24 @@ class ArgList { } + int rows(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + + if (items_[i].type == ItemType::IntVector) { + return items_[i].intVec.size(); + } else { + return items_[i].mat.rows(); + } + } + + matrix operator[](int i) { return get_as_mat(i); } + // Method to recycle elements, rows, and columns to make operands compatible for binary operations ArgList recycle_for_bin_op() const { ArgList result = *this; // Create a new ArgList as a copy of the current instance @@ -1396,11 +1410,11 @@ class ExprEvaluator { // #' // #' * `sum(...)` -- Sum all of the elements of all of the // #' matrices passed to `...`. - // #' * `colSums(x)` -- Row vector containing the sums + // #' * `col_sums(x)` -- Row vector containing the sums // #' of each column. - // #' * `rowSums(x)` -- Column vector containing the sums + // #' * `row_sums(x)` -- Column vector containing the sums // #' of each row. - // #' * `groupSums(x, f, n)` -- Column vector containing the + // #' * `group_sums(x, f, n)` -- Column vector containing the // #' sums of groups of elements in `x`. The groups are // #' determined by the integers in `f` and the order of // #' the sums in the output is determined by these @@ -1410,7 +1424,7 @@ class ExprEvaluator { // #' // #' * `...` -- Any number of matrices of any shape. // #' * `x` -- A matrix of any dimensions, except for - // #' `groupSums` that expects `x` to be a column vector. + // #' `group_sums` that expects `x` to be a column vector. // #' * `f` -- A column vector the same length as `x` // #' containing integers between `0` and `n-`. // #' * `n` -- Length of the output column vector. @@ -1459,26 +1473,23 @@ class ExprEvaluator { #endif return m; - case MP2_GROUPSUMS: // groupSums - // rows = CppAD::Integer(args[1].maxCoeff()+0.1f) + 1; + case MP2_GROUPSUMS: // group_sums m = args[0]; v1 = args.get_as_int_vec(1); - rows = args.get_as_int(2); + // rows = args.get_as_int(2); + rows = args.rows(2); m1 = matrix::Zero(rows, 1); + if (v1.size() != m.rows()) { + SetError(MP2_GROUPSUMS, "Number of rows in x must equal the number of indices in f in group_sums(x, f, x)", row); + return m; + } for (int i = 0; i < m.rows(); i++) { m1.coeffRef(v1[i], 0) += m.coeff(i, 0); } return m1; - // rows = CppAD::Integer(args[2].coeff(0,0)+0.1f); - // m = matrix::Zero(rows, 1); - // for (int i = 0; i < args[0].rows(); i++) { - // rowIndex = CppAD::Integer(args[1].coeff(i,0)+0.1f); - // m.coeffRef(rowIndex,0) += args[0].coeff(i,0); - // } - // return m; // #' ### Examples // #' // #' ``` @@ -1487,9 +1498,9 @@ class ExprEvaluator { // #' A = matrix(1:12, 4, 3) // #' engine_eval(~ sum(y), y = y) // #' engine_eval(~sum(x, y, A), x = x, y = y, z = z) - // #' engine_eval(~ colSums(A), A = A) - // #' engine_eval(~ rowSums(A), A = A) - // #' engine_eval(~ groupSums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) + // #' engine_eval(~ col_sums(A), A = A) + // #' engine_eval(~ row_sums(A), A = A) + // #' engine_eval(~ group_sums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) // #' ``` // #' diff --git a/misc/experiments/derivations/tmp_scrap.R b/misc/experiments/derivations/tmp_scrap.R index 903557b0..52305491 100644 --- a/misc/experiments/derivations/tmp_scrap.R +++ b/misc/experiments/derivations/tmp_scrap.R @@ -23,7 +23,7 @@ self$standard_expressions = function(){ flw_frml = MathExpressionFromStrings("state[from]*rate", list("state", "from", "rate")) symblc_flw_frml = do.call(flw_frml$symbolic$evaluate, list("state", "from", "rate")) - stt_frml = MathExpressionFromStrings("state - groupSums(state, from, from_lngth)+groupSums(state, to, to_lngth)", list("state", "from", "from_lngth", "to", "to_lngth")) + stt_frml = MathExpressionFromStrings("state - group_sums(state, from, from_lngth)+group_sums(state, to, to_lngth)", list("state", "from", "from_lngth", "to", "to_lngth")) symblc_stt_frml = do.call(stt_frml$symbolic$evaluate, list("state", "from", "from_lngth", "to", "to_lngth")) return(list(list("flow", symblc_flw_frml), list("state", symblc_stt_frml))) @@ -152,4 +152,4 @@ return(usr_exprs) # # 5. "during_post_update" expressions # # 6. "after" expressions # # Note: 2 - 5 should be collected together as "during" expressions -# } \ No newline at end of file +# } diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 985cecca..37a0b5ad 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -296,8 +296,8 @@ flows = mp_ledgers( vv = IndexedExpressions( flows_per_time ~ state[from] * flow_rates[link] - , inflow ~ groupSums(flows_per_time, to, state) - , outflow ~ groupSums(flows_per_time, from, state) + , inflow ~ group_sums(flows_per_time, to, state) + , outflow ~ group_sums(flows_per_time, from, state) , state ~ state + inflow - outflow , index_data = flows , vector_list = list( @@ -315,11 +315,11 @@ state_aggregation = mp_join( group_by = strata, by = list(alive.group_by = "Loc.Age") ) |> mp_ledgers() -## N ~ groupSums(state[alive], group_by, N) +## N ~ group_sums(state[alive], group_by, N) state_vector = Vector(state) state_vector$set_numbers(Epi = c(I = 1))$set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) hh = IndexedExpressions( - N ~ groupSums(state[alive], group_by, N) + N ~ group_sums(state[alive], group_by, N) , index_data = state_aggregation , vector_list = list( N = Vector(strata) @@ -372,7 +372,7 @@ if (FALSE) { self$reference_index_list$N self$frame self$reference_index_list$N$labels() -groupSums() +group_sums() mp_labels xx$reference_index_list$N$labelling_column_names yy = mp_ledgers(xx) @@ -423,7 +423,7 @@ influence = mp_join( # , Vital = "alive" # ) -mp_aggregate(N ~ groupSums(state) +mp_aggregate(N ~ group_sums(state) , group_by = "Loc.Age" , index = state , Vital = "alive" @@ -511,7 +511,7 @@ state_vector$set_numbers(Loc.Age = S_numbers, Epi = "S") alive_groups alive engine_eval( - ~ groupSums(state[alive], alive_groups, n_strata), + ~ group_sums(state[alive], alive_groups, n_strata), state = state_vector$numbers(), alive = alive, alive_groups = alive_groups, @@ -639,7 +639,7 @@ mp_join(N, mp_subset(state, "infectious", Epi = "I")) match(N$labels_frame()$active, state$labels()) - 1L match(N$labels_frame()$N, unique(N$labels_frame()$N)) - 1L -groupSums(state[active], strata, length(active)) +group_sums(state[active], strata, length(active)) xx = mp_join( mp_subset(state, "state"), @@ -983,7 +983,7 @@ cartesian_self(v, "infection", "infectious", "Type") if (FALSE) { engine_eval(~1) -engine_eval(~ groupSums(x, i, 2), x = 1:6, i = c(0,0,0,1,1,1)) +engine_eval(~ group_sums(x, i, 2), x = 1:6, i = c(0,0,0,1,1,1)) oor_debug$flag(".simulation_formatter", "TMBSimulator") #options(macpan2_dll = "dev") @@ -1067,15 +1067,15 @@ m = TMBModel( , absolute_inflow ~ flow[absolute_inflow_flow] , absolute_outflow ~ flow[absolute_outflow_flow] , total_inflow ~ - groupSums(per_capita, per_capita_to, state_length) + - groupSums(absolute, absolute_to, state_length) + - groupSums(per_capita_inflow, per_capita_inflow_to, state_length) + - groupSums(absolute_inflow, absolute_inflow_to, state_length) + group_sums(per_capita, per_capita_to, state_length) + + group_sums(absolute, absolute_to, state_length) + + group_sums(per_capita_inflow, per_capita_inflow_to, state_length) + + group_sums(absolute_inflow, absolute_inflow_to, state_length) , total_outflow ~ - groupSums(per_capita, per_capita_from, state_length) + - groupSums(absolute, absolute_from, state_length) + - groupSums(per_capita_inflow, per_capita_outflow_from, state_length) + - groupSums(absolute_inflow, absolute_outflow_from, state_length) + group_sums(per_capita, per_capita_from, state_length) + + group_sums(absolute, absolute_from, state_length) + + group_sums(per_capita_inflow, per_capita_outflow_from, state_length) + + group_sums(absolute_inflow, absolute_outflow_from, state_length) , state ~ state + total_inflow - total_outflow ) ), @@ -1359,14 +1359,14 @@ TMBModel( , get_per_capita_outflow = ~ state[per_capita_outflow_from] * flow[per_capita_outflow_flow] , get_absolute_inflow = ~ flow[absolute_inflow_flow] , get_absolute_outflow = ~ flow[absolute_outflow_flow] - , get_per_capita_state_in = ~ groupSums(per_capita, per_capita_to, state_length) - , get_absolute_state_in = ~ groupSums(absolute, absolute_to, state_length) - , get_per_capita_inflow_state_in = ~ groupSums(per_capita_inflow, per_capita_inflow_to, state_length) - , get_absolute_inflow_state_in = ~ groupSums(absolute_inflow, absolute_inflow_to, state_length) - , get_per_capita_state_out = ~ groupSums(per_capita, per_capita_from, state_length) - , get_absolute_state_out = ~ groupSums(absolute, absolute_from, state_length) - , get_per_capita_outflow_state_out = ~ groupSums(per_capita_outflow, per_capita_outflow_from, state_length) - , get_absolute_outflow_state_out = ~ groupSums(absolute_outflow, absolute_outflow_from, state_length) + , get_per_capita_state_in = ~ group_sums(per_capita, per_capita_to, state_length) + , get_absolute_state_in = ~ group_sums(absolute, absolute_to, state_length) + , get_per_capita_inflow_state_in = ~ group_sums(per_capita_inflow, per_capita_inflow_to, state_length) + , get_absolute_inflow_state_in = ~ group_sums(absolute_inflow, absolute_inflow_to, state_length) + , get_per_capita_state_out = ~ group_sums(per_capita, per_capita_from, state_length) + , get_absolute_state_out = ~ group_sums(absolute, absolute_from, state_length) + , get_per_capita_outflow_state_out = ~ group_sums(per_capita_outflow, per_capita_outflow_from, state_length) + , get_absolute_outflow_state_out = ~ group_sums(absolute_outflow, absolute_outflow_from, state_length) ), int_vecs = IntVecs( state_length = length(model$labels$state()) @@ -1441,8 +1441,8 @@ m = TMBModel( , infectious_states = ~ state[infectious_indices] , update_infection_flows = flow[infection_indices] ~ per_capita_transmission_matrix %*% state[infectious_indices] - , inflow = ~ groupSums(per_capita, to_indices, state_length) - , outflow = ~ groupSums(per_capita, from_indices, state_length) + , inflow = ~ group_sums(per_capita, to_indices, state_length) + , outflow = ~ group_sums(per_capita, from_indices, state_length) , beta = ~ time_var(beta_ts, beta_cp, beta_n, beta_group) ), int_vecs = IntVecs( diff --git a/misc/experiments/structured_expressions/age.R b/misc/experiments/structured_expressions/age.R index 794c3019..1658bae8 100644 --- a/misc/experiments/structured_expressions/age.R +++ b/misc/experiments/structured_expressions/age.R @@ -23,7 +23,7 @@ correct_answer = function(){ for(i in 1:2){ flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state[1] = state[1]+sum(state)*birth_rate state[number_of_groups] = state[number_of_groups] - state[number_of_groups]*death_rate @@ -56,7 +56,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) @@ -92,7 +92,7 @@ vital_dynamics = parse_expr(vital_dynamics_expr) valid_vars = c(valid_vars, list(vital_dynamics = rep(0, number_of_groups))) literals_list = c(literals_list, list(vital_dynamics$valid_literals)) -state_update_expr = ~ state - rowSums(flowmat) + t(colSums(flowmat)) + vital_dynamics +state_update_expr = ~ state - row_sums(flowmat) + t(col_sums(flowmat)) + vital_dynamics state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/macpan_base.R b/misc/experiments/structured_expressions/macpan_base.R index 4b810db2..8713e440 100644 --- a/misc/experiments/structured_expressions/macpan_base.R +++ b/misc/experiments/structured_expressions/macpan_base.R @@ -93,7 +93,7 @@ correct_answer = function(){ ) flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) N_hist = c(N_hist, list(as.matrix(N))) foi_hist = c(foi_hist, list(as.matrix(foi))) @@ -138,7 +138,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) @@ -262,7 +262,7 @@ flowmat = parse_expr(flowmat_expr) valid_vars = c(valid_vars, list(flowmat = matrix(0, 12, 12))) literals_list = c(literals_list, list(flowmat$valid_literals)) -state_update_expr = ~ state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~ state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/rbind_lag_test.R b/misc/experiments/structured_expressions/rbind_lag_test.R index 42aeabe5..9677f192 100644 --- a/misc/experiments/structured_expressions/rbind_lag_test.R +++ b/misc/experiments/structured_expressions/rbind_lag_test.R @@ -31,7 +31,7 @@ correct_answer = function(beta = 0.3) { ), 3, 3, byrow = TRUE ) flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) N_hist = c(N_hist, list(as.matrix(N))) foi_hist = c(foi_hist, list(as.matrix(foi))) @@ -93,7 +93,7 @@ flowmat = parse_expr(flowmat_expr) valid_vars = c(valid_vars, list(flowmat = matrix(0, 3, 3))) literals_list = c(literals_list, list(flowmat$valid_literals)) -state_update_expr = ~ state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~ state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/sequencing_colon.R b/misc/experiments/structured_expressions/sequencing_colon.R index f35d8357..e69ea765 100644 --- a/misc/experiments/structured_expressions/sequencing_colon.R +++ b/misc/experiments/structured_expressions/sequencing_colon.R @@ -14,7 +14,7 @@ correct_answer = function(){ state_hist = list(as.matrix(state)) for(i in 1:2){ flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) +t(colSums(flowmat)) + state = state - row_sums(flowmat) +t(col_sums(flowmat)) state_hist = c(state_hist, list(as.matrix(state))) } state = state_hist @@ -53,7 +53,7 @@ flowmat = parse_expr(flowmat_expr) valid_vars = c(valid_vars, list(flowmat = matrix(0, 7, 7))) literals_list = c(literals_list, list(flowmat$valid_literals)) -state_update_expr = ~state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/sequencing_seq.R b/misc/experiments/structured_expressions/sequencing_seq.R index 8eb0d6ca..1dd1ee82 100644 --- a/misc/experiments/structured_expressions/sequencing_seq.R +++ b/misc/experiments/structured_expressions/sequencing_seq.R @@ -14,7 +14,7 @@ correct_answer = function(){ state_hist = list(as.matrix(state)) for(i in 1:2){ flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) +t(colSums(flowmat)) + state = state - row_sums(flowmat) +t(col_sums(flowmat)) state_hist = c(state_hist, list(as.matrix(state))) } state = state_hist @@ -53,7 +53,7 @@ flowmat = parse_expr(flowmat_expr) valid_vars = c(valid_vars, list(flowmat = matrix(0, 7, 7))) literals_list = c(literals_list, list(flowmat$valid_literals)) -state_update_expr = ~state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/sir.R b/misc/experiments/structured_expressions/sir.R index 5a8241e7..7d8ea477 100644 --- a/misc/experiments/structured_expressions/sir.R +++ b/misc/experiments/structured_expressions/sir.R @@ -28,7 +28,7 @@ correct_answer = function(beta = 0.3) { ), 3, 3, byrow = TRUE ) flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) N_hist = c(N_hist, list(as.matrix(N))) foi_hist = c(foi_hist, list(as.matrix(foi))) @@ -52,7 +52,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) @@ -93,7 +93,7 @@ valid_vars = c(valid_vars, list(flowmat = matrix(0, 3, 3))) literals_list = c(literals_list, list(flowmat$valid_literals)) #valid_literals = c(valid_literals, flowmat$valid_literals) -state_update_expr = ~ state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~ state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/sir_alt.R b/misc/experiments/structured_expressions/sir_alt.R index a64dc347..218aee11 100644 --- a/misc/experiments/structured_expressions/sir_alt.R +++ b/misc/experiments/structured_expressions/sir_alt.R @@ -57,7 +57,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) valid_funcs = macpan2:::valid_funcs diff --git a/misc/experiments/structured_expressions/sir_from_model_def.R b/misc/experiments/structured_expressions/sir_from_model_def.R index fc020900..82dccd46 100644 --- a/misc/experiments/structured_expressions/sir_from_model_def.R +++ b/misc/experiments/structured_expressions/sir_from_model_def.R @@ -28,7 +28,7 @@ correct_answer = function(beta = 0.3) { ), 3, 3, byrow = TRUE ) flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) N_hist = c(N_hist, list(as.matrix(N))) foi_hist = c(foi_hist, list(as.matrix(foi))) @@ -64,7 +64,7 @@ sir = TMBModel( foi, 0, 0, 0, gamma, 0), 3, 3), flowmat ~ ratemat * state, - state ~ state - rowSums(flowmat) + t(colSums(flowmat)) + state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) ) ), params = OptParamsList(0.3 diff --git a/misc/experiments/structured_expressions/sir_objective_function.R b/misc/experiments/structured_expressions/sir_objective_function.R index 2064eccc..c15c80fe 100644 --- a/misc/experiments/structured_expressions/sir_objective_function.R +++ b/misc/experiments/structured_expressions/sir_objective_function.R @@ -28,7 +28,7 @@ correct_answer = function(beta = 0.3) { ), 3, 3, byrow = TRUE ) flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) N_hist = c(N_hist, list(as.matrix(N))) foi_hist = c(foi_hist, list(as.matrix(foi))) @@ -53,7 +53,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) @@ -94,7 +94,7 @@ valid_vars = c(valid_vars, list(flowmat = matrix(0, 3, 3))) literals_list = c(literals_list, list(flowmat$valid_literals)) #valid_literals = c(valid_literals, flowmat$valid_literals) -state_update_expr = ~ state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~ state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/sir_segfault_example.R b/misc/experiments/structured_expressions/sir_segfault_example.R index 8b0be234..f0787882 100644 --- a/misc/experiments/structured_expressions/sir_segfault_example.R +++ b/misc/experiments/structured_expressions/sir_segfault_example.R @@ -29,7 +29,7 @@ correct_answer = function() { ), 3, 3, byrow = TRUE ) flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + colSums(flowmat) + state = state - row_sums(flowmat) + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) N_hist = c(N_hist, list(as.matrix(N))) foi_hist = c(foi_hist, list(as.matrix(foi))) @@ -53,7 +53,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) @@ -94,7 +94,7 @@ valid_vars = c(valid_vars, list(flowmat = matrix(0, 3, 3))) literals_list = c(literals_list, list(flowmat$valid_literals)) #valid_literals = c(valid_literals, flowmat$valid_literals) -state_update_expr = ~ state - rowSums(flowmat) + t(colSums(flowmat)) +state_update_expr = ~ state - row_sums(flowmat) + t(col_sums(flowmat)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/experiments/structured_expressions/sir_with_convolution.R b/misc/experiments/structured_expressions/sir_with_convolution.R index 50b5daee..e99926e6 100644 --- a/misc/experiments/structured_expressions/sir_with_convolution.R +++ b/misc/experiments/structured_expressions/sir_with_convolution.R @@ -59,7 +59,7 @@ valid_funcs = nlist( `(`, `c`, `matrix`, `%*%`, `sum`, `rep`, - `rowSums`, `colSums`, + `row_sums`, `col_sums`, `[`, `t` ) diff --git a/misc/experiments/structured_expressions/testing_status.R b/misc/experiments/structured_expressions/testing_status.R index 749831f4..3a5b152f 100644 --- a/misc/experiments/structured_expressions/testing_status.R +++ b/misc/experiments/structured_expressions/testing_status.R @@ -29,7 +29,7 @@ correct_answer = function(){ for(i in 1:2){ flowmat = sweep(ratemat, 1, state, "*") - state = state - rowSums(flowmat) + flowmat[,5] + colSums(flowmat) + state = state - row_sums(flowmat) + flowmat[,5] + col_sums(flowmat) state_hist = c(state_hist, list(as.matrix(state))) } state = state_hist @@ -50,7 +50,7 @@ input_mats = list( # `(`, # `c`, `matrix`, # `%*%`, `sum`, `rep`, -# `rowSums`, `colSums`, +# `row_sums`, `col_sums`, # `[`, `t` # ) @@ -97,7 +97,7 @@ flowmat_out = parse_expr(flowmat_out_expr) valid_vars = c(valid_vars, list(flowmat_out = matrix(0, 5, 5))) literals_list = c(literals_list, list(flowmat_out$valid_literals)) -state_update_expr = ~state - rowSums(flowmat_out) + t(colSums(flowmat_in)) +state_update_expr = ~state - row_sums(flowmat_out) + t(col_sums(flowmat_in)) state_update = parse_expr(state_update_expr) literals_list = c(literals_list, list(state_update$valid_literals)) diff --git a/misc/old-r-source/group_sums.R b/misc/old-r-source/group_sums.R index 97b6de2b..50ce098a 100644 --- a/misc/old-r-source/group_sums.R +++ b/misc/old-r-source/group_sums.R @@ -14,7 +14,7 @@ # @param n Length of the output column vector. # -groupSums = function(x, f, n) { +group_sums = function(x, f, n) { ## Column vector containing the ## sums of groups of elements in `x`. The groups are ## determined by the integers in `f` and the order of diff --git a/misc/old-r-source/symbolizer.R b/misc/old-r-source/symbolizer.R index 535836a1..c9ec7e16 100644 --- a/misc/old-r-source/symbolizer.R +++ b/misc/old-r-source/symbolizer.R @@ -63,8 +63,8 @@ symbolizer = function(f) { `%*%` = function(x, y) binop(" %*% ", x, y), `sum` = function(...) fwrap("sum", csv(...)), `rep` = function(x, n) fwrap("rep", csv(x, n)), - `rowSums` = function(x) fwrap("rowSums", x), - `colSums` = function(x) fwrap("colSums", x), + `row_sums` = function(x) fwrap("row_sums", x), + `col_sums` = function(x) fwrap("col_sums", x), `[` = function(x, ...) bwrap(x, csv(...)) ) }) diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 1156ae31..cfd7d7aa 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -75,9 +75,9 @@ enum macpan2_func { , MP2_MATRIX_MULTIPLY = 11 // binop,null: `%*%`(x, y) , MP2_SUM = 12 // null,null: sum(...) , MP2_REPLICATE = 13 // fwrap,null: rep(x, times) - , MP2_ROWSUMS = 14 // fwrap,null: rowSums(x) - , MP2_COLSUMS = 15 // fwrap,null: colSums(x) - , MP2_GROUPSUMS = 16 // fwrap,null: groupSums(x, f, n) + , MP2_ROWSUMS = 14 // fwrap,null: row_sums(x) + , MP2_COLSUMS = 15 // fwrap,null: col_sums(x) + , MP2_GROUPSUMS = 16 // fwrap,null: group_sums(x, f, n) , MP2_SQUARE_BRACKET = 17 // null,null: `[`(x, i, j) , MP2_BLOCK = 18 // fwrap,fail: block(x, i, j, n, m) , MP2_TRANSPOSE = 19 // fwrap,null: t(x) @@ -422,10 +422,24 @@ class ArgList { } + int rows(int i) { + if (i < 0 || i >= items_.size()) { + throw std::out_of_range("Index out of range"); + } + + if (items_[i].type == ItemType::IntVector) { + return items_[i].intVec.size(); + } else { + return items_[i].mat.rows(); + } + } + + matrix operator[](int i) { return get_as_mat(i); } + // Method to recycle elements, rows, and columns to make operands compatible for binary operations ArgList recycle_for_bin_op() const { ArgList result = *this; // Create a new ArgList as a copy of the current instance @@ -1397,11 +1411,11 @@ class ExprEvaluator { // #' // #' * `sum(...)` -- Sum all of the elements of all of the // #' matrices passed to `...`. - // #' * `colSums(x)` -- Row vector containing the sums + // #' * `col_sums(x)` -- Row vector containing the sums // #' of each column. - // #' * `rowSums(x)` -- Column vector containing the sums + // #' * `row_sums(x)` -- Column vector containing the sums // #' of each row. - // #' * `groupSums(x, f, n)` -- Column vector containing the + // #' * `group_sums(x, f, n)` -- Column vector containing the // #' sums of groups of elements in `x`. The groups are // #' determined by the integers in `f` and the order of // #' the sums in the output is determined by these @@ -1411,7 +1425,7 @@ class ExprEvaluator { // #' // #' * `...` -- Any number of matrices of any shape. // #' * `x` -- A matrix of any dimensions, except for - // #' `groupSums` that expects `x` to be a column vector. + // #' `group_sums` that expects `x` to be a column vector. // #' * `f` -- A column vector the same length as `x` // #' containing integers between `0` and `n-`. // #' * `n` -- Length of the output column vector. @@ -1460,26 +1474,23 @@ class ExprEvaluator { #endif return m; - case MP2_GROUPSUMS: // groupSums - // rows = CppAD::Integer(args[1].maxCoeff()+0.1f) + 1; + case MP2_GROUPSUMS: // group_sums m = args[0]; v1 = args.get_as_int_vec(1); - rows = args.get_as_int(2); + // rows = args.get_as_int(2); + rows = args.rows(2); m1 = matrix::Zero(rows, 1); + if (v1.size() != m.rows()) { + SetError(MP2_GROUPSUMS, "Number of rows in x must equal the number of indices in f in group_sums(x, f, x)", row); + return m; + } for (int i = 0; i < m.rows(); i++) { m1.coeffRef(v1[i], 0) += m.coeff(i, 0); } return m1; - // rows = CppAD::Integer(args[2].coeff(0,0)+0.1f); - // m = matrix::Zero(rows, 1); - // for (int i = 0; i < args[0].rows(); i++) { - // rowIndex = CppAD::Integer(args[1].coeff(i,0)+0.1f); - // m.coeffRef(rowIndex,0) += args[0].coeff(i,0); - // } - // return m; // #' ### Examples // #' // #' ``` @@ -1488,9 +1499,9 @@ class ExprEvaluator { // #' A = matrix(1:12, 4, 3) // #' engine_eval(~ sum(y), y = y) // #' engine_eval(~sum(x, y, A), x = x, y = y, z = z) - // #' engine_eval(~ colSums(A), A = A) - // #' engine_eval(~ rowSums(A), A = A) - // #' engine_eval(~ groupSums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) + // #' engine_eval(~ col_sums(A), A = A) + // #' engine_eval(~ row_sums(A), A = A) + // #' engine_eval(~ group_sums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4) // #' ``` // #' diff --git a/tests/testthat/test-sums.R b/tests/testthat/test-sums.R index 0f6cebd8..f72d292f 100644 --- a/tests/testthat/test-sums.R +++ b/tests/testthat/test-sums.R @@ -1,10 +1,10 @@ test_that("summation works correctly", { expect_equal(engine_eval(~sum(x, 5), x = 1:4)[,], 15) A = matrix(1:12, 4, 3) - expect_equal(engine_eval(~rowSums(A), A = A), cbind(c(15, 18, 21, 24))) - expect_equal(engine_eval(~colSums(A), A = A), rbind(c(10, 26, 42))) + expect_equal(engine_eval(~row_sums(A), A = A), cbind(c(15, 18, 21, 24))) + expect_equal(engine_eval(~col_sums(A), A = A), rbind(c(10, 26, 42))) expect_equal( - engine_eval(~ groupSums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4), + engine_eval(~ group_sums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4), matrix(c(1, 5, 15, 34)) ) }) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 075c45dd..9af840b9 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -515,8 +515,8 @@ two_strain_model <- SIR_starter( two_strain_simulator <- mp_tmb_simulator( dynamic_model = two_strain_model, vectors = list( - state = c(S. = 998, I.A = 1, I.B = 1, R. = 0), - rate = c(beta.A = 0.25, gamma. = 0.1, beta.B = 0.2) + state = c(S = 998, I.A = 1, I.B = 1, R = 0), + rate = c(beta.A = 0.25, gamma = 0.1, beta.B = 0.2) ), time_steps = 100 ) From bcd78da15b877ca6b4511b36d7650a96cf44887f Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Tue, 21 Nov 2023 10:56:45 -0500 Subject: [PATCH 121/332] Create BIRS-workshop-debrief.md --- misc/BIRS-workshop-debrief.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 misc/BIRS-workshop-debrief.md diff --git a/misc/BIRS-workshop-debrief.md b/misc/BIRS-workshop-debrief.md new file mode 100644 index 00000000..299867d7 --- /dev/null +++ b/misc/BIRS-workshop-debrief.md @@ -0,0 +1,15 @@ +- intro video + hands on sesh was a good format +- presentation: work a bit bring the big picture together (based on 1:1 discussions with participants afterwards) +- current challenge: how to connect bookkeeping stuff to the dynamics (expressions/how people simulate models), which i understand—-what the vignette tries to start explaining without getting into expressions per se + - make a *real* quickstart (simulate an SIR) + - change the name of the current quickstart + add a section at the end explaining the suppressed expressions list, initial vecs, unstructured matrices + - follow up with a more structured model, calibration, and ideally forecasting (generating ensembles in the future) +- dev focus: making macpan2 friendlier (it's powerful, but is it friendly) + - making the interface simpler for basic models so people don't *have* to get into the specifics of Euler, RK4, etc, but they can if they want to, maybe this involves writing friendly wrappers that don’t expose *everything* to the user to start + - connected to coming up with a real quickstart +- ideas for a “friendlier” interface + - make reproducible chunks (modules) for components of the expression list and/or an expression list shared between models (classify models based on this) + - modules for transmission, post-processing delays, etc. + - think about reducing the naming burden (unique column names in ledgers for different expressions) + - expression list that doesn’t directly reference the ledgers but just expresses the logic… e.g. `lambda = beta*S` vs `rate[force_of_infection] ~ rate[transmission_rate]*state[from_state]` + - make the expression list look more like deSolve From 4e308fd87144be7651799a891b2ca8e16b5f28eb Mon Sep 17 00:00:00 2001 From: Irena Papst Date: Wed, 22 Nov 2023 14:26:49 -0500 Subject: [PATCH 122/332] Update BIRS-workshop-debrief.md --- misc/BIRS-workshop-debrief.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/misc/BIRS-workshop-debrief.md b/misc/BIRS-workshop-debrief.md index 299867d7..7c645e60 100644 --- a/misc/BIRS-workshop-debrief.md +++ b/misc/BIRS-workshop-debrief.md @@ -13,3 +13,14 @@ - think about reducing the naming burden (unique column names in ledgers for different expressions) - expression list that doesn’t directly reference the ledgers but just expresses the logic… e.g. `lambda = beta*S` vs `rate[force_of_infection] ~ rate[transmission_rate]*state[from_state]` - make the expression list look more like deSolve + +--- + +# To do + +- merge `main` into `refactorcpp` and clean up techincal debt (make tests pass, get rid of old code/examples except for model examples (later?), etc.) +- merge `refactorcpp` into `main` as v1.0.0 (full API change will be v2.0.0) +- a real quickstart (simulation) +- go back and translate model examples into new interface and use that to inform the development of a "friendlier" `macpan2` API +- developing a friendlier `macpan2` API +- "quickstart: calibration" and "quickstart: product models" From 27b1ef6e54d84a878eff5e9a2970931474471594 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 24 Nov 2023 16:11:23 -0500 Subject: [PATCH 123/332] post birs --- DESCRIPTION | 2 - Makefile | 6 ++ NAMESPACE | 2 + R/engine_functions.R | 1 + R/enum.R | 1 + R/index.R | 8 +- R/index_to_tmb.R | 16 ++- R/lists.R | 6 ++ R/model_def_run.R | 14 ++- R/vector.R | 3 + README.md | 226 +++++++++++++++++++++++++++++++++------- man/engine_functions.Rd | 1 + misc/build/README.Rmd | 134 ++++++++++++++++++++++++ misc/dev/dev.cpp | 10 +- src/macpan2.cpp | 10 +- 15 files changed, 393 insertions(+), 47 deletions(-) create mode 100644 misc/build/README.Rmd diff --git a/DESCRIPTION b/DESCRIPTION index b4aae444..efb81e88 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -44,8 +44,6 @@ LinkingTo: Rcpp VignetteBuilder: knitr -Remotes: - canmod/oor Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true diff --git a/Makefile b/Makefile index 20810939..9ad3bef8 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,12 @@ coverage.html: R/*.R src/macpan2.cpp tests/testthat/*.R Rscript -e "covr::report(file = \"coverage.html\")" +readme:: README.md +README.md: misc/build/README.Rmd + Rscript -e "rmarkdown::render('misc/build/README.Rmd', output_dir = '.', output_file = 'README.md', output_format = 'md_document', knit_root_dir = '../..')" + echo '' | cat - $@ > temp && mv temp $@ + + enum-update:: R/enum.R R/enum.R: misc/dev/dev.cpp misc/build/enum_tail.R echo "## Auto-generated - do not edit by hand" > $@ diff --git a/NAMESPACE b/NAMESPACE index 88e59d4b..f6cd63e5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,6 +39,7 @@ S3method(mp_union,Index) S3method(mp_union,Ledger) S3method(mp_vector,Index) S3method(mp_vector,Link) +S3method(mp_vector,Vector) S3method(mp_vector,character) S3method(mp_vector,data.frame) S3method(mp_vector,numeric) @@ -184,6 +185,7 @@ export(mp_indices) export(mp_join) export(mp_labels) export(mp_ledgers) +export(mp_library) export(mp_linear) export(mp_reference) export(mp_rename) diff --git a/R/engine_functions.R b/R/engine_functions.R index 33d856a7..4acd407c 100644 --- a/R/engine_functions.R +++ b/R/engine_functions.R @@ -775,4 +775,5 @@ #' @aliases from_diag #' @aliases time_group #' @aliases cos +#' @aliases print NULL diff --git a/R/enum.R b/R/enum.R index 2684f6f7..4e76661c 100644 --- a/R/enum.R +++ b/R/enum.R @@ -44,6 +44,7 @@ valid_func_sigs = c( , "fwrap,fail: from_diag(x)" , "fwrap,fail: time_group(i, change_points)" , "fwrap,null: cos(x)" + , "fwrap,null: print(x)" ) process_enum = function(x) { RE = "(null|fail|binop|fwrap|bwrap|pwrap)[ ]*,[ ]*(null|fail|binop|fwrap|bwrap|pwrap)[ ]*:[ ]*\\`?([^`]*)\\`?\\((.*)(\\,.*)*\\)" diff --git a/R/index.R b/R/index.R index 70244072..ba7f9e7a 100644 --- a/R/index.R +++ b/R/index.R @@ -219,6 +219,12 @@ mp_index.data.frame = function(..., labelling_column_names) { #' @export mp_catalogue = function(name, ..., labelling_column_names) { l = list(...) - for (i in seq_along(l)) l[[i]] = do.call(mp_index, setNames(l[i], name)) + for (i in seq_along(l)) { + args = setNames(l[i], name) + if (!missing(labelling_column_names)) { + args = c(args, list(labelling_column_names = labelling_column_names)) + } + l[[i]] = do.call(mp_index, args) + } l } diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 9b5af227..3cba8929 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -33,8 +33,8 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model , time_steps = 0L , vectors = NULL , unstruc_mats = NULL - , mats_to_save = names(vectors) - , mats_to_return = mats_to_save + , mats_to_save = NULL + , mats_to_return = NULL , params = OptParamsList(0) , random = OptParamsList() , obj_fn = ObjectiveFunction(~0) @@ -52,9 +52,12 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model |> unname() |> unlist(recursive = FALSE) ) + if (is.null(int_vecs)) int_vecs = list() if (is.null(vectors)) { indexed_mats = dynamic_model$init_vecs - mats_to_save = names(indexed_mats) + if (length(indexed_mats) != 0L) { + mats_to_save = names(indexed_mats) + } } else { for (v in names(vectors)) { vectors[[v]] = Vector( @@ -74,6 +77,13 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model names(int_vecs), names(indexed_mats), names(unstruc_mats) )) + ## FIXME: default should be anything on the left-hand-side + ## in a during expression + supplied_nms = c(names(indexed_mats), names(unstruc_mats)) + + if (is.null(mats_to_return)) mats_to_return = supplied_nms + if (is.null(mats_to_save)) mats_to_save = mats_to_return + derived_mats = (empty_matrix |> list() |> rep(length(derived_nms)) diff --git a/R/lists.R b/R/lists.R index 881d0d1c..d9a04e69 100644 --- a/R/lists.R +++ b/R/lists.R @@ -31,3 +31,9 @@ assert_named_list = function(l) { } self_named_vector = function(...) c(...) |> setNames(c(...)) + +## works even if names are not unique +get_elements_by_names = function(l, ...) { + nms = list(...) |> lapply(as.character) |> unlist(use.names = FALSE) |> unique() + l[names(l) %in% nms] +} diff --git a/R/model_def_run.R b/R/model_def_run.R index c4460787..238dcd5a 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -1,3 +1,8 @@ +#' @export +mp_library = function(...) { + system.file("model_library", ..., package = "macpan2") |> Compartmental2() +} + #' @export Compartmental2 = function(model_directory) { self = Base() @@ -100,11 +105,16 @@ LabelsDynamic = function(dynamic_model) { } #' @export -DynamicModel = function(expr_list = ExprList(), ledgers = list(), init_vecs = list(), unstruc_mats = list()) { +DynamicModel = function( + expr_list = ExprList() + , ledgers = list() + , init_vecs = list() + , unstruc_mats = list() + ) { self = Base() self$expr_list = expr_list self$ledgers = ledgers - self$init_vecs = init_vecs + self$init_vecs = lapply(init_vecs, mp_vector) self$unstruc_mats = unstruc_mats self$int_vec_names = function() { lapply(self$ledgers, getElement, "table_names") |> unlist(use.names = TRUE) |> unique() diff --git a/R/vector.R b/R/vector.R index 513cd516..43a95f7c 100644 --- a/R/vector.R +++ b/R/vector.R @@ -187,6 +187,9 @@ zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' @export mp_vector = function(x, ...) UseMethod("mp_vector") +#' @export +mp_vector.Vector = function(x, ...) x + #' @export mp_vector.Index = Vector.Index diff --git a/README.md b/README.md index 55cb2284..02f50335 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,214 @@ + # macpan2 + [![R-CMD-check](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml) -[![test coverage](https://byob.yarr.is/canmod/macpan2/coverage)](https://github.com/canmod/macpan2/actions/workflows/test-coverage.yaml) -[![commit activity](https://img.shields.io/github/commit-activity/m/canmod/macpan2)](https://github.com/canmod/macpan2/commits) +[![test +coverage](https://byob.yarr.is/canmod/macpan2/coverage)](https://github.com/canmod/macpan2/actions/workflows/test-coverage.yaml) +[![commit +activity](https://img.shields.io/github/commit-activity/m/canmod/macpan2)](https://github.com/canmod/macpan2/commits) [![contributors](https://img.shields.io/github/contributors/canmod/macpan2)](https://github.com/canmod/macpan2/graphs/contributors) [![release](https://img.shields.io/github/v/release/canmod/macpan2?include_prereleases)](https://github.com/canmod/macpan2/releases/latest) -[McMasterPandemic](https://github.com/mac-theobio/McMasterPandemic) was developed to provide forecasts and insights to Canadian public health agencies throughout the COVID-19 pandemic. [Much was learned](https://canmod.github.io/macpan-book/index.html#vision-and-direction) about developing general purpose compartmental modelling software during this experience, but the pressure to deliver regular forecasts made it difficult to focus on the software itself. The goal of this `macpan2` project is to re-imagine `McMasterPandemic`, building it from the ground up with architectural and technological decisions that address the many lessons that we learned from COVID-19 about software. - -Although `macpan2` is designed as a compartmental modelling tool that is agnostic about the underlying computational engine, it currently makes use of [template model builder](https://github.com/kaskr/adcomp). Template model builder (TMB) is an `R` modelling package based on a `C++` framework incorporating mature [automatic differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) and [matrix algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) libraries. - -The [Public Health Risk Sciences Division](https://github.com/phac-nml-phrsd) at the [Public Health Agency of Canada](https://www.canada.ca/en/public-health.html) uses `macpan2` (for example, [here](https://phac-nml-phrsd.github.io/EPACmodel/)). +[McMasterPandemic](https://github.com/mac-theobio/McMasterPandemic) was +developed to provide forecasts and insights to Canadian public health +agencies throughout the COVID-19 pandemic. [Much was +learned](https://canmod.github.io/macpan-book/index.html#vision-and-direction) +about developing general purpose compartmental modelling software during +this experience, but the pressure to deliver regular forecasts made it +difficult to focus on the software itself. The goal of this `macpan2` +project is to re-imagine `McMasterPandemic`, building it from the ground +up with architectural and technological decisions that address the many +lessons that we learned from COVID-19 about software. + +Although `macpan2` is designed as a compartmental modelling tool that is +agnostic about the underlying computational engine, it currently makes +use of [template model builder](https://github.com/kaskr/adcomp). +Template model builder (TMB) is an `R` modelling package based on a +`C++` framework incorporating mature [automatic +differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) +and [matrix +algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) +libraries. + +The [Public Health Risk Sciences +Division](https://github.com/phac-nml-phrsd) at the [Public Health +Agency of Canada](https://www.canada.ca/en/public-health.html) uses +`macpan2` (for example, +[here](https://phac-nml-phrsd.github.io/EPACmodel/)). ## Documentation -* [Package reference](https://canmod.github.io/macpan2/) -* [Quick-start guide](https://canmod.github.io/macpan2/articles/quickstart) -* [Representation of compartmental models](https://canmod.github.io/macpan2/articles/model_definitions) [specification document] -* [`C++` engine](https://canmod.github.io/macpan2/articles/cpp_side) [specification document] -* [Project history and trajectory](https://canmod.net/misc/macpan2_presentation) [slides] +- [Package reference](https://canmod.github.io/macpan2/) +- [Quick-start + guide](https://canmod.github.io/macpan2/articles/quickstart) +- [Representation of compartmental + models](https://canmod.github.io/macpan2/articles/model_definitions) + \[specification document\] +- [`C++` engine](https://canmod.github.io/macpan2/articles/cpp_side) + \[specification document\] +- [Project history and + trajectory](https://canmod.net/misc/macpan2_presentation) \[slides\] ## Installation -If you're on a Windows system, please install `Rtools` matching your R version from [here](https://cran.r-project.org/bin/windows/Rtools/). This ensures you have a C++ compiler, which is required to install `macpan2` from source (as below). +If you’re on a Windows system, please install `Rtools` matching your R +version from [here](https://cran.r-project.org/bin/windows/Rtools/). +This ensures you have a C++ compiler, which is required to install +`macpan2` from source (as below). Then, install the `macpan2` package with the following R command. -``` -remotes::install_github("canmod/macpan2") -``` + remotes::install_github("canmod/macpan2") + +For projects in production one should install a specific version, as in +the following command. -For projects in production one should install a specific version, as in the following command. -``` -remotes::install_github("canmod/macpan2@v0.0.3") -``` + remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World -This [quick-start guide](https://canmod.github.io/macpan2/articles/quickstart) describes the following hello-world SIR model. - -``` -library(macpan2) -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -N = 100 -simulator = sir$simulators$tmb(time_steps = 100 - , state = c(S = N - 1, I = 1, R = 0) - , flow = c(foi = 0, gamma = 0.1) - , N = N - , beta = 0.2 -) -sir_sims = simulator$report() -``` +This [quick-start +guide](https://canmod.github.io/macpan2/articles/quickstart) describes +the following hello-world SIR model. + + library(macpan2) + sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) + N = 100 + simulator = sir$simulators$tmb(time_steps = 100 + , state = c(S = N - 1, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.1) + , N = N + , beta = 0.2 + ) + sir_sims = simulator$report() ## Product Management -* [Roadmap](https://github.com/orgs/canmod/projects/2) +The [project board](https://github.com/orgs/canmod/projects/2) tracks +the details of bugs, tasks, and feature development. But the following +narrative will provide context on product development themes, their +current state, and plans for improvement and implementation. + +### General Dynamic Simulation + +One can define a generic set of update steps that are iterated to +produce a dynamic simulation. + + library(macpan2) + si = mp_dynamic_model( + expr_list = ExprList( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection + ) + ), + unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) + ) + print(si) + + ## --------------------- + ## At every iteration of the simulation loop (t = 1 to T): + ## --------------------- + ## 1: infection ~ beta * S * I/N + ## 2: S ~ S - infection + ## 3: I ~ I + infection + +Simulating from this model takes the following steps. + + getwd() + + ## [1] "/Users/stevenwalker/Development/macpan2" + + (si + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") + |> mp_report() + ) + + ## matrix time row col value + ## 1 I 1 0 0 1.247500 + ## 2 I 2 0 0 1.555484 + ## 3 I 3 0 0 1.938307 + ## 4 I 4 0 0 2.413491 + ## 5 I 5 0 0 3.002301 + ## 6 I 6 0 0 3.730342 + ## 7 I 7 0 0 4.628139 + ## 8 I 8 0 0 5.731624 + ## 9 I 9 0 0 7.082401 + ## 10 I 10 0 0 8.727601 + +This part of the package is general, stable, and flexible. It also meets +many modellers where they are, which is with the ability to write down a +set of transitions/state updates. + +But it is not convenient if you would just like to simulate from it, +which is what the model library is for. + +### Model Library + + ("unstructured/si" + |> mp_library() + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") + |> mp_report() + ) + + ## matrix time row col value + ## 1 I 1 0 0 1.247500 + ## 2 I 2 0 0 1.555484 + ## 3 I 3 0 0 1.938307 + ## 4 I 4 0 0 2.413491 + ## 5 I 5 0 0 3.002301 + ## 6 I 6 0 0 3.730342 + ## 7 I 7 0 0 4.628139 + ## 8 I 8 0 0 5.731624 + ## 9 I 9 0 0 7.082401 + ## 10 I 10 0 0 8.727601 + +TODO: - \[ \] Reuse the tools for the older concept of starter models. - +\[ \] Establish a specification + +### Calibration + +### Model Structure and Bookkeeping + +### Alternative Engines + +### Combining Expression Lists + +Because expression lists are really just lists of expressions, they can +be combined as lists would normally be combined. In this example we keep +the dynamics of the si model separate from under-reporting and reporting +delay corrections to the raw prevalence (TODO: should really use +incidence). + + library(macpan2) + si_dynamics = list( + transition_rate = infection ~ beta * S * I / N + , state_update = S ~ S - infection + , state_update = I ~ I + infection + ) + reporting_correction = list( + post_processing = reports ~ convolution(I, c(0.5, 0.25, 0.25)) + ) + si = mp_dynamic_model( + expr_list = ExprList(during = c(si_dynamics, reporting_correction)), + unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) + ) + (si + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "reports") + |> mp_report() + ) + + ## matrix time row col value + ## 1 reports 1 0 0 0.6237500 + ## 2 reports 2 0 0 0.7777422 + ## 3 reports 3 0 0 0.9691533 + ## 4 reports 4 0 0 1.2067453 + ## 5 reports 5 0 0 1.5011505 + ## 6 reports 6 0 0 1.8651709 + ## 7 reports 7 0 0 2.3140693 + ## 8 reports 8 0 0 2.8658120 + ## 9 reports 9 0 0 3.5412006 + ## 10 reports 10 0 0 4.3638003 diff --git a/man/engine_functions.Rd b/man/engine_functions.Rd index 93f5c827..05a71c37 100644 --- a/man/engine_functions.Rd +++ b/man/engine_functions.Rd @@ -46,6 +46,7 @@ \alias{from_diag} \alias{time_group} \alias{cos} +\alias{print} \title{Engine Functions} \description{ Functions currently supported by the C++ TMB engine diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd new file mode 100644 index 00000000..82f41862 --- /dev/null +++ b/misc/build/README.Rmd @@ -0,0 +1,134 @@ +# macpan2 + + +[![R-CMD-check](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml) +[![test coverage](https://byob.yarr.is/canmod/macpan2/coverage)](https://github.com/canmod/macpan2/actions/workflows/test-coverage.yaml) +[![commit activity](https://img.shields.io/github/commit-activity/m/canmod/macpan2)](https://github.com/canmod/macpan2/commits) +[![contributors](https://img.shields.io/github/contributors/canmod/macpan2)](https://github.com/canmod/macpan2/graphs/contributors) +[![release](https://img.shields.io/github/v/release/canmod/macpan2?include_prereleases)](https://github.com/canmod/macpan2/releases/latest) + +[McMasterPandemic](https://github.com/mac-theobio/McMasterPandemic) was developed to provide forecasts and insights to Canadian public health agencies throughout the COVID-19 pandemic. [Much was learned](https://canmod.github.io/macpan-book/index.html#vision-and-direction) about developing general purpose compartmental modelling software during this experience, but the pressure to deliver regular forecasts made it difficult to focus on the software itself. The goal of this `macpan2` project is to re-imagine `McMasterPandemic`, building it from the ground up with architectural and technological decisions that address the many lessons that we learned from COVID-19 about software. + +Although `macpan2` is designed as a compartmental modelling tool that is agnostic about the underlying computational engine, it currently makes use of [template model builder](https://github.com/kaskr/adcomp). Template model builder (TMB) is an `R` modelling package based on a `C++` framework incorporating mature [automatic differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) and [matrix algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) libraries. + +The [Public Health Risk Sciences Division](https://github.com/phac-nml-phrsd) at the [Public Health Agency of Canada](https://www.canada.ca/en/public-health.html) uses `macpan2` (for example, [here](https://phac-nml-phrsd.github.io/EPACmodel/)). + +## Documentation + +* [Package reference](https://canmod.github.io/macpan2/) +* [Quick-start guide](https://canmod.github.io/macpan2/articles/quickstart) +* [Representation of compartmental models](https://canmod.github.io/macpan2/articles/model_definitions) [specification document] +* [`C++` engine](https://canmod.github.io/macpan2/articles/cpp_side) [specification document] +* [Project history and trajectory](https://canmod.net/misc/macpan2_presentation) [slides] + +## Installation + +If you're on a Windows system, please install `Rtools` matching your R version from [here](https://cran.r-project.org/bin/windows/Rtools/). This ensures you have a C++ compiler, which is required to install `macpan2` from source (as below). + +Then, install the `macpan2` package with the following R command. + +``` +remotes::install_github("canmod/macpan2") +``` + +For projects in production one should install a specific version, as in the following command. +``` +remotes::install_github("canmod/macpan2@v0.0.3") +``` + +## Hello World + +This [quick-start guide](https://canmod.github.io/macpan2/articles/quickstart) describes the following hello-world SIR model. + +``` +library(macpan2) +sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) +N = 100 +simulator = sir$simulators$tmb(time_steps = 100 + , state = c(S = N - 1, I = 1, R = 0) + , flow = c(foi = 0, gamma = 0.1) + , N = N + , beta = 0.2 +) +sir_sims = simulator$report() +``` + +## Product Management + +The [project board](https://github.com/orgs/canmod/projects/2) tracks the details of bugs, tasks, and feature development. But the following narrative will provide context on product development themes, their current state, and plans for improvement and implementation. + +### General Dynamic Simulation + +One can define a generic set of update steps that are iterated to produce a dynamic simulation. + +```{r} +library(macpan2) +si = mp_dynamic_model( + expr_list = ExprList( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection + ) + ), + unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) +) +print(si) +``` + +Simulating from this model takes the following steps. +```{r} +getwd() +(si + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") + |> mp_report() +) +``` + +This part of the package is general, stable, and flexible. It also meets many modellers where they are, which is with the ability to write down a set of transitions/state updates. + +But it is not convenient if you would just like to simulate from it, which is what the model library is for. + +### Model Library + +```{r} +("unstructured/si" + |> mp_library() + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") + |> mp_report() +) +``` + +TODO: +- [ ] Reuse the tools for the older concept of starter models. +- [ ] Establish a specification + +### Calibration + +### Model Structure and Bookkeeping + +### Alternative Engines + +### Combining Expression Lists + +Because expression lists are really just lists of expressions, they can be combined as lists would normally be combined. In this example we keep the dynamics of the si model separate from under-reporting and reporting delay corrections to the raw prevalence (TODO: should really use incidence). + +```{r} +library(macpan2) +si_dynamics = list( + transition_rate = infection ~ beta * S * I / N + , state_update = S ~ S - infection + , state_update = I ~ I + infection +) +reporting_correction = list( + post_processing = reports ~ convolution(I, c(0.5, 0.25, 0.25)) +) +si = mp_dynamic_model( + expr_list = ExprList(during = c(si_dynamics, reporting_correction)), + unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) +) +(si + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "reports") + |> mp_report() +) +``` diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 114a3972..165b26f0 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -105,8 +105,9 @@ enum macpan2_func { , MP2_FROM_DIAG = 42 // fwrap,fail: from_diag(x) , MP2_TIME_GROUP = 43 //fwrap,fail: time_group(i, change_points) , MP2_COS = 44 // fwrap,null: cos(x) - //, MP2_LOGISTIC = 45 // fwrap,null: logistic(x) - //, MP2_LOGIT = 46 // fwrap,null: logit(x) + , MP2_PRINT = 45 // fwrap,null: print(x) + //, MP2_LOGISTIC = 46 // fwrap,null: logistic(x) + //, MP2_LOGIT = 47 // fwrap,null: logit(x) }; enum macpan2_meth { @@ -2365,6 +2366,11 @@ class ExprEvaluator { } return m; + case MP2_PRINT: + std::cout << "printing matrix number " << index2mats[0] << " :" << std::endl; + std::cout << args[0] << std::endl; + return m; + default: SetError(255, "invalid operator in arithmetic expression", row); return m; diff --git a/src/macpan2.cpp b/src/macpan2.cpp index cfd7d7aa..e7a93b95 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -106,8 +106,9 @@ enum macpan2_func { , MP2_FROM_DIAG = 42 // fwrap,fail: from_diag(x) , MP2_TIME_GROUP = 43 //fwrap,fail: time_group(i, change_points) , MP2_COS = 44 // fwrap,null: cos(x) - //, MP2_LOGISTIC = 45 // fwrap,null: logistic(x) - //, MP2_LOGIT = 46 // fwrap,null: logit(x) + , MP2_PRINT = 45 // fwrap,null: print(x) + //, MP2_LOGISTIC = 46 // fwrap,null: logistic(x) + //, MP2_LOGIT = 47 // fwrap,null: logit(x) }; enum macpan2_meth { @@ -2366,6 +2367,11 @@ class ExprEvaluator { } return m; + case MP2_PRINT: + std::cout << "printing matrix number " << index2mats[0] << " :" << std::endl; + std::cout << args[0] << std::endl; + return m; + default: SetError(255, "invalid operator in arithmetic expression", row); return m; From bbbd8b2becba4c5bb7cab9be832383e5857c37d9 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 4 Dec 2023 11:09:35 -0500 Subject: [PATCH 124/332] readme --- README.md | 164 ++++++++++++++++++++++++++++++++++++++++++ misc/build/README.Rmd | 111 ++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) diff --git a/README.md b/README.md index 02f50335..6e4d5ac5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ + # macpan2 @@ -173,6 +174,169 @@ TODO: - \[ \] Reuse the tools for the older concept of starter models. - ### Model Structure and Bookkeeping +Structured models are combinations of simpler modular model components. +For example one might combine an SIR model with an age-group contact +model to produce an age structured model. The modular model components +are called atomic models. + +Models are composed of expression lists. Each expression in an +unstructured model can be converted into a structured expression to +create a structured model. For example, the following unstructured +expression defines the rate at which new infections emerge. + + infection ~ beta * S * I / N + +Each symbol in this expression has a certain type within a structured +model, and this type determines how it gets translated into a structured +expression. The simplest structured model is one that collects `S` and +`I` into a `state` vector with elements `S` and `I`. With this +interpretation of the `S` and `I` symbols, the structured infection +expression gets translated internally to the following. + + infection ~ beta * state[S] * state[I] / N + +Here `S` and `I` become symbols for extracting subsets of the `state` +vector. In this case the expression itself remains a scalar expression +but two of the scalars are obtained by extracting subsets of the `state` +vector. It won’t take much imagination to think of examples where +multiple paths to infection are required, and therefore the single +scalar-valued infection expression will be insufficient. + +We will have a vector-valued expression, for example, in a model with an +expanded state vector that tracks the geographic location of `S` and `I` +individuals. For example, a two patch model with an `east` and `west` +patch would involve a four-dimensional state vector with the following +elements: `S.east`, `S.west`, `I.east`, and `I.west`. In this case we +now have two scalar-valued infection expressions. + + infection[east] ~ beta * state[S.east] * state[I.east] / N + infection[west] ~ beta * state[S.west] * state[I.west] / N + +With two patches it is fine to write out all scalar-valued infection +expressions, but with more patches and with different types of structure +(e.g. age groups, infection status, hospitalization, immunity status, +etc …) it will become crucial to have software that handles the +bookkeeping internally. + +To see how easy this can be, note that this two-patch infection +expression can be powerfully and compactly expressed as our original +unstructured expression, `infection ~ beta * S * I / N`, where +`S = c(state[S.east], state[S.west])` and +`I = c(state[I.east], state[I.west])`. + +Why is this powerful? Because it separates the math of the dynamic +mechanism, `infection ~ beta * S * I / N`, from the bookkeeping required +in structured models where the same mechanism is applied again and again +to different model strata. This is often how modellers think. For +example, I might have a location-structured SIR model that I need to +expand to be both age- and location-structured. In this case, infection +is still the same process, whereby a susceptible individual contacts an +infectious individual to create a flow from susceptible individuals to +infectious individuals. The same math applies to all strata of the +model. The boring but necessary part is to connect the math to the +bookkeeping associated with the model structure, and so software should +focus on making these bookkeeping changes as easy as possible and with +minimal changes required to the underlying mathematical expressions. + +Let’s look at more examples of infection, and watch the bookkeeping get +more annoying. In an age-stratified model with two age groups, we now +get four scalar-valued infection expressions of the form +`infection ~ beta * S * I / N`. + + infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] + infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] + infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] + infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N[old] + +Here the first expression is for a young individual infecting an old +individual, the second is for an old individual infecting a young +individual, etc … Things get worse if we have two age groups in two +patches. + + infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] + infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] + infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] + infection[old.old.east] ~ beta[old.old.east] * state[S.old.east] * state[I.old.east] / N[old.east] + infection[young.young.west] ~ beta[young.young.west] * state[S.young.west] * state[I.young.west] / N[young.west] + infection[young.old.west] ~ beta[young.old.west] * state[S.young.west] * state[I.old.west] / N[old.west] + infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * state[I.young.west] / N[young.west] + infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] + +This still isn’t so bad, as we just have the first four expressions for +`east` and the last four for `west`. But now let’s introduce two +infection status categories: `mild` and `severe`. + + infection[young.young.east.mild] ~ beta[young.young.east.mild] * state[S.young.east.] * state[I.young.east.mild] / N[young.east] + infection[young.old.east.mild] ~ beta[young.old.east.mild] * state[S.young.east.] * state[I.old.east.mild] / N[old.east] + infection[old.young.east.mild] ~ beta[old.young.east.mild] * state[S.old.east.] * state[I.young.east.mild] / N[young.east] + infection[old.old.east.mild] ~ beta[old.old.east.mild] * state[S.old.east.] * state[I.old.east.mild] / N[old.east] + infection[young.young.east.severe] ~ beta[young.young.east.severe] * state[S.young.east.] * state[I.young.east.severe] / N[young.east] + infection[young.old.east.severe] ~ beta[young.old.east.severe] * state[S.young.east.] * state[I.old.east.severe] / N[old.east] + infection[old.young.east.severe] ~ beta[old.young.east.severe] * state[S.old.east.] * state[I.young.east.severe] / N[young.east] + infection[old.old.east.severe] ~ beta[old.old.east.severe] * state[S.old.east.] * state[I.old.east.severe] / N[old.east] + infection[young.young.west.mild] ~ beta[young.young.west.mild] * state[S.young.west.] * state[I.young.west.mild] / N[young.west] + infection[young.old.west.mild] ~ beta[young.old.west.mild] * state[S.young.west.] * state[I.old.west.mild] / N[old.west] + infection[old.young.west.mild] ~ beta[old.young.west.mild] * state[S.old.west.] * state[I.young.west.mild] / N[young.west] + infection[old.old.west.mild] ~ beta[old.old.west.mild] * state[S.old.west.] * state[I.old.west.mild] / N[old.west] + infection[young.young.west.severe] ~ beta[young.young.west.severe] * state[S.young.west.] * state[I.young.west.severe] / N[young.west] + infection[young.old.west.severe] ~ beta[young.old.west.severe] * state[S.young.west.] * state[I.old.west.severe] / N[old.west] + infection[old.young.west.severe] ~ beta[old.young.west.severe] * state[S.old.west.] * state[I.young.west.severe] / N[young.west] + infection[old.old.west.severe] ~ beta[old.old.west.severe] * state[S.old.west.] * state[I.old.west.severe] / N[old.west] + +Above we used dot-concatenation to represent the two model strata +(epidemiological status and geographic location) in the state variable +names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state +vector gets more structured it becomes more convenient to describe its +variables using an index table, the rows of which describe each state +variable. + + state = mp_cartesian( + mp_index(Epi = c("S", "I")), + mp_index(Loc = c("east", "west")) + ) + state + + ## Epi Loc + ## S east + ## I east + ## S west + ## I west + +With this representation we can get subsets of the state vector that +represent each epidemiological status. + + mp_subset(state, Epi = "S") + + ## Epi Loc + ## S east + ## S west + + mp_subset(state, Epi = "I") + + ## Epi Loc + ## I east + ## I west + +- The symbols `S` and `I` are subsets of a structured vector, in this + case called `state`, and so are replaced in the following way. + - `S` -> `state[S_infection]` + - `I` -> `state[I_infection]` +- The functions `*` and `/` are binary operators in this case. In the + structured model they are vectorized, which means that `*` and `/` + is applied element-wise. For example, `S * I` means that the + resulting vector has a first element given by the product of the + first element of `S` and the first element of `I`, etc … + + + + flows_per_time[infection] ~ beta[infection_beta] * state[infection_S] * state[infection_I] / N[infection_N] + +#### Structured Vectors + +These are column vectors, the rows of which + +### Structure Encourages Reparameterization + ### Alternative Engines ### Combining Expression Lists diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index 82f41862..9460bedd 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -107,6 +107,117 @@ TODO: ### Model Structure and Bookkeeping +Structured models are combinations of simpler modular model components. For example one might combine an SIR model with an age-group contact model to produce an age structured model. The modular model components are called atomic models. + +Models are composed of expression lists. Each expression in an unstructured model can be converted into a structured expression to create a structured model. For example, the following unstructured expression defines the rate at which new infections emerge. + +```{r, eval = FALSE} +infection ~ beta * S * I / N +``` + +Each symbol in this expression has a certain type within a structured model, and this type determines how it gets translated into a structured expression. The simplest structured model is one that collects `S` and `I` into a `state` vector with elements `S` and `I`. With this interpretation of the `S` and `I` symbols, the structured infection expression gets translated internally to the following. + +```{r, eval = FALSE} +infection ~ beta * state[S] * state[I] / N +``` + + +Here `S` and `I` become symbols for extracting subsets of the `state` vector. In this case the expression itself remains a scalar expression but two of the scalars are obtained by extracting subsets of the `state` vector. It won't take much imagination to think of examples where multiple paths to infection are required, and therefore the single scalar-valued infection expression will be insufficient. + +We will have a vector-valued expression, for example, in a model with an expanded state vector that tracks the geographic location of `S` and `I` individuals. For example, a two patch model with an `east` and `west` patch would involve a four-dimensional state vector with the following elements: `S.east`, `S.west`, `I.east`, and `I.west`. In this case we now have two scalar-valued infection expressions. + +```{r, eval = FALSE} +infection[east] ~ beta * state[S.east] * state[I.east] / N +infection[west] ~ beta * state[S.west] * state[I.west] / N +``` + +With two patches it is fine to write out all scalar-valued infection expressions, but with more patches and with different types of structure (e.g. age groups, infection status, hospitalization, immunity status, etc ...) it will become crucial to have software that handles the bookkeeping internally. + +To see how easy this can be, note that this two-patch infection expression can be powerfully and compactly expressed as our original unstructured expression, `infection ~ beta * S * I / N`, where `S = c(state[S.east], state[S.west])` and `I = c(state[I.east], state[I.west])`. + +Why is this powerful? Because it separates the math of the dynamic mechanism, `infection ~ beta * S * I / N`, from the bookkeeping required in structured models where the same mechanism is applied again and again to different model strata. This is often how modellers think. For example, I might have a location-structured SIR model that I need to expand to be both age- and location-structured. In this case, infection is still the same process, whereby a susceptible individual contacts an infectious individual to create a flow from susceptible individuals to infectious individuals. The same math applies to all strata of the model. The boring but necessary part is to connect the math to the bookkeeping associated with the model structure, and so software should focus on making these bookkeeping changes as easy as possible and with minimal changes required to the underlying mathematical expressions. + +Let's look at more examples of infection, and watch the bookkeeping get more annoying. In an age-stratified model with two age groups, we now get four scalar-valued infection expressions of the form `infection ~ beta * S * I / N`. +```{r, eval = FALSE} +infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] +infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] +infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] +infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N[old] +``` + +Here the first expression is for a young individual infecting an old individual, the second is for an old individual infecting a young individual, etc ... Things get worse if we have two age groups in two patches. + +```{r, eval = FALSE} +infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] +infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] +infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] +infection[old.old.east] ~ beta[old.old.east] * state[S.old.east] * state[I.old.east] / N[old.east] +infection[young.young.west] ~ beta[young.young.west] * state[S.young.west] * state[I.young.west] / N[young.west] +infection[young.old.west] ~ beta[young.old.west] * state[S.young.west] * state[I.old.west] / N[old.west] +infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * state[I.young.west] / N[young.west] +infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] +``` + +This still isn't so bad, as we just have the first four expressions for `east` and the last four for `west`. But now let's introduce two infection status categories: `mild` and `severe`. + +```{r, eval = FALSE} +infection[young.young.east.mild] ~ beta[young.young.east.mild] * state[S.young.east.] * state[I.young.east.mild] / N[young.east] +infection[young.old.east.mild] ~ beta[young.old.east.mild] * state[S.young.east.] * state[I.old.east.mild] / N[old.east] +infection[old.young.east.mild] ~ beta[old.young.east.mild] * state[S.old.east.] * state[I.young.east.mild] / N[young.east] +infection[old.old.east.mild] ~ beta[old.old.east.mild] * state[S.old.east.] * state[I.old.east.mild] / N[old.east] +infection[young.young.east.severe] ~ beta[young.young.east.severe] * state[S.young.east.] * state[I.young.east.severe] / N[young.east] +infection[young.old.east.severe] ~ beta[young.old.east.severe] * state[S.young.east.] * state[I.old.east.severe] / N[old.east] +infection[old.young.east.severe] ~ beta[old.young.east.severe] * state[S.old.east.] * state[I.young.east.severe] / N[young.east] +infection[old.old.east.severe] ~ beta[old.old.east.severe] * state[S.old.east.] * state[I.old.east.severe] / N[old.east] +infection[young.young.west.mild] ~ beta[young.young.west.mild] * state[S.young.west.] * state[I.young.west.mild] / N[young.west] +infection[young.old.west.mild] ~ beta[young.old.west.mild] * state[S.young.west.] * state[I.old.west.mild] / N[old.west] +infection[old.young.west.mild] ~ beta[old.young.west.mild] * state[S.old.west.] * state[I.young.west.mild] / N[young.west] +infection[old.old.west.mild] ~ beta[old.old.west.mild] * state[S.old.west.] * state[I.old.west.mild] / N[old.west] +infection[young.young.west.severe] ~ beta[young.young.west.severe] * state[S.young.west.] * state[I.young.west.severe] / N[young.west] +infection[young.old.west.severe] ~ beta[young.old.west.severe] * state[S.young.west.] * state[I.old.west.severe] / N[old.west] +infection[old.young.west.severe] ~ beta[old.young.west.severe] * state[S.old.west.] * state[I.young.west.severe] / N[young.west] +infection[old.old.west.severe] ~ beta[old.old.west.severe] * state[S.old.west.] * state[I.old.west.severe] / N[old.west] +``` + + + +Above we used dot-concatenation to represent the two model strata (epidemiological status and geographic location) in the state variable names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state vector gets more structured it becomes more convenient to describe its variables using an index table, the rows of which describe each state variable. + +```{r} +state = mp_cartesian( + mp_index(Epi = c("S", "I")), + mp_index(Loc = c("east", "west")) +) +state +``` + +With this representation we can get subsets of the state vector that represent each epidemiological status. + +```{r} +mp_subset(state, Epi = "S") +mp_subset(state, Epi = "I") +``` + +* The symbols `S` and `I` are subsets of a structured vector, in this case called `state`, and so are replaced in the following way. + * `S` -> `state[S_infection]` + * `I` -> `state[I_infection]` +* The functions `*` and `/` are binary operators in this case. In the structured model they are vectorized, which means that `*` and `/` is applied element-wise. For example, `S * I` means that the resulting vector has a first element given by the product of the first element of `S` and the first element of `I`, etc ... + + +```{r, eval = FALSE} +flows_per_time[infection] ~ beta[infection_beta] * state[infection_S] * state[infection_I] / N[infection_N] +``` + +#### Structured Vectors + +These are column vectors, the rows of which + + +### Structure Encourages Reparameterization + + + + ### Alternative Engines ### Combining Expression Lists From 34d2c3f3f33cb780e7f3f3da11760849af508083 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 4 Dec 2023 13:12:26 -0500 Subject: [PATCH 125/332] experiments --- Makefile | 3 +- NAMESPACE | 4 +++ R/formula_utils.R | 29 ++++++++++++++++ R/lists.R | 52 ++++++++++++++++++++++++++--- R/mp.R | 5 +++ man/mp_extract_exprs.Rd | 20 +++++++++++ misc/build/README.Rmd | 73 +++++++++++++++++++++++++---------------- 7 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 man/mp_extract_exprs.Rd diff --git a/Makefile b/Makefile index 9ad3bef8..f3a181ba 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,8 @@ coverage.html: R/*.R src/macpan2.cpp tests/testthat/*.R readme:: README.md README.md: misc/build/README.Rmd Rscript -e "rmarkdown::render('misc/build/README.Rmd', output_dir = '.', output_file = 'README.md', output_format = 'md_document', knit_root_dir = '../..')" - echo '' | cat - $@ > temp && mv temp $@ + echo '' > temp + echo '' | cat - $@ >> temp && mv temp $@ enum-update:: R/enum.R diff --git a/NAMESPACE b/NAMESPACE index f6cd63e5..6f95df7e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,6 +28,9 @@ S3method(length,Vector) S3method(mp_extract,DynamicModel) S3method(mp_extract,Ledger) S3method(mp_extract,ModelDefRun) +S3method(mp_extract_exprs,DynamicModel) +S3method(mp_extract_exprs,ExprList) +S3method(mp_extract_exprs,list) S3method(mp_index,character) S3method(mp_index,data.frame) S3method(mp_labels,Index) @@ -177,6 +180,7 @@ export(mp_expr_binop) export(mp_expr_group_sum) export(mp_expr_list) export(mp_extract) +export(mp_extract_exprs) export(mp_group) export(mp_index) export(mp_indexed_exprs) diff --git a/R/formula_utils.R b/R/formula_utils.R index d5e65f20..627649fb 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -189,3 +189,32 @@ formula_components = function(formula, side = c("both", "left", "right")) { literals = parse_table$x[is_var_or_lit & is_lit] |> as.numeric() |> unique() ) } + +lhs_pieces = function(formula) { + table = method_parser(formula) + + subset_vector_case = all( + identical(table$x[1:2], c("~", "[")), + identical(table$n, c(1L, 2L, 0L, 0L)), + identical(table$i, c(1L, 2L, 0L, 0L)) + ) + + matrix_case = all( + identical(table$x[1], "~"), + identical(table$n = c(1L, 0L)), + identical(table$i = c(1L, 0L)) + ) + + if (subset_vector_case) { + list( + variable = table$x[3L], + positions = table$x[4L] + ) + } else if (matrix_case) { + list( + variable = table$x[2L], + positions = character() + ) + } + +} diff --git a/R/lists.R b/R/lists.R index d9a04e69..465c3105 100644 --- a/R/lists.R +++ b/R/lists.R @@ -32,8 +32,52 @@ assert_named_list = function(l) { self_named_vector = function(...) c(...) |> setNames(c(...)) -## works even if names are not unique -get_elements_by_names = function(l, ...) { - nms = list(...) |> lapply(as.character) |> unlist(use.names = FALSE) |> unique() - l[names(l) %in% nms] +#' Extract Expressions by Name +#' +#' @param x Object containing named expressions. +#' @param ... Character vectors containing names of expressions. +#' +#' @returns An object the same type as \code{x} but only with +#' those expressions identified by the names in \code{...}. +#' +#' @export +mp_extract_exprs = function(x, ...) UseMethod("mp_extract_exprs") + +#' @export +mp_extract_exprs.list = function(x, ...) { + nms = (list(...) + |> lapply(as.character) + |> unlist(use.names = FALSE) + |> unique() + ) + x[names(x) %in% nms] +} + +#' @export +mp_extract_exprs.ExprList = function(x, ...) { + nms = (list(...) + |> lapply(as.character) + |> unlist(use.names = FALSE) + |> unique() + ) + ExprList( + before = mp_extract_exprs(x$before, ...), + during = mp_extract_exprs(x$during, ...), + after = mp_extract_exprs(x$after, ...), + .simulate_exprs = intersect(x$.simulate_exprs, nms) + ) } + +#' @export +mp_extract_exprs.DynamicModel = function(x, ...) { + mp_extract_exprs(x$expr_list, ...) +} + + +#' @export +mp_combine_exprs = function(...) { + UseMethod("mp_combine_exprs") +} + +#' @export +mp_combine_exprs.list = function(...) c(...) diff --git a/R/mp.R b/R/mp.R index c158591b..355652f4 100644 --- a/R/mp.R +++ b/R/mp.R @@ -588,6 +588,11 @@ mp_reference.Ledger = function(x, dimension_name) { ii } +#' @export +mp_reference.Index = function(x, dimension_name) { + x$reference_index +} + #' @export mp_extract = function(x, dimension_name) { UseMethod("mp_extract") diff --git a/man/mp_extract_exprs.Rd b/man/mp_extract_exprs.Rd new file mode 100644 index 00000000..dd8b3ae4 --- /dev/null +++ b/man/mp_extract_exprs.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lists.R +\name{mp_extract_exprs} +\alias{mp_extract_exprs} +\title{Extract Expressions by Name} +\usage{ +mp_extract_exprs(x, ...) +} +\arguments{ +\item{x}{Object containing named expressions.} + +\item{...}{Character vectors containing names of expressions.} +} +\value{ +An object the same type as \code{x} but only with +those expressions identified by the names in \code{...}. +} +\description{ +Extract Expressions by Name +} diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index 9460bedd..52c84de6 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -109,6 +109,8 @@ TODO: Structured models are combinations of simpler modular model components. For example one might combine an SIR model with an age-group contact model to produce an age structured model. The modular model components are called atomic models. +#### Structure in Expressions + Models are composed of expression lists. Each expression in an unstructured model can be converted into a structured expression to create a structured model. For example, the following unstructured expression defines the rate at which new infections emerge. ```{r, eval = FALSE} @@ -131,7 +133,7 @@ infection[east] ~ beta * state[S.east] * state[I.east] / N infection[west] ~ beta * state[S.west] * state[I.west] / N ``` -With two patches it is fine to write out all scalar-valued infection expressions, but with more patches and with different types of structure (e.g. age groups, infection status, hospitalization, immunity status, etc ...) it will become crucial to have software that handles the bookkeeping internally. +With two patches it is fine to write out all scalar-valued infection expressions, but with more patches and with different types of structure (e.g. age groups, symptom status, hospitalization, immunity status, etc ...) it will become crucial to have software that handles the bookkeeping internally. To see how easy this can be, note that this two-patch infection expression can be powerfully and compactly expressed as our original unstructured expression, `infection ~ beta * S * I / N`, where `S = c(state[S.east], state[S.west])` and `I = c(state[I.east], state[I.west])`. @@ -158,30 +160,50 @@ infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * sta infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] ``` -This still isn't so bad, as we just have the first four expressions for `east` and the last four for `west`. But now let's introduce two infection status categories: `mild` and `severe`. +This still isn't so bad, as we just have the first four expressions for `east` and the last four for `west`. But now let's introduce two symptom status categories: `mild` and `severe`. ```{r, eval = FALSE} -infection[young.young.east.mild] ~ beta[young.young.east.mild] * state[S.young.east.] * state[I.young.east.mild] / N[young.east] -infection[young.old.east.mild] ~ beta[young.old.east.mild] * state[S.young.east.] * state[I.old.east.mild] / N[old.east] -infection[old.young.east.mild] ~ beta[old.young.east.mild] * state[S.old.east.] * state[I.young.east.mild] / N[young.east] -infection[old.old.east.mild] ~ beta[old.old.east.mild] * state[S.old.east.] * state[I.old.east.mild] / N[old.east] -infection[young.young.east.severe] ~ beta[young.young.east.severe] * state[S.young.east.] * state[I.young.east.severe] / N[young.east] -infection[young.old.east.severe] ~ beta[young.old.east.severe] * state[S.young.east.] * state[I.old.east.severe] / N[old.east] -infection[old.young.east.severe] ~ beta[old.young.east.severe] * state[S.old.east.] * state[I.young.east.severe] / N[young.east] -infection[old.old.east.severe] ~ beta[old.old.east.severe] * state[S.old.east.] * state[I.old.east.severe] / N[old.east] -infection[young.young.west.mild] ~ beta[young.young.west.mild] * state[S.young.west.] * state[I.young.west.mild] / N[young.west] -infection[young.old.west.mild] ~ beta[young.old.west.mild] * state[S.young.west.] * state[I.old.west.mild] / N[old.west] -infection[old.young.west.mild] ~ beta[old.young.west.mild] * state[S.old.west.] * state[I.young.west.mild] / N[young.west] -infection[old.old.west.mild] ~ beta[old.old.west.mild] * state[S.old.west.] * state[I.old.west.mild] / N[old.west] -infection[young.young.west.severe] ~ beta[young.young.west.severe] * state[S.young.west.] * state[I.young.west.severe] / N[young.west] -infection[young.old.west.severe] ~ beta[young.old.west.severe] * state[S.young.west.] * state[I.old.west.severe] / N[old.west] -infection[old.young.west.severe] ~ beta[old.young.west.severe] * state[S.old.west.] * state[I.young.west.severe] / N[young.west] -infection[old.old.west.severe] ~ beta[old.old.west.severe] * state[S.old.west.] * state[I.old.west.severe] / N[old.west] +infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] +infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] +infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] +infection[young.young.east.severe.severe] ~ beta[young.young.east.severe.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] +infection[young.old.east.mild.mild] ~ beta[young.old.east.mild.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] +infection[young.old.east.mild.severe] ~ beta[young.old.east.mild.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] +infection[young.old.east.severe.mild] ~ beta[young.old.east.severe.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] +infection[young.old.east.severe.severe] ~ beta[young.old.east.severe.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] +infection[old.young.east.mild.mild] ~ beta[old.young.east.mild.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] +infection[old.young.east.mild.severe] ~ beta[old.young.east.mild.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] +infection[old.young.east.severe.mild] ~ beta[old.young.east.severe.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] +infection[old.young.east.severe.severe] ~ beta[old.young.east.severe.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] +infection[old.old.east.mild.mild] ~ beta[old.old.east.mild.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] +infection[old.old.east.mild.severe] ~ beta[old.old.east.mild.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] +infection[old.old.east.severe.mild] ~ beta[old.old.east.severe.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] +infection[old.old.east.severe.severe] ~ beta[old.old.east.severe.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] +infection[young.young.west.mild.mild] ~ beta[young.young.west.mild.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] +infection[young.young.west.mild.severe] ~ beta[young.young.west.mild.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] +infection[young.young.west.severe.mild] ~ beta[young.young.west.severe.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] +infection[young.young.west.severe.severe] ~ beta[young.young.west.severe.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] +infection[young.old.west.mild.mild] ~ beta[young.old.west.mild.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] +infection[young.old.west.mild.severe] ~ beta[young.old.west.mild.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] +infection[young.old.west.severe.mild] ~ beta[young.old.west.severe.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] +infection[young.old.west.severe.severe] ~ beta[young.old.west.severe.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] +infection[old.young.west.mild.mild] ~ beta[old.young.west.mild.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] +infection[old.young.west.mild.severe] ~ beta[old.young.west.mild.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] +infection[old.young.west.severe.mild] ~ beta[old.young.west.severe.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] +infection[old.young.west.severe.severe] ~ beta[old.young.west.severe.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] +infection[old.old.west.mild.mild] ~ beta[old.old.west.mild.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] +infection[old.old.west.mild.severe] ~ beta[old.old.west.mild.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] +infection[old.old.west.severe.mild] ~ beta[old.old.west.severe.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] +infection[old.old.west.severe.severe] ~ beta[old.old.west.severe.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] ``` +This is intense. The names in square brackets get much less clear in several ways as the model gets more structured. This lack of clarity makes it difficult to see a variety of model assumptions by looking at scalar-valued expressions. The `infection` and `beta` vectors depend on two age categories and two symptom statuses, but only one location. This is because young people can infect old people (and vice versa), because mildly infectious people can cause severe infection (and vice versa), and because infectious people in the east cannot infect people in the west (and vice versa). For labels associated with two ages, what does the first age mean, relative to the second age? To discover this you need to know to look at the ages associated with the `S` and `I` states, and once you do this you can see that the first age category is associated with the susceptible individual and the second with the infectious individual. There is a related issue with symptom status, but it is expressed differently because `S` individuals are not structured by symptom status. In this case we match the second symptom status associated with `infection` and `beta` to the symptom status of the `I` states, which means that the first symptom status implicitly refers to the status of the newly infected individuals and not the infectious individuals. Another way to look at this last issue is that `I` boxes play two different roles. The first role is as an individual that infects an `S` individual, and the second is as the individual that that `S` individual becomes after it is infected. None of this is obvious from the scalar-valued expressions above, and it is difficult to imagine a clearer way to explicitly write each expression. + +Our approach is to do the bookkeeping in a different way. In particular we believe that a constructive approach to structure provides a more comprehensible description, as we describe next. In brief, we believe that a grammar for specifying the steps associated with adding structure can be clearer than a description of the final structured model. +#### Constructive Descriptions of Model Structure -Above we used dot-concatenation to represent the two model strata (epidemiological status and geographic location) in the state variable names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state vector gets more structured it becomes more convenient to describe its variables using an index table, the rows of which describe each state variable. +The first step to being more constructive is to have a better representation of the structured vectors. Above we used dot-concatenation to represent the model strata. For example, in the two-patch SI model we have both epidemiological status and geographic location in the state variable names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state vector gets more structured it becomes more convenient to describe its variables using an index table, the rows of which describe each state variable. ```{r} state = mp_cartesian( @@ -191,6 +213,10 @@ state = mp_cartesian( state ``` +```{r} +beta = mp_group(state, "Epi") +``` + With this representation we can get subsets of the state vector that represent each epidemiological status. ```{r} @@ -198,15 +224,6 @@ mp_subset(state, Epi = "S") mp_subset(state, Epi = "I") ``` -* The symbols `S` and `I` are subsets of a structured vector, in this case called `state`, and so are replaced in the following way. - * `S` -> `state[S_infection]` - * `I` -> `state[I_infection]` -* The functions `*` and `/` are binary operators in this case. In the structured model they are vectorized, which means that `*` and `/` is applied element-wise. For example, `S * I` means that the resulting vector has a first element given by the product of the first element of `S` and the first element of `I`, etc ... - - -```{r, eval = FALSE} -flows_per_time[infection] ~ beta[infection_beta] * state[infection_S] * state[infection_I] / N[infection_N] -``` #### Structured Vectors From a8d02fb4907a8e509701048495436d8c99adce4d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 4 Dec 2023 14:41:24 -0500 Subject: [PATCH 126/332] readme update --- NAMESPACE | 3 ++ R/formula_utils.R | 4 +- README.md | 128 ++++++++++++++++++++++++++++++---------------- 3 files changed, 90 insertions(+), 45 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 6f95df7e..a76f4030 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -25,6 +25,7 @@ S3method(labels,TMBCompartmentalSimulator) S3method(labels,TMBDynamicSimulator) S3method(labels,VariableLabels) S3method(length,Vector) +S3method(mp_combine_exprs,list) S3method(mp_extract,DynamicModel) S3method(mp_extract,Ledger) S3method(mp_extract,ModelDefRun) @@ -35,6 +36,7 @@ S3method(mp_index,character) S3method(mp_index,data.frame) S3method(mp_labels,Index) S3method(mp_labels,Ledger) +S3method(mp_reference,Index) S3method(mp_reference,Ledger) S3method(mp_tmb_simulator,DynamicModel) S3method(mp_tmb_simulator,ModelDefRun) @@ -174,6 +176,7 @@ export(mp_cartesian) export(mp_catalogue) export(mp_choose) export(mp_choose_out) +export(mp_combine_exprs) export(mp_decompose) export(mp_dynamic_model) export(mp_expr_binop) diff --git a/R/formula_utils.R b/R/formula_utils.R index 627649fb..a740a5fd 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -201,8 +201,8 @@ lhs_pieces = function(formula) { matrix_case = all( identical(table$x[1], "~"), - identical(table$n = c(1L, 0L)), - identical(table$i = c(1L, 0L)) + identical(table$n, c(1L, 0L)), + identical(table$i, c(1L, 0L)) ) if (subset_vector_case) { diff --git a/README.md b/README.md index 6e4d5ac5..534177ad 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ For example one might combine an SIR model with an age-group contact model to produce an age structured model. The modular model components are called atomic models. +#### Structure in Expressions + Models are composed of expression lists. Each expression in an unstructured model can be converted into a structured expression to create a structured model. For example, the following unstructured @@ -214,9 +216,9 @@ now have two scalar-valued infection expressions. With two patches it is fine to write out all scalar-valued infection expressions, but with more patches and with different types of structure -(e.g. age groups, infection status, hospitalization, immunity status, -etc …) it will become crucial to have software that handles the -bookkeeping internally. +(e.g. age groups, symptom status, hospitalization, immunity status, etc +…) it will become crucial to have software that handles the bookkeeping +internally. To see how easy this can be, note that this two-patch infection expression can be powerfully and compactly expressed as our original @@ -263,32 +265,84 @@ patches. infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] This still isn’t so bad, as we just have the first four expressions for -`east` and the last four for `west`. But now let’s introduce two -infection status categories: `mild` and `severe`. - - infection[young.young.east.mild] ~ beta[young.young.east.mild] * state[S.young.east.] * state[I.young.east.mild] / N[young.east] - infection[young.old.east.mild] ~ beta[young.old.east.mild] * state[S.young.east.] * state[I.old.east.mild] / N[old.east] - infection[old.young.east.mild] ~ beta[old.young.east.mild] * state[S.old.east.] * state[I.young.east.mild] / N[young.east] - infection[old.old.east.mild] ~ beta[old.old.east.mild] * state[S.old.east.] * state[I.old.east.mild] / N[old.east] - infection[young.young.east.severe] ~ beta[young.young.east.severe] * state[S.young.east.] * state[I.young.east.severe] / N[young.east] - infection[young.old.east.severe] ~ beta[young.old.east.severe] * state[S.young.east.] * state[I.old.east.severe] / N[old.east] - infection[old.young.east.severe] ~ beta[old.young.east.severe] * state[S.old.east.] * state[I.young.east.severe] / N[young.east] - infection[old.old.east.severe] ~ beta[old.old.east.severe] * state[S.old.east.] * state[I.old.east.severe] / N[old.east] - infection[young.young.west.mild] ~ beta[young.young.west.mild] * state[S.young.west.] * state[I.young.west.mild] / N[young.west] - infection[young.old.west.mild] ~ beta[young.old.west.mild] * state[S.young.west.] * state[I.old.west.mild] / N[old.west] - infection[old.young.west.mild] ~ beta[old.young.west.mild] * state[S.old.west.] * state[I.young.west.mild] / N[young.west] - infection[old.old.west.mild] ~ beta[old.old.west.mild] * state[S.old.west.] * state[I.old.west.mild] / N[old.west] - infection[young.young.west.severe] ~ beta[young.young.west.severe] * state[S.young.west.] * state[I.young.west.severe] / N[young.west] - infection[young.old.west.severe] ~ beta[young.old.west.severe] * state[S.young.west.] * state[I.old.west.severe] / N[old.west] - infection[old.young.west.severe] ~ beta[old.young.west.severe] * state[S.old.west.] * state[I.young.west.severe] / N[young.west] - infection[old.old.west.severe] ~ beta[old.old.west.severe] * state[S.old.west.] * state[I.old.west.severe] / N[old.west] - -Above we used dot-concatenation to represent the two model strata -(epidemiological status and geographic location) in the state variable -names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state -vector gets more structured it becomes more convenient to describe its -variables using an index table, the rows of which describe each state -variable. +`east` and the last four for `west`. But now let’s introduce two symptom +status categories: `mild` and `severe`. + + infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] + infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] + infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] + infection[young.young.east.severe.severe] ~ beta[young.young.east.severe.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] + infection[young.old.east.mild.mild] ~ beta[young.old.east.mild.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] + infection[young.old.east.mild.severe] ~ beta[young.old.east.mild.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] + infection[young.old.east.severe.mild] ~ beta[young.old.east.severe.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] + infection[young.old.east.severe.severe] ~ beta[young.old.east.severe.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] + infection[old.young.east.mild.mild] ~ beta[old.young.east.mild.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] + infection[old.young.east.mild.severe] ~ beta[old.young.east.mild.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] + infection[old.young.east.severe.mild] ~ beta[old.young.east.severe.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] + infection[old.young.east.severe.severe] ~ beta[old.young.east.severe.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] + infection[old.old.east.mild.mild] ~ beta[old.old.east.mild.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] + infection[old.old.east.mild.severe] ~ beta[old.old.east.mild.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] + infection[old.old.east.severe.mild] ~ beta[old.old.east.severe.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] + infection[old.old.east.severe.severe] ~ beta[old.old.east.severe.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] + infection[young.young.west.mild.mild] ~ beta[young.young.west.mild.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] + infection[young.young.west.mild.severe] ~ beta[young.young.west.mild.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] + infection[young.young.west.severe.mild] ~ beta[young.young.west.severe.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] + infection[young.young.west.severe.severe] ~ beta[young.young.west.severe.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] + infection[young.old.west.mild.mild] ~ beta[young.old.west.mild.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] + infection[young.old.west.mild.severe] ~ beta[young.old.west.mild.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] + infection[young.old.west.severe.mild] ~ beta[young.old.west.severe.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] + infection[young.old.west.severe.severe] ~ beta[young.old.west.severe.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] + infection[old.young.west.mild.mild] ~ beta[old.young.west.mild.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] + infection[old.young.west.mild.severe] ~ beta[old.young.west.mild.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] + infection[old.young.west.severe.mild] ~ beta[old.young.west.severe.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] + infection[old.young.west.severe.severe] ~ beta[old.young.west.severe.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] + infection[old.old.west.mild.mild] ~ beta[old.old.west.mild.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] + infection[old.old.west.mild.severe] ~ beta[old.old.west.mild.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] + infection[old.old.west.severe.mild] ~ beta[old.old.west.severe.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] + infection[old.old.west.severe.severe] ~ beta[old.old.west.severe.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] + +This is intense. The names in square brackets get much less clear in +several ways as the model gets more structured. This lack of clarity +makes it difficult to see a variety of model assumptions by looking at +scalar-valued expressions. The `infection` and `beta` vectors depend on +two age categories and two symptom statuses, but only one location. This +is because young people can infect old people (and vice versa), because +mildly infectious people can cause severe infection (and vice versa), +and because infectious people in the east cannot infect people in the +west (and vice versa). For labels associated with two ages, what does +the first age mean, relative to the second age? To discover this you +need to know to look at the ages associated with the `S` and `I` states, +and once you do this you can see that the first age category is +associated with the susceptible individual and the second with the +infectious individual. There is a related issue with symptom status, but +it is expressed differently because `S` individuals are not structured +by symptom status. In this case we match the second symptom status +associated with `infection` and `beta` to the symptom status of the `I` +states, which means that the first symptom status implicitly refers to +the status of the newly infected individuals and not the infectious +individuals. Another way to look at this last issue is that `I` boxes +play two different roles. The first role is as an individual that +infects an `S` individual, and the second is as the individual that that +`S` individual becomes after it is infected. None of this is obvious +from the scalar-valued expressions above, and it is difficult to imagine +a clearer way to explicitly write each expression. + +Our approach is to do the bookkeeping in a different way. In particular +we believe that a constructive approach to structure provides a more +comprehensible description, as we describe next. In brief, we believe +that a grammar for specifying the steps associated with adding structure +can be clearer than a description of the final structured model. + +#### Constructive Descriptions of Model Structure + +The first step to being more constructive is to have a better +representation of the structured vectors. Above we used +dot-concatenation to represent the model strata. For example, in the +two-patch SI model we have both epidemiological status and geographic +location in the state variable names: `S.east`, `S.west`, `I.east`, and +`I.west`. But as the state vector gets more structured it becomes more +convenient to describe its variables using an index table, the rows of +which describe each state variable. state = mp_cartesian( mp_index(Epi = c("S", "I")), @@ -302,6 +356,8 @@ variable. ## S west ## I west + beta = mp_group(state, "Epi") + With this representation we can get subsets of the state vector that represent each epidemiological status. @@ -317,20 +373,6 @@ represent each epidemiological status. ## I east ## I west -- The symbols `S` and `I` are subsets of a structured vector, in this - case called `state`, and so are replaced in the following way. - - `S` -> `state[S_infection]` - - `I` -> `state[I_infection]` -- The functions `*` and `/` are binary operators in this case. In the - structured model they are vectorized, which means that `*` and `/` - is applied element-wise. For example, `S * I` means that the - resulting vector has a first element given by the product of the - first element of `S` and the first element of `I`, etc … - - - - flows_per_time[infection] ~ beta[infection_beta] * state[infection_S] * state[infection_I] / N[infection_N] - #### Structured Vectors These are column vectors, the rows of which From c146b61519c340e54f3e192b00c7bdcb2e81aa75 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 5 Dec 2023 15:24:57 -0500 Subject: [PATCH 127/332] readme update --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ misc/build/README.Rmd | 18 ++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/README.md b/README.md index 534177ad..e64ae257 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,47 @@ TODO: - \[ \] Reuse the tools for the older concept of starter models. - ### Calibration +We will build a function, `mp_calibrate`, which takes a `DynamicModel` +object and other information for calibrating it. This information +includes. + +- A data frame (or data frames) containing observed (possibly uneven) + time series to compare with model simulations. What form should this + take? Could be the same as the output of `mp_report`. This would + have several benefits but also disadvantages. + - Consistency with input and output formats, making it a little + easier to learn. + - Easy to manipulate output into input for testing calibration + functionality. + - Possibly simpler argument list to `mp_calibrate` because we + would just relate the observed data to simulated data with the + same name, of course we would still need an interface for + distributional assumptions. + - Naturally handles missing values +- Distributional assumptions. Probably should be a few ways to do this + depending on how many different assumptions need to be made. At one + extreme every observation gets the same distribution, which is + easily specified in an argument to `mp_calibrate`. At the other + extreme each observation gets its own distribution, which could be + specified by adding additional columns to the data frame with + observed values. Designs for interfaces for use cases that are + somewhere between these two extremes seem less obvious. +- Identifying existing quantities to be fitted, creating new + quantities to be fitted (e.g. distributional scale parameters + created in the previous bullet point), and the scale (e.g. log, + logit) on which to fit these parameters. The new distributional + parameters should go into a new indexed vector called something like + `distributional_parameters`. (TODO: more general name for new + parameters that are part of the observation model). + +The output of `mp_calibrate` should be a `DynamicModel` that contains +new default parameter values given by fits and additional stochasticity +resulting from parameter estimation uncertainty. + +### Time-Varying Parameters + +TODO + ### Model Structure and Bookkeeping Structured models are combinations of simpler modular model components. @@ -381,6 +422,8 @@ These are column vectors, the rows of which ### Alternative Engines +TODO + ### Combining Expression Lists Because expression lists are really just lists of expressions, they can diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index 52c84de6..d7d8a533 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -105,6 +105,22 @@ TODO: ### Calibration +We will build a function, `mp_calibrate`, which takes a `DynamicModel` object and other information for calibrating it. This information includes. + +* A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this take? Could be the same as the output of `mp_report`. This would have several benefits but also disadvantages. + * Consistency with input and output formats, making it a little easier to learn. + * Easy to manipulate output into input for testing calibration functionality. + * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. + * Naturally handles missing values +* Distributional assumptions. Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own distribution, which could be specified by adding additional columns to the data frame with observed values. Designs for interfaces for use cases that are somewhere between these two extremes seem less obvious. +* Identifying existing quantities to be fitted, creating new quantities to be fitted (e.g. distributional scale parameters created in the previous bullet point), and the scale (e.g. log, logit) on which to fit these parameters. The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model). + +The output of `mp_calibrate` should be a `DynamicModel` that contains new default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. + +### Time-Varying Parameters + +TODO + ### Model Structure and Bookkeeping Structured models are combinations of simpler modular model components. For example one might combine an SIR model with an age-group contact model to produce an age structured model. The modular model components are called atomic models. @@ -237,6 +253,8 @@ These are column vectors, the rows of which ### Alternative Engines +TODO + ### Combining Expression Lists Because expression lists are really just lists of expressions, they can be combined as lists would normally be combined. In this example we keep the dynamics of the si model separate from under-reporting and reporting delay corrections to the raw prevalence (TODO: should really use incidence). From 5b4dcf572f4a50d0f5bb0705d659cb4d40505940 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 5 Dec 2023 15:26:30 -0500 Subject: [PATCH 128/332] readme update --- README.md | 6 ++++-- misc/build/README.Rmd | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e64ae257..e80c0867 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,10 @@ which is what the model library is for. ## 9 I 9 0 0 7.082401 ## 10 I 10 0 0 8.727601 -TODO: - \[ \] Reuse the tools for the older concept of starter models. - -\[ \] Establish a specification +TODO: + +- ☐ Reuse the tools for the older concept of starter models +- ☐ Establish a specification ### Calibration diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index d7d8a533..9f865a35 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -100,7 +100,8 @@ But it is not convenient if you would just like to simulate from it, which is wh ``` TODO: -- [ ] Reuse the tools for the older concept of starter models. + +- [ ] Reuse the tools for the older concept of starter models - [ ] Establish a specification ### Calibration From 397e03af98991f56157b2da09e934bd65abc26d2 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 5 Dec 2023 15:45:36 -0500 Subject: [PATCH 129/332] readme update --- README.md | 84 +++++++++++++++++++++++++------------------ misc/build/README.Rmd | 27 +++++++++++--- 2 files changed, 71 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index e80c0867..9cf82782 100644 --- a/README.md +++ b/README.md @@ -174,43 +174,57 @@ TODO: ### Calibration -We will build a function, `mp_calibrate`, which takes a `DynamicModel` -object and other information for calibrating it. This information -includes. - -- A data frame (or data frames) containing observed (possibly uneven) - time series to compare with model simulations. What form should this - take? Could be the same as the output of `mp_report`. This would - have several benefits but also disadvantages. - - Consistency with input and output formats, making it a little - easier to learn. - - Easy to manipulate output into input for testing calibration - functionality. - - Possibly simpler argument list to `mp_calibrate` because we - would just relate the observed data to simulated data with the - same name, of course we would still need an interface for - distributional assumptions. - - Naturally handles missing values -- Distributional assumptions. Probably should be a few ways to do this - depending on how many different assumptions need to be made. At one - extreme every observation gets the same distribution, which is - easily specified in an argument to `mp_calibrate`. At the other - extreme each observation gets its own distribution, which could be - specified by adding additional columns to the data frame with - observed values. Designs for interfaces for use cases that are - somewhere between these two extremes seem less obvious. -- Identifying existing quantities to be fitted, creating new - quantities to be fitted (e.g. distributional scale parameters - created in the previous bullet point), and the scale (e.g. log, - logit) on which to fit these parameters. The new distributional - parameters should go into a new indexed vector called something like - `distributional_parameters`. (TODO: more general name for new - parameters that are part of the observation model). - -The output of `mp_calibrate` should be a `DynamicModel` that contains -new default parameter values given by fits and additional stochasticity +We will build a function, `mp_calibrate`, which takes (1) an object for +simulating model trajectories and (2) other information for calibrating +certain quantities of this model. This second type of information is +detailed in the following sections. The output of `mp_calibrate` should +be another object for simulating model trajectories that contains new +default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. +#### Specifying Data to Fit + +A data frame (or data frames) containing observed (possibly uneven) time +series to compare with model simulations. What form should this data +frame take? + +One option is the same format as the output of `mp_report`. This would +have several benefits. \* Consistency with input and output formats, +making it a little easier to learn. \* Easy to manipulate output into +input for testing calibration functionality. \* Possibly simpler +argument list to `mp_calibrate` because we would just relate the +observed data to simulated data with the same name, of course we would +still need an interface for distributional assumptions. \* Naturally +handles missing values + +The main disadvantage of this is that format could differ from the +indexed vectors discussed below. + +#### Specifying Distributional Assumptions + +Probably should be a few ways to do this depending on how many different +assumptions need to be made. At one extreme every observation gets the +same distribution, which is easily specified in an argument to +`mp_calibrate`. At the other extreme each observation gets its own +distribution (including hyperparameters like spread and shape), which +could be specified by adding additional columns to the data frame with +observed values. Designs for interfaces for use cases that are somewhere +between these two extremes seem less obvious. + +#### Specifying Parameters to Fit + +There are two kinds of parameters to fit. + +- Existing quantities to be fitted (e.g. `beta`, initial number of + susceptible individuals `S`). +- Creating new quantities to be fitted (e.g. distributional scale + parameters declared along with (#distributional-assumptions). + +and the scale (e.g. log, logit) on which to fit these parameters. The +new distributional parameters should go into a new indexed vector called +something like `distributional_parameters`. (TODO: more general name for +new parameters that are part of the observation model). + ### Time-Varying Parameters TODO diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index 9f865a35..e99df494 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -106,17 +106,34 @@ TODO: ### Calibration -We will build a function, `mp_calibrate`, which takes a `DynamicModel` object and other information for calibrating it. This information includes. +We will build a function, `mp_calibrate`, which takes (1) an object for simulating model trajectories and (2) other information for calibrating certain quantities of this model. This second type of information is detailed in the following sections. The output of `mp_calibrate` should be another object for simulating model trajectories that contains new default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. -* A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this take? Could be the same as the output of `mp_report`. This would have several benefits but also disadvantages. +#### Specifying Data to Fit + +A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this data frame take? + +One option is the same format as the output of `mp_report`. This would have several benefits. * Consistency with input and output formats, making it a little easier to learn. * Easy to manipulate output into input for testing calibration functionality. * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. * Naturally handles missing values -* Distributional assumptions. Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own distribution, which could be specified by adding additional columns to the data frame with observed values. Designs for interfaces for use cases that are somewhere between these two extremes seem less obvious. -* Identifying existing quantities to be fitted, creating new quantities to be fitted (e.g. distributional scale parameters created in the previous bullet point), and the scale (e.g. log, logit) on which to fit these parameters. The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model). -The output of `mp_calibrate` should be a `DynamicModel` that contains new default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. +The main disadvantage of this is that format could differ from the indexed vectors discussed below. + + +#### Specifying Distributional Assumptions + +Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own distribution (including hyperparameters like spread and shape), which could be specified by adding additional columns to the data frame with observed values. Designs for interfaces for use cases that are somewhere between these two extremes seem less obvious. + +#### Specifying Parameters to Fit + +There are two kinds of parameters to fit. + +* Existing quantities to be fitted (e.g. `beta`, initial number of susceptible individuals `S`). +* Creating new quantities to be fitted (e.g. distributional scale parameters declared along with (#distributional-assumptions). + +and the scale (e.g. log, logit) on which to fit these parameters. The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model). + ### Time-Varying Parameters From 1b58c1b2cff2bd818a0c180a5a976606dcdbc75d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 5 Dec 2023 15:46:02 -0500 Subject: [PATCH 130/332] readme update --- README.md | 13 ++++++------- misc/build/README.Rmd | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9cf82782..996f1e03 100644 --- a/README.md +++ b/README.md @@ -189,13 +189,12 @@ series to compare with model simulations. What form should this data frame take? One option is the same format as the output of `mp_report`. This would -have several benefits. \* Consistency with input and output formats, -making it a little easier to learn. \* Easy to manipulate output into -input for testing calibration functionality. \* Possibly simpler -argument list to `mp_calibrate` because we would just relate the -observed data to simulated data with the same name, of course we would -still need an interface for distributional assumptions. \* Naturally -handles missing values +have several benefits. + + * Consistency with input and output formats, making it a little easier to learn. + * Easy to manipulate output into input for testing calibration functionality. + * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. + * Naturally handles missing values The main disadvantage of this is that format could differ from the indexed vectors discussed below. diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index e99df494..8f03d28a 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -113,6 +113,7 @@ We will build a function, `mp_calibrate`, which takes (1) an object for simulati A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this data frame take? One option is the same format as the output of `mp_report`. This would have several benefits. + * Consistency with input and output formats, making it a little easier to learn. * Easy to manipulate output into input for testing calibration functionality. * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. From 42482010d7ac72b7bfd1e63394364ec685897fa1 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 5 Dec 2023 15:49:37 -0500 Subject: [PATCH 131/332] readme update --- README.md | 13 +++++++++---- misc/build/README.Rmd | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 996f1e03..0cc52456 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,15 @@ frame take? One option is the same format as the output of `mp_report`. This would have several benefits. - * Consistency with input and output formats, making it a little easier to learn. - * Easy to manipulate output into input for testing calibration functionality. - * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. - * Naturally handles missing values +- Consistency with input and output formats, making it a little easier + to learn. +- Easy to manipulate output into input for testing calibration + functionality. +- Possibly simpler argument list to `mp_calibrate` because we would + just relate the observed data to simulated data with the same name, + of course we would still need an interface for distributional + assumptions. +- Naturally handles missing values The main disadvantage of this is that format could differ from the indexed vectors discussed below. diff --git a/misc/build/README.Rmd b/misc/build/README.Rmd index 8f03d28a..6fc59183 100644 --- a/misc/build/README.Rmd +++ b/misc/build/README.Rmd @@ -114,10 +114,10 @@ A data frame (or data frames) containing observed (possibly uneven) time series One option is the same format as the output of `mp_report`. This would have several benefits. - * Consistency with input and output formats, making it a little easier to learn. - * Easy to manipulate output into input for testing calibration functionality. - * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. - * Naturally handles missing values +* Consistency with input and output formats, making it a little easier to learn. +* Easy to manipulate output into input for testing calibration functionality. +* Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. +* Naturally handles missing values The main disadvantage of this is that format could differ from the indexed vectors discussed below. From 468477dc077acdcc6922fb3e12c6edd2467c7991 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 6 Dec 2023 12:53:34 -0500 Subject: [PATCH 132/332] readme update --- README.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0cc52456..432546c9 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ the following hello-world SIR model. ) sir_sims = simulator$report() +## Architecture + +![](misc/diagrams/engine-dsl-separation.svg) + ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks @@ -182,6 +186,11 @@ be another object for simulating model trajectories that contains new default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. +A big question with calibration is do we want there to be an +engine-agnostic DSL layer, or do we just want it to make sense for +engines where it makes sense? I think the latter, because otherwise we +are making things difficult. + #### Specifying Data to Fit A data frame (or data frames) containing observed (possibly uneven) time @@ -210,10 +219,10 @@ Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own -distribution (including hyperparameters like spread and shape), which -could be specified by adding additional columns to the data frame with -observed values. Designs for interfaces for use cases that are somewhere -between these two extremes seem less obvious. +distribution (including distributional parameters like spread and +shape), which could be specified by adding additional columns to the +data frame with observed values. Designs for interfaces for use cases +that are somewhere between these two extremes seem less obvious. #### Specifying Parameters to Fit @@ -222,12 +231,16 @@ There are two kinds of parameters to fit. - Existing quantities to be fitted (e.g. `beta`, initial number of susceptible individuals `S`). - Creating new quantities to be fitted (e.g. distributional scale - parameters declared along with (#distributional-assumptions). + parameters declared along with [distributional + asumptions](#specifying-distributional-assumptions). + +The scale (e.g. log, logit) on which to fit these parameters must also +be specified. -and the scale (e.g. log, logit) on which to fit these parameters. The -new distributional parameters should go into a new indexed vector called -something like `distributional_parameters`. (TODO: more general name for -new parameters that are part of the observation model). +The new distributional parameters should go into a new indexed vector +called something like `distributional_parameters`. (TODO: more general +name for new parameters that are part of the observation model, +e.g. convolution kernel parameters). ### Time-Varying Parameters From ed7f952386ec31de62058bd426a459b3b57b39a7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 6 Dec 2023 12:54:20 -0500 Subject: [PATCH 133/332] architecture --- misc/diagrams/engine-dsl-separation.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 misc/diagrams/engine-dsl-separation.svg diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg new file mode 100644 index 00000000..2180a0ca --- /dev/null +++ b/misc/diagrams/engine-dsl-separation.svg @@ -0,0 +1,3 @@ + + +

    Legend

    Legend
    Engine-Agnostic Domain
    Specific Language
    Engine-Agnostic Domain...
    numerical objects
    numerical objects
    model structure
    (e.g. indexes, ledgers)
    model structure...
    objects that specify
    dynamics
    objects that specify...
    Engine
    (e.g. TMB, AdaptiveTau)
    Engine...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. graphs, forecasts)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required Input/Output:
    Required Input/Output:
    Optional Input:
    Optional Input:
    Text is not SVG - cannot display
    \ No newline at end of file From 63b5f556406611e39426c9a0fc25042e1e20c811 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 6 Dec 2023 12:55:28 -0500 Subject: [PATCH 134/332] architecture --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 2180a0ca..9b9c0e80 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -

    Legend

    Legend
    Engine-Agnostic Domain
    Specific Language
    Engine-Agnostic Domain...
    numerical objects
    numerical objects
    model structure
    (e.g. indexes, ledgers)
    model structure...
    objects that specify
    dynamics
    objects that specify...
    Engine
    (e.g. TMB, AdaptiveTau)
    Engine...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. graphs, forecasts)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required Input/Output:
    Required Input/Output:
    Optional Input:
    Optional Input:
    Text is not SVG - cannot display
    \ No newline at end of file +

    Legend

    Legend
    Engine-Agnostic Domain
    Specific Language
    Engine-Agnostic Domain...
    numerical objects
    numerical objects
    model structure
    (e.g. indexes, ledgers)
    model structure...
    objects that specify
    dynamics
    objects that specify...
    Engine
    (e.g. TMB, AdaptiveTau)
    Engine...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. graphs, forecasts)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file From 02e02ad5de93859218f3b250d96946b635d50c1f Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 6 Dec 2023 13:31:22 -0500 Subject: [PATCH 135/332] diagrams --- README.md | 41 +++++++++++++++++++++++-- misc/diagrams/engine-dsl-separation.svg | 2 +- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 432546c9..991c11a8 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,41 @@ the following hello-world SIR model. ## Architecture +Modularity is a key principle of `macpan2` design in a few ways. + +First, `macpan2` is meant to plug into standard R workflows for data +pre-processing and simulation post-processing. There is very little +functionality in `macpan2` for configuring how data are prepared as +input and out simulation outputs are processed. Instead, `macpan2` +accepts standard data objects (data frames, matrices, vectors) and +returns simulations as long-format data frames that can be processed +using standard tools like `dplyr` and `ggplot2`. This design principle +is illustrated in the architecture diagram below that has two outer +layers representing standard non-`macpan2` workflows that contain two +inner layers representing workflows that depend on `macpan2` data +structures and objects. The challenges of building the inner layers is +big enough that we prefer to avoid reinventing the wheel of pre- and +post-processing. + +Second, `macpan2` uses an engine plug-in architecture. The third layer +in the diagram below represents an engine that can be swapped out if +necessary. Loosely speaking an engine is wrapping around an existing +modelling tool that allows it to be controlled by our structured +compartmental modelling grammar/language, which is represented by the +second layer in the diagram. Currently we only have a single engine, +which is a wrapping around the TMB package. We are currently considering +building upon AdaptiveTau, which can be used for Gillespie simulation. + +Third, each of the middle `macpan2` layers can be used on their own. For +example, the TMB engine is quite powerful and flexible and can be used +to quickly [specify dynamic model +simulators](#general-dynamic-simulation-with-tmb) and calibrators that +are executed in C++, without needing to write in C++. This approach +would bypass the second structured modelling layer. Conversely, one +could use the structured modelling layer to build descriptions of +structured models and data without using them to interface with an +engine. + ![](misc/diagrams/engine-dsl-separation.svg) ## Product Management @@ -96,10 +131,10 @@ the details of bugs, tasks, and feature development. But the following narrative will provide context on product development themes, their current state, and plans for improvement and implementation. -### General Dynamic Simulation +### General Dynamic Simulation with TMB One can define a generic set of update steps that are iterated to -produce a dynamic simulation. +produce a dynamic simulation model in TMB. library(macpan2) si = mp_dynamic_model( @@ -149,7 +184,7 @@ many modellers where they are, which is with the ability to write down a set of transitions/state updates. But it is not convenient if you would just like to simulate from it, -which is what the model library is for. +which is what the [model library](#model-library) is for. ### Model Library diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 9b9c0e80..e2a2d832 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -

    Legend

    Legend
    Engine-Agnostic Domain
    Specific Language
    Engine-Agnostic Domain...
    numerical objects
    numerical objects
    model structure
    (e.g. indexes, ledgers)
    model structure...
    objects that specify
    dynamics
    objects that specify...
    Engine
    (e.g. TMB, AdaptiveTau)
    Engine...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. graphs, forecasts)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file +

    Legend

    Legend
    Structured Compartmental
    Modelling -- Inside macapn2
    Structured Compartmental...
    numerical objects
    numerical objects
    model structure
    (e.g. indexes, ledgers)
    model structure...
    objects that specify
    dynamics
    objects that specify...
    Engine -- macpan2 wrapped
    (e.g. TMB, AdaptiveTau)
    Engine -- macpan2 wrapped...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. ggplot2 graphics)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file From 8b3af074b0dff0c887dec895d64ea652b51bf8ee Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 6 Dec 2023 16:25:24 -0500 Subject: [PATCH 136/332] arch --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index e2a2d832..8db1eada 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -

    Legend

    Legend
    Structured Compartmental
    Modelling -- Inside macapn2
    Structured Compartmental...
    numerical objects
    numerical objects
    model structure
    (e.g. indexes, ledgers)
    model structure...
    objects that specify
    dynamics
    objects that specify...
    Engine -- macpan2 wrapped
    (e.g. TMB, AdaptiveTau)
    Engine -- macpan2 wrapped...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. ggplot2 graphics)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file +

    Legend

    Legend
    Structured Compartmental
    Modelling -- Inside macapn2
    Structured Compartmental...
    numerical objects
    numerical objects
    rules for repeating model calculations in different strata
    rules for repeating...
    model calculations
    model calculations
    Engine -- macpan2 wrapped
    (e.g. TMB, AdaptiveTau)
    Engine -- macpan2 wrapped...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. ggplot2 graphics)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file From e0c64975a0f2fa17b9604e9f15ff01c263a5723e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 6 Dec 2023 16:31:28 -0500 Subject: [PATCH 137/332] readme --- README.md | 16 ++++++++-------- misc/diagrams/engine-dsl-separation.svg | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 991c11a8..48a1168e 100644 --- a/README.md +++ b/README.md @@ -96,21 +96,21 @@ input and out simulation outputs are processed. Instead, `macpan2` accepts standard data objects (data frames, matrices, vectors) and returns simulations as long-format data frames that can be processed using standard tools like `dplyr` and `ggplot2`. This design principle -is illustrated in the architecture diagram below that has two outer +is illustrated in the architecture diagram below that has two blue outer layers representing standard non-`macpan2` workflows that contain two -inner layers representing workflows that depend on `macpan2` data +red inner layers representing workflows that depend on `macpan2` data structures and objects. The challenges of building the inner layers is big enough that we prefer to avoid reinventing the wheel of pre- and post-processing. Second, `macpan2` uses an engine plug-in architecture. The third layer in the diagram below represents an engine that can be swapped out if -necessary. Loosely speaking an engine is wrapping around an existing -modelling tool that allows it to be controlled by our structured -compartmental modelling grammar/language, which is represented by the -second layer in the diagram. Currently we only have a single engine, -which is a wrapping around the TMB package. We are currently considering -building upon AdaptiveTau, which can be used for Gillespie simulation. +necessary. An engine is a wrapper around an existing modelling tool that +allows it to be controlled by our structured compartmental modelling +grammar/language, which is represented by the second layer in the +diagram. Currently we only have a single engine, which is a wrapping +around the TMB package. We are currently considering building upon +AdaptiveTau, which can be used for Gillespie simulation. Third, each of the middle `macpan2` layers can be used on their own. For example, the TMB engine is quite powerful and flexible and can be used diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 8db1eada..ce467e42 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -

    Legend

    Legend
    Structured Compartmental
    Modelling -- Inside macapn2
    Structured Compartmental...
    numerical objects
    numerical objects
    rules for repeating model calculations in different strata
    rules for repeating...
    model calculations
    model calculations
    Engine -- macpan2 wrapped
    (e.g. TMB, AdaptiveTau)
    Engine -- macpan2 wrapped...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. ggplot2 graphics)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file +

    Legend

    Legend
    Modelling Language
    Inside macapn2
    Modelling Language...
    numerical objects
    numerical objects
    rules for repeating model calculations in different strata
    rules for repeating...
    model calculations
    model calculations
    Engine
    Inside macpan2
    Engine...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. ggplot2 graphics)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file From 6655aa0845e1cf5f0e594927b934289f9a6851ee Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 7 Dec 2023 12:00:57 -0500 Subject: [PATCH 138/332] readme --- README.md | 36 ++++++++++++++++++++++--- misc/diagrams/engine-dsl-separation.svg | 2 +- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 48a1168e..d97e0953 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,37 @@ the following hello-world SIR model. ## Architecture +The high-level design of `macpan2` is given in the following diagram, +which we describe immediately below. + +![](misc/diagrams/engine-dsl-separation.svg) + +### Flow of Information + +Information flows from the top to the bottom of the diagram. Note that +[users are not required to follow this entire +path](#architectural-layers-of-modularity), but we will start with this +assumption to describe the full vision. The major steps of data +transformation are numbered in the diagram, and we describe each of +these in what follows. + +1. The flow begins with accessing and preparing numerical information + from various data sources, with the output being numerical R + objects. Depending on the nature of the analysis to follow, this + information could include default parameter values + (e.g. transmission rate), initial values for the state variables + (e.g. initial number of infectious individuals), operational + schedules (e.g. timing of lockdown events or vaccine roll-out + schedules), and data for model fitting (e.g. time series of hospital + utilization). This step could involve connecting to real-time + surveillance platforms or reading in static data files. There is not + any functionality within `macpan2` for conducting this step – + `macpan2` [does not try to reinvent the wheel in data access and + preparation](#architectural-layers-of-modularity). +2. + +### Architectural Layers of Modularity + Modularity is a key principle of `macpan2` design in a few ways. First, `macpan2` is meant to plug into standard R workflows for data @@ -122,8 +153,6 @@ could use the structured modelling layer to build descriptions of structured models and data without using them to interface with an engine. -![](misc/diagrams/engine-dsl-separation.svg) - ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks @@ -224,7 +253,8 @@ resulting from parameter estimation uncertainty. A big question with calibration is do we want there to be an engine-agnostic DSL layer, or do we just want it to make sense for engines where it makes sense? I think the latter, because otherwise we -are making things difficult. +are making things difficult. We can try to be wise making reusable +calibration machinery across engines if it comes to that. #### Specifying Data to Fit diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index ce467e42..31525af8 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -

    Legend

    Legend
    Modelling Language
    Inside macapn2
    Modelling Language...
    numerical objects
    numerical objects
    rules for repeating model calculations in different strata
    rules for repeating...
    model calculations
    model calculations
    Engine
    Inside macpan2
    Engine...
    engine-specific
    inputs
    (e.g. ExprList)
    engine-specific...
    engine outputs
    (e.g. simulations)
    engine outputs...
    engine output generators
    (e.g. TMB simulator)
    engine output genera...
    engine adaptor
    engine adaptor
    Post-Processing
    Outside macpan2
    Post-Processing...
    results
    (e.g. ggplot2 graphics)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    prepped data and workflow outputs
    prepped data and wor...
    data sources and configuration
    data sources and con...
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Text is not SVG - cannot display
    \ No newline at end of file +
    Modelling Language
    Inside macapn2
    Modelling Language...
    rules for repeating model calculations in different strata
    rules for repeating model ca...
    engine-agnostic model calculations
    engine-agnostic mode...
    2a
    2a
    model library item
    model library item
    2b
    2b
    Engine (one of many)
    Inside macpan2
    Engine (one of many)...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model calculations
    engine-specific...
    2d
    2d
    2c
    2c
    Post-Processing
    Outside macpan2
    Post-Processing...
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    1
    1
    data sources
    data sources
    numerical
    information in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical...

    Legend

    Legend
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Outside macpan2:
    Outside macpan2:
    Inside macpan2:
    Inside macpan2:
    Text is not SVG - cannot display
    \ No newline at end of file From 944b28f57581abb50f07b86ba8f396b32fea3369 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 7 Dec 2023 22:22:41 -0500 Subject: [PATCH 139/332] readme --- README.md | 21 +++++++++++++++++++++ misc/diagrams/engine-dsl-separation.svg | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d97e0953..df3e7218 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,27 @@ could use the structured modelling layer to build descriptions of structured models and data without using them to interface with an engine. +- Transition + - Purpose: Defining state transitions + - Arguments: + - `from`: + - `rate`: Expression defining the rate of flow of individuals + from the `from` box to the `to` box + - Sub-Types: + - Absolute: Default + - Per Capita: Specify transition rates proportional to the + from box + - +- State Update +- Raw + - Purpose: Passing engine-specific calculations with + engine-agnostic calculations + - Arguments: + - Named list of (optionally named) lists + - Outer names give the name of the engine + - Inner names are used by the specific engine + - + ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 31525af8..e4e38465 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -
    Modelling Language
    Inside macapn2
    Modelling Language...
    rules for repeating model calculations in different strata
    rules for repeating model ca...
    engine-agnostic model calculations
    engine-agnostic mode...
    2a
    2a
    model library item
    model library item
    2b
    2b
    Engine (one of many)
    Inside macpan2
    Engine (one of many)...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model calculations
    engine-specific...
    2d
    2d
    2c
    2c
    Post-Processing
    Outside macpan2
    Post-Processing...
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Pre-Processing
    Outside macapn2
    Pre-Processing...
    1
    1
    data sources
    data sources
    numerical
    information in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical...

    Legend

    Legend
    Function family:
    Function family:
    Object family:
    Object family:
    Required input/output:
    Required input/output:
    Optional input:
    Optional input:
    Outside macpan2:
    Outside macpan2:
    Inside macpan2:
    Inside macpan2:
    Text is not SVG - cannot display
    \ No newline at end of file +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and
    Calibration Engine
    Simulation and...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    inside macpan2
    inside macpan2
    outside macpan2
    outside macpan2
    outside macpan2
    outside macpan2

    Legend

    Legend
    Function family:
    Function...
    Object family:
    Object fa...
    Text is not SVG - cannot display
    \ No newline at end of file From 497fa8827a37f0cd979556705a50640804993ca7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 7 Dec 2023 22:24:29 -0500 Subject: [PATCH 140/332] readme --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index e4e38465..5eee2854 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and
    Calibration Engine
    Simulation and...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    inside macpan2
    inside macpan2
    outside macpan2
    outside macpan2
    outside macpan2
    outside macpan2

    Legend

    Legend
    Function family:
    Function...
    Object family:
    Object fa...
    Text is not SVG - cannot display
    \ No newline at end of file +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and
    Calibration Engine
    Simulation and...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    inside macpan2
    inside macpan2
    outside macpan2
    outside macpan2
    outside macpan2
    outside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From c06763df9a7fa7604c14b67417d43c8859659572 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 09:02:21 -0500 Subject: [PATCH 141/332] diagram format --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 5eee2854..016a48bc 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and
    Calibration Engine
    Simulation and...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    inside macpan2
    inside macpan2
    outside macpan2
    outside macpan2
    outside macpan2
    outside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and
    Calibration Engine
    Simulation and...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From 844f078ad45a92910802bcc1d6403bd937fdab96 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:10:04 -0500 Subject: [PATCH 142/332] auto-commit --- README.md | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index df3e7218..bdbc58ef 100644 --- a/README.md +++ b/README.md @@ -153,26 +153,12 @@ could use the structured modelling layer to build descriptions of structured models and data without using them to interface with an engine. -- Transition - - Purpose: Defining state transitions - - Arguments: - - `from`: - - `rate`: Expression defining the rate of flow of individuals - from the `from` box to the `to` box - - Sub-Types: - - Absolute: Default - - Per Capita: Specify transition rates proportional to the - from box - - -- State Update -- Raw - - Purpose: Passing engine-specific calculations with - engine-agnostic calculations - - Arguments: - - Named list of (optionally named) lists - - Outer names give the name of the engine - - Inner names are used by the specific engine - - +### Engine-Agnostic Model Specifications + +Here we zoom into the 2c family of functions for specifying models in an +engine-agnostic fashion. + +![](misc/diagrams/engine-agnostic-model-specification.svg) ## Product Management From 3b8eb1e7e5f2f8e334cc9e0d61fe3bc19b74a6ff Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:10:24 -0500 Subject: [PATCH 143/332] auto-commit --- misc/diagrams/engine-agnostic-model-specification.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 misc/diagrams/engine-agnostic-model-specification.svg diff --git a/misc/diagrams/engine-agnostic-model-specification.svg b/misc/diagrams/engine-agnostic-model-specification.svg new file mode 100644 index 00000000..0761485a --- /dev/null +++ b/misc/diagrams/engine-agnostic-model-specification.svg @@ -0,0 +1 @@ +
    Pre-Processing
    Pre-Processing
    numerical information in standard formats
    numerical information in s...
    Modelling Language
    Modelling Language
    2
    2
    model calculation specification
    model calculation sp...
    3
    3
    indexed vectors
    indexed vectors
    indexes
    indexes
    1
    1
    engine object
    engine object
    engine-agnostic model object
    engine-agnostic mode...
    4
    4
    Simulation and
    Calibration Engine
    Simulation and...
    engine-specific
    model object
    engine-specific...
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2
    Text is not SVG - cannot display
    \ No newline at end of file From 82ab6f977e80e44b6bf7f72b84efecb3d6242b88 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:47:29 -0500 Subject: [PATCH 144/332] auto-commit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bdbc58ef..e01c3e73 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,10 @@ engine-agnostic fashion. ![](misc/diagrams/engine-agnostic-model-specification.svg) +### Calibrating Models in the TMB Engine + +![](misc/diagrams/tmb-calibration.svg) + ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks From f2a22ab3fd744a8b4af5fb1442139e5c86f4503e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:47:31 -0500 Subject: [PATCH 145/332] auto-commit --- misc/diagrams/tmb-calibration.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 misc/diagrams/tmb-calibration.svg diff --git a/misc/diagrams/tmb-calibration.svg b/misc/diagrams/tmb-calibration.svg new file mode 100644 index 00000000..0c587487 --- /dev/null +++ b/misc/diagrams/tmb-calibration.svg @@ -0,0 +1 @@ +
    Pre-Processing
    Pre-Processing
    time series to compare with model simulations
    time series to compa...
    TMB Simulation and
    Calibration Engine
    TMB Simulation and...
    initial model object
    initial model o...
    distributional assumptions and parameters to be fitted
    distributional assumpti...
    calibrated model object
    calibrated mode...
    1
    1
    model with
    objective function
    model with...
    2
    2
    optional optimizer settings
    optional optimizer sett...
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From e7c45903090a9005021bc494cd85a61c3d9f1847 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:48:20 -0500 Subject: [PATCH 146/332] auto-commit --- misc/diagrams/engine-agnostic-model-specification.svg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/misc/diagrams/engine-agnostic-model-specification.svg b/misc/diagrams/engine-agnostic-model-specification.svg index 0761485a..69c75d89 100644 --- a/misc/diagrams/engine-agnostic-model-specification.svg +++ b/misc/diagrams/engine-agnostic-model-specification.svg @@ -1 +1,3 @@ -
    Pre-Processing
    Pre-Processing
    numerical information in standard formats
    numerical information in s...
    Modelling Language
    Modelling Language
    2
    2
    model calculation specification
    model calculation sp...
    3
    3
    indexed vectors
    indexed vectors
    indexes
    indexes
    1
    1
    engine object
    engine object
    engine-agnostic model object
    engine-agnostic mode...
    4
    4
    Simulation and
    Calibration Engine
    Simulation and...
    engine-specific
    model object
    engine-specific...
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2
    Text is not SVG - cannot display
    \ No newline at end of file + + +
    Pre-Processing
    Pre-Processing
    numerical information in standard formats
    numerical information in s...
    Modelling Language
    Modelling Language
    2
    2
    model calculation specification
    model calculation sp...
    3
    3
    indexed vectors
    indexed vectors
    indexes
    indexes
    1
    1
    engine object
    engine object
    engine-agnostic model object
    engine-agnostic mode...
    4
    4
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    engine-specific
    model object
    engine-specific...
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From 6d977886e2d04a786a2cb11450bba4ec824b69aa Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:48:21 -0500 Subject: [PATCH 147/332] auto-commit --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 016a48bc..9a7ecbd7 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and
    Calibration Engine
    Simulation and...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From 36dbef595169b6eb93c6570b339d9670f7f25863 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 11:54:37 -0500 Subject: [PATCH 148/332] auto-commit --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 9a7ecbd7..0c7ecf43 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From 9aa0dd37d7508a7b876f849cd68a3d7aa1c363ed Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 13:16:38 -0500 Subject: [PATCH 149/332] auto-commit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e01c3e73..aeb0a5dc 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ engine-agnostic fashion. ![](misc/diagrams/tmb-calibration.svg) +### Model Library + +![](misc/diagrams/model-library.svg) + ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks From 8f804b32a0ade48ce76885a7f84ddc580e40dfd3 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 13:16:47 -0500 Subject: [PATCH 150/332] auto-commit --- misc/diagrams/model-library.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 misc/diagrams/model-library.svg diff --git a/misc/diagrams/model-library.svg b/misc/diagrams/model-library.svg new file mode 100644 index 00000000..f701c878 --- /dev/null +++ b/misc/diagrams/model-library.svg @@ -0,0 +1,3 @@ + + +
    Pre-Processing
    Pre-Processing
    optional numerical information
    to override model defaults
    optional numerical information...
    Modelling Language
    Modelling Langu...
    specific path in the
     model library
    specific path in the...
    optional calculations to override model defaults
    optional calculations to...
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model object
    model object
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From ae45aab22c7096f12ae2f81a23c36bc1cc9bb41c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 13:41:45 -0500 Subject: [PATCH 151/332] auto-commit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index aeb0a5dc..b376c535 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ engine-agnostic fashion. ![](misc/diagrams/tmb-calibration.svg) +### Specification of Models Directly in the TMB Engine + +![](misc/diagrams/tmb-model-specification.svg) + ### Model Library ![](misc/diagrams/model-library.svg) From fd57e673df814d0fab48c1fc22ebda35dc9d2c05 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 13:41:48 -0500 Subject: [PATCH 152/332] auto-commit --- misc/diagrams/tmb-model-specification.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 misc/diagrams/tmb-model-specification.svg diff --git a/misc/diagrams/tmb-model-specification.svg b/misc/diagrams/tmb-model-specification.svg new file mode 100644 index 00000000..fff42206 --- /dev/null +++ b/misc/diagrams/tmb-model-specification.svg @@ -0,0 +1 @@ +
    Pre-Processing
    Pre-Processing
    numerical information
    in standard formats
    numerical information...
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model object
    model object
    expression list
    expression list
    2
    2
    1
    1
    initial numeric matrix and
    integer vector lists
    initial numeric matrix and...
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From 5fa1d38dfeb0b392c74a5b29613111b7a4f1e41a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 13:50:29 -0500 Subject: [PATCH 153/332] auto-commit --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b376c535..76bc24c3 100644 --- a/README.md +++ b/README.md @@ -153,24 +153,29 @@ could use the structured modelling layer to build descriptions of structured models and data without using them to interface with an engine. -### Engine-Agnostic Model Specifications +### Methods for Producing a Model Object + +Here we zoom into parts of the architectural diagram to illustrate the +(currently four) ways to produce a model object. + +#### (2a) Model Library + +![](misc/diagrams/model-library.svg) + +#### (2b) Engine-Agnostic Model Specifications Here we zoom into the 2c family of functions for specifying models in an engine-agnostic fashion. ![](misc/diagrams/engine-agnostic-model-specification.svg) -### Calibrating Models in the TMB Engine - -![](misc/diagrams/tmb-calibration.svg) - -### Specification of Models Directly in the TMB Engine +#### (2c) Specification of Models Directly in the TMB Engine ![](misc/diagrams/tmb-model-specification.svg) -### Model Library +#### (2d) Calibrating Models in the TMB Engine -![](misc/diagrams/model-library.svg) +![](misc/diagrams/tmb-calibration.svg) ## Product Management From 353c6d78237d5fa32faec3cc570d902511e89531 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 13:51:51 -0500 Subject: [PATCH 154/332] auto-commit --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 76bc24c3..955c92d1 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,6 @@ Here we zoom into parts of the architectural diagram to illustrate the #### (2b) Engine-Agnostic Model Specifications -Here we zoom into the 2c family of functions for specifying models in an -engine-agnostic fashion. - ![](misc/diagrams/engine-agnostic-model-specification.svg) #### (2c) Specification of Models Directly in the TMB Engine From ba80bc9212edc9bd1a89aeb14134b83040f8b7bf Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:10:23 -0500 Subject: [PATCH 155/332] auto-commit --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 955c92d1..927dffba 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,54 @@ Here we zoom into parts of the architectural diagram to illustrate the ![](misc/diagrams/tmb-model-specification.svg) + si = TMBModel( + expr_list = ExprList( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection + ) + ) + , init_mats = MatsList( + S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix + , .mats_to_return = "I", .mats_to_save = "I" + ) + , time_steps = Time(10L) + )$simulator() + print(si) + + ## Classes 'TMBSimulator', 'TMBSimulationFormatter', 'Base' + ## ad_fun : function () + ## cov.fixed : function () + ## error_code : function (...) + ## gradient : function (...) + ## hessian : function (...) + ## matrix : function (..., matrix_name, time_step, .phases = "during") + ## matrix_names : function () + ## objective : function (...) + ## par.fixed : function () + ## report : function (..., .phases = "during") + ## report_ensemble : function (..., .phases = "during", .n = 100, .probs = c(0.025, 0.5, 0.975)) + ## report_values : function (..., .phases = "during") + ## sdreport : function () + ## simulate : function (..., .phases = "during") + +Simulating from this model can be done like so. + + si$report() + + ## matrix time row col value + ## 2 I 1 0 0 1.247500 + ## 3 I 2 0 0 1.555484 + ## 4 I 3 0 0 1.938307 + ## 5 I 4 0 0 2.413491 + ## 6 I 5 0 0 3.002301 + ## 7 I 6 0 0 3.730342 + ## 8 I 7 0 0 4.628139 + ## 9 I 8 0 0 5.731624 + ## 10 I 9 0 0 7.082401 + ## 11 I 10 0 0 8.727601 + #### (2d) Calibrating Models in the TMB Engine ![](misc/diagrams/tmb-calibration.svg) From dfdd010c1e6e96045c49634e45af2728375d8d29 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:11:53 -0500 Subject: [PATCH 156/332] readme --- README.md | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 927dffba..933f3abe 100644 --- a/README.md +++ b/README.md @@ -183,28 +183,20 @@ Here we zoom into parts of the architectural diagram to illustrate the , .mats_to_return = "I", .mats_to_save = "I" ) , time_steps = Time(10L) - )$simulator() + ) print(si) - ## Classes 'TMBSimulator', 'TMBSimulationFormatter', 'Base' - ## ad_fun : function () - ## cov.fixed : function () - ## error_code : function (...) - ## gradient : function (...) - ## hessian : function (...) - ## matrix : function (..., matrix_name, time_step, .phases = "during") - ## matrix_names : function () - ## objective : function (...) - ## par.fixed : function () - ## report : function (..., .phases = "during") - ## report_ensemble : function (..., .phases = "during", .n = 100, .probs = c(0.025, 0.5, 0.975)) - ## report_values : function (..., .phases = "during") - ## sdreport : function () - ## simulate : function (..., .phases = "during") + ## Classes 'TMBModel', 'Base' + ## ad_fun : function (tmb_cpp = getOption("macpan2_dll")) + ## data_arg : function () + ## make_ad_fun_arg : function (tmb_cpp = getOption("macpan2_dll")) + ## param_arg : function () + ## random_arg : function () + ## simulator : function (tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE) Simulating from this model can be done like so. - si$report() + si$simulator()$report() ## matrix time row col value ## 2 I 1 0 0 1.247500 From c27572612e7a6c4b49d4b1a80e288e018ddc29f8 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:12:30 -0500 Subject: [PATCH 157/332] auto-commit --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 933f3abe..43d2be41 100644 --- a/README.md +++ b/README.md @@ -184,15 +184,14 @@ Here we zoom into parts of the architectural diagram to illustrate the ) , time_steps = Time(10L) ) - print(si) + print(si$expr_list) - ## Classes 'TMBModel', 'Base' - ## ad_fun : function (tmb_cpp = getOption("macpan2_dll")) - ## data_arg : function () - ## make_ad_fun_arg : function (tmb_cpp = getOption("macpan2_dll")) - ## param_arg : function () - ## random_arg : function () - ## simulator : function (tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE) + ## --------------------- + ## At every iteration of the simulation loop (t = 1 to T): + ## --------------------- + ## 1: infection ~ beta * S * I/N + ## 2: S ~ S - infection + ## 3: I ~ I + infection Simulating from this model can be done like so. From 7fcdcb694ca3036ca04c5b4c980f16bcdc933218 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:14:50 -0500 Subject: [PATCH 158/332] auto-commit --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 43d2be41..24f6db53 100644 --- a/README.md +++ b/README.md @@ -195,19 +195,13 @@ Here we zoom into parts of the architectural diagram to illustrate the Simulating from this model can be done like so. - si$simulator()$report() + (si$simulator()$report() + |> rename(prevalence = value) + |> ggplot() + + geom_line(aes(time, prevalence)) + ) - ## matrix time row col value - ## 2 I 1 0 0 1.247500 - ## 3 I 2 0 0 1.555484 - ## 4 I 3 0 0 1.938307 - ## 5 I 4 0 0 2.413491 - ## 6 I 5 0 0 3.002301 - ## 7 I 6 0 0 3.730342 - ## 8 I 7 0 0 4.628139 - ## 9 I 8 0 0 5.731624 - ## 10 I 9 0 0 7.082401 - ## 11 I 10 0 0 8.727601 +![](/Users/stevenwalker/Development/macpan2/README_files/figure-markdown_strict/unnamed-chunk-7-1.png) #### (2d) Calibrating Models in the TMB Engine From b04fc4a9b0c2d6b44ec725fc9ce441a1eea71802 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:20:38 -0500 Subject: [PATCH 159/332] auto-commit --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 24f6db53..a411d078 100644 --- a/README.md +++ b/README.md @@ -197,11 +197,10 @@ Simulating from this model can be done like so. (si$simulator()$report() |> rename(prevalence = value) - |> ggplot() - + geom_line(aes(time, prevalence)) + |> ggplot() + geom_line(aes(time, prevalence)) ) -![](/Users/stevenwalker/Development/macpan2/README_files/figure-markdown_strict/unnamed-chunk-7-1.png) +![](/Users/stevenwalker/Development/macpan2/README_files/figure-markdown_strict/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine From d4dbad7e1ad11de08457a2606b4359bd07b35dfc Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:22:53 -0500 Subject: [PATCH 160/332] auto-commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a411d078..b9fd7e2d 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Simulating from this model can be done like so. |> ggplot() + geom_line(aes(time, prevalence)) ) -![](/Users/stevenwalker/Development/macpan2/README_files/figure-markdown_strict/plot-tmb-si-1.png) +![](misc/build/figuresplot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine From 7fff16e508f307a8220b62beb59cf8e87763d95f Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:23:12 -0500 Subject: [PATCH 161/332] auto-commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9fd7e2d..c32bf997 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Simulating from this model can be done like so. |> ggplot() + geom_line(aes(time, prevalence)) ) -![](misc/build/figuresplot-tmb-si-1.png) +![](misc/build/figures/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine From 911f11af5faa480f152fafd13fbd22ef5551ebf5 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 8 Dec 2023 14:29:58 -0500 Subject: [PATCH 162/332] auto-commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c32bf997..a3c882c0 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Simulating from this model can be done like so. |> ggplot() + geom_line(aes(time, prevalence)) ) -![](misc/build/figures/plot-tmb-si-1.png) +![](figures/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine From 8bc9f1fd15e4cda7a94d4276e9864cf8a8c15003 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 11:31:19 -0500 Subject: [PATCH 163/332] readme --- README.md | 2 +- misc/build/figures/plot-tmb-si-1.png | Bin 0 -> 25062 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 misc/build/figures/plot-tmb-si-1.png diff --git a/README.md b/README.md index a3c882c0..67492779 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Simulating from this model can be done like so. |> ggplot() + geom_line(aes(time, prevalence)) ) -![](figures/plot-tmb-si-1.png) +![](misc/build/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine diff --git a/misc/build/figures/plot-tmb-si-1.png b/misc/build/figures/plot-tmb-si-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f54b75eb3da7442cebcdc73f7f97c006c431a4 GIT binary patch literal 25062 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 literal 0 HcmV?d00001 From 3984a402247e5597958f9613ba9f5a7dc8f4e66a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 12:02:27 -0500 Subject: [PATCH 164/332] readme tools --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 67492779..de97abcf 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ the following hello-world SIR model. The high-level design of `macpan2` is given in the following diagram, which we describe immediately below. -![](misc/diagrams/engine-dsl-separation.svg) +![](../diagrams/engine-dsl-separation.svg) ### Flow of Information @@ -160,15 +160,15 @@ Here we zoom into parts of the architectural diagram to illustrate the #### (2a) Model Library -![](misc/diagrams/model-library.svg) +![](../diagrams/model-library.svg) #### (2b) Engine-Agnostic Model Specifications -![](misc/diagrams/engine-agnostic-model-specification.svg) +![](../diagrams/engine-agnostic-model-specification.svg) #### (2c) Specification of Models Directly in the TMB Engine -![](misc/diagrams/tmb-model-specification.svg) +![](../diagrams/tmb-model-specification.svg) si = TMBModel( expr_list = ExprList( @@ -200,11 +200,11 @@ Simulating from this model can be done like so. |> ggplot() + geom_line(aes(time, prevalence)) ) -![](misc/build/plot-tmb-si-1.png) +![](misc/build/figure_test/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine -![](misc/diagrams/tmb-calibration.svg) +![](../diagrams/tmb-calibration.svg) ## Product Management @@ -242,7 +242,7 @@ Simulating from this model takes the following steps. getwd() - ## [1] "/Users/stevenwalker/Development/macpan2" + ## [1] "/Users/stevenwalker/Development/macpan2/misc/build" (si |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") From b59679e135ad4953b0cfa43d8c895e9d8778f71e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 12:49:16 -0500 Subject: [PATCH 165/332] readme reorg --- misc/build/README.Rmd => README.Rmd | 123 ++++++++++++++++++++++++++-- README.md | 18 ++-- 2 files changed, 128 insertions(+), 13 deletions(-) rename misc/build/README.Rmd => README.Rmd (77%) diff --git a/misc/build/README.Rmd b/README.Rmd similarity index 77% rename from misc/build/README.Rmd rename to README.Rmd index 6fc59183..56672eab 100644 --- a/misc/build/README.Rmd +++ b/README.Rmd @@ -1,5 +1,25 @@ +--- +output: github_document +--- + # macpan2 +```{r} +print(getwd()) +``` + +```{r opts, echo = FALSE} +knitr::opts_chunk$set( + fig.path = "misc/build/figures/" +) +``` + +```{r, echo = FALSE, eval = TRUE, message=FALSE, warning=FALSE, error=FALSE} +library(macpan2) +library(ggplot2) +library(dplyr) +``` + [![R-CMD-check](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml) [![test coverage](https://byob.yarr.is/canmod/macpan2/coverage)](https://github.com/canmod/macpan2/actions/workflows/test-coverage.yaml) @@ -53,13 +73,100 @@ simulator = sir$simulators$tmb(time_steps = 100 sir_sims = simulator$report() ``` + +## Architecture + +The high-level design of `macpan2` is given in the following diagram, which we describe immediately below. + +```{r, echo=FALSE} +knitr::include_graphics("misc/diagrams/engine-dsl-separation.svg") +``` + +### Flow of Information + +Information flows from the top to the bottom of the diagram. Note that [users are not required to follow this entire path](#architectural-layers-of-modularity), but we will start with this assumption to describe the full vision. The major steps of data transformation are numbered in the diagram, and we describe each of these in what follows. + +1. The flow begins with accessing and preparing numerical information from various data sources, with the output being numerical R objects. Depending on the nature of the analysis to follow, this information could include default parameter values (e.g. transmission rate), initial values for the state variables (e.g. initial number of infectious individuals), operational schedules (e.g. timing of lockdown events or vaccine roll-out schedules), and data for model fitting (e.g. time series of hospital utilization). This step could involve connecting to real-time surveillance platforms or reading in static data files. There is not any functionality within `macpan2` for conducting this step -- `macpan2` [does not try to reinvent the wheel in data access and preparation](#architectural-layers-of-modularity). +2. + +### Architectural Layers of Modularity + +Modularity is a key principle of `macpan2` design in a few ways. + +First, `macpan2` is meant to plug into standard R workflows for data pre-processing and simulation post-processing. There is very little functionality in `macpan2` for configuring how data are prepared as input and out simulation outputs are processed. Instead, `macpan2` accepts standard data objects (data frames, matrices, vectors) and returns simulations as long-format data frames that can be processed using standard tools like `dplyr` and `ggplot2`. This design principle is illustrated in the architecture diagram below that has two blue outer layers representing standard non-`macpan2` workflows that contain two red inner layers representing workflows that depend on `macpan2` data structures and objects. The challenges of building the inner layers is big enough that we prefer to avoid reinventing the wheel of pre- and post-processing. + +Second, `macpan2` uses an engine plug-in architecture. The third layer in the diagram below represents an engine that can be swapped out if necessary. An engine is a wrapper around an existing modelling tool that allows it to be controlled by our structured compartmental modelling grammar/language, which is represented by the second layer in the diagram. Currently we only have a single engine, which is a wrapping around the TMB package. We are currently considering building upon AdaptiveTau, which can be used for Gillespie simulation. + +Third, each of the middle `macpan2` layers can be used on their own. For example, the TMB engine is quite powerful and flexible and can be used to quickly [specify dynamic model simulators](#general-dynamic-simulation-with-tmb) and calibrators that are executed in C++, without needing to write in C++. This approach would bypass the second structured modelling layer. Conversely, one could use the structured modelling layer to build descriptions of structured models and data without using them to interface with an engine. + +### Methods for Producing a Model Object + +Here we zoom into parts of the architectural diagram to illustrate the (currently four) ways to produce a model object. + +#### (2a) Model Library + +```{r, echo=FALSE} +knitr::include_graphics("misc/diagrams/model-library.svg") +``` + + +#### (2b) Engine-Agnostic Model Specifications + +```{r, echo=FALSE} +knitr::include_graphics("misc/diagrams/engine-agnostic-model-specification.svg") +``` + + +#### (2c) Specification of Models Directly in the TMB Engine + +```{r, echo=FALSE} +knitr::include_graphics("misc/diagrams/tmb-model-specification.svg") +``` + +```{r} +si = TMBModel( + expr_list = ExprList( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection + ) + ) + , init_mats = MatsList( + S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix + , .mats_to_return = "I", .mats_to_save = "I" + ) + , time_steps = Time(10L) +) +print(si$expr_list) +``` + +Simulating from this model can be done like so. +```{r plot-tmb-si} +(si$simulator()$report() + |> rename(prevalence = value) + |> ggplot() + geom_line(aes(time, prevalence)) +) +``` + + +#### (2d) Calibrating Models in the TMB Engine + +```{r, echo=FALSE} +knitr::include_graphics("misc/diagrams/tmb-calibration.svg") +``` + + + + + ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks the details of bugs, tasks, and feature development. But the following narrative will provide context on product development themes, their current state, and plans for improvement and implementation. -### General Dynamic Simulation +### General Dynamic Simulation with TMB -One can define a generic set of update steps that are iterated to produce a dynamic simulation. +One can define a generic set of update steps that are iterated to produce a dynamic simulation model in TMB. ```{r} library(macpan2) @@ -87,7 +194,7 @@ getwd() This part of the package is general, stable, and flexible. It also meets many modellers where they are, which is with the ability to write down a set of transitions/state updates. -But it is not convenient if you would just like to simulate from it, which is what the model library is for. +But it is not convenient if you would just like to simulate from it, which is what the [model library](#model-library) is for. ### Model Library @@ -108,6 +215,8 @@ TODO: We will build a function, `mp_calibrate`, which takes (1) an object for simulating model trajectories and (2) other information for calibrating certain quantities of this model. This second type of information is detailed in the following sections. The output of `mp_calibrate` should be another object for simulating model trajectories that contains new default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. +A big question with calibration is do we want there to be an engine-agnostic DSL layer, or do we just want it to make sense for engines where it makes sense? I think the latter, because otherwise we are making things difficult. We can try to be wise making reusable calibration machinery across engines if it comes to that. + #### Specifying Data to Fit A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this data frame take? @@ -124,16 +233,18 @@ The main disadvantage of this is that format could differ from the indexed vecto #### Specifying Distributional Assumptions -Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own distribution (including hyperparameters like spread and shape), which could be specified by adding additional columns to the data frame with observed values. Designs for interfaces for use cases that are somewhere between these two extremes seem less obvious. +Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own distribution (including distributional parameters like spread and shape), which could be specified by adding additional columns to the data frame with observed values. Designs for interfaces for use cases that are somewhere between these two extremes seem less obvious. #### Specifying Parameters to Fit There are two kinds of parameters to fit. * Existing quantities to be fitted (e.g. `beta`, initial number of susceptible individuals `S`). -* Creating new quantities to be fitted (e.g. distributional scale parameters declared along with (#distributional-assumptions). +* Creating new quantities to be fitted (e.g. distributional scale parameters declared along with [distributional asumptions](#specifying-distributional-assumptions). + +The scale (e.g. log, logit) on which to fit these parameters must also be specified. -and the scale (e.g. log, logit) on which to fit these parameters. The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model). +The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model, e.g. convolution kernel parameters). ### Time-Varying Parameters diff --git a/README.md b/README.md index de97abcf..9f519a4e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ # macpan2 + print(getwd()) + + ## [1] "/Users/stevenwalker/Development/macpan2" + [![R-CMD-check](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml) @@ -90,7 +94,7 @@ the following hello-world SIR model. The high-level design of `macpan2` is given in the following diagram, which we describe immediately below. -![](../diagrams/engine-dsl-separation.svg) +![](misc/diagrams/engine-dsl-separation.svg) ### Flow of Information @@ -160,15 +164,15 @@ Here we zoom into parts of the architectural diagram to illustrate the #### (2a) Model Library -![](../diagrams/model-library.svg) +![](misc/diagrams/model-library.svg) #### (2b) Engine-Agnostic Model Specifications -![](../diagrams/engine-agnostic-model-specification.svg) +![](misc/diagrams/engine-agnostic-model-specification.svg) #### (2c) Specification of Models Directly in the TMB Engine -![](../diagrams/tmb-model-specification.svg) +![](misc/diagrams/tmb-model-specification.svg) si = TMBModel( expr_list = ExprList( @@ -200,11 +204,11 @@ Simulating from this model can be done like so. |> ggplot() + geom_line(aes(time, prevalence)) ) -![](misc/build/figure_test/plot-tmb-si-1.png) +![](misc/build/figures/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine -![](../diagrams/tmb-calibration.svg) +![](misc/diagrams/tmb-calibration.svg) ## Product Management @@ -242,7 +246,7 @@ Simulating from this model takes the following steps. getwd() - ## [1] "/Users/stevenwalker/Development/macpan2/misc/build" + ## [1] "/Users/stevenwalker/Development/macpan2" (si |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") From c834614897804b5c5489039fdd6e57af85c71e08 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 13:08:55 -0500 Subject: [PATCH 166/332] auto-commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f519a4e..3ae4bfd4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # macpan2 print(getwd()) From 9dfb8898d3005498119dcabe372ccc106ea8ee72 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 13:08:56 -0500 Subject: [PATCH 167/332] auto-commit --- misc/diagrams/engine-dsl-separation.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/diagrams/engine-dsl-separation.svg b/misc/diagrams/engine-dsl-separation.svg index 0c7ecf43..f88a9c5c 100644 --- a/misc/diagrams/engine-dsl-separation.svg +++ b/misc/diagrams/engine-dsl-separation.svg @@ -1,3 +1,3 @@ -
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model outputs
    (e.g. simulations)
    model outputs...
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    engine-agnostic model specification
    engine-agnostic mode...
    2a
    2a
    model library
    model library
    2b
    2b
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model outputs
    model outputs
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From c97e24c0e7734297a5cdb1bdee09b5f86b90ddba Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 15:15:59 -0500 Subject: [PATCH 168/332] dsl --- Makefile | 33 ++++++++++++++++++------ NAMESPACE | 1 + R/dsl_expr.R | 16 ++++++++++++ R/engine_defs.R | 11 ++++++++ R/expr_list.R | 15 ++++++++--- R/index.R | 53 ++++++++++++++++++++++++++++++++++---- R/index_utils.R | 56 +++++++++++++++++++++++++++++++++++++++++ R/labelled_partitions.R | 6 ++++- R/model_def_run.R | 1 + R/mp.R | 53 +++++++++++++++++++++++++++++++++++--- R/msg_utils.R | 7 +++--- R/parse_expr.R | 37 +++++++++++++++++++-------- R/tmb_model.R | 5 ++++ man/Clause.Rd | 11 ++++++++ man/mp_cartesian.Rd | 6 ++--- man/mp_index.Rd | 4 ++- man/mp_lookup.Rd | 18 +++++++++++++ 17 files changed, 294 insertions(+), 39 deletions(-) create mode 100644 R/dsl_expr.R create mode 100644 R/engine_defs.R create mode 100644 man/Clause.Rd create mode 100644 man/mp_lookup.Rd diff --git a/Makefile b/Makefile index f3a181ba..0d14dddf 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ ALIAS_RE = [ ]*MP2_\(.*\)\: \(.*\)(\(.*\)) ROXY_RE = ^.*\(\#'.*\)$ VERSION := $(shell sed -n '/^Version: /s///p' DESCRIPTION) TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") - +IMAGES := $(shell grep 'knitr::include_graphics' README.Rmd | sed 's|knitr::include_graphics||' | tr -d '()"' | tr '\n' ' ') +FIGURES := $(shell ls misc/build/figures/*.png | tr '\n' ' ') all: make full-install @@ -71,11 +72,30 @@ coverage.html: R/*.R src/macpan2.cpp tests/testthat/*.R Rscript -e "covr::report(file = \"coverage.html\")" +misc/dev/%.run: misc/dev/%.R + cd misc/dev; Rscript ../../$^ + + +svg:: misc/diagrams/*.svg +misc/diagrams/%.svg: misc/diagrams/%.drawio + draw.io --export $^ --format svg + + +png:: misc/diagrams/*.png +misc/diagrams/%.png: misc/diagrams/%.drawio + draw.io --export $^ --format png + +# TODO: make this work for others -- currently convenience for sw +push-readme: + make README.md + git_push_one_at_a_time.sh $(IMAGES) $(FIGURES) README.md + + readme:: README.md -README.md: misc/build/README.Rmd - Rscript -e "rmarkdown::render('misc/build/README.Rmd', output_dir = '.', output_file = 'README.md', output_format = 'md_document', knit_root_dir = '../..')" +README.md: README.Rmd $(IMAGES) + Rscript -e "rmarkdown::render('README.Rmd', output_dir = '.', output_file = 'README.md', output_format = 'md_document')" echo '' > temp - echo '' | cat - $@ >> temp && mv temp $@ + echo '' | cat - $@ >> temp && mv temp $@ enum-update:: R/enum.R @@ -133,11 +153,10 @@ compile-dev: misc/dev/dev.cpp cd misc/dev; echo "TMB::compile(\"dev.cpp\")" | R --slave -misc/dev/%.run: misc/dev/%.R - cd misc/dev; Rscript ../../$^ - inst/model_library/%/README.md: inst/model_library/%/README.Rmd echo "rmarkdown::render(input = \"$^\", intermediates_dir = NULL)" | R --slave cat $(dir $@)/header.yaml $(dir $@)/README.md > $(dir $@)/tmp.md cp $(dir $@)/tmp.md $(dir $@)/README.md rm $(dir $@)/tmp.md + + diff --git a/NAMESPACE b/NAMESPACE index a76f4030..7d39c3d1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -194,6 +194,7 @@ export(mp_labels) export(mp_ledgers) export(mp_library) export(mp_linear) +export(mp_lookup) export(mp_reference) export(mp_rename) export(mp_report) diff --git a/R/dsl_expr.R b/R/dsl_expr.R new file mode 100644 index 00000000..12afc0d6 --- /dev/null +++ b/R/dsl_expr.R @@ -0,0 +1,16 @@ +#' Compartmental Modelling Language +#' +#' +Clause = function() { + self = Base() + self$render = function(engine) {} + return_object(self, "Calculation") +} + +Transition = function(from, to, rate) { + self = Calculation() + self$from = from + self$to = to + self$rate = rate + return_object(self, "Transition") +} diff --git a/R/engine_defs.R b/R/engine_defs.R new file mode 100644 index 00000000..ffed296c --- /dev/null +++ b/R/engine_defs.R @@ -0,0 +1,11 @@ +Engine = function() { + self = Base() + self$null_simulator = function() stop("abstract method") + return_object(self, "Engine") +} + +TMB = function() { + self = Engine() + self$null_simulator = function() TMBSimulator(TMBModel()) + return_object(self, "TMB") +} diff --git a/R/expr_list.R b/R/expr_list.R index 5f3a62b1..6343f4e2 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -10,6 +10,7 @@ ExprListUtils = function() { .get_formula_side = rhs , .existing_literals = numeric(0L) ) { + ## TODO: handle the null case (self$formula_list() |> lapply(.get_formula_side) |> parse_expr_list( @@ -47,15 +48,21 @@ ExprListUtils = function() { } as.integer(m - 1L) } + self$.init_table = function(parse_table, prefix) { + if (is.null(parse_table)) { + p_table = data.frame(x = integer(), n = integer(), i = integer()) + } else { + p_table = parse_table[c("x", "n", "i")] |> as.list() + } + self$.set_name_prefix(p_table, prefix) + } self$.p_table = function() { l = self$.parsed_expr_list(rhs) - p_table = l$parse_table[c("x", "n", "i")] |> as.list() - self$.set_name_prefix(p_table, "p_table_") + p_table = self$.init_table(l$parse_table, "p_table_") } self$.a_table = function() { l = self$.parsed_expr_list(lhs, self$.expr_literals()) - a_table = l$parse_table[c("x", "n", "i")] |> as.list() - self$.set_name_prefix(a_table, "a_table_") + p_table = self$.init_table(l$parse_table, "a_table_") } self$.expr_literals = function() { self$.parsed_expr_list(rhs)$valid_literals diff --git a/R/index.R b/R/index.R index ba7f9e7a..f4b0138f 100644 --- a/R/index.R +++ b/R/index.R @@ -70,6 +70,8 @@ #' being described. The \code{labelling_column_names} cannot have duplicates #' and must contain at least one name. The index given by the #' \code{labelling_column_names} must uniquely identify each row. +#' The default \code{NULL} gives the set of columns, in order starting with +#' the first column, that are required to uniquely identify each row. #' #' @examples #' state = mp_index( @@ -94,11 +96,14 @@ mp_index = function(..., labelling_column_names) UseMethod("mp_index") #' @param partition A data frame (or data frame-like) object containing #' definitions of the index. This object can be a \code{\link{data.frame}}, #' \code{\link{Partition}}, or another \code{\link{Index}} object. +#' @param vector_name An optional \code{\link{character}} string giving the +#' name of the vector being indexed. #' @param labelling_column_names A \code{\link{character}} vector of the names of the #' index that will be used to label the model components (i.e. rows) being #' described. The \code{labelling_column_names} cannot have duplicates and must #' contain at least one name. The index given by the \code{labelling_column_names} -#' must uniquely identify each row. +#' must uniquely identify each row. The default \code{NULL} uses the minimal +#' number of #' @param reference_index (Advanced) An optional partition to use when #' computing subset indices. #' @param x \code{Index} object. @@ -108,19 +113,30 @@ mp_index = function(..., labelling_column_names) UseMethod("mp_index") #' @noRd #' @keywords internal #' @export -Index = function(partition, labelling_column_names = names(partition), reference_index = NULL) { +Index = function(partition + , vector_name = NULL + , labelling_column_names = NULL + , reference_index = NULL + ) { UseMethod("Index") } #' @export Index.Partition = function(partition - , labelling_column_names = names(partition) + , vector_name = NULL + , labelling_column_names = NULL , reference_index = NULL ) { + + if (is.null(labelling_column_names)) { + labelling_column_names = infer_labelling_columns(partition) + } + self = Base() ## Args self$partition = partition + self$vector_name = vector_name self$labelling_column_names = to_names(labelling_column_names) ## Private Arg @@ -157,12 +173,20 @@ Index.Partition = function(partition } #' @export -Index.data.frame = function(partition, labelling_column_names = names(partition), reference_index = NULL) { +Index.data.frame = function(partition + , vector_name = NULL + , labelling_column_names = NULL + , reference_index = NULL + ) { partition |> Partition() |> Index(labelling_column_names, reference_index) } #' @export -Index.Index = function(partition, labelling_column_names = names(partition), reference_index = NULL) { +Index.Index = function(partition + , vector_name = NULL + , labelling_column_names = NULL + , reference_index = NULL + ) { partition$partition |> Index(labelling_column_names, reference_index) } @@ -228,3 +252,22 @@ mp_catalogue = function(name, ..., labelling_column_names) { } l } + + +# compute the incremental contribution of each column in a partition +# to uniquely identifying each row +info_curve = function(partition) { + nms = names(partition) + get_nrows = function(i) nrow(partition$select(nms[1:i])$frame()) + (nms + |> seq_along() + |> vapply(get_nrows, integer(1L)) + |> setNames(nms) + ) +} + +# return minimal set of columns, in order starting with the first column, +# that are required to uniquely identify each row. +infer_labelling_columns = function(partition) { + names(partition)[seq_len(which.max(info_curve(partition)))] +} diff --git a/R/index_utils.R b/R/index_utils.R index a8757590..52e3d13a 100644 --- a/R/index_utils.R +++ b/R/index_utils.R @@ -79,3 +79,59 @@ make_expr_id = function(expr_id, expr_names) { expr_id$indices[missing_id] = match(expr_id$labels[missing_id], expr_names) return(expr_id$indices) } + +## permutations + +apply_permutations = function(x) { + matrix(x[permutations(length(x))], ncol = length(x)) +} +apply_k_int_permutations = function(n, k) { + (n + |> increasing_int_seq(k) + |> lapply(apply_permutations) + |> do.call(what = rbind) + ) +} +apply_k_incr_int_permutations = function(n, k) { + do.call(rbind, increasing_int_seq(n, k)) +} + +# return a list with all strictly increasing length-m +# sequences of integers between 1 and n +increasing_int_seq = function(n, m, x = list(1:m)) { + l = length(x) + for (i in 0:(m - 1L)) { + if (x[[l]][m - i] != (n - i)) { + x[[l + 1]] = x[[l]] + place = x[[l]][m - i] + 1L + place_indices = (m - i):m + place_length = i + 1L + x[[l + 1]][place_indices] = seq(from = place, length = place_length) + break + } + } + final_sequence = (n - m + 1L):n + last_sequence = x[[l + 1L]] + if (identical(last_sequence, final_sequence)) { + return(x) ## done + } else { + increasing_int_seq(n, m, x) ## recursion to find more sequences + } +} +increasing_int_seq = memoise(increasing_int_seq) + +## https://stackoverflow.com/questions/11095992/generating-all-distinct-permutations-of-a-list-in-r +permutations <- function(n) { + if (n==1){ + return(matrix(1)) + } else { + sp <- permutations(n-1) + p <- nrow(sp) + A <- matrix(nrow = n * p, ncol = n) + for(i in 1:n){ + A[(i-1)*p+1:p,] <- cbind(i,sp+(sp>=i)) + } + return(A) + } +} +permutations = memoise(permutations) diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index e3a15e18..b8a3912a 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -65,7 +65,11 @@ Partition = function(frame) { } self$.filter = function(..., .wrt, .comparison_function, .filter_type) { if (missing(.wrt)) .wrt = self$name() - .wrt = name_set_op(self$name(), .wrt, intersect) + + ## order in .wrt matters. .wrt has to come first or ordering can get + ## mangled by the set intersection operation + .wrt = name_set_op(.wrt, self$name(), intersect) + nms = to_names(.wrt) labels = list_to_labels(...) if (is.null(labels)) { ## no filtering names are supplied diff --git a/R/model_def_run.R b/R/model_def_run.R index 238dcd5a..12b53dda 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -104,6 +104,7 @@ LabelsDynamic = function(dynamic_model) { return_object(self, "LabelsDynamic") } + #' @export DynamicModel = function( expr_list = ExprList() diff --git a/R/mp.R b/R/mp.R index 355652f4..ad068228 100644 --- a/R/mp.R +++ b/R/mp.R @@ -18,8 +18,7 @@ mp = function(mp_func) { #' of the input tables. This is useful for producing product models #' that expand model components through stratification. #' -#' @param x Index table (see \code{\link{mp_index}}). -#' @param y Index table (see \code{\link{mp_index}}). +#' @param ... Index tables (see \code{\link{mp_index}}). #' #' @examples #' mp_cartesian( @@ -49,7 +48,9 @@ mp = function(mp_func) { #' @family indexes #' @family products #' @export -mp_cartesian = function(x, y) { +mp_cartesian = function(...) Reduce(mp_cartesian_binary, list(...)) + +mp_cartesian_binary = function(x, y) { shared_columns = intersect(names(x), names(y)) if (length(shared_columns) != 0) { msg_break( @@ -590,7 +591,7 @@ mp_reference.Ledger = function(x, dimension_name) { #' @export mp_reference.Index = function(x, dimension_name) { - x$reference_index + x$reference_index() } #' @export @@ -749,3 +750,47 @@ mp_expr_binop = function(x, y , length_name , ... ) {} + + +#' Lookup +#' +#' Lookup a subset or factor index associated with a symbol, and return the +#' index associated with that symbol. +#' +#' @param index Index table (see \code{\link{mp_index}}). +#' @param symbol Character string that could possibly be assoicated with a +#' subset or factor of `index`. +#' +#' @export +mp_lookup = function(index, symbol) { + + ## check if we are referring to possibly multiple grouping factors + dim_names = to_names(symbol) + if (all(dim_names %in% names(index))) { + return(mp_group(index, symbol)) + } + all_dim_names = names(index) + ii = increasing_int_seq(length(all_dim_names), length(dim_names)) + for (i in ii) { + named_symbol = (symbol + |> list() + |> setNames(to_name(all_dim_names[i])) + ) + args = c(list(index), named_symbol) + guess = try(do.call(mp_subset, args), silent = TRUE) + if (!inherits(guess, "try-error")) return(guess) + } + for (i in ii) { + all_perms = apply_permutations(i) + for (j in seq_len(nrow(all_perms))) { + named_symbol = (symbol + |> list() + |> setNames(to_name(all_dim_names[all_perms[j,]])) + ) + args = c(list(index), named_symbol) + guess = try(do.call(mp_subset, args), silent = TRUE) + if (!inherits(guess, "try-error")) return(guess) + } + } + stop("failed to find symbol") +} diff --git a/R/msg_utils.R b/R/msg_utils.R index 028de6b0..b5aa8199 100644 --- a/R/msg_utils.R +++ b/R/msg_utils.R @@ -33,15 +33,16 @@ msg <- function(..., .sep = "", .max_char_limit = getOption("width")) { (result |> trimws(whitespace = "[ \t]") |> break_start() - |> paste0(collapse = "") + |> paste0(collapse = "\n") ) } -msg_hline = function(.max_char_limit = getOption("width")) { - ("-" +msg_hline = function(x, .max_char_limit = getOption("width")) { + line = ("-" |> rep(.max_char_limit) |> paste0(collapse = "") ) + sprintf("\n%s\n%s", line, x) } msg_colon = function(x, y) sprintf("%s:\n%s", x, y) msg_break = function(...) paste(paste(..., sep = "\n"), collapse = "\n") diff --git a/R/parse_expr.R b/R/parse_expr.R index ebee7f08..a137b7bd 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -176,19 +176,36 @@ finalizer_index = function(x) { is_meth = x_char %in% names(valid_methods) is_int_vec = x_char %in% names(valid_int_vecs) is_var = x_char %in% names(valid_vars) - is_not_found = !(is_literal | is_func | is_meth | is_int_vec | is_var) + is_found = is_literal | is_func | is_meth | is_int_vec | is_var + is_not_found = !is_found + is_broad_sense_var = is_var | is_meth | is_int_vec if (any(is_not_found)) { missing_items = x_char[is_not_found] - stop( - "\nthe expression given by:\n", - x$expr_as_string, "\n\n", - "contained the following symbols:\n", - paste0(unique(missing_items), collapse = " "), "\n\n", - " that were not found in the list of available symbols:\n", - paste0(x_char[!is_literal], collapse = " "), # TODO: smarter pasting when this list gets big - "\n\nConsider adding these missing symbols somewhere in your model." + available_items = x_char[is_broad_sense_var] + expr_msg = msg_colon( + "The expression given by" + , msg_indent(x$input_expr_as_string) ) + missing_msg = msg_colon( + msg( + "contained the following symbols" + , "representing model variables" + ) + , msg_indent(missing_items) + ) + if (length(available_items) == 0L) { + issue_msg = "but no variables were declared in the model." + } else { + issue_msg = msg_colon( + msg( + "that were neither functions nor one of" + , "the following valid variables" + ), + msg_indent(available_items) + ) + } + msg_break(expr_msg, missing_msg, issue_msg) |> msg_hline() |> stop() } # identify literals with -1 in the 'number of arguments' @@ -391,4 +408,4 @@ parse_expr_list = function(expr_list num_p_table_rows = vapply(p_tables, nrow, integer(1L)) ) } -parse_expr_list = memoise(parse_expr_list) +#parse_expr_list = memoise(parse_expr_list) diff --git a/R/tmb_model.R b/R/tmb_model.R index b310bf5b..b759f45b 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -167,6 +167,11 @@ TMBModel = function( ) } +mp_tmb_model = function(expr_list, init_mats, time_steps) { + +} + + TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { self = tmb_simulator diff --git a/man/Clause.Rd b/man/Clause.Rd new file mode 100644 index 00000000..dba2de1e --- /dev/null +++ b/man/Clause.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dsl_expr.R +\name{Clause} +\alias{Clause} +\title{Compartmental Modelling Language} +\usage{ +Clause() +} +\description{ +Compartmental Modelling Language +} diff --git a/man/mp_cartesian.Rd b/man/mp_cartesian.Rd index 2021f549..b283d96e 100644 --- a/man/mp_cartesian.Rd +++ b/man/mp_cartesian.Rd @@ -4,12 +4,10 @@ \alias{mp_cartesian} \title{Cartesian Product of Index Tables} \usage{ -mp_cartesian(x, y) +mp_cartesian(...) } \arguments{ -\item{x}{Index table (see \code{\link{mp_index}}).} - -\item{y}{Index table (see \code{\link{mp_index}}).} +\item{...}{Index tables (see \code{\link{mp_index}}).} } \description{ Produce a new index table by taking all possible pairwise combinations diff --git a/man/mp_index.Rd b/man/mp_index.Rd index e4c60910..42a283ad 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -31,7 +31,9 @@ possible.} of the index that will be used to label the model components (i.e. rows) being described. The \code{labelling_column_names} cannot have duplicates and must contain at least one name. The index given by the -\code{labelling_column_names} must uniquely identify each row.} +\code{labelling_column_names} must uniquely identify each row. +The default \code{NULL} gives the set of columns, in order starting with +the first column, that are required to uniquely identify each row.} } \description{ Make an index table to enumerate model quantity labels by category. These diff --git a/man/mp_lookup.Rd b/man/mp_lookup.Rd new file mode 100644 index 00000000..eb2d7119 --- /dev/null +++ b/man/mp_lookup.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp.R +\name{mp_lookup} +\alias{mp_lookup} +\title{Lookup} +\usage{ +mp_lookup(index, symbol) +} +\arguments{ +\item{index}{Index table (see \code{\link{mp_index}}).} + +\item{symbol}{Character string that could possibly be assoicated with a +subset or factor of \code{index}.} +} +\description{ +Lookup a subset or factor index associated with a symbol, and return the +index associated with that symbol. +} From 4f3a96bef6bff282129d7bdbdca1fe0ae1b25e20 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 15:28:31 -0500 Subject: [PATCH 169/332] auto-commit --- README.md | 279 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 155 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 3ae4bfd4..4a447a61 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ -# macpan2 - - print(getwd()) - ## [1] "/Users/stevenwalker/Development/macpan2" +# macpan2 @@ -94,7 +91,7 @@ the following hello-world SIR model. The high-level design of `macpan2` is given in the following diagram, which we describe immediately below. -![](misc/diagrams/engine-dsl-separation.svg) +![](misc/diagrams/engine-dsl-separation.svg) ### Flow of Information @@ -164,31 +161,33 @@ Here we zoom into parts of the architectural diagram to illustrate the #### (2a) Model Library -![](misc/diagrams/model-library.svg) +![](misc/diagrams/model-library.svg) #### (2b) Engine-Agnostic Model Specifications -![](misc/diagrams/engine-agnostic-model-specification.svg) +![](misc/diagrams/engine-agnostic-model-specification.svg) #### (2c) Specification of Models Directly in the TMB Engine -![](misc/diagrams/tmb-model-specification.svg) - - si = TMBModel( - expr_list = ExprList( - during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection - ) - ) - , init_mats = MatsList( - S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix - , .mats_to_return = "I", .mats_to_save = "I" +![](misc/diagrams/tmb-model-specification.svg) + +``` r +si = TMBModel( + expr_list = ExprList( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection ) - , time_steps = Time(10L) ) - print(si$expr_list) + , init_mats = MatsList( + S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix + , .mats_to_return = "I", .mats_to_save = "I" + ) + , time_steps = Time(10L) +) +print(si$expr_list) +``` ## --------------------- ## At every iteration of the simulation loop (t = 1 to T): @@ -199,16 +198,18 @@ Here we zoom into parts of the architectural diagram to illustrate the Simulating from this model can be done like so. - (si$simulator()$report() - |> rename(prevalence = value) - |> ggplot() + geom_line(aes(time, prevalence)) - ) +``` r +(si$simulator()$report() + |> rename(prevalence = value) + |> ggplot() + geom_line(aes(time, prevalence)) +) +``` -![](misc/build/figures/plot-tmb-si-1.png) +![](misc/build/figures/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine -![](misc/diagrams/tmb-calibration.svg) +![](misc/diagrams/tmb-calibration.svg) ## Product Management @@ -222,18 +223,20 @@ current state, and plans for improvement and implementation. One can define a generic set of update steps that are iterated to produce a dynamic simulation model in TMB. - library(macpan2) - si = mp_dynamic_model( - expr_list = ExprList( - during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection - ) - ), - unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) +``` r +library(macpan2) +si = mp_dynamic_model( + expr_list = ExprList( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection ) - print(si) + ), + unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) +) +print(si) +``` ## --------------------- ## At every iteration of the simulation loop (t = 1 to T): @@ -244,14 +247,18 @@ produce a dynamic simulation model in TMB. Simulating from this model takes the following steps. - getwd() +``` r +getwd() +``` ## [1] "/Users/stevenwalker/Development/macpan2" - (si - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") - |> mp_report() - ) +``` r +(si + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") + |> mp_report() +) +``` ## matrix time row col value ## 1 I 1 0 0 1.247500 @@ -274,11 +281,13 @@ which is what the [model library](#model-library) is for. ### Model Library - ("unstructured/si" - |> mp_library() - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") - |> mp_report() - ) +``` r +("unstructured/si" + |> mp_library() + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") + |> mp_report() +) +``` ## matrix time row col value ## 1 I 1 0 0 1.247500 @@ -294,8 +303,8 @@ which is what the [model library](#model-library) is for. TODO: -- ☐ Reuse the tools for the older concept of starter models -- ☐ Establish a specification +- [ ] Reuse the tools for the older concept of starter models +- [ ] Establish a specification ### Calibration @@ -382,7 +391,9 @@ unstructured model can be converted into a structured expression to create a structured model. For example, the following unstructured expression defines the rate at which new infections emerge. - infection ~ beta * S * I / N +``` r +infection ~ beta * S * I / N +``` Each symbol in this expression has a certain type within a structured model, and this type determines how it gets translated into a structured @@ -391,7 +402,9 @@ expression. The simplest structured model is one that collects `S` and interpretation of the `S` and `I` symbols, the structured infection expression gets translated internally to the following. - infection ~ beta * state[S] * state[I] / N +``` r +infection ~ beta * state[S] * state[I] / N +``` Here `S` and `I` become symbols for extracting subsets of the `state` vector. In this case the expression itself remains a scalar expression @@ -407,8 +420,10 @@ patch would involve a four-dimensional state vector with the following elements: `S.east`, `S.west`, `I.east`, and `I.west`. In this case we now have two scalar-valued infection expressions. - infection[east] ~ beta * state[S.east] * state[I.east] / N - infection[west] ~ beta * state[S.west] * state[I.west] / N +``` r +infection[east] ~ beta * state[S.east] * state[I.east] / N +infection[west] ~ beta * state[S.west] * state[I.west] / N +``` With two patches it is fine to write out all scalar-valued infection expressions, but with more patches and with different types of structure @@ -441,61 +456,67 @@ more annoying. In an age-stratified model with two age groups, we now get four scalar-valued infection expressions of the form `infection ~ beta * S * I / N`. - infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] - infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] - infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] - infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N[old] +``` r +infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] +infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] +infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] +infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N[old] +``` Here the first expression is for a young individual infecting an old individual, the second is for an old individual infecting a young individual, etc … Things get worse if we have two age groups in two patches. - infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] - infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] - infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] - infection[old.old.east] ~ beta[old.old.east] * state[S.old.east] * state[I.old.east] / N[old.east] - infection[young.young.west] ~ beta[young.young.west] * state[S.young.west] * state[I.young.west] / N[young.west] - infection[young.old.west] ~ beta[young.old.west] * state[S.young.west] * state[I.old.west] / N[old.west] - infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * state[I.young.west] / N[young.west] - infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] +``` r +infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] +infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] +infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] +infection[old.old.east] ~ beta[old.old.east] * state[S.old.east] * state[I.old.east] / N[old.east] +infection[young.young.west] ~ beta[young.young.west] * state[S.young.west] * state[I.young.west] / N[young.west] +infection[young.old.west] ~ beta[young.old.west] * state[S.young.west] * state[I.old.west] / N[old.west] +infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * state[I.young.west] / N[young.west] +infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] +``` This still isn’t so bad, as we just have the first four expressions for `east` and the last four for `west`. But now let’s introduce two symptom status categories: `mild` and `severe`. - infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] - infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] - infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] - infection[young.young.east.severe.severe] ~ beta[young.young.east.severe.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] - infection[young.old.east.mild.mild] ~ beta[young.old.east.mild.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] - infection[young.old.east.mild.severe] ~ beta[young.old.east.mild.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] - infection[young.old.east.severe.mild] ~ beta[young.old.east.severe.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] - infection[young.old.east.severe.severe] ~ beta[young.old.east.severe.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] - infection[old.young.east.mild.mild] ~ beta[old.young.east.mild.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] - infection[old.young.east.mild.severe] ~ beta[old.young.east.mild.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] - infection[old.young.east.severe.mild] ~ beta[old.young.east.severe.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] - infection[old.young.east.severe.severe] ~ beta[old.young.east.severe.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] - infection[old.old.east.mild.mild] ~ beta[old.old.east.mild.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] - infection[old.old.east.mild.severe] ~ beta[old.old.east.mild.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] - infection[old.old.east.severe.mild] ~ beta[old.old.east.severe.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] - infection[old.old.east.severe.severe] ~ beta[old.old.east.severe.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] - infection[young.young.west.mild.mild] ~ beta[young.young.west.mild.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] - infection[young.young.west.mild.severe] ~ beta[young.young.west.mild.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] - infection[young.young.west.severe.mild] ~ beta[young.young.west.severe.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] - infection[young.young.west.severe.severe] ~ beta[young.young.west.severe.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] - infection[young.old.west.mild.mild] ~ beta[young.old.west.mild.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] - infection[young.old.west.mild.severe] ~ beta[young.old.west.mild.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] - infection[young.old.west.severe.mild] ~ beta[young.old.west.severe.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] - infection[young.old.west.severe.severe] ~ beta[young.old.west.severe.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] - infection[old.young.west.mild.mild] ~ beta[old.young.west.mild.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] - infection[old.young.west.mild.severe] ~ beta[old.young.west.mild.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] - infection[old.young.west.severe.mild] ~ beta[old.young.west.severe.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] - infection[old.young.west.severe.severe] ~ beta[old.young.west.severe.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] - infection[old.old.west.mild.mild] ~ beta[old.old.west.mild.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] - infection[old.old.west.mild.severe] ~ beta[old.old.west.mild.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] - infection[old.old.west.severe.mild] ~ beta[old.old.west.severe.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] - infection[old.old.west.severe.severe] ~ beta[old.old.west.severe.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] +``` r +infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] +infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] +infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] +infection[young.young.east.severe.severe] ~ beta[young.young.east.severe.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] +infection[young.old.east.mild.mild] ~ beta[young.old.east.mild.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] +infection[young.old.east.mild.severe] ~ beta[young.old.east.mild.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] +infection[young.old.east.severe.mild] ~ beta[young.old.east.severe.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] +infection[young.old.east.severe.severe] ~ beta[young.old.east.severe.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] +infection[old.young.east.mild.mild] ~ beta[old.young.east.mild.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] +infection[old.young.east.mild.severe] ~ beta[old.young.east.mild.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] +infection[old.young.east.severe.mild] ~ beta[old.young.east.severe.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] +infection[old.young.east.severe.severe] ~ beta[old.young.east.severe.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] +infection[old.old.east.mild.mild] ~ beta[old.old.east.mild.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] +infection[old.old.east.mild.severe] ~ beta[old.old.east.mild.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] +infection[old.old.east.severe.mild] ~ beta[old.old.east.severe.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] +infection[old.old.east.severe.severe] ~ beta[old.old.east.severe.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] +infection[young.young.west.mild.mild] ~ beta[young.young.west.mild.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] +infection[young.young.west.mild.severe] ~ beta[young.young.west.mild.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] +infection[young.young.west.severe.mild] ~ beta[young.young.west.severe.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] +infection[young.young.west.severe.severe] ~ beta[young.young.west.severe.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] +infection[young.old.west.mild.mild] ~ beta[young.old.west.mild.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] +infection[young.old.west.mild.severe] ~ beta[young.old.west.mild.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] +infection[young.old.west.severe.mild] ~ beta[young.old.west.severe.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] +infection[young.old.west.severe.severe] ~ beta[young.old.west.severe.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] +infection[old.young.west.mild.mild] ~ beta[old.young.west.mild.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] +infection[old.young.west.mild.severe] ~ beta[old.young.west.mild.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] +infection[old.young.west.severe.mild] ~ beta[old.young.west.severe.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] +infection[old.young.west.severe.severe] ~ beta[old.young.west.severe.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] +infection[old.old.west.mild.mild] ~ beta[old.old.west.mild.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] +infection[old.old.west.mild.severe] ~ beta[old.old.west.mild.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] +infection[old.old.west.severe.mild] ~ beta[old.old.west.severe.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] +infection[old.old.west.severe.severe] ~ beta[old.old.west.severe.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] +``` This is intense. The names in square brackets get much less clear in several ways as the model gets more structured. This lack of clarity @@ -540,11 +561,13 @@ location in the state variable names: `S.east`, `S.west`, `I.east`, and convenient to describe its variables using an index table, the rows of which describe each state variable. - state = mp_cartesian( - mp_index(Epi = c("S", "I")), - mp_index(Loc = c("east", "west")) - ) - state +``` r +state = mp_cartesian( + mp_index(Epi = c("S", "I")), + mp_index(Loc = c("east", "west")) +) +state +``` ## Epi Loc ## S east @@ -552,18 +575,24 @@ which describe each state variable. ## S west ## I west - beta = mp_group(state, "Epi") +``` r +beta = mp_group(state, "Epi") +``` With this representation we can get subsets of the state vector that represent each epidemiological status. - mp_subset(state, Epi = "S") +``` r +mp_subset(state, Epi = "S") +``` ## Epi Loc ## S east ## S west - mp_subset(state, Epi = "I") +``` r +mp_subset(state, Epi = "I") +``` ## Epi Loc ## I east @@ -587,23 +616,25 @@ the dynamics of the si model separate from under-reporting and reporting delay corrections to the raw prevalence (TODO: should really use incidence). - library(macpan2) - si_dynamics = list( - transition_rate = infection ~ beta * S * I / N - , state_update = S ~ S - infection - , state_update = I ~ I + infection - ) - reporting_correction = list( - post_processing = reports ~ convolution(I, c(0.5, 0.25, 0.25)) - ) - si = mp_dynamic_model( - expr_list = ExprList(during = c(si_dynamics, reporting_correction)), - unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) - ) - (si - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "reports") - |> mp_report() - ) +``` r +library(macpan2) +si_dynamics = list( + transition_rate = infection ~ beta * S * I / N + , state_update = S ~ S - infection + , state_update = I ~ I + infection +) +reporting_correction = list( + post_processing = reports ~ convolution(I, c(0.5, 0.25, 0.25)) +) +si = mp_dynamic_model( + expr_list = ExprList(during = c(si_dynamics, reporting_correction)), + unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) +) +(si + |> mp_tmb_simulator(time_steps = 10, mats_to_return = "reports") + |> mp_report() +) +``` ## matrix time row col value ## 1 reports 1 0 0 0.6237500 From 4b6d26920d4bfd8a417dcf516a3309c7b8a16039 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 11 Dec 2023 15:28:49 -0500 Subject: [PATCH 170/332] readme fuss --- README.Rmd | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.Rmd b/README.Rmd index 56672eab..4491aefe 100644 --- a/README.Rmd +++ b/README.Rmd @@ -4,10 +4,6 @@ output: github_document # macpan2 -```{r} -print(getwd()) -``` - ```{r opts, echo = FALSE} knitr::opts_chunk$set( fig.path = "misc/build/figures/" From 7d54f7df0e0a63c12cce8c4306a15a2643a12e8b Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 12 Dec 2023 10:29:42 -0500 Subject: [PATCH 171/332] starting directory for readme dependencies --- misc/readme/engine-dsl-separation.drawio | 202 +++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 misc/readme/engine-dsl-separation.drawio diff --git a/misc/readme/engine-dsl-separation.drawio b/misc/readme/engine-dsl-separation.drawio new file mode 100644 index 00000000..451fb0c6 --- /dev/null +++ b/misc/readme/engine-dsl-separation.drawio @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 46396575eca25f1f663f127be0d86a7053fe526e Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 12 Dec 2023 13:09:02 -0500 Subject: [PATCH 172/332] modular expression list for basic SIR --- inst/starter_models/sir/model.R | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 inst/starter_models/sir/model.R diff --git a/inst/starter_models/sir/model.R b/inst/starter_models/sir/model.R new file mode 100644 index 00000000..891aa24c --- /dev/null +++ b/inst/starter_models/sir/model.R @@ -0,0 +1,62 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, I, R) +) + +## absolute flow rates (per time only) +flow_rates = list( + infection ~ S * I * beta / N + , recovered ~ gamma * I +) + +## state updates +state_updates = list( + S ~ S - infection + , I ~ I + infection - recovered + , R ~ R + recovered +) + +## simple unstructured scalar expression +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) +) + +## simulate +################################################### + +tmb_simulator = TMBModel( + init_mats = MatsList( S = 99 + , I = 1 + , R = 0 + , beta = 0.2 + , gamma = 0.1 + , N = 100 + , infection = empty_matrix + , recovered = empty_matrix + , .mats_to_save = c("I") + , .mats_to_return = c("I") + ) + , expr_list = expr_list + , time_steps = Time(100L) + , params = OptParamsList( 0.2, 0.3 + , par_id = c(0L,1L) + , mat = c("beta","gamma") + , row_id = c(0L,0L) + , col_id = c(0L,0L) + ) + +)$simulator() + +plot(tmb_simulator$report_values(0.4,0.4)) + + + From 0197de0b5a709a32d086e522f407caeb18b534d1 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 12 Dec 2023 16:23:51 -0500 Subject: [PATCH 173/332] calibrate sir example --- Makefile | 8 +++- R/msg_utils.R | 2 +- R/tmb_model.R | 1 + R/tmb_model_editors.R | 4 +- README.Rmd | 2 + inst/starter_models/sir/example.R | 51 ++++++++++++++++++++++++ inst/starter_models/sir/model.R | 64 +++++++++++++++++-------------- 7 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 inst/starter_models/sir/example.R diff --git a/Makefile b/Makefile index 0d14dddf..6dcf2243 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,17 @@ VERSION := $(shell sed -n '/^Version: /s///p' DESCRIPTION) TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") IMAGES := $(shell grep 'knitr::include_graphics' README.Rmd | sed 's|knitr::include_graphics||' | tr -d '()"' | tr '\n' ' ') FIGURES := $(shell ls misc/build/figures/*.png | tr '\n' ' ') +DRAWIO := $(shell echo $(IMAGES) | sed -e 's|\.svg|\.drawio|') +IMAGES := $(shell grep 'knitr::include_graphics' README.Rmd | sed 's|knitr::include_graphics||' | tr -d '()"' | tr '\n' ' ') + + all: make full-install make pkg-check +print-stuff: + echo $(DRAWIO) install-deps: Rscript -e "remotes::install_github('canmod/oor@validity')" @@ -93,7 +99,7 @@ push-readme: readme:: README.md README.md: README.Rmd $(IMAGES) - Rscript -e "rmarkdown::render('README.Rmd', output_dir = '.', output_file = 'README.md', output_format = 'md_document')" + Rscript -e "rmarkdown::render('README.Rmd')" echo '' > temp echo '' | cat - $@ >> temp && mv temp $@ diff --git a/R/msg_utils.R b/R/msg_utils.R index b5aa8199..90353c5b 100644 --- a/R/msg_utils.R +++ b/R/msg_utils.R @@ -37,7 +37,7 @@ msg <- function(..., .sep = "", .max_char_limit = getOption("width")) { ) } -msg_hline = function(x, .max_char_limit = getOption("width")) { +msg_hline = function(x = "", .max_char_limit = getOption("width")) { line = ("-" |> rep(.max_char_limit) |> paste0(collapse = "") diff --git a/R/tmb_model.R b/R/tmb_model.R index b759f45b..6292d563 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -391,6 +391,7 @@ TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initializ self$insert = TMBSimulatorInserter(self) self$add = TMBSimulatorAdder(self) self$replace = TMBSimulatorReplacer(self) + self$update = TMBSimulatorUpdater(self) self$current = TMBCurrentParams(self) self$get = TMBSimulatorGetters(self) diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index 8eaeb7a8..6b74cbce 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -217,7 +217,7 @@ TMBUpdater = function(model) { return_object(self, "TMBUpdater") } -TMBSimulationUpdater = function(simulator) { +TMBSimulatorUpdater = function(simulator) { self = TMBUpdater(simulator$tmb_model) self$simulator = simulator self$matrices = function(... @@ -251,5 +251,5 @@ TMBSimulationUpdater = function(simulator) { self$simulator$cache$invalidate() invisible(self$simulator) } - return_object(self, "TMBSimulationUpdater") + return_object(self, "TMBSimulatorUpdater") } diff --git a/README.Rmd b/README.Rmd index 4491aefe..a3549702 100644 --- a/README.Rmd +++ b/README.Rmd @@ -1,5 +1,7 @@ --- output: github_document + github_document: + toc: true --- # macpan2 diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R new file mode 100644 index 00000000..ee7e8368 --- /dev/null +++ b/inst/starter_models/sir/example.R @@ -0,0 +1,51 @@ +source("inst/starter_models/sir/model.R") + +## ------------------------- +## simulate fake data +## ------------------------- + +true_beta = 0.4 +observed_data = tmb_simulator$report(true_beta) +observed_data$value = rpois(100, observed_data$value) + +if (interactive()) { + plot(observed_data$value, type = "l") +} + + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +tmb_simulator$update$matrices( + I_obs = observed_data$value + , I_obs_times = observed_data$time +) + + +## ------------------------- +## plot likelihood surface +## ------------------------- + +if (interactive()) { + betas = seq(from = 0.01, to = 1, length = 100) + ll = vapply( + betas + , tmb_simulator$objective + , numeric(1L) + ) + plot(betas, ll, type = "l") + abline(v = true_beta) +} + + +## ------------------------- +## optimize the model +## ------------------------- + +tmb_simulator$optimize$nlminb() + +if (interactive()) { + plot(observed_data$value, type = "l") + lines(tmb_simulator$report_values(), col = "red") +} diff --git a/inst/starter_models/sir/model.R b/inst/starter_models/sir/model.R index 891aa24c..1e915fdc 100644 --- a/inst/starter_models/sir/model.R +++ b/inst/starter_models/sir/model.R @@ -21,42 +21,50 @@ state_updates = list( , R ~ R + recovered ) +model_evaluation = list( + log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +) + +## data model comparison +obj_fn = ~ -sum(log_likelihood) + ## simple unstructured scalar expression expr_list = ExprList( before = computations - , during = c( - flow_rates - , state_updates - ) + , during = c(flow_rates, state_updates) + , after = model_evaluation +) + +## defaults +init_mats = MatsList( + S = 99 + , I = 1 + , R = 0 + , beta = 0.2 + , gamma = 0.1 + , N = 100 + , infection = empty_matrix + , recovered = empty_matrix + , log_likelihood = empty_matrix + , I_obs = empty_matrix + , I_obs_times = empty_matrix + , .mats_to_save = "I" + , .mats_to_return = "I" ) -## simulate +## simulator ################################################### tmb_simulator = TMBModel( - init_mats = MatsList( S = 99 - , I = 1 - , R = 0 - , beta = 0.2 - , gamma = 0.1 - , N = 100 - , infection = empty_matrix - , recovered = empty_matrix - , .mats_to_save = c("I") - , .mats_to_return = c("I") - ) + init_mats = init_mats , expr_list = expr_list , time_steps = Time(100L) - , params = OptParamsList( 0.2, 0.3 - , par_id = c(0L,1L) - , mat = c("beta","gamma") - , row_id = c(0L,0L) - , col_id = c(0L,0L) - ) - + , params = OptParamsList( + init_mats$get("beta") + , par_id = c(0L) + , mat = c("beta") + , row_id = c(0L) + , col_id = c(0L) + ) + , obj_fn = ObjectiveFunction(obj_fn) )$simulator() - -plot(tmb_simulator$report_values(0.4,0.4)) - - - From 7ea905111745e9dcbeae818cbb8883c369065f6c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 13 Dec 2023 10:51:10 -0500 Subject: [PATCH 174/332] fix long-standing bug when time_steps=0 and when there are expressions after the simulation loop --- NAMESPACE | 1 + R/tmb_model.R | 5 ++++- misc/dev/dev.cpp | 24 ++++++++++++++++++------ src/macpan2.cpp | 24 ++++++++++++++++++------ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 7d39c3d1..a653f8e2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -64,6 +64,7 @@ S3method(print,Partition) S3method(print,Quantities) S3method(print,String) S3method(print,StringData) +S3method(print,TMBSimulator) S3method(print,Vector) S3method(print,summary.Ledger) S3method(split_by,character) diff --git a/R/tmb_model.R b/R/tmb_model.R index 6292d563..2f8fa7b8 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -408,4 +408,7 @@ TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initializ return_object(self, "TMBSimulator") } - +#' @export +print.TMBSimulator = function(x, ...) { + print(x$tmb_model$expr_list$print_exprs()) +} diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 165b26f0..00086569 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -693,7 +693,6 @@ class ExprEvaluator { // Check if error has already happened at some point // of the recursive call. if (GetErrorCode()) return m; - switch (table_n[row]) { case -2: // methods (pre-processed matrices) @@ -1656,13 +1655,15 @@ class ExprEvaluator { case MP2_RBIND_LAG: args[1] = -args[1]; args[1].array() += t; // += t+0.1f; // +0.1 won't work when t<0 + std::cout << "here lag" << std::endl; case MP2_RBIND_TIME: + std::cout << "here time" << std::endl; if (t == 0) { SetError(154, "The simulation loop has not yet begun and so rbind_time (or rbind_lag) cannot be used", row); return args[0]; } matIndex = index2mats[0]; // m - if (matIndex == -1) { + if (matIndex < 0) { SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) named matrices not expressions of matrices", row); return args[0]; } @@ -1675,6 +1676,11 @@ class ExprEvaluator { } else { timeIndex = args[1]; } + std::cout << "here out" << std::endl; + if (timeIndex.size() == 0) { + std::cout << "here in" << std::endl; + return m; // return empty matrix if no time indices are provided + } if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) initialized matrices with saved history", row); return args[0]; @@ -2623,6 +2629,7 @@ Type objective_function::operator() () // Flags DATA_INTEGER(values_adreport); + #ifdef MP_VERBOSE std::cout << "params = " << params << std::endl; @@ -2718,6 +2725,7 @@ Type objective_function::operator() () literals ); + // 3 Pre-simulation int expr_index = 0; int p_table_row = 0; @@ -2751,7 +2759,7 @@ Type objective_function::operator() () p_table_row += expr_num_p_table_rows[i]; a_table_row += assign_num_a_table_rows[i]; - } + } // p_table_row is fine here //simulation_history[0] = mats; UpdateSimulationHistory( @@ -2765,8 +2773,10 @@ Type objective_function::operator() () // 4 During simulation expr_index += eval_schedule[0]; - int p_table_row2; - int a_table_row2; + // p_table_row2 lets us restart the parse table row every time the + // simulation loop is iterated + int p_table_row2 = p_table_row; + int a_table_row2 = a_table_row; for (int k=0; k::operator() () ); } } - else + else { result = exprEvaluator.EvalExpr( simulation_history, time_steps+1, mats, p_table_row ); + } if (exprEvaluator.GetErrorCode()) { REPORT_ERROR @@ -2928,6 +2939,7 @@ Type objective_function::operator() () } } + REPORT(values) if (values_adreport == 1) { ADREPORT(values) diff --git a/src/macpan2.cpp b/src/macpan2.cpp index e7a93b95..62a4d876 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -694,7 +694,6 @@ class ExprEvaluator { // Check if error has already happened at some point // of the recursive call. if (GetErrorCode()) return m; - switch (table_n[row]) { case -2: // methods (pre-processed matrices) @@ -1657,13 +1656,15 @@ class ExprEvaluator { case MP2_RBIND_LAG: args[1] = -args[1]; args[1].array() += t; // += t+0.1f; // +0.1 won't work when t<0 + std::cout << "here lag" << std::endl; case MP2_RBIND_TIME: + std::cout << "here time" << std::endl; if (t == 0) { SetError(154, "The simulation loop has not yet begun and so rbind_time (or rbind_lag) cannot be used", row); return args[0]; } matIndex = index2mats[0]; // m - if (matIndex == -1) { + if (matIndex < 0) { SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) named matrices not expressions of matrices", row); return args[0]; } @@ -1676,6 +1677,11 @@ class ExprEvaluator { } else { timeIndex = args[1]; } + std::cout << "here out" << std::endl; + if (timeIndex.size() == 0) { + std::cout << "here in" << std::endl; + return m; // return empty matrix if no time indices are provided + } if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { SetError(MP2_RBIND_TIME, "Can only rbind_time (or rbind_lag) initialized matrices with saved history", row); return args[0]; @@ -2624,6 +2630,7 @@ Type objective_function::operator() () // Flags DATA_INTEGER(values_adreport); + #ifdef MP_VERBOSE std::cout << "params = " << params << std::endl; @@ -2719,6 +2726,7 @@ Type objective_function::operator() () literals ); + // 3 Pre-simulation int expr_index = 0; int p_table_row = 0; @@ -2752,7 +2760,7 @@ Type objective_function::operator() () p_table_row += expr_num_p_table_rows[i]; a_table_row += assign_num_a_table_rows[i]; - } + } // p_table_row is fine here //simulation_history[0] = mats; UpdateSimulationHistory( @@ -2766,8 +2774,10 @@ Type objective_function::operator() () // 4 During simulation expr_index += eval_schedule[0]; - int p_table_row2; - int a_table_row2; + // p_table_row2 lets us restart the parse table row every time the + // simulation loop is iterated + int p_table_row2 = p_table_row; + int a_table_row2 = a_table_row; for (int k=0; k::operator() () ); } } - else + else { result = exprEvaluator.EvalExpr( simulation_history, time_steps+1, mats, p_table_row ); + } if (exprEvaluator.GetErrorCode()) { REPORT_ERROR @@ -2929,6 +2940,7 @@ Type objective_function::operator() () } } + REPORT(values) if (values_adreport == 1) { ADREPORT(values) From 9f603bbbb2d1efff97104c4c2f567a8223b78373 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 13 Dec 2023 11:13:05 -0500 Subject: [PATCH 175/332] model printing --- R/expr_list.R | 15 ++++++++++++--- R/tmb_model.R | 6 +++++- misc/dev/dev.cpp | 3 --- src/macpan2.cpp | 3 --- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/R/expr_list.R b/R/expr_list.R index 6343f4e2..980cb21b 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -199,14 +199,23 @@ ExprList = function( input$.simulate_exprs = unique(c(self$.simulate_exprs, .simulate_exprs)) do.call(ExprList, input) } - self$print_exprs = function(file = "") { + self$print_exprs = function(file = "", time_steps = "T") { to = cumsum(self$.eval_schedule()) from = c(0L, to[1:2]) + 1L + if (is.numeric(time_steps)) { + time_steps_p1 = as.character(as.integer(time_steps + 1)) + } else { + time_steps_p1 = paste(as.character(time_steps), "1", sep = " + ") + } msgs = c( "Before the simulation loop (t = 0):", - "At every iteration of the simulation loop (t = 1 to T):", - "After the simulation loop (t = T):" + sprintf("At every iteration of the simulation loop (t = 1 to %s):", as.character(time_steps)), + sprintf("After the simulation loop (t = %s):", time_steps_p1) ) + + ## TODO: give better advice here on how to engage the simulation loop. + if (time_steps == 0) msgs[2L] = "At every iteration of the simulation loop (number of iterations = 0):" + for (i in 1:3) { if (self$.eval_schedule()[i] > 0L) { expr_strings = lapply(self$formula_list()[from[i]:to[i]], deparse) diff --git a/R/tmb_model.R b/R/tmb_model.R index 2f8fa7b8..8c7bd03e 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -410,5 +410,9 @@ TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initializ #' @export print.TMBSimulator = function(x, ...) { - print(x$tmb_model$expr_list$print_exprs()) + m = x$tmb_model + time_steps = m$time_steps$time_steps + printer = m$expr_list$print_exprs + print(printer(time_steps = time_steps)) + invisible(x) } diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 00086569..2c92bd81 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -1657,7 +1657,6 @@ class ExprEvaluator { args[1].array() += t; // += t+0.1f; // +0.1 won't work when t<0 std::cout << "here lag" << std::endl; case MP2_RBIND_TIME: - std::cout << "here time" << std::endl; if (t == 0) { SetError(154, "The simulation loop has not yet begun and so rbind_time (or rbind_lag) cannot be used", row); return args[0]; @@ -1676,9 +1675,7 @@ class ExprEvaluator { } else { timeIndex = args[1]; } - std::cout << "here out" << std::endl; if (timeIndex.size() == 0) { - std::cout << "here in" << std::endl; return m; // return empty matrix if no time indices are provided } if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 62a4d876..e1af49e0 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -1658,7 +1658,6 @@ class ExprEvaluator { args[1].array() += t; // += t+0.1f; // +0.1 won't work when t<0 std::cout << "here lag" << std::endl; case MP2_RBIND_TIME: - std::cout << "here time" << std::endl; if (t == 0) { SetError(154, "The simulation loop has not yet begun and so rbind_time (or rbind_lag) cannot be used", row); return args[0]; @@ -1677,9 +1676,7 @@ class ExprEvaluator { } else { timeIndex = args[1]; } - std::cout << "here out" << std::endl; if (timeIndex.size() == 0) { - std::cout << "here in" << std::endl; return m; // return empty matrix if no time indices are provided } if (mats_save_hist[matIndex]==0 && !(timeIndex.size()==1 && CppAD::Integer(timeIndex.coeff(0,0))==t)) { From afaf0fbc4bdad5b2a89f0cc8e231bb08c7696d24 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 13 Dec 2023 11:28:38 -0500 Subject: [PATCH 176/332] sir model update --- inst/starter_models/sir/example.R | 19 +++++++++++++++---- inst/starter_models/sir/model.R | 23 +++++++---------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R index ee7e8368..8eb29a03 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/example.R @@ -1,11 +1,22 @@ source("inst/starter_models/sir/model.R") +## ------------------------- +## parameterize model +## ------------------------- + +tmb_simulator$update$transformations(Log("beta")) +tmb_simulator$replace$params(log(init_mats$get("beta")), "log_beta") +tmb_simulator ## note the new expression before the simulation loop + ## ------------------------- ## simulate fake data ## ------------------------- +time_steps = 100L true_beta = 0.4 -observed_data = tmb_simulator$report(true_beta) + +tmb_simulator$replace$time_steps(100L) +observed_data = tmb_simulator$report(log(true_beta)) observed_data$value = rpois(100, observed_data$value) if (interactive()) { @@ -28,13 +39,13 @@ tmb_simulator$update$matrices( ## ------------------------- if (interactive()) { - betas = seq(from = 0.01, to = 1, length = 100) + log_betas = seq(from = log(0.1), to = log(1), length = 100) ll = vapply( - betas + log_betas , tmb_simulator$objective , numeric(1L) ) - plot(betas, ll, type = "l") + plot(exp(log_betas), ll, type = "l") abline(v = true_beta) } diff --git a/inst/starter_models/sir/model.R b/inst/starter_models/sir/model.R index 1e915fdc..7e58d459 100644 --- a/inst/starter_models/sir/model.R +++ b/inst/starter_models/sir/model.R @@ -11,23 +11,20 @@ computations = list( ## absolute flow rates (per time only) flow_rates = list( infection ~ S * I * beta / N - , recovered ~ gamma * I + , recovery ~ gamma * I ) ## state updates state_updates = list( S ~ S - infection - , I ~ I + infection - recovered - , R ~ R + recovered + , I ~ I + infection - recovery + , R ~ R + recovery ) model_evaluation = list( log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) ) -## data model comparison -obj_fn = ~ -sum(log_likelihood) - ## simple unstructured scalar expression expr_list = ExprList( before = computations @@ -44,7 +41,7 @@ init_mats = MatsList( , gamma = 0.1 , N = 100 , infection = empty_matrix - , recovered = empty_matrix + , recovery = empty_matrix , log_likelihood = empty_matrix , I_obs = empty_matrix , I_obs_times = empty_matrix @@ -52,19 +49,13 @@ init_mats = MatsList( , .mats_to_return = "I" ) +obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) + ## simulator ################################################### tmb_simulator = TMBModel( init_mats = init_mats , expr_list = expr_list - , time_steps = Time(100L) - , params = OptParamsList( - init_mats$get("beta") - , par_id = c(0L) - , mat = c("beta") - , row_id = c(0L) - , col_id = c(0L) - ) - , obj_fn = ObjectiveFunction(obj_fn) + , obj_fn = obj_fn )$simulator() From b57013820607d6cf08b62ce92735b5afe8de91dd Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 13 Dec 2023 12:30:11 -0500 Subject: [PATCH 177/332] minor --- inst/starter_models/sir/example.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R index 8eb29a03..7852cf4a 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/example.R @@ -20,7 +20,7 @@ observed_data = tmb_simulator$report(log(true_beta)) observed_data$value = rpois(100, observed_data$value) if (interactive()) { - plot(observed_data$value, type = "l") + plot(observed_data$value, type = "l", las = 1) } @@ -45,7 +45,7 @@ if (interactive()) { , tmb_simulator$objective , numeric(1L) ) - plot(exp(log_betas), ll, type = "l") + plot(exp(log_betas), ll, type = "l", las = 1) abline(v = true_beta) } @@ -57,6 +57,6 @@ if (interactive()) { tmb_simulator$optimize$nlminb() if (interactive()) { - plot(observed_data$value, type = "l") + plot(observed_data$value, type = "l", las = 1) lines(tmb_simulator$report_values(), col = "red") } From 1d22b2c8f67f0d4d10270ef4e12c32bb6886f6c6 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Thu, 14 Dec 2023 07:59:39 -0500 Subject: [PATCH 178/332] expression lists for remaining unstructured models --- inst/starter_models/macpan_base/model.R | 52 ++++++++++++++++++++++ inst/starter_models/seir/model.R | 33 ++++++++++++++ inst/starter_models/sir_demo/model.R | 36 +++++++++++++++ inst/starter_models/sir_waning/model.R | 32 ++++++++++++++ inst/starter_models/ww/model.R | 59 +++++++++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 inst/starter_models/macpan_base/model.R create mode 100644 inst/starter_models/seir/model.R create mode 100644 inst/starter_models/sir_demo/model.R create mode 100644 inst/starter_models/sir_waning/model.R create mode 100644 inst/starter_models/ww/model.R diff --git a/inst/starter_models/macpan_base/model.R b/inst/starter_models/macpan_base/model.R new file mode 100644 index 00000000..a9a8e5e5 --- /dev/null +++ b/inst/starter_models/macpan_base/model.R @@ -0,0 +1,52 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) +) + +## absolute flow rates (per time only) +flow_rates = list( + SE ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , EIa ~ E * alpha * sigma + , EIp ~ E * (1 - alpha)* sigma + , IaR ~ Ia * gamma_a + , IpIm ~ Ip * mu * gamma_p + , ImR ~ Im * gamma_m + , IpIs ~ Ip * (1 - mu) * gamma_p + , IsICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , ICUsH2 ~ ICUs * psi1 + , H2R ~ H2 * psi3 + , IsH ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUdD ~ ICUd * psi2 + , HR ~ H * rho +) + +## state updates +state_updates = list( + S ~ S - SE + , E ~ E + SE - EIa - EIp + , Ia ~ Ia + EIa - IaR + , Ip ~ Ip + EIp - IpIm - IpIs + , Im ~ Im + IpIm + , Is ~ Is + IpIs - IsICUs - IsH - IsICUd + , R ~ R + IaR + H2R + HR + , H ~ H + IsH - HR + , ICUs ~ ICUs + IsICUs - ICUsH2 + , ICUd ~ ICUd + IsICUd - ICUdD + , H2 ~ H2 + ICUsH2 - H2R + , D ~ D + ICUdD +) + +## simple unstructured scalar expression +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) +) \ No newline at end of file diff --git a/inst/starter_models/seir/model.R b/inst/starter_models/seir/model.R new file mode 100644 index 00000000..028da131 --- /dev/null +++ b/inst/starter_models/seir/model.R @@ -0,0 +1,33 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, E, I, R) +) + +## absolute flow rates (per time only) +flow_rates = list( + infection ~ S * I * beta / N + , exposure ~ alpha * E + , recovery ~ gamma * I +) + +## state updates +state_updates = list( + S ~ S - exposure + , E ~ E + exposure - infection + , I ~ I + infection - recovery + , R ~ R + recovery +) + +## simple unstructured scalar expression +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) +) diff --git a/inst/starter_models/sir_demo/model.R b/inst/starter_models/sir_demo/model.R new file mode 100644 index 00000000..b0e59856 --- /dev/null +++ b/inst/starter_models/sir_demo/model.R @@ -0,0 +1,36 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, I, R) +) + +## absolute flow rates (per time only) +flow_rates = list( + birth ~ birth_rate * N + , infection ~ S * I * beta / N + , recovery ~ gamma * I +) + +## state updates +state_updates = list( + S ~ S - infection + birth - death_rate * S + , I ~ I + infection - recovery - death_rate * I + , R ~ R + recovery - death_rate * R +) + +## simple unstructured scalar expression +## in the general case, +## birth rate and death rate can differ, N might not be constant +## nothing can be computed before simulation loop +expr_list = ExprList( + during = c( + computations + , flow_rates + , state_updates + ) +) + diff --git a/inst/starter_models/sir_waning/model.R b/inst/starter_models/sir_waning/model.R new file mode 100644 index 00000000..a517f30e --- /dev/null +++ b/inst/starter_models/sir_waning/model.R @@ -0,0 +1,32 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, I, R) +) + +## absolute flow rates (per time only) +flow_rates = list( + infection ~ S * I * beta / N + , recovery ~ gamma * I + , waning_immunity ~ wane * R +) + +## state updates +state_updates = list( + S ~ S - infection + waning_immunity + , I ~ I + infection - recovery + , R ~ R + recovery - waning_immunity +) + +## simple unstructured scalar expression +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) +) diff --git a/inst/starter_models/ww/model.R b/inst/starter_models/ww/model.R new file mode 100644 index 00000000..415f898c --- /dev/null +++ b/inst/starter_models/ww/model.R @@ -0,0 +1,59 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D, W, A) +) + +## absolute flow rates (per time only) +flow_rates = list( + SE ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , EIa ~ E * alpha * sigma + , EIp ~ E * (1 - alpha)* sigma + , IaR ~ Ia * gamma_a + , IpIm ~ Ip * mu * gamma_p + , ImR ~ Im * gamma_m + , IpIs ~ Ip * (1 - mu) * gamma_p + , IsICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , ICUsH2 ~ ICUs * psi1 + , H2R ~ H2 * psi3 + , IsH ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUdD ~ ICUd * psi2 + , HR ~ H * rho + , IaW ~ Ia * nu # or * or N? + , IpW ~ Ip * nu + , ImW ~ Im * nu + , IsW ~ Is * nu + , WA ~ W * xi +) + +## state updates +state_updates = list( + S ~ S - SE + , E ~ E + SE - EIa - EIp + , Ia ~ Ia + EIa - IaR + , Ip ~ Ip + EIp - IpIm - IpIs + , Im ~ Im + IpIm + , Is ~ Is + IpIs - IsICUs - IsH - IsICUd + , R ~ R + IaR + H2R + HR + , H ~ H + IsH - HR + , ICUs ~ ICUs + IsICUs - ICUsH2 + , ICUd ~ ICUd + IsICUd - ICUdD + , H2 ~ H2 + ICUsH2 - H2R + , D ~ D + ICUdD + , W ~ W + IaW + IpW + ImW + IsW - WA + , A ~ A + WA +) + +## simple unstructured scalar expression +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) +) \ No newline at end of file From 8a5d7c10be46208e635aa8abb75be0596f11f84f Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Thu, 14 Dec 2023 08:56:39 -0500 Subject: [PATCH 179/332] adding Lotka Volterra to model library --- inst/starter_models/lotka_volterra/README.md | 19 ++++++++++ inst/starter_models/lotka_volterra/model.R | 39 ++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 inst/starter_models/lotka_volterra/README.md create mode 100644 inst/starter_models/lotka_volterra/model.R diff --git a/inst/starter_models/lotka_volterra/README.md b/inst/starter_models/lotka_volterra/README.md new file mode 100644 index 00000000..64c3dde7 --- /dev/null +++ b/inst/starter_models/lotka_volterra/README.md @@ -0,0 +1,19 @@ +--- +title: "Lotka-Volterra" +index_entry: "simple two-species competition model" +author: Jennifer Freeman +--- + +- $X$ - number of individuals in species $X$ +- $Y$ - number of individuals in species $Y$ +- $r_i$ - growth rate of species $I$ +- $a_{ij}$ - intra/inter-specific density dependence, ``effect of species $j$ on species $i$'' (Hastings, 1997) + +$$ +\begin{align*} +\frac{dX}{dt} &= r_x X (1 - a_{xx}X - a_{xy}Y) \\ +\frac{dY}{dt} &= r_y Y (1 - a_{yy}Y - a_{yx}X) +\end{align*} +$$ + +Hastings, A. (1997). Competition. In: Population Biology. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_7 \ No newline at end of file diff --git a/inst/starter_models/lotka_volterra/model.R b/inst/starter_models/lotka_volterra/model.R new file mode 100644 index 00000000..5961f7ab --- /dev/null +++ b/inst/starter_models/lotka_volterra/model.R @@ -0,0 +1,39 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +## none + +## absolute flow rates (per time only) +## reference +flow_rates = list( + ## growth rate of species X and Y + growth_x ~ rx * X + , growth_y ~ ry * Y + ## intraspecific effect + ## species X on X + , intraspecific_x ~ growth_x * axx * X + ## species Y on Y + , intraspecific_y ~ growth_y * ayy * Y + ## interspecific effect + ## species X on Y + , interspecific_xy ~ growth_x * ayx * Y + ## species Y on X + , interspecific_yx ~ growth_y * axy * X +) + +## state updates +state_updates = list( + X ~ X + growth_x - intraspecific_x - interspecific_xy + , Y ~ Y + growth_y - intraspecific_y - interspecific_yx +) + +## simple unstructured scalar expression +expr_list = ExprList( + during = c( + flow_rates + , state_updates + ) +) From 71603be97d80a0dadf38ee9183babd9931f36eb0 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 14 Dec 2023 10:33:29 -0500 Subject: [PATCH 180/332] more comments in SIR example --- inst/starter_models/sir/example.R | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R index 7852cf4a..60015108 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/example.R @@ -15,15 +15,21 @@ tmb_simulator ## note the new expression before the simulation loop time_steps = 100L true_beta = 0.4 -tmb_simulator$replace$time_steps(100L) +## set time_steps value +tmb_simulator$replace$time_steps(time_steps) + +## feed log(true_beta) to the simulator because we have +## already specified log-transformation of this parameter observed_data = tmb_simulator$report(log(true_beta)) -observed_data$value = rpois(100, observed_data$value) + +## .mats_to_return is set to "I", so observed_data$value is +## the prevalence (density of I) over time +observed_data$value = rpois(time_steps, observed_data$value) if (interactive()) { plot(observed_data$value, type = "l", las = 1) } - ## ------------------------- ## update simulator with fake data to fit to ## ------------------------- @@ -35,7 +41,7 @@ tmb_simulator$update$matrices( ## ------------------------- -## plot likelihood surface +## plot likelihood surface (curve) ## ------------------------- if (interactive()) { @@ -51,11 +57,12 @@ if (interactive()) { ## ------------------------- -## optimize the model +## fit parameters ## ------------------------- tmb_simulator$optimize$nlminb() +## plot observed vs predicted value if (interactive()) { plot(observed_data$value, type = "l", las = 1) lines(tmb_simulator$report_values(), col = "red") From 109319dbf5b54c582a134e66d2caa85d4b8d84a5 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 11:42:16 -0500 Subject: [PATCH 181/332] calibration sir example --- inst/starter_models/sir/example.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R index 7852cf4a..0eec0003 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/example.R @@ -15,9 +15,9 @@ tmb_simulator ## note the new expression before the simulation loop time_steps = 100L true_beta = 0.4 -tmb_simulator$replace$time_steps(100L) +tmb_simulator$replace$time_steps(time_steps) observed_data = tmb_simulator$report(log(true_beta)) -observed_data$value = rpois(100, observed_data$value) +observed_data$value = rpois(time_steps, observed_data$value) if (interactive()) { plot(observed_data$value, type = "l", las = 1) @@ -57,6 +57,7 @@ if (interactive()) { tmb_simulator$optimize$nlminb() if (interactive()) { + print(tmb_simulator$current$params_frame()) plot(observed_data$value, type = "l", las = 1) lines(tmb_simulator$report_values(), col = "red") } From 58d31edb7b431b7ca6033d4dd4691c8caf327500 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 11:44:20 -0500 Subject: [PATCH 182/332] sir example --- inst/starter_models/sir/model.R | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/inst/starter_models/sir/model.R b/inst/starter_models/sir/model.R index 7e58d459..f9c23304 100644 --- a/inst/starter_models/sir/model.R +++ b/inst/starter_models/sir/model.R @@ -9,12 +9,19 @@ computations = list( ) ## absolute flow rates (per time only) + +## engine-agnostic (not done) +## infection = mp_flow(~ I * beta / N, from = S, to = I) +## recovery = mp_flow(~ gamma, from = I, to = R) +## state_update = mp_update(type = "R4K") + +## engine-specific flow_rates = list( infection ~ S * I * beta / N , recovery ~ gamma * I ) -## state updates +## state_updates = list( S ~ S - infection , I ~ I + infection - recovery From 6ea0b601a19c179c8f391eddb8c88cf90bdc4604 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 12:18:44 -0500 Subject: [PATCH 183/332] auto-commit --- README.Rmd | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.Rmd b/README.Rmd index a3549702..318ae0f5 100644 --- a/README.Rmd +++ b/README.Rmd @@ -1,5 +1,5 @@ --- -output: github_document +output: github_document: toc: true --- @@ -8,7 +8,7 @@ output: github_document ```{r opts, echo = FALSE} knitr::opts_chunk$set( - fig.path = "misc/build/figures/" + fig.path = "misc/readme/" ) ``` @@ -74,10 +74,8 @@ sir_sims = simulator$report() ## Architecture -The high-level design of `macpan2` is given in the following diagram, which we describe immediately below. - ```{r, echo=FALSE} -knitr::include_graphics("misc/diagrams/engine-dsl-separation.svg") +knitr::include_graphics("misc/readme/design-concepts.svg") ``` ### Flow of Information From 83ae30aefc7c29c8d91bafa810389e255afe351d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 12:18:45 -0500 Subject: [PATCH 184/332] auto-commit --- README.md | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4a447a61..69d2eace 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,39 @@ +- macpan2 + - Documentation + - Installation + - Hello World + - Architecture + - Flow of + Information + - Architectural Layers of + Modularity + - Methods for Producing a + Model Object + - Product + Management + - General Dynamic Simulation + with TMB + - Model Library + - Calibration + - Time-Varying Parameters + - Model Structure and + Bookkeeping + - Structure Encourages + Reparameterization + - Alternative + Engines + - Combining Expression Lists + # macpan2 @@ -88,10 +121,7 @@ the following hello-world SIR model. ## Architecture -The high-level design of `macpan2` is given in the following diagram, -which we describe immediately below. - -![](misc/diagrams/engine-dsl-separation.svg) +![](misc/readme/design-concepts.svg) ### Flow of Information @@ -205,7 +235,7 @@ Simulating from this model can be done like so. ) ``` -![](misc/build/figures/plot-tmb-si-1.png) +![](misc/readme/plot-tmb-si-1.png) #### (2d) Calibrating Models in the TMB Engine From f65d3116d08a872a5009adabc87db5995b66ab69 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 12:18:50 -0500 Subject: [PATCH 185/332] auto-commit --- misc/readme/design-concepts.svg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 misc/readme/design-concepts.svg diff --git a/misc/readme/design-concepts.svg b/misc/readme/design-concepts.svg new file mode 100644 index 00000000..a9f708e7 --- /dev/null +++ b/misc/readme/design-concepts.svg @@ -0,0 +1,2 @@ +
    Data Manipulation
    Data Manipulation
    Results
    (e.g. forecasts)
    (e.g. plots)
    (e.g. reports)
    (e.g. diagnostics)
    Results...
    Data sources
    Data sources
    Numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. R matrices)
    (e.g. R data frames)
    (e.g. CSV files)
    Numerical information...
    Outside macpan2
    Outside macpan2
    1
    1
    5
    5
    Inside macpan2
    Inside macpan2
    Model Use
    Model Use
    Modelling outputs
    (e.g. incidence time-series)
    (e.g. reproduction numbers)
    Modelling outputs...
    Model simulator
    Model simulator
    3
    3
    4
    4
    Model Definition
    Model Definition
    Library model
    Library model
    Engine-agnostic model specification
    Engine-agnostic mode...
    Engine adaptor
    (e.g. TMB)
    (e.g. TMB-stan)
    (e.g. TMB-AdaptiveTau)
    (e.g. deSolve)
    Engine adaptor...
    2c
    2c
    Engine-specific
    model specification
    Engine-specific...
    2b
    2b
    Model structure updates
    (in an engine-agnostic modelling language)
    Model structure upda...
    2a
    2a

    Glossary

    Engine: A set of computational tools that macpan2 uses to produce modelling outputs, and to calibrate model simulators; macpan2 is designed to support multiple engines.

    Engine-agnostic model specification: A model written as a set of declarations in a language that is independent of the computational engine, allowing for a choice of computational tools.

    Engine-specific model specification: A model stored in a format that can be used by a specific engine, often more flexible but also more technical than an engine-agnostic model specification.

    Engine adaptor: A mechanism for users to choose an engine, and for developers to define how engine-agnostic model specifications are translated into engine-specific model specifications.

    Library model: A compartmental model, available online, and defined using an engine-agnostic model specification language.

    Model simulator: An engine-enabled compartmental model for generating model outputs.

    Modelling outputs: Raw numerical outputs from a model simulator. Examples include time-series of simulated prevalence, incidence, and hospital utilization, as well as model summaries like 


    Glossary...

    Workflow Steps

    1. Access and prepare data
    2. Define models (2a, 2b, and 2c are alternative entry points)
    3. Calibrate models or define scenarios and counterfactuals
    4. Generate model outputs (e.g. simulations, reproduction numbers)
    5. Prepare modelling results

    Repeat 1-5 as required

    Workflow Steps...

    Legend


    Legend +
    Family of objects
    Family of...
    Family of functions
    Family of...
    Inside macpan2
    Inside mac...
    Outside macpan2
    Outside ma...
    $$\mathc...
    Text is not SVG - cannot display
    \ No newline at end of file From 1a9cd950127caae8a9c272e304beb281b6cb266a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 12:18:51 -0500 Subject: [PATCH 186/332] auto-commit --- misc/readme/engine-dsl-separation.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 misc/readme/engine-dsl-separation.svg diff --git a/misc/readme/engine-dsl-separation.svg b/misc/readme/engine-dsl-separation.svg new file mode 100644 index 00000000..bd7b9751 --- /dev/null +++ b/misc/readme/engine-dsl-separation.svg @@ -0,0 +1,3 @@ + + +
    Pre-Processing
    Pre-Processing
    1
    1
    data sources
    data sources
    numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. CSV files)
    numerical information...
    Post-Processing
    Post-Processing
    4
    4
    results
    (e.g. ggplot2 graphics, CSV files with forecasts,
    estimates of reproduction numbers)
    results...
    Modelling Language
    Modelling Langu...
    model library
    model library
    2a
    2a
    engine-agnostic model specification
    engine-agnostic mode...
    2b
    2b
    Simulation and/or
    Calibration Engine
    Simulation and/or...
    model outputs
    model outputs
    3
    3
    model object
    model object
    engine-specific
    model specification
    engine-specific...
    2d
    2d
    2c
    2c
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Outside macpan2
    Inside macpan2
    Inside macpan2

    Legend

    Legend
    Family of functions:
    Family of functions:
    Family of objects:
    Family of objects:
    Text is not SVG - cannot display
    \ No newline at end of file From 51769123f863fb978ff05d77561c467e849dc26b Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 12:18:52 -0500 Subject: [PATCH 187/332] auto-commit --- misc/readme/plot-tmb-si-1.png | Bin 0 -> 25062 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 misc/readme/plot-tmb-si-1.png diff --git a/misc/readme/plot-tmb-si-1.png b/misc/readme/plot-tmb-si-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f54b75eb3da7442cebcdc73f7f97c006c431a4 GIT binary patch literal 25062 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 literal 0 HcmV?d00001 From 3521fc2a5f3ef2cd8a264412c090aa112aea6b9d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 13:24:35 -0500 Subject: [PATCH 188/332] clean up --- Makefile | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 6dcf2243..c398fe6a 100644 --- a/Makefile +++ b/Makefile @@ -5,19 +5,12 @@ ALIAS_RE = [ ]*MP2_\(.*\)\: \(.*\)(\(.*\)) ROXY_RE = ^.*\(\#'.*\)$ VERSION := $(shell sed -n '/^Version: /s///p' DESCRIPTION) TEST := testthat::test_package(\"macpan2\", reporter = \"progress\") -IMAGES := $(shell grep 'knitr::include_graphics' README.Rmd | sed 's|knitr::include_graphics||' | tr -d '()"' | tr '\n' ' ') -FIGURES := $(shell ls misc/build/figures/*.png | tr '\n' ' ') -DRAWIO := $(shell echo $(IMAGES) | sed -e 's|\.svg|\.drawio|') -IMAGES := $(shell grep 'knitr::include_graphics' README.Rmd | sed 's|knitr::include_graphics||' | tr -d '()"' | tr '\n' ' ') - all: make full-install make pkg-check -print-stuff: - echo $(DRAWIO) install-deps: Rscript -e "remotes::install_github('canmod/oor@validity')" @@ -82,28 +75,31 @@ misc/dev/%.run: misc/dev/%.R cd misc/dev; Rscript ../../$^ -svg:: misc/diagrams/*.svg -misc/diagrams/%.svg: misc/diagrams/%.drawio +misc/**/%.svg: misc/**/%.drawio draw.io --export $^ --format svg -png:: misc/diagrams/*.png -misc/diagrams/%.png: misc/diagrams/%.drawio +misc/**/%.png: misc/**/%.drawio draw.io --export $^ --format png -# TODO: make this work for others -- currently convenience for sw -push-readme: - make README.md - git_push_one_at_a_time.sh $(IMAGES) $(FIGURES) README.md + +svg-readme:: misc/readme/*.svg +png-readme:: misc/readme/*.png readme:: README.md -README.md: README.Rmd $(IMAGES) +README.md: README.Rmd misc/readme/*.svg Rscript -e "rmarkdown::render('README.Rmd')" echo '' > temp echo '' | cat - $@ >> temp && mv temp $@ +# TODO: make this work for others -- currently convenience for sw +push-readme: + make README.md + git_push_one_at_a_time.sh misc/readme/*.svg misc/readme/*.png README.md README.Rmd + + enum-update:: R/enum.R R/enum.R: misc/dev/dev.cpp misc/build/enum_tail.R echo "## Auto-generated - do not edit by hand" > $@ @@ -164,5 +160,3 @@ inst/model_library/%/README.md: inst/model_library/%/README.Rmd cat $(dir $@)/header.yaml $(dir $@)/README.md > $(dir $@)/tmp.md cp $(dir $@)/tmp.md $(dir $@)/README.md rm $(dir $@)/tmp.md - - From dda6ce682886a86def14b513eb36d5a8ee426d71 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:18:38 -0500 Subject: [PATCH 189/332] auto-commit --- README.Rmd | 111 +++++++++++++++++++---------------------------------- 1 file changed, 40 insertions(+), 71 deletions(-) diff --git a/README.Rmd b/README.Rmd index 318ae0f5..9eb4fa48 100644 --- a/README.Rmd +++ b/README.Rmd @@ -56,69 +56,6 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World -This [quick-start guide](https://canmod.github.io/macpan2/articles/quickstart) describes the following hello-world SIR model. - -``` -library(macpan2) -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -N = 100 -simulator = sir$simulators$tmb(time_steps = 100 - , state = c(S = N - 1, I = 1, R = 0) - , flow = c(foi = 0, gamma = 0.1) - , N = N - , beta = 0.2 -) -sir_sims = simulator$report() -``` - - -## Architecture - -```{r, echo=FALSE} -knitr::include_graphics("misc/readme/design-concepts.svg") -``` - -### Flow of Information - -Information flows from the top to the bottom of the diagram. Note that [users are not required to follow this entire path](#architectural-layers-of-modularity), but we will start with this assumption to describe the full vision. The major steps of data transformation are numbered in the diagram, and we describe each of these in what follows. - -1. The flow begins with accessing and preparing numerical information from various data sources, with the output being numerical R objects. Depending on the nature of the analysis to follow, this information could include default parameter values (e.g. transmission rate), initial values for the state variables (e.g. initial number of infectious individuals), operational schedules (e.g. timing of lockdown events or vaccine roll-out schedules), and data for model fitting (e.g. time series of hospital utilization). This step could involve connecting to real-time surveillance platforms or reading in static data files. There is not any functionality within `macpan2` for conducting this step -- `macpan2` [does not try to reinvent the wheel in data access and preparation](#architectural-layers-of-modularity). -2. - -### Architectural Layers of Modularity - -Modularity is a key principle of `macpan2` design in a few ways. - -First, `macpan2` is meant to plug into standard R workflows for data pre-processing and simulation post-processing. There is very little functionality in `macpan2` for configuring how data are prepared as input and out simulation outputs are processed. Instead, `macpan2` accepts standard data objects (data frames, matrices, vectors) and returns simulations as long-format data frames that can be processed using standard tools like `dplyr` and `ggplot2`. This design principle is illustrated in the architecture diagram below that has two blue outer layers representing standard non-`macpan2` workflows that contain two red inner layers representing workflows that depend on `macpan2` data structures and objects. The challenges of building the inner layers is big enough that we prefer to avoid reinventing the wheel of pre- and post-processing. - -Second, `macpan2` uses an engine plug-in architecture. The third layer in the diagram below represents an engine that can be swapped out if necessary. An engine is a wrapper around an existing modelling tool that allows it to be controlled by our structured compartmental modelling grammar/language, which is represented by the second layer in the diagram. Currently we only have a single engine, which is a wrapping around the TMB package. We are currently considering building upon AdaptiveTau, which can be used for Gillespie simulation. - -Third, each of the middle `macpan2` layers can be used on their own. For example, the TMB engine is quite powerful and flexible and can be used to quickly [specify dynamic model simulators](#general-dynamic-simulation-with-tmb) and calibrators that are executed in C++, without needing to write in C++. This approach would bypass the second structured modelling layer. Conversely, one could use the structured modelling layer to build descriptions of structured models and data without using them to interface with an engine. - -### Methods for Producing a Model Object - -Here we zoom into parts of the architectural diagram to illustrate the (currently four) ways to produce a model object. - -#### (2a) Model Library - -```{r, echo=FALSE} -knitr::include_graphics("misc/diagrams/model-library.svg") -``` - - -#### (2b) Engine-Agnostic Model Specifications - -```{r, echo=FALSE} -knitr::include_graphics("misc/diagrams/engine-agnostic-model-specification.svg") -``` - - -#### (2c) Specification of Models Directly in the TMB Engine - -```{r, echo=FALSE} -knitr::include_graphics("misc/diagrams/tmb-model-specification.svg") -``` - ```{r} si = TMBModel( expr_list = ExprList( @@ -129,29 +66,61 @@ si = TMBModel( ) ) , init_mats = MatsList( - S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix - , .mats_to_return = "I", .mats_to_save = "I" + S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix + , .mats_to_return = "I", .mats_to_save = "I" ) - , time_steps = Time(10L) -) -print(si$expr_list) + , time_steps = Time(100L) +)$simulator() +print(si) ``` Simulating from this model can be done like so. ```{r plot-tmb-si} -(si$simulator()$report() +(si$report() |> rename(prevalence = value) |> ggplot() + geom_line(aes(time, prevalence)) ) ``` -#### (2d) Calibrating Models in the TMB Engine +## Design Concepts ```{r, echo=FALSE} -knitr::include_graphics("misc/diagrams/tmb-calibration.svg") +knitr::include_graphics("misc/readme/design-concepts.svg") ``` +### Information Processing + +Like other statistical modelling software, the high-level purpose of `macpan2` is to process data sources (top-left) into results (bottom-left). In the case of `macpan2`, this processing is done using compartmental modelling (right). The major steps of this information processing are numbered in the diagram, and we describe each of these. + +1. Information processing begins with accessing and preparing numerical information from various data sources, with the output being standard numerical R objects. Depending on the nature of the analysis to follow, this information could include default values for parameters (e.g. transmission rate), initial values for the state variables (e.g. initial number of infectious individuals), operational schedules (e.g. timing of lockdown events or vaccine roll-out schedules), and data for model fitting (e.g. time series of hospital utilization). This step could involve connecting to real-time surveillance platforms or reading in static data files. There is not any functionality within `macpan2` for conducting this step -- [`macpan2` does not try to reinvent the wheel in data access and preparation](#modularity). +2. The structure of a compartmental model is defined in one of three ways. In all cases the output is ultimately a [model simulator](#model-simulator). + a. A model is chosen from a model library and read into `R`, optionally updating the model structure using an [engine-agnostic model specification language](#engine-agnostic-model-specification-language). + b. A model is written from scratch using the [engine-agnostic model specification language](#engine-agnostic-model-specification-language). + c. A model is written from scratch using one of the [engine-specific model specification languages](#engine-specific-model-specification-languages). +These three are alternatives, in that if 2a is chosen then 2b and 2c are automatically executed and if 2b is chosen that 2c is automatic. The choice here is just how close (2c) or far (2a) from the actual computation engine do you want to be when specifying models. There are several [considerations when choosing a model specification workflow](#engine-agnostic-versus-engine-specific) when deciding which alternative to use. No matter which of these approaches is taken, the output of step 2 is a model simulator that can be used to generate modelling outputs like simulated incidence time-series or reproduction numbers. +3. Although model simulators come with default initial values so that they can be used immediately, typically one would like to modify these values without needing to edit the model specifications from step 2. There are two main use-cases involving such numerical modifications to the model simulators: In order to formally calibrate model parameters by fitting the model to observed time-series data and/or modifying default parameter values to reflect a what-if scenario. In both use-cases, a model simulator is used as input and another model simulator is produced as output. +4. Once the model defining and numerical initialization steps have been completed, model outputs are produced in long-format data frames. +5. Finally these model outputs are incorporated into forecasts, plots, reports, and diagnostics using standard tools outside of `macpan2`. + +### Modularity + +Modularity is a key principle of `macpan2` design in a few ways. + +First, `macpan2` is meant to plug into standard R workflows for data pre-processing and simulation post-processing. There is very little functionality in `macpan2` for configuring how data are prepared as input and modelling outputs are processed. Instead, `macpan2` accepts standard data objects (data frames, matrices, vectors) and returns simulations as long-format data frames that can be processed using standard tools like `dplyr` and `ggplot2`. This design principle is illustrated in the architecture diagram above that has blue steps representing standard non-`macpan2` workflows and red steps representing workflows that depend on `macpan2` data structures and objects. The challenges of building the red steps is big enough that we prefer to avoid reinventing the wheel of pre- and post-processing. + +Second, `macpan2` uses an engine plug-in architecture. Models defined in the [engine-agnostic model specification language](#engine-agnostic model specification language) can be rendered in a particular computational engine so that multiple computational approaches can be used to generate modelling outputs for a single model definition. This can be useful if different model outputs are more efficient or convenient for different computational approaches. For example, engines such as TMB that are capable of automatic differentiation are great for fast optimization of parameters and for computing $\mathcal{R}_0$ in models with arbitrary complexity, whereas other engines such as Adaptive Tau are better at stochastic simulation techniques like the Gillespie algorithm. Sometimes an engine will be unable to generate a particular output at all or with sufficient difficulty on the part of the user so as to render the use-case practically impossible. For example, it is not possible to conveniently utilize differential equation solvers in the TMB engine, limiting it to Euler or simple RK4-type solvers. Being able to swap out the TMB engine for one based on `deSolve` (or other similar package) would allow for more convenient and accurate solutions to differential equations without having to leave `macpan2`. + +Third, TODO: describe how the model specification language can be used to build up models modularly (e.g. swap out alternative state-updaters as discussed above but also add in model structures like age-groups and spatial structure to a simple unstructured model) + + +### Engine-Agnostic Model Specification Language + +TODO + +### Engine-Specific Model Specification Languages + +TODO From 54a8ee3c963cf26613ad04b0a702f2c2889a1e31 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:18:39 -0500 Subject: [PATCH 190/332] auto-commit --- README.md | 251 +++++++++++++++++++++++++++++------------------------- 1 file changed, 136 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 69d2eace..9b13e8fc 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,16 @@ - Documentation - Installation - Hello World - - Architecture - - Flow of - Information - - Architectural Layers of - Modularity - - Methods for Producing a - Model Object + - Design Concepts + - Information Processing + - Modularity + - Engine-Agnostic + Model Specification Language + - Engine-Specific + Model Specification Languages - Product Management - - -### Flow of Information - -Information flows from the top to the bottom of the diagram. Note that -[users are not required to follow this entire -path](#architectural-layers-of-modularity), but we will start with this -assumption to describe the full vision. The major steps of data -transformation are numbered in the diagram, and we describe each of -these in what follows. - -1. The flow begins with accessing and preparing numerical information - from various data sources, with the output being numerical R - objects. Depending on the nature of the analysis to follow, this - information could include default parameter values - (e.g. transmission rate), initial values for the state variables - (e.g. initial number of infectious individuals), operational - schedules (e.g. timing of lockdown events or vaccine roll-out - schedules), and data for model fitting (e.g. time series of hospital - utilization). This step could involve connecting to real-time - surveillance platforms or reading in static data files. There is not - any functionality within `macpan2` for conducting this step – - `macpan2` [does not try to reinvent the wheel in data access and - preparation](#architectural-layers-of-modularity). -2. - -### Architectural Layers of Modularity - -Modularity is a key principle of `macpan2` design in a few ways. - -First, `macpan2` is meant to plug into standard R workflows for data -pre-processing and simulation post-processing. There is very little -functionality in `macpan2` for configuring how data are prepared as -input and out simulation outputs are processed. Instead, `macpan2` -accepts standard data objects (data frames, matrices, vectors) and -returns simulations as long-format data frames that can be processed -using standard tools like `dplyr` and `ggplot2`. This design principle -is illustrated in the architecture diagram below that has two blue outer -layers representing standard non-`macpan2` workflows that contain two -red inner layers representing workflows that depend on `macpan2` data -structures and objects. The challenges of building the inner layers is -big enough that we prefer to avoid reinventing the wheel of pre- and -post-processing. - -Second, `macpan2` uses an engine plug-in architecture. The third layer -in the diagram below represents an engine that can be swapped out if -necessary. An engine is a wrapper around an existing modelling tool that -allows it to be controlled by our structured compartmental modelling -grammar/language, which is represented by the second layer in the -diagram. Currently we only have a single engine, which is a wrapping -around the TMB package. We are currently considering building upon -AdaptiveTau, which can be used for Gillespie simulation. - -Third, each of the middle `macpan2` layers can be used on their own. For -example, the TMB engine is quite powerful and flexible and can be used -to quickly [specify dynamic model -simulators](#general-dynamic-simulation-with-tmb) and calibrators that -are executed in C++, without needing to write in C++. This approach -would bypass the second structured modelling layer. Conversely, one -could use the structured modelling layer to build descriptions of -structured models and data without using them to interface with an -engine. - -### Methods for Producing a Model Object - -Here we zoom into parts of the architectural diagram to illustrate the -(currently four) ways to produce a model object. - -#### (2a) Model Library - -![](misc/diagrams/model-library.svg) - -#### (2b) Engine-Agnostic Model Specifications - -![](misc/diagrams/engine-agnostic-model-specification.svg) - -#### (2c) Specification of Models Directly in the TMB Engine - -![](misc/diagrams/tmb-model-specification.svg) - ``` r si = TMBModel( expr_list = ExprList( @@ -211,25 +115,27 @@ si = TMBModel( ) ) , init_mats = MatsList( - S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix - , .mats_to_return = "I", .mats_to_save = "I" + S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix + , .mats_to_return = "I", .mats_to_save = "I" ) - , time_steps = Time(10L) -) -print(si$expr_list) + , time_steps = Time(100L) +)$simulator() +print(si) ``` ## --------------------- - ## At every iteration of the simulation loop (t = 1 to T): + ## At every iteration of the simulation loop (t = 1 to 100): ## --------------------- ## 1: infection ~ beta * S * I/N ## 2: S ~ S - infection ## 3: I ~ I + infection + ## + ## NULL Simulating from this model can be done like so. ``` r -(si$simulator()$report() +(si$report() |> rename(prevalence = value) |> ggplot() + geom_line(aes(time, prevalence)) ) @@ -237,9 +143,124 @@ Simulating from this model can be done like so. ![](misc/readme/plot-tmb-si-1.png) -#### (2d) Calibrating Models in the TMB Engine +## Design Concepts + +![](misc/readme/design-concepts.svg) -![](misc/diagrams/tmb-calibration.svg) +### Information Processing + +Like other statistical modelling software, the high-level purpose of +`macpan2` is to process data sources (top-left) into results +(bottom-left). In the case of `macpan2`, this processing is done using +compartmental modelling (right). The major steps of this information +processing are numbered in the diagram, and we describe each of these. + +1. Information processing begins with accessing and preparing numerical + information from various data sources, with the output being + standard numerical R objects. Depending on the nature of the + analysis to follow, this information could include default values + for parameters (e.g. transmission rate), initial values for the + state variables (e.g. initial number of infectious individuals), + operational schedules (e.g. timing of lockdown events or vaccine + roll-out schedules), and data for model fitting (e.g. time series of + hospital utilization). This step could involve connecting to + real-time surveillance platforms or reading in static data files. + There is not any functionality within `macpan2` for conducting this + step – [`macpan2` does not try to reinvent the wheel in data access + and preparation](#modularity). +2. The structure of a compartmental model is defined in one of three + ways. In all cases the output is ultimately a [model + simulator](#model-simulator). + + + +1. A model is chosen from a model library and read into `R`, optionally + updating the model structure using an [engine-agnostic model + specification + language](#engine-agnostic-model-specification-language). +2. A model is written from scratch using the [engine-agnostic model + specification + language](#engine-agnostic-model-specification-language). +3. A model is written from scratch using one of the [engine-specific + model specification + languages](#engine-specific-model-specification-languages). These + three are alternatives, in that if 2a is chosen then 2b and 2c are + automatically executed and if 2b is chosen that 2c is automatic. The + choice here is just how close (2c) or far (2a) from the actual + computation engine do you want to be when specifying models. There + are several [considerations when choosing a model specification + workflow](#engine-agnostic-versus-engine-specific) when deciding + which alternative to use. No matter which of these approaches is + taken, the output of step 2 is a model simulator that can be used to + generate modelling outputs like simulated incidence time-series or + reproduction numbers. + + + +3. Although model simulators come with default initial values so that + they can be used immediately, typically one would like to modify + these values without needing to edit the model specifications from + step 2. There are two main use-cases involving such numerical + modifications to the model simulators: In order to formally + calibrate model parameters by fitting the model to observed + time-series data and/or modifying default parameter values to + reflect a what-if scenario. In both use-cases, a model simulator is + used as input and another model simulator is produced as output. +4. Once the model defining and numerical initialization steps have been + completed, model outputs are produced in long-format data frames. +5. Finally these model outputs are incorporated into forecasts, plots, + reports, and diagnostics using standard tools outside of `macpan2`. + +### Modularity + +Modularity is a key principle of `macpan2` design in a few ways. + +First, `macpan2` is meant to plug into standard R workflows for data +pre-processing and simulation post-processing. There is very little +functionality in `macpan2` for configuring how data are prepared as +input and modelling outputs are processed. Instead, `macpan2` accepts +standard data objects (data frames, matrices, vectors) and returns +simulations as long-format data frames that can be processed using +standard tools like `dplyr` and `ggplot2`. This design principle is +illustrated in the architecture diagram above that has blue steps +representing standard non-`macpan2` workflows and red steps representing +workflows that depend on `macpan2` data structures and objects. The +challenges of building the red steps is big enough that we prefer to +avoid reinventing the wheel of pre- and post-processing. + +Second, `macpan2` uses an engine plug-in architecture. Models defined in +the [engine-agnostic model specification +language](#engine-agnostic%20model%20specification%20language) can be +rendered in a particular computational engine so that multiple +computational approaches can be used to generate modelling outputs for a +single model definition. This can be useful if different model outputs +are more efficient or convenient for different computational approaches. +For example, engines such as TMB that are capable of automatic +differentiation are great for fast optimization of parameters and for +computing $\mathcal{R}_0$ in models with arbitrary complexity, whereas +other engines such as Adaptive Tau are better at stochastic simulation +techniques like the Gillespie algorithm. Sometimes an engine will be +unable to generate a particular output at all or with sufficient +difficulty on the part of the user so as to render the use-case +practically impossible. For example, it is not possible to conveniently +utilize differential equation solvers in the TMB engine, limiting it to +Euler or simple RK4-type solvers. Being able to swap out the TMB engine +for one based on `deSolve` (or other similar package) would allow for +more convenient and accurate solutions to differential equations without +having to leave `macpan2`. + +Third, TODO: describe how the model specification language can be used +to build up models modularly (e.g. swap out alternative state-updaters +as discussed above but also add in model structures like age-groups and +spatial structure to a simple unstructured model) + +### Engine-Agnostic Model Specification Language + +TODO + +### Engine-Specific Model Specification Languages + +TODO ## Product Management From 285714c9e3e6262f3f579956d10f89f900970beb Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:18:40 -0500 Subject: [PATCH 191/332] auto-commit --- misc/readme/design-concepts.svg | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/misc/readme/design-concepts.svg b/misc/readme/design-concepts.svg index a9f708e7..e344a23c 100644 --- a/misc/readme/design-concepts.svg +++ b/misc/readme/design-concepts.svg @@ -1,2 +1,3 @@ -
    Data Manipulation
    Data Manipulation
    Results
    (e.g. forecasts)
    (e.g. plots)
    (e.g. reports)
    (e.g. diagnostics)
    Results...
    Data sources
    Data sources
    Numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. R matrices)
    (e.g. R data frames)
    (e.g. CSV files)
    Numerical information...
    Outside macpan2
    Outside macpan2
    1
    1
    5
    5
    Inside macpan2
    Inside macpan2
    Model Use
    Model Use
    Modelling outputs
    (e.g. incidence time-series)
    (e.g. reproduction numbers)
    Modelling outputs...
    Model simulator
    Model simulator
    3
    3
    4
    4
    Model Definition
    Model Definition
    Library model
    Library model
    Engine-agnostic model specification
    Engine-agnostic mode...
    Engine adaptor
    (e.g. TMB)
    (e.g. TMB-stan)
    (e.g. TMB-AdaptiveTau)
    (e.g. deSolve)
    Engine adaptor...
    2c
    2c
    Engine-specific
    model specification
    Engine-specific...
    2b
    2b
    Model structure updates
    (in an engine-agnostic modelling language)
    Model structure upda...
    2a
    2a

    Glossary

    Engine: A set of computational tools that macpan2 uses to produce modelling outputs, and to calibrate model simulators; macpan2 is designed to support multiple engines.

    Engine-agnostic model specification: A model written as a set of declarations in a language that is independent of the computational engine, allowing for a choice of computational tools.

    Engine-specific model specification: A model stored in a format that can be used by a specific engine, often more flexible but also more technical than an engine-agnostic model specification.

    Engine adaptor: A mechanism for users to choose an engine, and for developers to define how engine-agnostic model specifications are translated into engine-specific model specifications.

    Library model: A compartmental model, available online, and defined using an engine-agnostic model specification language.

    Model simulator: An engine-enabled compartmental model for generating model outputs.

    Modelling outputs: Raw numerical outputs from a model simulator. Examples include time-series of simulated prevalence, incidence, and hospital utilization, as well as model summaries like 


    Glossary...

    Workflow Steps

    1. Access and prepare data
    2. Define models (2a, 2b, and 2c are alternative entry points)
    3. Calibrate models or define scenarios and counterfactuals
    4. Generate model outputs (e.g. simulations, reproduction numbers)
    5. Prepare modelling results

    Repeat 1-5 as required

    Workflow Steps...

    Legend


    Legend -
    Family of objects
    Family of...
    Family of functions
    Family of...
    Inside macpan2
    Inside mac...
    Outside macpan2
    Outside ma...
    $$\mathc...
    Text is not SVG - cannot display
    \ No newline at end of file + + +
    Data Manipulation
    Data Manipulation
    Results
    (e.g. forecasts)
    (e.g. plots)
    (e.g. reports)
    (e.g. diagnostics)
    Results...
    Data sources
    Data sources
    Numerical information
    in standard formats
    (e.g. R vectors)
    (e.g. R matrices)
    (e.g. R data frames)
    (e.g. CSV files)
    Numerical information...
    Outside macpan2
    Outside macpan2
    1
    1
    5
    5
    Inside macpan2
    Inside macpan2
    Model Use
    Model Use
    Modelling outputs
    (e.g. incidence time-series)
    (e.g. reproduction numbers)
    Modelling outputs...
    Model simulator
    Model simulator
    3
    3
    4
    4
    Model Definition
    Model Definition
    Library model
    Library model
    Engine-agnostic model specification
    Engine-agnostic mode...
    Engine adapter
    (e.g. TMB)
    (e.g. TMB-stan)
    (e.g. TMB-AdaptiveTau)
    (e.g. deSolve)
    Engine adapter...
    2c
    2c
    Engine-specific
    model specification
    Engine-specific...
    2b
    2b
    Model structure updates
    (in an engine-agnostic modelling language)
    Model structure upda...
    2a
    2a

    Glossary

    Engine: A set of computational tools that macpan2 uses to produce modelling outputs, and to calibrate model simulators; macpan2 is designed to support multiple engines.

    Engine-agnostic model specification: A model written as a set of declarations in a language that is independent of the computational engine, allowing for a choice of computational tools.

    Engine-specific model specification: A model stored in a format that can be used by a specific engine, often more flexible but also more technical than an engine-agnostic model specification.

    Engine adapter: A mechanism for users to choose an engine, and for developers to define how engine-agnostic model specifications are translated into engine-specific model specifications.

    Library model: A compartmental model, available online, and defined using an engine-agnostic model specification language.

    Model simulator: An engine-enabled compartmental model for generating model outputs.

    Modelling outputs: Raw numerical outputs from a model simulator. Examples include time-series of simulated prevalence, incidence, and hospital utilization, as well as model summaries like 


    Glossary...

    Workflow Steps

    1. Access and prepare data
    2. Define models (2a, 2b, and 2c are alternative entry points)
    3. Calibrate models or define scenarios and counterfactuals
    4. Generate model outputs (e.g. simulations, reproduction numbers)
    5. Prepare modelling results

    Repeat 1-5 as required

    Workflow Steps...

    Legend


    Legend
    Family of objects
    Family of...
    Family of functions
    Family of...
    Inside macpan2
    Inside mac...
    Outside macpan2
    Outside ma...
    $$\mathc...
    Text is not SVG - cannot display
    \ No newline at end of file From 2d5aead7ddb49ff90add0b249558da1b58d06041 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:18:41 -0500 Subject: [PATCH 192/332] auto-commit --- misc/readme/plot-tmb-si-1.png | Bin 25062 -> 23682 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/misc/readme/plot-tmb-si-1.png b/misc/readme/plot-tmb-si-1.png index f0f54b75eb3da7442cebcdc73f7f97c006c431a4..bf6a1174d799d12a330e375315bbb2466117137f 100644 GIT binary patch literal 23682 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 From 2b49e0c52dc4f587925b0cbeee3760e7d31a0cb2 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:20:30 -0500 Subject: [PATCH 193/332] auto-commit --- README.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.Rmd b/README.Rmd index 9eb4fa48..045d89c0 100644 --- a/README.Rmd +++ b/README.Rmd @@ -69,7 +69,7 @@ si = TMBModel( S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix , .mats_to_return = "I", .mats_to_save = "I" ) - , time_steps = Time(100L) + , time_steps = Time(50L) )$simulator() print(si) ``` From d2e979b981a89be88466f56196c1b5f5ca71f19a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:20:31 -0500 Subject: [PATCH 194/332] auto-commit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b13e8fc..d87f9c9d 100644 --- a/README.md +++ b/README.md @@ -118,13 +118,13 @@ si = TMBModel( S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix , .mats_to_return = "I", .mats_to_save = "I" ) - , time_steps = Time(100L) + , time_steps = Time(50L) )$simulator() print(si) ``` ## --------------------- - ## At every iteration of the simulation loop (t = 1 to 100): + ## At every iteration of the simulation loop (t = 1 to 50): ## --------------------- ## 1: infection ~ beta * S * I/N ## 2: S ~ S - infection From 77db5c4067cf72bd1c8b6befed3937a2a0b6b5af Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:20:32 -0500 Subject: [PATCH 195/332] auto-commit --- misc/readme/plot-tmb-si-1.png | Bin 23682 -> 25993 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/misc/readme/plot-tmb-si-1.png b/misc/readme/plot-tmb-si-1.png index bf6a1174d799d12a330e375315bbb2466117137f..1249799a1af360c09cdb01e968ca8a78ad270f35 100644 GIT binary patch literal 25993 zcmdSB`9GEI_cm_twxYq5xEstNbD7G#GbFPp^HiA$nP;06Qjswk3n631%o>FXnP(N5 zGla~Zb#C|P{(N8G*YgiNKfLc(Z!XuquJc^yTE{w$W9=|ab;TnGPaGs6AvvOSSze2T z1e-=ef*B>-4?ppKFd|Gsf}^mJlhd@5Qz&RQFQO=RL}E(qwc3OGCwDALZVmEA+m~L9Y?>WPw&4)^xs_qV zB~5#g>P_}&^yseP8%`G6+_`YR=cSs@ad*zFwQVRq`$kVbQz=wZ9sSy_wqYBUG2N$K^ z-@N8ib86Bx=jHkK2A-_W1S_hMJDzTDB%43}mftEnXPR`-&0j0&@GnY6lF=XBd_~@W z*%*t|Y`8;P9_m_)HZFy(z5YdKiS_%{X)H+$x?{#xh;$^o@ z`6_5XvP<1FNjmKM>8#t<%7(jcast_pC^7m&Cmyg1=#(wWehSbnuYQ^hyA^*bKTm=EuGjp&nXEgMcr9D z#Tq?!_BcyAa};Njw)q3|c5!72k;XVy7xO=r?VVm2Hm%h2;Wgot9v46ESeZW=Ep7dv zoO+(Bx3#y`t~s{H!9?88K{veml1^%Mx6734X{!^qe6FARJCmJTci);$^c0y+oHsbU zbHIOJz|4S7g~*KXXPtw7RgQ-575_-7n@eZ%U9-PllKxy@L}vrb)I#D{=~+!?wcl`S z>~E7ylwH%`C?<}8pTlF)-@m$~I;Y+qA$&3(<##r9HoeW{lB#Ii=`}BG_q#Ct)xlpj z@mB^@Wes}yB+r=Nm3sT`t+|TEZQ2-oW3u9&I|2Rcla61rX+Km_3o`|@8G$W7w-NlQvHR2A6*vhB}s4in)(V$ z)5^Scx^sLayza{EVyE*|aSO$d2R6y2WlyK?ziS%%dE;QAAUECN@rJZBGgXt-r`lgo zl*L<*lN9*~UU=)Mw6je=;FW3NlDhxG7N^8tgK{y-(Y$?ao7OF&1goF5^CWrFFFaSZ zJUQRr7{OlXy7TUhuEvShT&9^7Lr(|xVUk|oT9e()ZEwFn`#MKRUZ?Hu+hzAW^xESA6yI#sKVDx&lEV@g#)4NkJMJ8Z)`k6OF zs{ioh*qHZ&`}b+)rUcHNYq#UfEzDlYwop}9FZa$iX^fZfaQGwcw&W4=i)6#Z)wwk$ zut3poccLX;pra=_ou)16bi~G8dIko*#l^*h8-0HF>fV$liQO))3aopxtS~?3-MM6A zEMCdDQkxy-E-M?`3Zk#jCf5t*-FU-;wY zGTq6Qs~gg@I5qLIjy^fboc@d^zLX+=Dd8Q9XQ639Ug&>oGWw8&Jo~(D-ktybNg4d4 z$MygDYw-A^ZPA<>&6F}*kt|>9cQ#bw_zXEhD4EpHYcL5IS6y|L{@?H;2;s1CgcK{jofL!6}W`==Iql zZ^{3{k*X*QkEdy?-`z1}^ctg9x_tR^R;k~n`})r2yv2|AVN0_mt;(CLa~8ioCs-yx z5VjOuf0vZh6*$eh>?@PT`rfC>= zQqS6Uy<|Um-XSI<<8)uGPenpj3q6mH>e8<-JU*+lO4Z_jssDQ{`!kU|ROfW_4Z3E3 z4LH`TRaEW!V`^Vgqm71PN(azF**%%W;o28R(bNbu-LrJftwbBRl{6>*36JM2X-1uR!TY7H# zo7YCt&Qw*wW>ZsB&R^r!tlF;4jjR?6_g(nSnsY$G+@xf_OHcl=#%AxzR%S)PxKTyH zdO@T95VybOpN_9CLmorZ;^NnGb<`|BKe>Kl`SVf#ovbVqOA*r3m@mA9&``2JH=Mro z&_~yO67Vybn5j`Q-O4wt5EW2QE)2`zIo{QnqJHJdmFEFsefP394=*omSWq25ZZo_P zIP8(ON<7LNx1FV?H|1uuWS%68Px%-Js3wWIe9TdZF{<48vzVKDOFen{k5T=^FxDp=p}ca?G)KVKXu1 zm%P0CZNj*<(xHQY!duJ3X*sF*@e$x}DZ7a4SoF!=Q_fk+UbAG2eyV@Xbp_ zwZkhCOTDjd(l9YO(kS%Ig&pPmCe=M*9Izpko645r8#g(lv%6d^Hs`nPa9b*CB+KA$ z{LYj|-i%mnVM}#N$KRUa9^4g2;|PJ;uJw&1o`lc8Usb!&oIGh4Wy874+}Uxw?CZW2 zf7fxxgszL4N88|(HbZdpRlCm#hD}(d`f;pGcL@m;Y^=<*JXZ*pHx$#&(_3&y*c| zil4dFziE@&G0_skb(2Q=7VQn$23;(y>dKde_eV7@_Y@dsYCe^B>+vYIJj)V`I<%rHo}e_9PUK2o)_{7O1?b@0|Lqjv4~ z`}6`1JKR^Z%^X&)3QHMHi?gd8#rIVD$MFhZWEs7Sa_HwD9e<>j=beu>O^jdlq(G&W z`3ctB+Nk=-V3k|$@qEXD@>?8JgVi36(;q~<{hu@b?(S~BcjTmU<0FPl*Qsy6y!We} zEsH;3e2+X(leS16d*c2=%yD#|h7D~0jY^9ay1r7IXr&7xQ=Rp4AruLtWs74^?F!Ci zMV48&zg_!kdGF~@F>x0oL!%An1%HLHp8J1t3zW5ORc!O8(S+KbchDK9Gm+}n@TBS7= zyuhk{wR`^7)}Pj2^YNzs=ZqJvb1gsIKc*IxBlXISSw|<<442-_V_-t?2#tH(x(#TDfi2hK-x%BG*A| z91Sz3?5Z2pVH6F7LY#1AXRu3C6#LTLhi7RkPF3m(_i4}hsgDHgZgYI^>{jF7zZ5Gz zaIrxOp7(E(-a+EJ?Z@|~Lp=Jo5@;Ev&LE}{a=t6KZJ^YK?Mvc$dIq-$e_@7Q!8C&CQFuE2 zyVU%|r+cD4L@!`sAEhq!IVCEJRm8S4Qrd6hNk)lQrt(=90^u&;F+L-);Cl!>EXZJL z`;92`1J=HWEN%ZCNt6~XyYpAq#OuR@>@2ne=iquN+-3aS6%F|?xIQ?qp}@G>915YJ zT{mw#b?8goLjLqi52JkMYjuD8_#xYUH;B#53f@|xhw&APWr|~MJiRM5>bK^$epk<{ zR$TV(+IUAPV^5Ko`Was8?K4lUx`79*EeXf6J zkZ<4;Ngy+MBw;^!$`C6Gm{9xPqBlvc5EZMfVy=^#UJGvyum|jHEx?}MfmLBJs-!)F zp5tZ1uP=#eKXQz@V!5v=Uy5Mlqm*1AVWZJ6dmDNP!*nQ;u<=jK7h({a^A`bB@Y-JK zvH1S>u2!DjIj@y2-ApLeG{=u8tSf)MNP=mEhY&AN=AwcHHMMjCs^0@XthR;`={e&W{KW_%AhaX8Qd7ZDcg&Vj~(#kb-xc(|44A z2$$;Vm~C&Ye5-IY?JKqjU!H82gA~kh_Yi%#eGLvmA`s5Uks2`{d05|5Tu#3S#k_t# zJU=$%F%o<5826LK{=)hlZ!!O^*K92+&mbB2zyr_E=^aH8K3b2p>&|%>8$` z)+dp!?Z=036=!F`O9;sC$I!GLJ}ntYeBy$J1R*Q)u}_Z$ZN8n((aOBW~EBNo#{|ilA&eg`Mfn7i!9ib9oa)`aHQ= z2!&l8)#yL>1!(*BqrE>Hxc+A%v0GhgM0<1f^t!7asrD&c5HDogv4duRQNoJFxsE-j zX1nynMJiFmj^hwLe+&(~vL2WG4T#PIWKLwmnApqI=#x#!lEmGbjr!{B`vehIKCa$D zKVWeZZqks69{>IV zJ9+46c%H#yr&<09M=|G4r&s4&DY-rt?Yucx(XzJqlU8ywNXit8)K-o1VM`NMuCB_$oou9>w`ig)kcjYDr@;`k04 z&Ao2eY(=y7kp|@vyW`0w5eGAF7H+z1)bDP}*z^@=b`}^LmJVdrc>URM@t7*l&(GU9 z7X9SOliyyPQf>1TeChQw=bl&^L>NeC1$azay}YCK{IJ0B?e95m|76co4C&VSt_!~Z z?)~gsAN6q%tWMN4UK4GF!FKC2rxp3sL(!=E|F~*~>UcGYu_4&9WXCEy4(d`;B z2vhe{(g~?UkLf7hYz8MhM6uOPqgUaO>^j?frJ#1zAmQB2OL#-+eD;gN5Sj{*+jzPU zLJ@={MAch#{f^Cg=XVjSFHd8~uNR$~y>|`a(L!`sU))qcht(SkKsJhkLHu%ppv72v zgfxv5-(7OpyrQQN&t0~`o_oi8xyG!v-?(+9JFXSB$fuHBM-HN8i)S=DJaS9rLpQpw z-J#{V0o|ST2`1FpW;%U;+f5NS33VX@K2WY|gG&-ic0pluu3{vvRc6;{*zd1#j!ynl z?fVL1@FdT$r-D4JKWOnn;4okqM*KRnuY4=9(x{xT+my{3je&Y=&x6O>4!mS@f+xqD zx5V*rEZTp|2-})5-3`K_XTA&1%=a>k8J;Ha0k;2~XFR{Lyu`sCxp{GAI@E@n9l(6W zl*#c?v%&vy3L{WF?5*|j5c6C}^IiNvZ?xziMoCGHZ|H~c?k-?@j^2@<=j@I5Wb3~Y z9nJeoZM2@r-fiK37mbqCHPQ=N@AZrBrD@!Os)qXd@`#Bi*yvZ=kBcfAEx;vpl2U z@)a(_@;vMMR8{nG#-tYxbQXvx4w5lbX<@^{!oHQ+>9&6tooG!J@cQ%XNY0YB9>9$_ z__&hk=x^{Ba-{ny8F^8K>npKRnfjK=V4!>-YQ-~FNVVJ|YGUxJ<=FloVi3<>Qj5qv+}YC&PdWRV^*F#IELP= zfh;2<<5P!*4Bz9&j|H8_J~hn7)S`IBP>oXgl0^2A!itYRBrv+0zPfQ&HCg-`G=;NY zIP?ynHOhdh6qiRABuf&gNxuf4G&UY@8drX6K)J;7i)`|pB{ryr0P7fZp3%Y|%nCqo zkxmU&SI}}_{bb~6zDsiji*})wBxJP1XMqgvNmfOmD5-RxGgx2y8Nn_pDmuAH1=)&u z)DB~S_v33`MNfT-%zp6om0{1HM_fMm0B~_BV%JfCH_JnjHdMtT2=_UMkcvv7Nv*2l zpWTg~dfB{XT6(DXN(8xuXIBSej$v@Td<%f*S^$HMm$XVpPfl8^G%7ox+^vqY#(W85 zoI!9-nx^f#=m=)vJrUfu;Sxa>c&}wAER7cjAYxCD$UVZGpy>VBw ziHjS4{1RSEuIv}rc}U8a@SB^REaxGzZ7)d`tD50Zs$Nqf&}quNDb`2Z#SrLNzpdl? zK7As^N1glH%dxNAyoh+~H31~LdhId>t|`EBswbVjS=&=+YMSEtgS?vV&TDjrBjBh? zaoi+D_c@QR7n59KUA3~Tq>faD<#d?2lheBm$fImJn)Y@y$Nc{9e*_pW^ zsGxCS#Rq@2J(J5+iso>hNcIrV)Qi~%_gGMdg)QvWLn(1V)ePXL^V-7o1PPAuCkDM7TDT&Qz3A3}s27yf48sZsgb(aU6(KJZ@Kg}HcL-nksqxIExcx^?oQC%6 z``h%km+s2pBv^d_G}4DoSrXIsv9+ZZfpMT1V#$wkTpq6Ra;XyQMy2@_3;s|x+sntY zaI%k)eoYc%le>5LG2HA^?cZU$4^(038OiSBH4X89=LR?jDjYR*7mA9`R(`*Ca%`+A zdTDdMJ}WV+m59gOssE91&zqbWd~jT7mPYV4icbtRnJpH;waz-Ur`m_AJmyV=B$HQT zE4){1+bQP(NM?QI#HeMC0>z+bWClQL2|A?an-NkR$9Qx~JxhZT9Kh)?XOmk)izUD7 zD(>++X+6zx)CvrMKZi+^%qw9K=%oax9(9qL!B21h7mO}@H4KpG-I^r}N zFBL#(5Vr4algua2F8BQUR73>`1ZKDCPHHL1RCn*O$BL>yUbc1>7!N$iPjav0{0s2m z%PJtn*(Td^1D);CB!DaB_XncQ*jQOr5q>a;S7%h>)GgTc749ozTzQ*5-V)yeB|6id z<7e%R&y}nvPyW#``B84r{d;ASqP+X2vhw|P7b`Cb{`Aybucfiaz|~9Ko7eel@Nuxx zMWTw)c;uwPSG+1D;8y4JahLHLNf%fS(eXw?pYURTE_t^4(_nw-*aWS{Vq&NuDTbxw@Fp9;n#ouLGBUvH7%MVcmx9 zv|nvH=cbPYEKX~wJPIVMgXnyRT%o)bE0Qz z9UHxUAx(Vw5Zk}#tFveq9;FVL&&^gu87ijgjkwr5Lx&3adZBDZ8QVo9x9FEzU+L&Q z3as744fEqZ_V#(!g~tF3>hoJoBs{%2HSy47j3Jb(I!;H$^I!D`>n&xZ%JesVWf!z7Z!1F!IczTzZC&{xJ3;O+E@w@;) z_Tss<&?2B;E*{+i;nrLIrAm)ZiLr{8KT633{l?!2 zfhSxs6`42orS9w2#Pb4jq2AZ3Q{*z~&rEswJ&h4WM_%XHKhnOhr{+tbMbSC(_%-^Y z7BZX^=8JkzT<~o0lL!**8#=uz*PJ~4l8>&{W8xlCz2{y9pw!xoy(n{_v-!e%M2?tq zrWr3-N1W={y$xlv*f41w-iZWDPs%%jRpujp6x|b{q*P=@P{1+!tvnzh9u9;IANI)Gb$9!3$5f5b%NQ-lfFw{@_Jw{afM*uN9_3J_ zVt5*hJX~@u5Zcqri$6Y$KUQRXWgGe)a1~jvEL=d>B_9a|%svp}>J!wfu>`A> z$Iu|)Bu=fuPLlN+z(w6%iVJYjnLuE!6q@4&1a1wKAL-;LHld?>n0r55?Yp$jLd}V7 z=rIl8liA%WQB9H5NBw@8emRQm;5=Az7Q+=24mz9(rBkJkPQ;|%CaJt`lZv}~a z-$&MleR_nd1Qi}qI)0-wNXvl+?Vczuli2T_fucfru8$U=xeD{Sb5|}sJi&f`#J6P7 zwL|SI_o+-2)Io5OHPvFD2rPl28E|REE)(hr7j8c<+s7`BqR>EHps#l0ZDU(QBtSrT)VZ%RsF>#LvZTqw~PR4zF|h+WET*B!RQI(venZ z*Ah<)S6d{Y?O`5GhFc6*zM_LA(PS1A(;xQVyk#@^`V};s1A*)rm(iIKfq2ZnLMac_ z5FM3Xsr6&~fpWEt)j7LBYcdQxhZ92mE;Dga5RwP5;=eX0+tbHCo^*^k>G1l^9T9!R z9^xCcp&MDb`d}Pz_+zZ8@8Vti;TrwrgPt7lOA^fP!8Yu)scHEm609hdQ<;A^^w%du z?XB;Gk^}OhDyo3zTrH4!4*SSN#tTxTsKekHIQUC%x_fc(A_TlU?29GmdtZ2-8bZn@ zDncsJ9BQXbbq*p@Tkj%(8=g+=Ik>b9SGO_E(+4f1FO;l{;eiZ@Rd~h$OO@SqVH}pq z73)~L5_c7(F9&F1TwDL3O~W??z-Mf<#_<6{|3F%|`C9^5F;nyA7%sYqm<9A2R^;#+ zc^er3ghm4i<^DPaM*2@h?0&s5;JuI5J5n3=)P?)g2AnFDKqmjSi$E-~ALUe!0q|lN z7np`{X*H~cZO>CS2Z)o$=qA~k7lCXqICU!|7X2P4Ne_&Pi%>WGJMj?W*O!2GX?}hC zr}cKJmoberoI7oJTc7TeSJ()63bgHZ1(lZ!%Iy>G1-ek)QKn~teF0d#j zJesnCzEdq(%BOo~TTx#AGB^On05TAnjp^7rgRh{b=7vnW`5J`uzjTB1LgYird=273 zuq;BNK0Szn&66|UozxMK=mdT?M#L*JdcB`n^L_O^cSS@@tdBmiWK8t=^XKyQH)xI? za==L`kXQDMhBUPH=LNCdD4n7e3DFHMvaB)Z{^)jWvr_zZl<@5L&2{fqVIh2N~MM6mP3)?2qfe{q`wC=cc9>%wup zy?SCUzaFGV+i%Zyv?hX(zR}5F^3djOV%K|?wLI4+fD|85_g1=OA&L^Psil9$g%@VD z#BTo?9I+!j4R7p_1i4l7dzj*mIH0U+|1pCA^z@JTs9%$;wjfmwbe;RCTxLOx0U7m`&p zYDpinDqu@>-HBd?$XFl`b;LgdQ!sGI7c_}qFA-HYP{&J)j)&8Ict&m8C}%if`!RDT ztU#T2hWNIZ^*|jFHdz8+jeogOiBS0C$fCw!+k`am_z-|X}ygo1hN+m(Diaad!1al zB*n|i`{Y@XBTA!zs<=UAyvumAg@L$6vUu!up@6N$kc9Z_`!!H8xJcF3-HIAV5N(2r zJls*8XC8e-FeJCWdNl@koU5Sha^~t5iVk$?wCk5xCOD`+J@5GYYn|SH_}GQqhSj-& zao}*{9fqou%*``Q|9<#L%t$r#PMfT!8Pvu=ER2Ur7{=KBt!@R*Y9`>kZ=hq&vze)( z4629)Xj?F8BMWD8xt*?WPqUN34XWb{YnGJtK}+xjEVTr*$o=yB-Ff;QTkC9R%h8j1 zvN}z+u^@&+q0u)YENs#ITHM}}7ZC=?vp;6?r9M0%5(TtW;@Oq6l0*uOG!S~+gg@(DxX%T%FHkoK$MdTbKu3bIfO7&F)bm3Q;Aw?idMCj~zAvlX!R zbBNx1#LsgVQDjNXqwd(3^vqM_J4a3kpB;Y$Nd&o0B<5?_zzX0TL~4Q3@B>~^51Vjg1ns${rA;0&Nni(DruKEK8KfJmEm=*iQ+`4yAN)OM8ydI|unQPI&B zZ-dEy{mF42Z^jG?RTCw=Phk)n*}w6Pelk9*c`^LP21^sI0^StXLsf2wJy-(nbK>;= zZnOlKm_T>LG)5S zpaVwI{UhUF101LvbbcQr-O~Ur5(q;mD9X&d8MECNjIz{|JDnZ+iWQ(UxB6(J?iWSu zVNyx`ch;?t%shGK7Ge%bu&Xpq5}0H_{;qTV*ANT+EEr5FaexCYJySSQkq#iAEK0O! zj(JSSXRu~RZ;!APzCi=)z?mlHy&MTDM(bghtdO4_>iyVelsj?C_=p-_(>kL|5B|BX ze&ArIKM}hU2#&dJBEmqVu6VqMS>mim50F ziUD3k$rEL0G>?X9W7%96PooMy!Hze~0y5dQg+d@Dxk_K#rKnQp#! zQAWWrfx+DH4eelEgQMY%sx^hDz9rw4Rg4wls=gm8AhTHLPJAaq+5xk zuK+0+e^_AYt7a*$oBj>~oemyvO}8Q61uqf|MLrR?9IYY!7@SNAi%4Gg2m|LH!^>zQ zgOo*Bm6HVduoAW2kiT~ZkAdim)}{od$VAawQE+IqSLa6r#CMAQ$q%0p{&XtNbT7>g zwmvKNf=p)-2_g-oMf+upvO#_cJkNi4IjtAb>l3KSPdWW1tE#6*O%fSsE>XvBxswO=#w#nnZxiQQZLG6HYJS zF7<&AXf z`fso3u1{s9UijOwD58de-X#V%SVv}qiYFvO72q;X^YW@XRF2cN=V}d<*>QqGO~EeI z40sYli;9SmTi3O3(0h^mjy2xfAZ^&sJrhOF8qNz5W*G@*T8M2A3K>Q4|==W(u{=j=^tC%OT-rf<6{yphD&_^Eemc$WXW7H76 zhKh$92vlOuW1*+n*%R~ute%y-ZL5;$-ZA#26bv@-s|?79;zQHviV<XQLI z9Fi(3X9}u6f6$ns8w2a%1$DflzJ&Yi6F84wZd;^7OAVjH3h743@^d6-s_#;9a=Q$4 z@FwhO{mr+RC$F@-)avBvT^CQ_*Duaca`KoTP8J_adGtEwyrT((d4!SQN>|Rx3%(4X zdcs;l$3$O7cSZOhXFXHdgMM9iSJ%?mTvoVZ;$d7n zsDi0k2Jyd>+@G+0W-iDRd*na-TH0re;l8MrrATEmf#RRD4W~URrIsVbXaR#e3rS9d zQnShL)+7L)aJS zB3F*Q&{i+ME^4+IpssEdW73jUcoH0Oajh#>i}8uN-~F~08;fr?Qmlh=d&)GTpeIjP zQ&g{M1w6u=S7cn4HI9Oh&LHC1!qBr7`s^`Yy~1OBsohr0FJ_~>ARR#G^=W5&^9#%E zThBUOikFkVnACbF;DGo3z62etr%_6CEKkeZyN5E(-UL|=LGb~jnsx?+xUh|$i}Hw1x)SgIrXLEXP{KtToL!*^6bxIa5MF>Zo8;&r18}FyI`ZY3m^m1CW z5<%R8UPHdzLe;gogHr%g9s|u0YT|me+Wl7hyKe97!Pqn2Reu4RC;@a;RqW`WWdJf= z0F0r1su7G*=0IWoqN+{tc}{-{xuRT1OIuB)o<6@>|8IIQgp61Uz3cPjk27BUPd#mA zl0i}x;yVT~V9_84@YZvB(J3+Vr-UkwpOa;Z8nG^kDzJGrYfgLGg#aC@EQHZlC%R!Oz)_2uJ-<^rR-7U^_)!a9J zxFoK$ZH$0^@q6Eo7q+>tT6HsL5>>^c09LyE*Oz6}Km{2(oem)QzO`vG5gVwJF4HpA z-lzh)Ol*jD25}T*y?9isCGK|fN4pNX*E(Gm_=nr)_JdVO?P06rM6}|`mNt~dr&rXF zWcYjRYfbH|>kO#;ksH(?$-aa<|76-vX1NFq`8WDEQ|?9=M{=f%XGy(J62 zM^Gfsc0ZU;e{j(A=R>VmHx&9_-%bZUbjX#F>4Z@X4RZ9P(I20V=!4^M`_(gn>wQ2Y zA#q+a?$VxhjU-4I4gEujoGO1J@kR6dzKX2w7cnp<&+xXS%IvW}^O?hF%ER)w67L zHX})i=N(0j&rXplL02a@bcR&e)*V1sIelAe6qvD}xHR7~{pa9bdVh@Cg-5NvAbmAM zjI#s5@Zi3%bsKa0cj1+T-5^`|l-{HRT-tW5jk@z=1e2q8dz=kC z&SkM3EZ$$N*_)99-mxJDCh<0{^ip7X*f0!r9^hSF>eSX4x0c955+1Xe#{Un1D4)M^ z<<*T389+A_1L^M*my!+T{|pvH1#p37f=}*ZpNzy_i&H=_3>p33#*U>Q>ct%wyioQ6 zuwUjJ3vl2<{T)zKp1}9^53#^d&^zUl3UhNSBMfp0ZvYsCrCsOB+8@O>s_RwbafYXXH|^g; zA?3O`VQ@TxSc@En#1V!u|NP|Ke~(;r4=uCFc%AdOGBWFrgAaoC*0Fs3k{9bGm#&0D zx1YzKmY)iOO$>M_4WzFAK>ZcfLOT4Nb=S*l<6XL@vBU@i%otPg71Kqn0u{z`WB0Hzw=(qDV$^MaK7d4Jtv=Ayu72Jy}Z-maC8(xF=MLH0NzC8Z3T>_83%2JhiDU>>Hsmv~X{z&vV?sW_^6 zYh6`IsWNZ>go|<9T~2EJ=<|kWav{j8iQocgR3se4O(P41mseGNfK11AI~#WT_5QWA z{6>1%OvG} zRor!wS^I_RW2o&Rai=nY;ISuN7PmsSqUBx z3&?B5HXR%;dTNa!J#`mhf`NfXj~33oHc>!0Uj*Isci(+pq$x0uT0?wmGVI%d zBm5W&k6J<1H1|FlMn-#TLAC3gx@}s?;;bk73yeAHvFjfLxcw6fMsjcA`YKfic9M9PC zHbe%a7`2TIlAc9V2V9|-dmdnuRjONIYZvn9?`PP@XFO-&ki+yzBmjiF{8(i0L_2{N zy&-U4!s@=K?la5ePj&8Wq99>(?WqiICKyt0MIRwHA{TUlg`Vi(d%#N%k(Ff(sO-sM)b`$NrJ>F73>P**#%v zv%Y`GiNK`60GqVWHh+Wp1n`ALV2ngn>T{A$z_s_#T(|QvKj$cW#S7?aaMgU#u1eK+G5aACFNk)*H zqTvGZLZ=vHzQH#XB3U3d{vq3_YT$=(ddMxug)A?W5MuVM<%QVm^qUJRi!+Sa*NXsc zy=yA~mN&)n#3xC3e4V+a=l22F|8{2%c6NQKtA#&1#1BI|3q~BD+9NRj1}41D_K=`+ z?4W+{NJBz9W9qjOYbqQ1;DNA(tG5()inqAieQ3P5?<=XQy4q5Iv3ePY28YI7HatT7 zUx=S!Z(c}gNtUR{d58D&nB0^GgOJ>*0?t!iSnt{1bGnt1bzkBIQo*@=S-t%GqWZp? zAP%ec_V%dANUBk(0}dXyZ{L1y>aAX&2+(;POgsAyBwA{~^Tk8A(-cH{XzQ0z8XogU z?rqb`6L3qXKaFPkYV-0#l*gfC+~zbT5W$9m(x$k~k&_z=Gh<9D&}@l36oLu8I%eSY zg|CPcY4KUe1Y!QeNs?ZI%x-Dqjygvt2$q@eByn)g(@aYXYgeB_i!JZ)`ujcCKc2)= zP9RicAK7BuD%yAAG#KUU}_t;R5nqwX{!$?hN7ZO0SufLpO9%x+2F!AZ@{wLALOVSrVCa?0F7lG;Y2qns?GAK8k5p#n3JL|hhM6Lq zc!};4=h)yt)J|GMK|lm1mr0E$VYS(T_#sLBU}#-F@xqsE43Doi{&pqadv*3d$p@+$ z=+b+RF_7*VzP2WbX(NBRcm^vXOJPOpV3J}b_B6~!T*m@iS2elz=_!Gz+e4;Nfd??P zE#||WVl9IHHvuTN<>@XJ=!qsGwXPf{E!)x?!SVtuS!I$$18YosFtFJI^w{~VBnb(lGFrU;xn?_Cy!ah>XQ+83|vnB zVjjI52#j>->SiCrtIJLn+aV>GO@NL5Uq_s;KpGn(;4Iw~{|%cF$0WYJDmQu$-udKZ z-XMZD4(X&IEfX;NP6NH6&P6wlZb4O)0GE8>e&9sCpaUU)0t)z~wPG@~|L#J0kP}dX ze?&Uz!`>Kd4}K0Z#6pSM_cFWu`urG@un$fxh8X#eXhBka+@<`C>au_n%=#WbcOzIB zyfVT(>dl-XRykAtlXIERL_ulz34<1W|02egPfazF* zhEWOHfN>b}bT=dC+UsZ%Wi-p&kZy} z984r#hU2oY%F&%jTqAM)bQ+P&OC&$d!O<+#D#-?(sd!M`nl0Op5=WcxXOOdx);2-# zUQ7dM(PMziV!$7)3c9WA%UN3J6yP{9BKJgkN+=Dc0#|O|#|qq;sGooL%WEzFn~m{@ z^q!LfSC(IF*Tat-eW%66`~QKupydyYs4Js*D)7tv#2DY+{2R|x@LD#-Z?&&Q;C%^k zFUj8MdUFi(TnDKwu5dY2OT zV1)P^g#NvaJh=O^%bgfu21R;nLGIlZ7*|0=ll)OlsTfZrZNet=$twIqrl)Jxnj$6W zyXIsAjW1$?s{(0`?S=lu&n(kFp(F4>K{ zYA9JIk|lfhpjMVT1NostD#kkVstJM$%lA!_!C-*E|JlE_>qslt2?q+*(#jOkK} z)||WS1G@4O*nblzjCO|ucKwi`?d1uBO)9X4#w9gXUCm7ZC3I){E!cYwwec@{pU8^S z-~`4l_dx;A;EG~D5Y^^jsM4x(wb5wet*ps?@JurTXvcDMhBek~&6kQ&2L^JYyVLzm z?VitdDz!|GF;N$Icg!E@@d%j_3mB?8K`S)i-}~XbTHA=o?$)1(O6worLzv>K=j#UE zdjofh{C-~TsWM>TF1P0b2S`*DyNaa_ozS$(3!##PXQroTg&{c$M_7cBK!_82M9noJs4?-mn;yKXD zMiqqxBkVa-U$#pR$eNb4t%)by=O9i5)1~tgI@?bxU-%ejM z^tSEJn=%MC#L1eB&J@Dj*RJ>fg24%aTP8@Gv_J zZnMxGV~3Z<)#WunEj`x-;?1uhx=)~jx0Qv;77J6U$E1SNhT;~7$;$e(YT(Ebwn>A$ z$%Yx~KqLJ_IzvI*68xY6#2WLZv8LyDE=-b8>?wR?y=o0h@ba6ttew5Hte-I)g5 z-Zz+T2>5;f7P0&@kDd=qV+{IhnA3X}jX8#|mbMy61@t%hEs2$GoIyktp&SDsKOt@!BF@nEuE_J4uZvC&hSBi_X#IA8uWzEp9eIp~Ki z?}e|dQ}ib?L~Fwt&4wUqm;fT?zTrdQW2$AaV;!Xr4A|WqfpjJBvQ`VP=HGYV zB$1LrBg?1L-gH`xkQuQSIgUu9KoPd>M8kD%|23+khXFNOb+TnT5gI_r%K)D&Vg%4g z9rQd0V(zmN8-M1=D+FM(o-Q25UZnPfv0HP%b%H*t_FOQ9p?SkBi}+MzI9Y%u_%kJx zxMJ$H#YQFtNW`=L@OD-u!eIL&0)h(IjM|xqNd{(wv|J`@fk*rGmhAv-*#CS~CRFh_ z$=px@z7g+Skq0)--D1B362uTG8UXc;7A^-_J{{2HepA) zuxk2cq-$Cm@tK|>JEymsdKe$;0`#x8Iz;(7J80E9>tcvi?} zQtd-G!fOKlxbya_x||xm+He}cIp|wWun1>hChOTHyw|@_=GZ9r+SP-ePHv zXqYW&G-{`7q4_K4KlJ9`Q_6#y3Z-HO96$9M)RpnO^d@Q5nJPw}|9+ne`I$l(y<%Pf zc3cT2u`@twcC8pYU@A&HpJtBm=RQ45?S`88=vL%cg2^s(kWO7I%-@K!o<`X*a1GZ%HZm^|*RBo>P%97F3@_rl3r!sjq*Y?MEuvZ> zxj)-GH6+-0xT5y(K@AoF;DD5#diNR|qIP_8ibHP?fd@t+7gfT`7il{*Yv2r2UD7YN z*9V9EBU(6Jrqws9_qOQ*vd&U>U1kYlFM^>Ro1TJXBbP5C9P7Y)1r*6%uiv|@7rv~g zQ}mibn$A%@Aa9kV!AF5#vMFn3VI-z~6&Q6ksPZQJ`VtGO2yw72Q#lq5wnafH%e|*; zw0EEXth_q(XjT{Qqg!Af)e3&J+z?gYf3H!7&(i9;cvgqLZL9>2C-f%A62O`?2G;m# zmZMC2F&Kx2*^OeUBCnAmQ8?NcCIFMiI$p-g?|oJ|VrvvcsLhK=fw{AysO|w%2THEz z&YgQh5w^Xz>t=?qZ38+Cfd^!f=ky*W6F-RBan--8T1tK4@6JY>*QI#MO#58nY>^hio8G7n3;Os;A4_+uW9iVAgml`$GYbAB+E8$XE7R%QpcY47rM87f_(F;tw?ptS= z4mG{?&3T|*Ktc_lyk&5084v=HIHgJTg;@Av7nqy4P{k;+E(z`%uB^hrymAI9@2tWu zd@Yl)OtFxQ(^c%tqG9y{RVmJa1^1CaBaN1+F+tn)d_yuT3)a}F^`nrnM zkLY8_{CTQvv}S*At^%p_&e}}`agi5@4tVnH;`ldm6nl(42@U9lRHph)UyMJN5A6bD8I|h_?Dn@woi z2zsiE_wVg}!2^_@f7{5BVk~?o5&Dt}WTHTg9q0)ByYsu-GVQ0A7rwuXdzjd;w`62) zu)LZ9q`jqEQ1jBk$DxOu-yS1=72p@T3}42>4quN{ViQJ&=Di`?x1kiAT;(pWJzgLixTt%|Y6CS%-l5xQQWqR;=qsFv!T~BKo)>!B z`)X{-!NUR^4i2_NuISs`|LNt*|DoL5aHO=Hw4IWQQjF|nsSq<6TlRf0wulgwHRh!# zEkcy+OQJC*yM~aZ&PhVkL{lWRBEv`;j@^5IjXLjN@cME1%#7!GzW06I*L_`=}c;&QkT+rJ*=3#)dj1sQ^?++`kgYp3P*x1Ubrgqr+14`&e@qR48SAiuJP#Ey&u zu2;|s#tt@6NVW#yQe6{nxm?y9c939zRI1~TwC?<=~+H7TQ<0i)A7c z0Ib?|PS*@#LjLJFM_-{+?e}BT*UQ|Fi_2d<%HHLrERsDo0@OwS%S>sgXE(on)9&g5 z>%WOUfbbXbsXS{g^>8s{c=!sqMY1-MLF)!#geV8`3F=^>@YXp+>Cje^k&Wfcvsj^u z=fztLy1BJz(gwa+Q}*4;YN);b*XCN~85*exS}Gj( z7Lbqa%>I8Acvbb=9)374ofa@Z^I;(gcxOGdNCcx<{|2?pXHF|)DZlE372J=+i)yy3 zk>44G#?%WL z9cz(BDMylKIa=SsP0g3;$Y?Y?U`{sdvNoVqTH~mn=(sE0zw{+Wik>{vO$J zW-)64-K}IuGO4SLX5Uc{oh*foORrc)F)_@T0<(IWmIb4a`S%dGg~;qy190$iM;i^e z=o8ce!@cSdZ~}`YQRPp}8IlIY7%H539Vrk6#*pD&VSdQilsf0VcR~3Z@KHcrBbaz$ z?abMku7!SZmbk%UcMMjg+c;77o)ZvF^aC-%&hrr29q3&8VE*TyfwA zu9}reE?ptqV?}r;MlEkS2Xe>Iu8O)k50f$d5K=UlbElfVcdcvpdDsJaKt>0pt?r*F zwli_KhJ>oz!3#Z}mrP+1CJ6)tkN=zMN=Me4f$4#o3*ZiA<|DE^!K2r=Nf`5_5NY4+ z^G)wE-KrMF_vkG;(S~4s1u{`2IHNPcF?poc{?>qyL;WXEOtj-R_QH8sIL4sn3>$k1 zl2wg#n+)(Frw*LvqI$TLx={5_HCny3q`xb`Vl) zRWIBA`wqzP7)WGyo#_MV0A19x?(sAI*ATTO0n9)NLYVr;H-aONd!3v^A{ICd*C+cs ziii>pH30_*7uN4QYzl@h59&U+a|UCc)a|0%mTpODbhsN42jht>01m}4+e**ZIyFIT zDQLjSTH4z08j}wH{@3~B_BuE7DS_PzoduoIwv<3`KObLaTUap|f|xEy;wxug?!Bt$* z{Pf7Ibz8n4>s##N8;@Te@p>1qqNFG%*`19xgdUU;)j|G@ppg;wJ*WPz77|eOXAT{o z8fRKL1%ncp7FyW5avOxdIj&6(y4=T}D6Gn=%or@O>4=k;ewOk&g~Ly&+y^>+8A^Kj z@HB9A2g}Ip&7I{>NJ1&|;d;i#sNMxvuNW%H`>+FKo&AjIHj?1oiLW_xM75A@C+o6p zuom)k_1eu3`t6|eZ82ICNNdCyPCJml$GyN9V6vxO2)pN;gTnMq4Gkm`98-&T*4TGR zO5_)7Sz~1&5KyoN5u400Go6`+i+Hj~MP4wrYi4}=ni8ubQ4IonPBl>b44FjxdQ7^j z#-ibT7&vWj9w0(l{U+}z&{XZHxv1t?e6S|Al%80d?>7OVO7=bSo~=C}pIl-W7=HbA zI+yWMd6SHAf!A8lLjA{=rprHGSqw%0dwSU`WI|SH6iGFov|G271P4cALa;QH*Xx3d zK02V>2RI(p-=dZOlNLFP7IWuZg8n~$vmwaWdHToTv5%gd_P;oe^7W; z1juNOVSm6|COz>$9jrA8FTu=r}>p2wn-(aOFshXGQJjl6vM7)+7k3JUE~ z z_ob9`37Enbv|fW>+(NmZ%GMgL7Lwk911OLPyjyzU0*^{3(M-nK-PIOgQA*!lI-&DP zb@p^3>5nP=D2$sob$5j^Lw|U>^}Wm__J;m6N{GpOyYrlk^x27F+mKI}1;vgWu?Imy z;&w{V%ou3WQx0`Q*WTy)EaoOaOv=lF8-aWpa~um4S)p{gyHo5g(t`qQ=-|^+x1(g0 zM{7OOm_ddbh9L7P8L&=H&p)F=>6sbI10^f?q^$3MLpMQ$hJ7`qe44d-lW2 zl`fNL#_Dc@&5gwl=D;wzu?&;zIt9qCA1!uQg+X;N-~PY-tV!4_vnohqtbEnO0lSxTmJNO(hxOGJrHg4K@*L28QKH9O z6!uP^G9hXQ<`}>JVZ%DAr4>2o-5XeCO{9(AUc<)TBGD|Z+=ODX>NzjfzUx6+n>gs= z&QE^g_je+ERy=5++DflYDSHlgUTQHC4J?Do8mt3}e~q2vJTl`StD40BMRWe%Tc@V% zF8#*!xkNTZW+qovRWV!IW`H|B7WP7TXpLo&ofQz^NurNI3cyX5r5IaC%7jQjQt?x= zuo#$A-=lfg|E1ixM>DHMAs-@4Jm1_;6pAW@P+fOh4c7-^54sK2h4a9H(cjIWJu!>( zR4xo#_}q5X-=`H&0zEE7ojeGW;{KyVstA65kC;!yF}2SvjrfZw7>rF2Y;%*uWx6-J zHm8!zYXx%e7s-=)Q;nIq;ssKz*_@_c6a~uIHT(>3WtQd-Clnjo615veNl zIY&e>-ew}|f27oyXG>D*7b@?p?>EJt;dxV#TS4to9-M>3)b{as;*K?c%GxR6GaX58 z73-q=I^)OBn!%JzPppJf<;TMDGIPW3&mx!)`ohhnA~za9vPN%QPZHgIr)+}S1RjnS z&Q3o_EYO41ls2&yfdWCsAX$WM5$iWQ>D!j2ZJ*#6ULYtoXoi7`gxBDCBYkjO@J`l$ zg1~doCY=W)ABj-sdkW9P)*pf=HW|EZQI#GSfLk&zaa7-tB&MV(0z2=)$^r1WC>FUD zn_I-mtseT|)e(qvsZrN;3LH)jQVmJtN_WcMmS72yY2IF&YCxp)lK@aD6*R3=v-d6VKhB$oYJw@mgXntsFGB?pgPpRyf$ji zqy9(mkCxz{t#v3O)xh*oFRDAIr>L>}r!(_)lLN}l)vttN zvx^D(5SFd-k0PNsp3&?eR@v~Tz<0b`^I1;iIVmDjKeAtGHBc19f+d zYu&(j{G<9^$~<)e<(lfZ!X53TkzeN5uu*_}erGw0;BXMxA(tBGQD~gYS1)=Mxi3vZ z?hW%!jW>6=UmU|P0mZ#?(-Lit-folvV+%L1F0wW}sJ(dZk0B25#m)Ik-fZwpKa2Up652|- zwn-Iyq3{Jrj~KrrO9XB3hOLJKS+`LfmjbVR?dU~>9g!DJ(IsAkDm)Bi#TQt0ZbdPD zG>2RE5Q_~(24=! z56BX&ilEG6Mu4P>x=~PvQfoldPxnvrU>$xBDF_LmWS}}V)T>}=U@cMz9(DGFDbIEu z9O?$LGcf4L9_kIw73#SMLQKrvH~3kxyHu@i(AuQK%Dxw~MLPvP1{-0p{t1!Gp%e+pWR8(Z^1r0F#UbwC1?ILgfw4cmKq8x_yMxy~7gF9zM6 z{_TCoZ&`Qfcadt`>DxQ-i(?BBCYG+8v{fL;$&fOInJ7RzIyTlQZ0FXkNhBF@}63G4T843CFA4ccf$=;?vb6!S=Mzm-%+%L zz>Sy;LOEKlMH>}=9UW&^6!zxZw^8-?E58L}zfk8vq_zD2{K@1(J?liPLWYFkGWa>B LZLC$S>3s3Oz2Z?y literal 23682 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 Date: Thu, 14 Dec 2023 16:21:15 -0500 Subject: [PATCH 196/332] auto-commit --- README.Rmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.Rmd b/README.Rmd index 045d89c0..a1fe603a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -95,9 +95,9 @@ Like other statistical modelling software, the high-level purpose of `macpan2` i 1. Information processing begins with accessing and preparing numerical information from various data sources, with the output being standard numerical R objects. Depending on the nature of the analysis to follow, this information could include default values for parameters (e.g. transmission rate), initial values for the state variables (e.g. initial number of infectious individuals), operational schedules (e.g. timing of lockdown events or vaccine roll-out schedules), and data for model fitting (e.g. time series of hospital utilization). This step could involve connecting to real-time surveillance platforms or reading in static data files. There is not any functionality within `macpan2` for conducting this step -- [`macpan2` does not try to reinvent the wheel in data access and preparation](#modularity). 2. The structure of a compartmental model is defined in one of three ways. In all cases the output is ultimately a [model simulator](#model-simulator). - a. A model is chosen from a model library and read into `R`, optionally updating the model structure using an [engine-agnostic model specification language](#engine-agnostic-model-specification-language). - b. A model is written from scratch using the [engine-agnostic model specification language](#engine-agnostic-model-specification-language). - c. A model is written from scratch using one of the [engine-specific model specification languages](#engine-specific-model-specification-languages). + * (2a) A model is chosen from a model library and read into `R`, optionally updating the model structure using an [engine-agnostic model specification language](#engine-agnostic-model-specification-language). + * (2b) A model is written from scratch using the [engine-agnostic model specification language](#engine-agnostic-model-specification-language). + * (2c) A model is written from scratch using one of the [engine-specific model specification languages](#engine-specific-model-specification-languages). These three are alternatives, in that if 2a is chosen then 2b and 2c are automatically executed and if 2b is chosen that 2c is automatic. The choice here is just how close (2c) or far (2a) from the actual computation engine do you want to be when specifying models. There are several [considerations when choosing a model specification workflow](#engine-agnostic-versus-engine-specific) when deciding which alternative to use. No matter which of these approaches is taken, the output of step 2 is a model simulator that can be used to generate modelling outputs like simulated incidence time-series or reproduction numbers. 3. Although model simulators come with default initial values so that they can be used immediately, typically one would like to modify these values without needing to edit the model specifications from step 2. There are two main use-cases involving such numerical modifications to the model simulators: In order to formally calibrate model parameters by fitting the model to observed time-series data and/or modifying default parameter values to reflect a what-if scenario. In both use-cases, a model simulator is used as input and another model simulator is produced as output. 4. Once the model defining and numerical initialization steps have been completed, model outputs are produced in long-format data frames. From a41e0799840cb695322bb4445397b7a50af1c48c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 14 Dec 2023 16:21:16 -0500 Subject: [PATCH 197/332] auto-commit --- README.md | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d87f9c9d..1bd75c9f 100644 --- a/README.md +++ b/README.md @@ -171,32 +171,27 @@ processing are numbered in the diagram, and we describe each of these. 2. The structure of a compartmental model is defined in one of three ways. In all cases the output is ultimately a [model simulator](#model-simulator). - - - -1. A model is chosen from a model library and read into `R`, optionally - updating the model structure using an [engine-agnostic model - specification - language](#engine-agnostic-model-specification-language). -2. A model is written from scratch using the [engine-agnostic model - specification - language](#engine-agnostic-model-specification-language). -3. A model is written from scratch using one of the [engine-specific - model specification - languages](#engine-specific-model-specification-languages). These - three are alternatives, in that if 2a is chosen then 2b and 2c are - automatically executed and if 2b is chosen that 2c is automatic. The - choice here is just how close (2c) or far (2a) from the actual - computation engine do you want to be when specifying models. There - are several [considerations when choosing a model specification - workflow](#engine-agnostic-versus-engine-specific) when deciding - which alternative to use. No matter which of these approaches is - taken, the output of step 2 is a model simulator that can be used to - generate modelling outputs like simulated incidence time-series or - reproduction numbers. - - - + - (2a) A model is chosen from a model library and read into `R`, + optionally updating the model structure using an + [engine-agnostic model specification + language](#engine-agnostic-model-specification-language). + - (2b) A model is written from scratch using the [engine-agnostic + model specification + language](#engine-agnostic-model-specification-language). + - (2c) A model is written from scratch using one of the + [engine-specific model specification + languages](#engine-specific-model-specification-languages). + These three are alternatives, in that if 2a is chosen then 2b + and 2c are automatically executed and if 2b is chosen that 2c is + automatic. The choice here is just how close (2c) or far (2a) + from the actual computation engine do you want to be when + specifying models. There are several [considerations when + choosing a model specification + workflow](#engine-agnostic-versus-engine-specific) when deciding + which alternative to use. No matter which of these approaches is + taken, the output of step 2 is a model simulator that can be + used to generate modelling outputs like simulated incidence + time-series or reproduction numbers. 3. Although model simulators come with default initial values so that they can be used immediately, typically one would like to modify these values without needing to edit the model specifications from From 7626a386084570d2b9f8949557365cc9d73b911a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 15 Dec 2023 09:29:54 -0500 Subject: [PATCH 198/332] auto-commit --- NAMESPACE | 5 +++-- R/tmb_model.R | 12 ++++++++++-- README.Rmd | 25 ++++++++++++------------- man/mp_tmb_simulator.Rd | 30 ++++-------------------------- 4 files changed, 29 insertions(+), 43 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index a653f8e2..3b9775ed 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,8 +38,6 @@ S3method(mp_labels,Index) S3method(mp_labels,Ledger) S3method(mp_reference,Index) S3method(mp_reference,Ledger) -S3method(mp_tmb_simulator,DynamicModel) -S3method(mp_tmb_simulator,ModelDefRun) S3method(mp_union,Index) S3method(mp_union,Ledger) S3method(mp_vector,Index) @@ -205,7 +203,10 @@ export(mp_square) export(mp_subset) export(mp_symmetric) export(mp_test_tmb) +export(mp_tmb_simulate) export(mp_tmb_simulator) +export(mp_tmb_simulator.DynamicModel) +export(mp_tmb_simulator.ModelDefRun) export(mp_triangle) export(mp_union) export(mp_vector) diff --git a/R/tmb_model.R b/R/tmb_model.R index 8c7bd03e..f4fc9af8 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -167,10 +167,18 @@ TMBModel = function( ) } -mp_tmb_model = function(expr_list, init_mats, time_steps) { - +#' @export +mp_tmb_simulator = function(before = list(), during = list(), after = list(), initial_values = list()) { + TMBModel( + init_mats = do.call(MatsList, initial_values), + expr_list = ExprList(before, during, after) + )$simulator() } +#' @export +mp_tmb_simulate = function(simulator, time_steps, mats_to_return) { + simulator$replace$time_steps(time_steps)$update$matrices(.mats_to_return = mats_to_return, .mats_to_save = mats_to_return)$report() +} TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { diff --git a/README.Rmd b/README.Rmd index a1fe603a..5c391574 100644 --- a/README.Rmd +++ b/README.Rmd @@ -57,26 +57,25 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World ```{r} -si = TMBModel( - expr_list = ExprList( - during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection - ) +si = mp_tmb_model_simulator( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection ) - , init_mats = MatsList( - S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix - , .mats_to_return = "I", .mats_to_save = "I" + , initial_values = list( + S = 99, I = 1 + , beta = 0.25, N = 100 + , infection = empty_matrix ) - , time_steps = Time(50L) -)$simulator() +) print(si) ``` Simulating from this model can be done like so. ```{r plot-tmb-si} -(si$report() +(si + |> mp_trajectory(time_steps = 50, matrices = "I") |> rename(prevalence = value) |> ggplot() + geom_line(aes(time, prevalence)) ) diff --git a/man/mp_tmb_simulator.Rd b/man/mp_tmb_simulator.Rd index f56b0b94..96fed9a7 100644 --- a/man/mp_tmb_simulator.Rd +++ b/man/mp_tmb_simulator.Rd @@ -5,43 +5,21 @@ \title{TMB Simulator from Dynamic Model} \usage{ mp_tmb_simulator( - dynamic_model, - time_steps = 0L, - vectors = NULL, - unstruc_mats = NULL, - mats_to_save = names(vectors), - mats_to_return = mats_to_save, - params = OptParamsList(0), - random = OptParamsList(), - obj_fn = ObjectiveFunction(~0), - log_file = LogFile(), - do_pred_sdreport = TRUE, - tmb_cpp = "macpan2", - initialize_ad_fun = TRUE, - ... + before = list(), + during = list(), + after = list(), + initial_values = list() ) } \arguments{ \item{dynamic_model}{Object product by \code{\link{dynamic_model}}.} -\item{time_steps}{An object of class \code{\link{Time}}.} - \item{vectors}{Named list of named vectors as initial values for the simulations that are referenced in the expression list in the dynamic model.} \item{unstruc_mats}{= Named list of objects that can be coerced to numerical matrices that are used in the expression list of the dynamic model.} - -\item{params}{An object of class \code{\link{OptParamsList}}.} - -\item{random}{An object of class \code{\link{OptParamsList}}.} - -\item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} - -\item{log_file}{An object of class \code{\link{LogFile}}.} - -\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} } \description{ TMB Simulator from Dynamic Model From d2e63e1d6106698376bb65c996ca60cd24d786cd Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 15 Dec 2023 09:29:55 -0500 Subject: [PATCH 199/332] auto-commit --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1bd75c9f..1807af60 100644 --- a/README.md +++ b/README.md @@ -106,25 +106,23 @@ the following command. ## Hello World ``` r -si = TMBModel( - expr_list = ExprList( - during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection - ) +si = mp_tmb_model_simulator( + during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection ) - , init_mats = MatsList( - S = 99, I = 1, beta = 0.25, N = 100, infection = empty_matrix - , .mats_to_return = "I", .mats_to_save = "I" + , initial_values = list( + S = 99, I = 1 + , beta = 0.25, N = 100 + , infection = empty_matrix ) - , time_steps = Time(50L) -)$simulator() +) print(si) ``` ## --------------------- - ## At every iteration of the simulation loop (t = 1 to 50): + ## At every iteration of the simulation loop (number of iterations = 0): ## --------------------- ## 1: infection ~ beta * S * I/N ## 2: S ~ S - infection @@ -135,7 +133,8 @@ print(si) Simulating from this model can be done like so. ``` r -(si$report() +(si + |> mp_trajectory(time_steps = 50, matrices = "I") |> rename(prevalence = value) |> ggplot() + geom_line(aes(time, prevalence)) ) From 2516ecb77c8bc38f3afd36c52c371180e2d106d0 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 15 Dec 2023 09:41:04 -0500 Subject: [PATCH 200/332] auto-commit --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1807af60..07b03cd8 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,6 @@ print(si) ## 1: infection ~ beta * S * I/N ## 2: S ~ S - infection ## 3: I ~ I + infection - ## - ## NULL Simulating from this model can be done like so. From df33ce0a96a6d092dc6da13c68b2e7f893ee7e7a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 15 Dec 2023 09:41:28 -0500 Subject: [PATCH 201/332] better printing --- Makefile | 2 +- NAMESPACE | 7 ++++--- R/index_to_tmb.R | 1 + R/tmb_model.R | 26 ++++++++++++++++++++++---- man/mp_tmb_simulator.Rd | 30 ++++++++++++++++++++++++++---- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index c398fe6a..861a3605 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ png-readme:: misc/readme/*.png readme:: README.md -README.md: README.Rmd misc/readme/*.svg +README.md: README.Rmd misc/readme/*.svg R/*.R Rscript -e "rmarkdown::render('README.Rmd')" echo '' > temp echo '' | cat - $@ >> temp && mv temp $@ diff --git a/NAMESPACE b/NAMESPACE index 3b9775ed..0131b9a0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,6 +38,8 @@ S3method(mp_labels,Index) S3method(mp_labels,Ledger) S3method(mp_reference,Index) S3method(mp_reference,Ledger) +S3method(mp_tmb_simulator,DynamicModel) +S3method(mp_tmb_simulator,ModelDefRun) S3method(mp_union,Index) S3method(mp_union,Ledger) S3method(mp_vector,Index) @@ -203,10 +205,9 @@ export(mp_square) export(mp_subset) export(mp_symmetric) export(mp_test_tmb) -export(mp_tmb_simulate) +export(mp_tmb_model_simulator) export(mp_tmb_simulator) -export(mp_tmb_simulator.DynamicModel) -export(mp_tmb_simulator.ModelDefRun) +export(mp_trajectory) export(mp_triangle) export(mp_union) export(mp_vector) diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 3cba8929..7a204b33 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -28,6 +28,7 @@ mp_tmb_simulator = function(dynamic_model UseMethod("mp_tmb_simulator") } + #' @export mp_tmb_simulator.DynamicModel = function(dynamic_model , time_steps = 0L diff --git a/R/tmb_model.R b/R/tmb_model.R index f4fc9af8..30728668 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -168,16 +168,34 @@ TMBModel = function( } #' @export -mp_tmb_simulator = function(before = list(), during = list(), after = list(), initial_values = list()) { +mp_tmb_model_simulator = function(before = list(), during = list(), after = list(), initial_values = list()) { + ## FIXME: don't love this name, but trying to avoid conflicting with + ## mp_tmb_simulator for now at least. ultimately this should all be one + ## concept, and probably mp_tmb_simulator will be the best final name. + ## using mp_tmb_model_simulator for now so that we can move forward with + ## the readme file on the refactorcpp branch. TMBModel( init_mats = do.call(MatsList, initial_values), expr_list = ExprList(before, during, after) )$simulator() } + #' @export -mp_tmb_simulate = function(simulator, time_steps, mats_to_return) { - simulator$replace$time_steps(time_steps)$update$matrices(.mats_to_return = mats_to_return, .mats_to_save = mats_to_return)$report() +mp_trajectory = function(model_simulator, ...) { + UseMethod("mp_trajectory") +} + +#' @export +mp_trajectory = function(model_simulator, time_steps, matrices, params = NULL) { + ## FIXME: this is just a stub to get the readme file working + (model_simulator + $replace + $time_steps(time_steps) + $update + $matrices(.mats_to_return = matrices, .mats_to_save = matrices) + $report(params) + ) } @@ -421,6 +439,6 @@ print.TMBSimulator = function(x, ...) { m = x$tmb_model time_steps = m$time_steps$time_steps printer = m$expr_list$print_exprs - print(printer(time_steps = time_steps)) + printer(time_steps = time_steps) invisible(x) } diff --git a/man/mp_tmb_simulator.Rd b/man/mp_tmb_simulator.Rd index 96fed9a7..f56b0b94 100644 --- a/man/mp_tmb_simulator.Rd +++ b/man/mp_tmb_simulator.Rd @@ -5,21 +5,43 @@ \title{TMB Simulator from Dynamic Model} \usage{ mp_tmb_simulator( - before = list(), - during = list(), - after = list(), - initial_values = list() + dynamic_model, + time_steps = 0L, + vectors = NULL, + unstruc_mats = NULL, + mats_to_save = names(vectors), + mats_to_return = mats_to_save, + params = OptParamsList(0), + random = OptParamsList(), + obj_fn = ObjectiveFunction(~0), + log_file = LogFile(), + do_pred_sdreport = TRUE, + tmb_cpp = "macpan2", + initialize_ad_fun = TRUE, + ... ) } \arguments{ \item{dynamic_model}{Object product by \code{\link{dynamic_model}}.} +\item{time_steps}{An object of class \code{\link{Time}}.} + \item{vectors}{Named list of named vectors as initial values for the simulations that are referenced in the expression list in the dynamic model.} \item{unstruc_mats}{= Named list of objects that can be coerced to numerical matrices that are used in the expression list of the dynamic model.} + +\item{params}{An object of class \code{\link{OptParamsList}}.} + +\item{random}{An object of class \code{\link{OptParamsList}}.} + +\item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} + +\item{log_file}{An object of class \code{\link{LogFile}}.} + +\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} } \description{ TMB Simulator from Dynamic Model From eccddd30bf085b4a4b8e6e3c1593d608b056e4ea Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 18 Dec 2023 09:08:48 -0500 Subject: [PATCH 202/332] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07b03cd8..8128cdf6 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ si = mp_tmb_model_simulator( , S ~ S - infection , I ~ I + infection ) - , initial_values = list( + , default_values = list( S = 99, I = 1 , beta = 0.25, N = 100 , infection = empty_matrix From 0094572d8a6a09ab2e8370dbc3453710a4f84eaa Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 18 Dec 2023 10:32:01 -0500 Subject: [PATCH 203/332] update readme --- README.Rmd | 2 +- misc/readme/design-concepts.drawio | 202 +++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 misc/readme/design-concepts.drawio diff --git a/README.Rmd b/README.Rmd index 5c391574..a043919d 100644 --- a/README.Rmd +++ b/README.Rmd @@ -63,7 +63,7 @@ si = mp_tmb_model_simulator( , S ~ S - infection , I ~ I + infection ) - , initial_values = list( + , default_values = list( S = 99, I = 1 , beta = 0.25, N = 100 , infection = empty_matrix diff --git a/misc/readme/design-concepts.drawio b/misc/readme/design-concepts.drawio new file mode 100644 index 00000000..30edf7df --- /dev/null +++ b/misc/readme/design-concepts.drawio @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9d1616af1f34614d313b7b668f3925180b0dd321 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 18 Dec 2023 10:37:59 -0500 Subject: [PATCH 204/332] fix bad auto-commit rules --- Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 861a3605..b9a76b02 100644 --- a/Makefile +++ b/Makefile @@ -94,10 +94,14 @@ README.md: README.Rmd misc/readme/*.svg R/*.R echo '' | cat - $@ >> temp && mv temp $@ -# TODO: make this work for others -- currently convenience for sw push-readme: make README.md - git_push_one_at_a_time.sh misc/readme/*.svg misc/readme/*.png README.md README.Rmd + git add misc/readme/*.svg + git add misc/readme/*.png + git add misc/readme/*.drawio + git add README.md README.Rmd + git commit -m "update readme" || true + git push || true enum-update:: R/enum.R From f670e66cdc08f569ed82df2002565defbcebcaa3 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 18 Dec 2023 13:51:48 -0500 Subject: [PATCH 205/332] auto-empty-matrix --- NAMESPACE | 4 ++++ R/expr_list.R | 24 +++++++++++++++++++++ R/formula_utils.R | 11 +++++++--- R/tmb_model.R | 54 +++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 0131b9a0..e629442d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -40,6 +40,7 @@ S3method(mp_reference,Index) S3method(mp_reference,Ledger) S3method(mp_tmb_simulator,DynamicModel) S3method(mp_tmb_simulator,ModelDefRun) +S3method(mp_trajectory,TMBSimulator) S3method(mp_union,Index) S3method(mp_union,Ledger) S3method(mp_vector,Index) @@ -179,17 +180,20 @@ export(mp_choose) export(mp_choose_out) export(mp_combine_exprs) export(mp_decompose) +export(mp_default) export(mp_dynamic_model) export(mp_expr_binop) export(mp_expr_group_sum) export(mp_expr_list) export(mp_extract) export(mp_extract_exprs) +export(mp_final) export(mp_group) export(mp_index) export(mp_indexed_exprs) export(mp_indicator) export(mp_indices) +export(mp_initial) export(mp_join) export(mp_labels) export(mp_ledgers) diff --git a/R/expr_list.R b/R/expr_list.R index 980cb21b..dc088d69 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -174,6 +174,30 @@ ExprList = function( |> unique() ) } + + self$all_derived_vars = function() { + all_vars = self$all_formula_vars() + all_exprs = self$formula_list() + lhs_list = (all_exprs + |> lapply(formula_components, "left") + |> lapply(getElement, "variables") + ) + rhs_list = (all_exprs + |> lapply(formula_components, "right") + |> lapply(getElement, "variables") + ) + full_matrix_assign = vapply(lhs_list, length, integer(1L)) == 1L + lhs_matrix_name = vapply(lhs_list, getElement, character(1L), 1L) + + is_var_derived = function(var_nm) { + made = (var_nm == lhs_matrix_name) & full_matrix_assign + used = vapply(rhs_list, `%in%`, logical(1L), x = var_nm) + if (!any(made)) return(FALSE) + if (!any(used)) return(TRUE) + which(made)[1L] < which(used)[1L] + } + Filter(is_var_derived, all_vars) + } self$data_arg = function() { r = c( diff --git a/R/formula_utils.R b/R/formula_utils.R index a740a5fd..6fd44a87 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -206,15 +206,20 @@ lhs_pieces = function(formula) { ) if (subset_vector_case) { - list( + l = list( variable = table$x[3L], positions = table$x[4L] ) } else if (matrix_case) { - list( + l = list( variable = table$x[2L], positions = character() ) + } else { + l = list( + variable = character(), + positions = character() + ) } - + return(l) } diff --git a/R/tmb_model.R b/R/tmb_model.R index 30728668..fc82871a 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -168,18 +168,64 @@ TMBModel = function( } #' @export -mp_tmb_model_simulator = function(before = list(), during = list(), after = list(), initial_values = list()) { +mp_tmb_model_simulator = function( + before = list() + , during = list() + , after = list() + , default_values = list() + ) { ## FIXME: don't love this name, but trying to avoid conflicting with ## mp_tmb_simulator for now at least. ultimately this should all be one ## concept, and probably mp_tmb_simulator will be the best final name. ## using mp_tmb_model_simulator for now so that we can move forward with ## the readme file on the refactorcpp branch. + e = ExprList(before, during, after) + dv = setdiff(e$all_derived_vars(), names(default_values)) + em = rep(list(empty_matrix), length(dv)) |> setNames(dv) + TMBModel( - init_mats = do.call(MatsList, initial_values), - expr_list = ExprList(before, during, after) + init_mats = do.call(MatsList, c(default_values, em)), + expr_list = e )$simulator() } +#' @export +mp_default = function(model_simulator, ...) { + UseMethod("mp_default") +} + +#' @export +mp_initial = function(model_simulator, ...) { + UseMethod("mp_initial") +} + +mp_initial.TMBSimulator = function(model_simulator, matrices, params = NULL) { + (model_simulator + $replace + $time_steps(time_steps) + $update + $matrices(.mats_to_return = matrices, .mats_to_save = matrices) + $report(params, .phases = "before") + ) +} + + +#' @export +mp_final = function(model_simulator, ...) { + UseMethod("mp_final") +} + + +mp_final.TMBSimulator = function(model_simulator, time_steps, matrices, params = NULL) { + (model_simulator + $replace + $time_steps(time_steps) + $update + $matrices(.mats_to_return = matrices, .mats_to_save = matrices) + $report(params, .phases = "after") + ) +} + #' @export mp_trajectory = function(model_simulator, ...) { @@ -187,7 +233,7 @@ mp_trajectory = function(model_simulator, ...) { } #' @export -mp_trajectory = function(model_simulator, time_steps, matrices, params = NULL) { +mp_trajectory.TMBSimulator = function(model_simulator, time_steps, matrices, params = NULL) { ## FIXME: this is just a stub to get the readme file working (model_simulator $replace From c7d28c8416a07c5fb7d673624e017fdf0b233440 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 18 Dec 2023 13:52:02 -0500 Subject: [PATCH 206/332] update readme --- README.Rmd | 9 +++------ README.md | 14 ++++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/README.Rmd b/README.Rmd index a043919d..a920d397 100644 --- a/README.Rmd +++ b/README.Rmd @@ -58,16 +58,13 @@ remotes::install_github("canmod/macpan2@v0.0.3") ```{r} si = mp_tmb_model_simulator( - during = list( + before = list(S ~ N - I) + , during = list( infection ~ beta * S * I / N , S ~ S - infection , I ~ I + infection ) - , default_values = list( - S = 99, I = 1 - , beta = 0.25, N = 100 - , infection = empty_matrix - ) + , default_values = list(N = 100, I = 1, beta = 0.25) ) print(si) ``` diff --git a/README.md b/README.md index 8128cdf6..c15df0c9 100644 --- a/README.md +++ b/README.md @@ -107,20 +107,22 @@ the following command. ``` r si = mp_tmb_model_simulator( - during = list( + before = list(S ~ N - I) + , during = list( infection ~ beta * S * I / N , S ~ S - infection , I ~ I + infection ) - , default_values = list( - S = 99, I = 1 - , beta = 0.25, N = 100 - , infection = empty_matrix - ) + , default_values = list(N = 100, I = 1, beta = 0.25) ) print(si) ``` + ## --------------------- + ## Before the simulation loop (t = 0): + ## --------------------- + ## 1: S ~ N - I + ## ## --------------------- ## At every iteration of the simulation loop (number of iterations = 0): ## --------------------- From e7bfac6ab57bffdfd87ec1ac7fe4e2803d40b825 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 19 Dec 2023 09:14:22 -0500 Subject: [PATCH 207/332] descriptions for basic epi models --- inst/starter_models/seir/README.md | 54 +++++++++++++++++++ inst/starter_models/sir/README.md | 54 +++++++++++++++++++ inst/starter_models/sir_demo/README.md | 68 +++++++++++++++++++++++- inst/starter_models/sir_waning/README.md | 56 ++++++++++++++++++- 4 files changed, 230 insertions(+), 2 deletions(-) diff --git a/inst/starter_models/seir/README.md b/inst/starter_models/seir/README.md index 411a3ca3..316eb370 100644 --- a/inst/starter_models/seir/README.md +++ b/inst/starter_models/seir/README.md @@ -4,4 +4,58 @@ index_entry: "vanilla epidemic model with an exposed class" author: Steve Walker --- +We introduce the *exposed* compartment, to capture the time period in which individuals have been exposed to the disease but are not able to infect others yet. +# States + +| variable | description | +| -- | -- | +| S | Number of susceptible individuals | +| E | Number of exposed individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | + +The size of the total population is, $ N = S + E+ I + R$. +# Parameters + +| variable | description | +| -- | -- | +| $\beta$ | per capita transmission rate | +| $\alpha$ | per capita rate of infectious progression | %% must be a better way to say this +| $\gamma$ | per capita recovery rate | + + +# Dynamics + + +```mermaid +%%{ + init: { + 'theme': 'base', + 'themeVariables': { + 'primaryColor': '#FDBF57', + 'primaryTextColor': '#000', + 'primaryBorderColor': '#5E6A71', + 'lineColor': '#7A003C', + 'secondaryColor': '#fff', + 'tertiaryColor': '#fff' + } + } +}%% + +flowchart LR + S(S) -- βI --> E(E) + E -- α --> I(I) + I -- γ --> R(R) +``` + + +$$ +\begin{align*} +\frac{dS}{dt} &= -\beta SI \\ +\frac{dE}{dt} &= \beta SI - \alpha E \\ +\frac{dI}{dt} &= \alpha E- \gamma I \\ +\frac{dR}{dt} &= \gamma I +\end{align*} +$$ + diff --git a/inst/starter_models/sir/README.md b/inst/starter_models/sir/README.md index 8fe15031..694dfe89 100644 --- a/inst/starter_models/sir/README.md +++ b/inst/starter_models/sir/README.md @@ -5,3 +5,57 @@ author: Steve Walker --- This is (nearly) the simplest possible 'vanilla' epidemic model, implemented as an example. + +# States + +| variable | description | +| -- | -- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | + +The size of the total population is, $ N = S + I + R$. +# Parameters + +| variable | description | +| -- | -- | +| $\beta$ | per capita transmission rate | +| $\gamma$ | per capita recovery rate | + + +# Dynamics + + +```mermaid +%%{ + init: { + 'theme': 'base', + 'themeVariables': { + 'primaryColor': '#FDBF57', + 'primaryTextColor': '#000', + 'primaryBorderColor': '#5E6A71', + 'lineColor': '#7A003C', + 'secondaryColor': '#fff', + 'tertiaryColor': '#fff' + } + } +}%% + +flowchart LR + S(S) -- βI--> I(I) + I -- γ --> R(R) +``` + + +$$ +\begin{align*} +\frac{dS}{dt} &= -\beta SI \\ +\frac{dI}{dt} &= \beta SI - \gamma I \\ +\frac{dR}{dt} &= \gamma I +\end{align*} +$$ + + +# References + +Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 \ No newline at end of file diff --git a/inst/starter_models/sir_demo/README.md b/inst/starter_models/sir_demo/README.md index b3c53c35..ac589cff 100644 --- a/inst/starter_models/sir_demo/README.md +++ b/inst/starter_models/sir_demo/README.md @@ -1,4 +1,70 @@ --- -title: "SIR demonstration" +title: "SIR with demography" index_entry: "An SIR model with birth and death" --- + +We assume new individuals (births) join the susceptible compartment, and individuals can leave the population (die) from any compartment. +# States + +| variable | description | +| -- | -- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | + +The size of the total population is, $ N = S + I + R$. +# Parameters + +| variable | description | +| -- | -- | +| $\beta$ | per capita transmission rate | +| $\gamma$ | per capita recovery rate | +| $\nu$ | per capita birth rate | +| $\mu$ | per capita mortality rate | + +The SIR model with demography often assumes that the time scale of epidemic changes is much shorter than demographic changes (Earn, 2008). This translates to a constant population size $N$ over time, with $\nu = \mu$. We parameterize birth and mortality rates separately to allow for the general case in which epidemic and demographic dynamics occur on similar time scales. + +# Dynamics + + +```mermaid +%%{ + init: { + 'theme': 'base', + 'themeVariables': { + 'primaryColor': '#FDBF57', + 'primaryTextColor': '#000', + 'primaryBorderColor': '#5E6A71', + 'lineColor': '#7A003C', + 'secondaryColor': '#fff', + 'tertiaryColor': '#fff' + } + } +}%% + +flowchart LR + + START:::hidden -- νN --> S(S) + S -- βI--> I(I) + I -- γ --> R(R) + S -- μ --> END1:::hidden + I -- μ --> END2:::hidden + R -- μ --> END3:::hidden + +%% not sure if you can combine vertical and horizontal arrows in the same graph, need to look at subgraphs (https://stackoverflow.com/questions/66631182/can-i-control-the-direction-of-flowcharts-in-mermaid) + +``` + + +$$ +\begin{align*} +\frac{dS}{dt} &= \nu N -\beta SI - \mu S \\ +\frac{dI}{dt} &= \beta SI - \gamma I - \mu I \\ +\frac{dR}{dt} &= \gamma I - \mu R +\end{align*} +$$ + + +# References + +Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 \ No newline at end of file diff --git a/inst/starter_models/sir_waning/README.md b/inst/starter_models/sir_waning/README.md index 092d84ca..30810a32 100644 --- a/inst/starter_models/sir_waning/README.md +++ b/inst/starter_models/sir_waning/README.md @@ -4,4 +4,58 @@ index_entry: "a basic SIR model with a flow from R back to S" author: Steve Walker --- -Endemic pathogens can sometimes be modelled by sending R back to S, thereby controlling susceptible depletion such that new infections to keep arising indefinitely. +Endemic pathogens can sometimes be modelled by sending R back to S, thereby controlling susceptible depletion such that new infections keep arising indefinitely. + +# States + +| variable | description | +| -- | -- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | + +The size of the total population is, $ N = S + I + R$. +# Parameters + +| variable | description | +| -- | -- | +| $\beta$ | per capita transmission rate | +| $\gamma$ | per capita recovery rate | +| $\delta$ | per capita waning immunity rate | + + +# Dynamics + + +```mermaid +%%{ + init: { + 'theme': 'base', + 'themeVariables': { + 'primaryColor': '#FDBF57', + 'primaryTextColor': '#000', + 'primaryBorderColor': '#5E6A71', + 'lineColor': '#7A003C', + 'secondaryColor': '#fff', + 'tertiaryColor': '#fff' + } + } +}%% + +flowchart LR + S(S) -- βI--> I(I) + I -- γ --> R(R) + R -- δ --> S +``` + + +$$ +\begin{align*} +\frac{dS}{dt} &= -\beta SI + \delta R\\ +\frac{dI}{dt} &= \beta SI - \gamma I \\ +\frac{dR}{dt} &= \gamma I - \delta R +\end{align*} +$$ + + + From 794b79eed75908ee57daca5c9df0ce9105947042 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 19 Dec 2023 09:21:58 -0500 Subject: [PATCH 208/332] testing mermaid rendering --- inst/starter_models/sir/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/inst/starter_models/sir/README.md b/inst/starter_models/sir/README.md index 694dfe89..577e7893 100644 --- a/inst/starter_models/sir/README.md +++ b/inst/starter_models/sir/README.md @@ -40,10 +40,9 @@ The size of the total population is, $ N = S + I + R$. } } }%% - -flowchart LR - S(S) -- βI--> I(I) - I -- γ --> R(R) +flowchart LR; + S(S) -- βI--> I(I); + I -- γ --> R(R); ``` From 5911da98ab35a0fe54c2053a278a13ca43bfd30b Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 19 Dec 2023 09:23:49 -0500 Subject: [PATCH 209/332] another mermaid test --- inst/starter_models/sir/README.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/inst/starter_models/sir/README.md b/inst/starter_models/sir/README.md index 577e7893..348ea3b4 100644 --- a/inst/starter_models/sir/README.md +++ b/inst/starter_models/sir/README.md @@ -27,19 +27,6 @@ The size of the total population is, $ N = S + I + R$. ```mermaid -%%{ - init: { - 'theme': 'base', - 'themeVariables': { - 'primaryColor': '#FDBF57', - 'primaryTextColor': '#000', - 'primaryBorderColor': '#5E6A71', - 'lineColor': '#7A003C', - 'secondaryColor': '#fff', - 'tertiaryColor': '#fff' - } - } -}%% flowchart LR; S(S) -- βI--> I(I); I -- γ --> R(R); From f23129fad237e9b3f86feb56a906641754c95b24 Mon Sep 17 00:00:00 2001 From: Steve Walker Date: Tue, 19 Dec 2023 10:38:33 -0500 Subject: [PATCH 210/332] Update README.md --- inst/starter_models/sir/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/starter_models/sir/README.md b/inst/starter_models/sir/README.md index 348ea3b4..4013b06e 100644 --- a/inst/starter_models/sir/README.md +++ b/inst/starter_models/sir/README.md @@ -14,7 +14,7 @@ This is (nearly) the simplest possible 'vanilla' epidemic model, implemented as | I | Number of infectious individuals | | R | Number of recovered individuals | -The size of the total population is, $ N = S + I + R$. +The size of the total population is, $N = S + I + R$. # Parameters | variable | description | @@ -44,4 +44,4 @@ $$ # References -Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 \ No newline at end of file +Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 From 61f997134625b89b11802feb76e425e5e78b861c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Dec 2023 12:27:40 -0500 Subject: [PATCH 211/332] model spec concept and engine-specific library --- NAMESPACE | 5 +++ R/model_def_run.R | 11 ++++++ R/tmb_model.R | 87 ++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index e629442d..d6048077 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -40,6 +40,7 @@ S3method(mp_reference,Index) S3method(mp_reference,Ledger) S3method(mp_tmb_simulator,DynamicModel) S3method(mp_tmb_simulator,ModelDefRun) +S3method(mp_trajectory,TMBModelSpec) S3method(mp_trajectory,TMBSimulator) S3method(mp_union,Index) S3method(mp_union,Ledger) @@ -65,6 +66,7 @@ S3method(print,Partition) S3method(print,Quantities) S3method(print,String) S3method(print,StringData) +S3method(print,TMBModelSpec) S3method(print,TMBSimulator) S3method(print,Vector) S3method(print,summary.Ledger) @@ -147,6 +149,7 @@ export(StandardExprHazard) export(StringDataFromDotted) export(StringDataFromFrame) export(TMBModel) +export(TMBModelSpec) export(TMBSimulator) export(TXTReader) export(Time) @@ -209,7 +212,9 @@ export(mp_square) export(mp_subset) export(mp_symmetric) export(mp_test_tmb) +export(mp_tmb_library) export(mp_tmb_model_simulator) +export(mp_tmb_model_spec) export(mp_tmb_simulator) export(mp_trajectory) export(mp_triangle) diff --git a/R/model_def_run.R b/R/model_def_run.R index 12b53dda..e5403324 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -3,6 +3,17 @@ mp_library = function(...) { system.file("model_library", ..., package = "macpan2") |> Compartmental2() } +#' @export +mp_tmb_library = function(...) { + model_directory = system.file(..., package = "macpan2") + def_env = new.env(parent = parent.frame()) + sys.source(file.path(model_directory, "tmb.R") + , envir = def_env + , chdir = TRUE + ) + def_env$spec +} + #' @export Compartmental2 = function(model_directory) { self = Base() diff --git a/R/tmb_model.R b/R/tmb_model.R index fc82871a..9c2e43d5 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -168,25 +168,78 @@ TMBModel = function( } #' @export -mp_tmb_model_simulator = function( +TMBModelSpec = function( before = list() , during = list() , after = list() - , default_values = list() + , default = list() + , integers = list() ) { + self = Base() + self$before = before + self$during = during + self$after = after + self$default = default + self$integers = integers + self$simulator_fresh = function( + time_steps = 0 + , sim_exprs = character(0L) + , mats_to_return = character(0L) + , mats_to_save = mats_to_return + ) { + TMBModel( + init_mats = do.call( + MatsList + , c( + self$default + , .mats_to_return = mats_to_return + , .mats_to_save = mats_to_save + ) + ) + , expr_list = ExprList( + before = self$before + , during = self$during + , after = self$after + , .simulate_exprs = sim_exprs + ) + , engine_methods = EngineMethods( + int_vecs = do.call(IntVecs, self$integers) + ) + , time_steps = Time(as.integer(time_steps)) + )$simulator() + } + self$simulator_cached = memoise(self$simulator_fresh) + return_object(self, "TMBModelSpec") +} + +#' @export +print.TMBModelSpec = function(x, ...) { + print(ExprList(x$before, x$during, x$after)) +} + +#' @export +mp_tmb_model_spec = function( + before = list() + , during = list() + , after = list() + , default = list() + , integers = list() + ) { + ## TODO: make this work when integers != list() + e = ExprList(before, during, after) + dv = setdiff(e$all_derived_vars(), names(default)) + em = rep(list(empty_matrix), length(dv)) |> setNames(dv) + TMBModelSpec(before, during, after, c(default, em), integers) +} + +#' @export +mp_tmb_model_simulator = function(model_spec, ...) { ## FIXME: don't love this name, but trying to avoid conflicting with ## mp_tmb_simulator for now at least. ultimately this should all be one ## concept, and probably mp_tmb_simulator will be the best final name. ## using mp_tmb_model_simulator for now so that we can move forward with ## the readme file on the refactorcpp branch. - e = ExprList(before, during, after) - dv = setdiff(e$all_derived_vars(), names(default_values)) - em = rep(list(empty_matrix), length(dv)) |> setNames(dv) - - TMBModel( - init_mats = do.call(MatsList, c(default_values, em)), - expr_list = e - )$simulator() + model_spec$simulator_fresh(...) } #' @export @@ -228,14 +281,14 @@ mp_final.TMBSimulator = function(model_simulator, time_steps, matrices, params = #' @export -mp_trajectory = function(model_simulator, ...) { +mp_trajectory = function(model, ...) { UseMethod("mp_trajectory") } #' @export -mp_trajectory.TMBSimulator = function(model_simulator, time_steps, matrices, params = NULL) { +mp_trajectory.TMBSimulator = function(model, time_steps, matrices, params = NULL) { ## FIXME: this is just a stub to get the readme file working - (model_simulator + (model $replace $time_steps(time_steps) $update @@ -244,6 +297,14 @@ mp_trajectory.TMBSimulator = function(model_simulator, time_steps, matrices, par ) } +#' @export +mp_trajectory.TMBModelSpec = function(model, time_steps, matrices, params = NULL) { + model$simulator_fresh( + time_steps = time_steps + , mats_to_return = matrices + )$report(params) +} + TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { self = tmb_simulator From 89fc18cfa8729db74ef28e709d2a25b31240a0ef Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Dec 2023 12:29:26 -0500 Subject: [PATCH 212/332] update readme --- README.Rmd | 8 +++++--- README.md | 22 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.Rmd b/README.Rmd index a920d397..96e56e40 100644 --- a/README.Rmd +++ b/README.Rmd @@ -27,6 +27,8 @@ library(dplyr) [McMasterPandemic](https://github.com/mac-theobio/McMasterPandemic) was developed to provide forecasts and insights to Canadian public health agencies throughout the COVID-19 pandemic. [Much was learned](https://canmod.github.io/macpan-book/index.html#vision-and-direction) about developing general purpose compartmental modelling software during this experience, but the pressure to deliver regular forecasts made it difficult to focus on the software itself. The goal of this `macpan2` project is to re-imagine `McMasterPandemic`, building it from the ground up with architectural and technological decisions that address the many lessons that we learned from COVID-19 about software. +Impactful applied public health modelling requires many interdisciplinary steps along the path from epidemiological research teams to operational decision makers. Researchers must quickly tailor a model to an emerging public-health concern, validate and calibrate it to data, work with decision makers to define model outputs useful for stakeholders, configure models to generate those outputs, and package up those insights in an appropriate format for stakeholders. Unlike traditional modelling approaches, `macpan2` tackles this challenge from a software-engineering perspective, which allows us to systematically address bottlenecks along this path to impact in ways that will make future solutions easier to achieve. The goal is to enable researchers to focus on their core strengths and fill knowledge gaps efficiently and effectively. + Although `macpan2` is designed as a compartmental modelling tool that is agnostic about the underlying computational engine, it currently makes use of [template model builder](https://github.com/kaskr/adcomp). Template model builder (TMB) is an `R` modelling package based on a `C++` framework incorporating mature [automatic differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) and [matrix algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) libraries. The [Public Health Risk Sciences Division](https://github.com/phac-nml-phrsd) at the [Public Health Agency of Canada](https://www.canada.ca/en/public-health.html) uses `macpan2` (for example, [here](https://phac-nml-phrsd.github.io/EPACmodel/)). @@ -57,14 +59,14 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World ```{r} -si = mp_tmb_model_simulator( +si = mp_tmb_model_spec( before = list(S ~ N - I) , during = list( - infection ~ beta * S * I / N + infection ~ beta * S * I/N , S ~ S - infection , I ~ I + infection ) - , default_values = list(N = 100, I = 1, beta = 0.25) + , default = list(N = 100, I = 1, beta = 0.25) ) print(si) ``` diff --git a/README.md b/README.md index c15df0c9..f263aa72 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,20 @@ project is to re-imagine `McMasterPandemic`, building it from the ground up with architectural and technological decisions that address the many lessons that we learned from COVID-19 about software. +Impactful applied public health modelling requires many +interdisciplinary steps along the path from epidemiological research +teams to operational decision makers. Researchers must quickly tailor a +model to an emerging public-health concern, validate and calibrate it to +data, work with decision makers to define model outputs useful for +stakeholders, configure models to generate those outputs, and package up +those insights in an appropriate format for stakeholders. Unlike +traditional modelling approaches, `macpan2` tackles this challenge from +a software-engineering perspective, which allows us to systematically +address bottlenecks along this path to impact in ways that will make +future solutions easier to achieve. The goal is to enable researchers to +focus on their core strengths and fill knowledge gaps efficiently and +effectively. + Although `macpan2` is designed as a compartmental modelling tool that is agnostic about the underlying computational engine, it currently makes use of [template model builder](https://github.com/kaskr/adcomp). @@ -106,14 +120,14 @@ the following command. ## Hello World ``` r -si = mp_tmb_model_simulator( +si = mp_tmb_model_spec( before = list(S ~ N - I) , during = list( - infection ~ beta * S * I / N + infection ~ beta * S * I/N , S ~ S - infection , I ~ I + infection ) - , default_values = list(N = 100, I = 1, beta = 0.25) + , default = list(N = 100, I = 1, beta = 0.25) ) print(si) ``` @@ -124,7 +138,7 @@ print(si) ## 1: S ~ N - I ## ## --------------------- - ## At every iteration of the simulation loop (number of iterations = 0): + ## At every iteration of the simulation loop (t = 1 to T): ## --------------------- ## 1: infection ~ beta * S * I/N ## 2: S ~ S - infection From 78efb363e9633f4acfd96ce94239e331b2af3f4a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Dec 2023 12:31:04 -0500 Subject: [PATCH 213/332] tmb library file example --- inst/starter_models/sir/tmb.R | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 inst/starter_models/sir/tmb.R diff --git a/inst/starter_models/sir/tmb.R b/inst/starter_models/sir/tmb.R new file mode 100644 index 00000000..54306899 --- /dev/null +++ b/inst/starter_models/sir/tmb.R @@ -0,0 +1,24 @@ +library(macpan2) + +initialize_state = list( + I ~ 1 + , R ~ 0 + , S ~ N - I +) + +compute_flow_rates = list( + infection ~ S * I * beta / N + , recovery ~ gamma * I +) + +update_state = list( + S ~ S - infection + , I ~ I + infection - recovery + , R ~ R + recovery +) + +spec = mp_tmb_model_spec( + before = initialize_state + , during = c(compute_flow_rates, update_state) + , default = list(N = 100, beta = 0.2, gamma = 0.1) +) From cdb804e9fb41ec508291ce4fc6ed09987c348172 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Dec 2023 12:34:56 -0500 Subject: [PATCH 214/332] update readme --- README.Rmd | 33 +++------------------- README.md | 81 +++++++++++------------------------------------------- 2 files changed, 20 insertions(+), 94 deletions(-) diff --git a/README.Rmd b/README.Rmd index 96e56e40..651d5c07 100644 --- a/README.Rmd +++ b/README.Rmd @@ -129,31 +129,7 @@ The [project board](https://github.com/orgs/canmod/projects/2) tracks the detail ### General Dynamic Simulation with TMB -One can define a generic set of update steps that are iterated to produce a dynamic simulation model in TMB. - -```{r} -library(macpan2) -si = mp_dynamic_model( - expr_list = ExprList( - during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection - ) - ), - unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) -) -print(si) -``` - -Simulating from this model takes the following steps. -```{r} -getwd() -(si - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") - |> mp_report() -) -``` +One can [define a generic set of update steps](#hello-world) that are iterated to produce a dynamic simulation model in TMB, and that can be used to generate model simulations. This part of the package is general, stable, and flexible. It also meets many modellers where they are, which is with the ability to write down a set of transitions/state updates. @@ -162,10 +138,9 @@ But it is not convenient if you would just like to simulate from it, which is wh ### Model Library ```{r} -("unstructured/si" - |> mp_library() - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") - |> mp_report() +("starter_models" + |> mp_tmb_library("sir") + |> mp_trajectory(time_steps = 10, matrices = "I") ) ``` diff --git a/README.md b/README.md index f263aa72..d9bbb848 100644 --- a/README.md +++ b/README.md @@ -279,57 +279,9 @@ current state, and plans for improvement and implementation. ### General Dynamic Simulation with TMB -One can define a generic set of update steps that are iterated to -produce a dynamic simulation model in TMB. - -``` r -library(macpan2) -si = mp_dynamic_model( - expr_list = ExprList( - during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection - ) - ), - unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) -) -print(si) -``` - - ## --------------------- - ## At every iteration of the simulation loop (t = 1 to T): - ## --------------------- - ## 1: infection ~ beta * S * I/N - ## 2: S ~ S - infection - ## 3: I ~ I + infection - -Simulating from this model takes the following steps. - -``` r -getwd() -``` - - ## [1] "/Users/stevenwalker/Development/macpan2" - -``` r -(si - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") - |> mp_report() -) -``` - - ## matrix time row col value - ## 1 I 1 0 0 1.247500 - ## 2 I 2 0 0 1.555484 - ## 3 I 3 0 0 1.938307 - ## 4 I 4 0 0 2.413491 - ## 5 I 5 0 0 3.002301 - ## 6 I 6 0 0 3.730342 - ## 7 I 7 0 0 4.628139 - ## 8 I 8 0 0 5.731624 - ## 9 I 9 0 0 7.082401 - ## 10 I 10 0 0 8.727601 +One can [define a generic set of update steps](#hello-world) that are +iterated to produce a dynamic simulation model in TMB, and that can be +used to generate model simulations. This part of the package is general, stable, and flexible. It also meets many modellers where they are, which is with the ability to write down a @@ -341,24 +293,23 @@ which is what the [model library](#model-library) is for. ### Model Library ``` r -("unstructured/si" - |> mp_library() - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "I") - |> mp_report() +("starter_models" + |> mp_tmb_library("sir") + |> mp_trajectory(time_steps = 10, matrices = "I") ) ``` ## matrix time row col value - ## 1 I 1 0 0 1.247500 - ## 2 I 2 0 0 1.555484 - ## 3 I 3 0 0 1.938307 - ## 4 I 4 0 0 2.413491 - ## 5 I 5 0 0 3.002301 - ## 6 I 6 0 0 3.730342 - ## 7 I 7 0 0 4.628139 - ## 8 I 8 0 0 5.731624 - ## 9 I 9 0 0 7.082401 - ## 10 I 10 0 0 8.727601 + ## 2 I 1 0 0 1.098000 + ## 3 I 2 0 0 1.205169 + ## 4 I 3 0 0 1.322276 + ## 5 I 4 0 0 1.450133 + ## 6 I 5 0 0 1.589599 + ## 7 I 6 0 0 1.741573 + ## 8 I 7 0 0 1.906995 + ## 9 I 8 0 0 2.086833 + ## 10 I 9 0 0 2.282085 + ## 11 I 10 0 0 2.493761 TODO: From 1a26bd0b826bf4b6bacd66de4c096f2867076e10 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 19 Dec 2023 12:39:27 -0500 Subject: [PATCH 215/332] update readme --- README.Rmd | 7 +++++-- README.md | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.Rmd b/README.Rmd index 651d5c07..ba4433c5 100644 --- a/README.Rmd +++ b/README.Rmd @@ -60,13 +60,16 @@ remotes::install_github("canmod/macpan2@v0.0.3") ```{r} si = mp_tmb_model_spec( - before = list(S ~ N - I) + before = list( + I ~ 1 + , S ~ N - I + ) , during = list( infection ~ beta * S * I/N , S ~ S - infection , I ~ I + infection ) - , default = list(N = 100, I = 1, beta = 0.25) + , default = list(N = 100, beta = 0.25) ) print(si) ``` diff --git a/README.md b/README.md index d9bbb848..4379a3ad 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,16 @@ the following command. ``` r si = mp_tmb_model_spec( - before = list(S ~ N - I) + before = list( + I ~ 1 + , S ~ N - I + ) , during = list( infection ~ beta * S * I/N , S ~ S - infection , I ~ I + infection ) - , default = list(N = 100, I = 1, beta = 0.25) + , default = list(N = 100, beta = 0.25) ) print(si) ``` @@ -135,7 +138,8 @@ print(si) ## --------------------- ## Before the simulation loop (t = 0): ## --------------------- - ## 1: S ~ N - I + ## 1: I ~ 1 + ## 2: S ~ N - I ## ## --------------------- ## At every iteration of the simulation loop (t = 1 to T): From 64f2eedda38f9c6c7fc12578bb50082dc902474c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Dec 2023 10:10:46 -0500 Subject: [PATCH 216/332] row and matrix quantities --- NAMESPACE | 5 +++ R/frame_utils.R | 57 ++++++++++++++++++++++++- R/mp.R | 105 ++++++++++++++++++++++++++++++++++++++++++++++- R/name_utils.R | 10 +++++ R/tmb_model.R | 42 ++++++++++++++----- R/vector.R | 1 + man/mp_lookup.Rd | 2 +- 7 files changed, 209 insertions(+), 13 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index d6048077..09c352c9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -98,6 +98,8 @@ S3method(to_names,Partition) S3method(to_names,Scalar) S3method(to_names,StringData) S3method(to_names,character) +S3method(to_positions,character) +S3method(to_positions,numeric) export(BinaryOperator) export(CSVReader) export(Collection) @@ -190,6 +192,7 @@ export(mp_expr_group_sum) export(mp_expr_list) export(mp_extract) export(mp_extract_exprs) +export(mp_factors) export(mp_final) export(mp_group) export(mp_index) @@ -208,6 +211,7 @@ export(mp_rename) export(mp_report) export(mp_set_numbers) export(mp_setdiff) +export(mp_slices) export(mp_square) export(mp_subset) export(mp_symmetric) @@ -234,6 +238,7 @@ export(simple_sims) export(to_labels) export(to_name) export(to_names) +export(to_positions) export(union_vars) importFrom(MASS,mvrnorm) importFrom(TMB,MakeADFun) diff --git a/R/frame_utils.R b/R/frame_utils.R index 3f116f7d..726b5878 100644 --- a/R/frame_utils.R +++ b/R/frame_utils.R @@ -1,18 +1,53 @@ ## from poorman +dotdotdot <- function(..., .impute_names = FALSE) { + dots <- eval(substitute(alist(...))) + if (isTRUE(.impute_names)) { + deparse_dots <- lapply(dots, deparse) + names_dots <- names(dots) + unnamed <- if (is.null(names_dots)) rep(TRUE, length(dots)) else nchar(names_dots) == 0L + names(dots)[unnamed] <- deparse_dots[unnamed] + } + dots +} + is_nested <- function(lst) vapply(lst, function(x) inherits(x[1L], "list"), FALSE) +have_name <- function(x) { + nms <- names(x) + if (is.null(nms)) rep(FALSE, length(x)) else !names_are_invalid(nms) +} + flatten <- function(lst) { nested <- is_nested(lst) res <- c(lst[!nested], unlist(lst[nested], recursive = FALSE)) if (sum(nested)) Recall(res) else return(res) } +check_filter <- function(conditions) { + named <- have_name(conditions) + for (i in which(named)) { + if (!is.logical(conditions[[i]])) { + stop( + sprintf("Problem with `filter()` input `..%s`.\n", i), + sprintf("Input `..%s` is named.\n", i), + "This usually means that you've used `=` instead of `==`.\n", + sprintf("Did you mean `%s == %s`?", names(conditions)[[i]], conditions[[i]]) + ) + } + } +} + bind_rows <- function(..., .id = NULL) { lsts <- list(...) + if (length(lsts) == 1L) if (length(lsts[[1L]]) == 0L) { + x = lsts[[1L]] + if (!is.null(.id)) x[[.id]] = character(0L) + return(x) + } lsts <- flatten(lsts) lsts <- Filter(Negate(is.null), lsts) - + if (!missing(.id)) { lsts <- lapply(seq_along(lsts), function(i) { nms <- names(lsts) @@ -34,3 +69,23 @@ bind_rows <- function(..., .id = NULL) { names(lsts) <- NULL do.call(rbind, lsts) } + + +filter <- function(.data, ...) { + conditions <- dotdotdot(...) + if (length(conditions) == 0L) return(.data) + check_filter(conditions) + cond_class <- vapply(conditions, typeof, NA_character_) + cond_class <- cond_class[!cond_class %in% c("language", "logical")] + if (length(cond_class) > 0L) stop("Conditions must be logical vectors") + eval_env = new.env() + eval_env$env <- parent.frame() + on.exit(rm(list = "env", envir = eval_env), add = TRUE) + rows <- lapply( + conditions, + \(cond, frame) eval(cond, .data, frame), + frame = eval_env$env + ) + rows <- Reduce("&", rows) + .data[rows & !is.na(rows), ] +} diff --git a/R/mp.R b/R/mp.R index ad068228..fb0ca3ec 100644 --- a/R/mp.R +++ b/R/mp.R @@ -758,7 +758,7 @@ mp_expr_binop = function(x, y #' index associated with that symbol. #' #' @param index Index table (see \code{\link{mp_index}}). -#' @param symbol Character string that could possibly be assoicated with a +#' @param symbol Character string that could possibly be associated with a #' subset or factor of `index`. #' #' @export @@ -794,3 +794,106 @@ mp_lookup = function(index, symbol) { } stop("failed to find symbol") } + + +# @param l result of mp_slices or mp_factor or just a named list of indices +mp_unpack = function(l, unpack = c('no', 'maybe', 'yes'), env) { + unpack = match.arg(unpack) + if (unpack %in% c('maybe', 'yes')) { + for (nm in names(l)) { + already_there = exists(nm, envir = env) + if (already_there) { + if (unpack == 'maybe') { + stop('cannot unpack because slice names already exist') + } else if (unpack == 'yes') { + warning('masking or overwriting existing objects with slices') + } + } + } + for (nm in names(l)) assign(nm, l[[nm]], envir = env) + } +} + +#' @export +mp_factors = function(index, unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + factors = list() + for (d in names(index)) factors[[d]] = mp_group(index, d) + pf = parent.frame() + force(pf) + mp_unpack(factors, unpack, pf) + factors +} + +mp_subset_list = function(index, ..., unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + subsets = list(...) + + for (s in names(subsets)) { + args = c(list(index), subsets[[s]]) + subsets[[s]] = do.call(mp_subset, args) + } + + pf = parent.frame() + force(pf) + mp_unpack(subsets, unpack, pf) + subsets +} + +mp_custom_slices = function(index, ..., unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + slice_refs = list(...) + nms = names(slice_refs) + for (i in seq_along(slice_refs)) { + r = slice_refs[[i]] + if (is.null(nms[i]) | nchar(nms[i]) == 0L) { + slice_refs[[i]] = (index + |> mp_group(r) + |> as.data.frame() + |> unlist(recursive = FALSE, use.names = FALSE) + ) + names(slice_refs)[[i]] = r + } + } + slices = list() + for (d in names(slice_refs)) { + for (s in slice_refs[[d]]) { + if (s %in% names(slices)) { + warning("duplicated slice") + } else { + args = c(list(index), setNames(list(s), d)) + slices[[s]] = do.call(mp_subset, args) + } + } + } + pf = parent.frame() + force(pf) + mp_unpack(slices, unpack, pf) + slices +} + +## TODO: list of indices where precedence is higher left and top +#' @export +mp_slices = function(index, unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + possible_slices = (index + |> as.data.frame() + |> lapply(unique) + |> lapply(setdiff, "") + ) + slices = list() + for (d in names(possible_slices)) { + for (s in possible_slices[[d]]) { + if (s %in% names(slices)) { + warning("duplicated slice") + } else { + args = c(list(index), setNames(list(s), d)) + slices[[s]] = do.call(mp_subset, args) + } + } + } + pf = parent.frame() + force(pf) + mp_unpack(slices, unpack, pf) + slices +} diff --git a/R/name_utils.R b/R/name_utils.R index 76e7db40..a911680c 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -111,6 +111,15 @@ to_name.Scalar = function(x) x$dot()$value() to_name.Names = function(x) x$dot()$value() +#' @export +to_positions = function(x) UseMethod("to_positions") + +#' @export +to_positions.character = function(x) setNames(seq_along(x) - 1L, x) + +#' @export +to_positions.numeric = as.integer + list_to_labels = function(...) unlist(lapply(list(...), to_labels), use.names = FALSE) list_to_names = function(...) unlist(lapply(list(...), to_names), use.names = FALSE) @@ -163,6 +172,7 @@ undot_anything = function(x) { ) } + wrap_colon_terms = function(x) { i = which(grepl(":", x)) x[i] = sprintf("(%s)", x[i]) diff --git a/R/tmb_model.R b/R/tmb_model.R index 9c2e43d5..34b31874 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -225,6 +225,14 @@ mp_tmb_model_spec = function( , default = list() , integers = list() ) { + integers = (default + |> lapply(names) + |> Filter(f = is.character) + |> lapply(to_positions) + |> unname() + |> unlist() + |> c(integers) + ) ## TODO: make this work when integers != list() e = ExprList(before, during, after) dv = setdiff(e$all_derived_vars(), names(default)) @@ -269,40 +277,54 @@ mp_final = function(model_simulator, ...) { } -mp_final.TMBSimulator = function(model_simulator, time_steps, matrices, params = NULL) { +mp_final.TMBSimulator = function(model_simulator, time_steps, quantities, ...) { (model_simulator $replace $time_steps(time_steps) $update - $matrices(.mats_to_return = matrices, .mats_to_save = matrices) - $report(params, .phases = "after") + $matrices(.mats_to_return = quantities, .mats_to_save = quantities) + $report(..., .phases = "after") ) } #' @export -mp_trajectory = function(model, ...) { +mp_trajectory = function(model, time_steps, quantities, ...) { UseMethod("mp_trajectory") } #' @export -mp_trajectory.TMBSimulator = function(model, time_steps, matrices, params = NULL) { +mp_trajectory.TMBSimulator = function(model, time_steps, quantities, ...) { ## FIXME: this is just a stub to get the readme file working (model $replace $time_steps(time_steps) $update - $matrices(.mats_to_return = matrices, .mats_to_save = matrices) - $report(params) + $matrices(.mats_to_return = quantities, .mats_to_save = quantities) + $report(...) ) } #' @export -mp_trajectory.TMBModelSpec = function(model, time_steps, matrices, params = NULL) { +mp_trajectory.TMBModelSpec = function(model, time_steps, quantities, ...) { + matrix_quantities = intersect(quantities, names(model$default)) + row_quantities = setdiff(quantities, matrix_quantities) + mats_to_return = (model$default + |> lapply(names) + |> Filter(f = is.character) + |> Filter(f = \(x) any(x %in% row_quantities)) + |> names() + |> c(matrix_quantities) + |> unique() + ) model$simulator_fresh( time_steps = time_steps - , mats_to_return = matrices - )$report(params) + , mats_to_return = mats_to_return + )$report(...) |> + macpan2:::filter( + (matrix %in% matrix_quantities) + | (row %in% row_quantities) + ) } diff --git a/R/vector.R b/R/vector.R index 43a95f7c..f474b5e0 100644 --- a/R/vector.R +++ b/R/vector.R @@ -145,6 +145,7 @@ as.matrix.Vector = function(x, ...) { zero_vector = function(labels) setNames(rep(0, length(labels)), labels) + #' Stub #' #' This documentation was originally in [mp_index()] and should be cleaned up diff --git a/man/mp_lookup.Rd b/man/mp_lookup.Rd index eb2d7119..10ca5803 100644 --- a/man/mp_lookup.Rd +++ b/man/mp_lookup.Rd @@ -9,7 +9,7 @@ mp_lookup(index, symbol) \arguments{ \item{index}{Index table (see \code{\link{mp_index}}).} -\item{symbol}{Character string that could possibly be assoicated with a +\item{symbol}{Character string that could possibly be associated with a subset or factor of \code{index}.} } \description{ From 45e708e23792e16eb16924530b20c4e3b0ca7019 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Dec 2023 10:10:55 -0500 Subject: [PATCH 217/332] update readme --- README.Rmd | 74 +++++++++++++++++++++++++++++++++++++++++------------- README.md | 58 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/README.Rmd b/README.Rmd index ba4433c5..a788393e 100644 --- a/README.Rmd +++ b/README.Rmd @@ -12,7 +12,7 @@ knitr::opts_chunk$set( ) ``` -```{r, echo = FALSE, eval = TRUE, message=FALSE, warning=FALSE, error=FALSE} +```{r packages, echo = FALSE, eval = TRUE, message=FALSE, warning=FALSE, error=FALSE} library(macpan2) library(ggplot2) library(dplyr) @@ -58,14 +58,14 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World -```{r} +```{r hello-world} si = mp_tmb_model_spec( before = list( I ~ 1 , S ~ N - I ) , during = list( - infection ~ beta * S * I/N + infection ~ beta * S * I / N , S ~ S - infection , I ~ I + infection ) @@ -77,7 +77,7 @@ print(si) Simulating from this model can be done like so. ```{r plot-tmb-si} (si - |> mp_trajectory(time_steps = 50, matrices = "I") + |> mp_trajectory(time_steps = 50, quantities = "I") |> rename(prevalence = value) |> ggplot() + geom_line(aes(time, prevalence)) ) @@ -86,7 +86,7 @@ Simulating from this model can be done like so. ## Design Concepts -```{r, echo=FALSE} +```{r design-concepts, echo=FALSE} knitr::include_graphics("misc/readme/design-concepts.svg") ``` @@ -140,10 +140,10 @@ But it is not convenient if you would just like to simulate from it, which is wh ### Model Library -```{r} +```{r tmb-library} ("starter_models" |> mp_tmb_library("sir") - |> mp_trajectory(time_steps = 10, matrices = "I") + |> mp_trajectory(time_steps = 10, quantities = "I") ) ``` @@ -192,6 +192,44 @@ The new distributional parameters should go into a new indexed vector called som TODO +### Vectors in the TMB Engine + +This is a TMB-engine-specific warm-up to [model structure](#model-structure-and-bookkeeping). + +```{r tmb-with-indices} +state_labels = c("S", "I", "R") +flow = data.frame( + rate = c("infection", "recovery") + , from = c("S" , "I" ) + , to = c("I" , "R" ) +) +sir = mp_tmb_model_spec( + before = list( + state[I] ~ 1 + , state[S] ~ N - 1 + , state[R] ~ 0 + ) + , during = list( + flow_rate[infection] ~ beta * state[S] * state[I] / N + , flow_rate[recovery] ~ gamma * state[I] + , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) + ) + , default = list( + state = macpan2:::zero_vector(state_labels) + , flow_rate = macpan2:::zero_vector(flow$rate) + , N = 100 + , beta = 0.25 + , gamma = 0.1 + ) + , integers = list( + from = mp_indices(flow$from, state_labels) + , to = mp_indices(flow$to , state_labels) + ) +) +mp_trajectory(sir, time_steps = 10, quantities = c("I")) +``` + + ### Model Structure and Bookkeeping Structured models are combinations of simpler modular model components. For example one might combine an SIR model with an age-group contact model to produce an age structured model. The modular model components are called atomic models. @@ -200,13 +238,13 @@ Structured models are combinations of simpler modular model components. For exam Models are composed of expression lists. Each expression in an unstructured model can be converted into a structured expression to create a structured model. For example, the following unstructured expression defines the rate at which new infections emerge. -```{r, eval = FALSE} +```{r scalar-infection, eval = FALSE} infection ~ beta * S * I / N ``` Each symbol in this expression has a certain type within a structured model, and this type determines how it gets translated into a structured expression. The simplest structured model is one that collects `S` and `I` into a `state` vector with elements `S` and `I`. With this interpretation of the `S` and `I` symbols, the structured infection expression gets translated internally to the following. -```{r, eval = FALSE} +```{r subset-scalar-infection, eval = FALSE} infection ~ beta * state[S] * state[I] / N ``` @@ -215,7 +253,7 @@ Here `S` and `I` become symbols for extracting subsets of the `state` vector. In We will have a vector-valued expression, for example, in a model with an expanded state vector that tracks the geographic location of `S` and `I` individuals. For example, a two patch model with an `east` and `west` patch would involve a four-dimensional state vector with the following elements: `S.east`, `S.west`, `I.east`, and `I.west`. In this case we now have two scalar-valued infection expressions. -```{r, eval = FALSE} +```{r vector-infection, eval = FALSE} infection[east] ~ beta * state[S.east] * state[I.east] / N infection[west] ~ beta * state[S.west] * state[I.west] / N ``` @@ -227,7 +265,7 @@ To see how easy this can be, note that this two-patch infection expression can b Why is this powerful? Because it separates the math of the dynamic mechanism, `infection ~ beta * S * I / N`, from the bookkeeping required in structured models where the same mechanism is applied again and again to different model strata. This is often how modellers think. For example, I might have a location-structured SIR model that I need to expand to be both age- and location-structured. In this case, infection is still the same process, whereby a susceptible individual contacts an infectious individual to create a flow from susceptible individuals to infectious individuals. The same math applies to all strata of the model. The boring but necessary part is to connect the math to the bookkeeping associated with the model structure, and so software should focus on making these bookkeeping changes as easy as possible and with minimal changes required to the underlying mathematical expressions. Let's look at more examples of infection, and watch the bookkeeping get more annoying. In an age-stratified model with two age groups, we now get four scalar-valued infection expressions of the form `infection ~ beta * S * I / N`. -```{r, eval = FALSE} +```{r age-structured-infection, eval = FALSE} infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] @@ -236,7 +274,7 @@ infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N Here the first expression is for a young individual infecting an old individual, the second is for an old individual infecting a young individual, etc ... Things get worse if we have two age groups in two patches. -```{r, eval = FALSE} +```{r age-location-structured-infection, eval = FALSE} infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] @@ -249,7 +287,7 @@ infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * sta This still isn't so bad, as we just have the first four expressions for `east` and the last four for `west`. But now let's introduce two symptom status categories: `mild` and `severe`. -```{r, eval = FALSE} +```{r age-location-symptom-structured-infection, eval = FALSE} infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] @@ -292,7 +330,7 @@ Our approach is to do the bookkeeping in a different way. In particular we belie The first step to being more constructive is to have a better representation of the structured vectors. Above we used dot-concatenation to represent the model strata. For example, in the two-patch SI model we have both epidemiological status and geographic location in the state variable names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state vector gets more structured it becomes more convenient to describe its variables using an index table, the rows of which describe each state variable. -```{r} +```{r cartesian} state = mp_cartesian( mp_index(Epi = c("S", "I")), mp_index(Loc = c("east", "west")) @@ -300,13 +338,13 @@ state = mp_cartesian( state ``` -```{r} +```{r group} beta = mp_group(state, "Epi") ``` With this representation we can get subsets of the state vector that represent each epidemiological status. -```{r} +```{r subset} mp_subset(state, Epi = "S") mp_subset(state, Epi = "I") ``` @@ -330,7 +368,7 @@ TODO Because expression lists are really just lists of expressions, they can be combined as lists would normally be combined. In this example we keep the dynamics of the si model separate from under-reporting and reporting delay corrections to the raw prevalence (TODO: should really use incidence). -```{r} +```{r combining-expression-lists} library(macpan2) si_dynamics = list( transition_rate = infection ~ beta * S * I / N @@ -349,3 +387,5 @@ si = mp_dynamic_model( |> mp_report() ) ``` + + diff --git a/README.md b/README.md index 4379a3ad..6ee78d0e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ -
    Calibration - Time-Varying Parameters + - Vectors in the TMB Engine - Model Structure and Bookkeeping @@ -126,7 +128,7 @@ si = mp_tmb_model_spec( , S ~ N - I ) , during = list( - infection ~ beta * S * I/N + infection ~ beta * S * I / N , S ~ S - infection , I ~ I + infection ) @@ -152,7 +154,7 @@ Simulating from this model can be done like so. ``` r (si - |> mp_trajectory(time_steps = 50, matrices = "I") + |> mp_trajectory(time_steps = 50, quantities = "I") |> rename(prevalence = value) |> ggplot() + geom_line(aes(time, prevalence)) ) @@ -299,7 +301,7 @@ which is what the [model library](#model-library) is for. ``` r ("starter_models" |> mp_tmb_library("sir") - |> mp_trajectory(time_steps = 10, matrices = "I") + |> mp_trajectory(time_steps = 10, quantities = "I") ) ``` @@ -391,6 +393,56 @@ e.g. convolution kernel parameters). TODO +### Vectors in the TMB Engine + +This is a TMB-engine-specific warm-up to [model +structure](#model-structure-and-bookkeeping). + +``` r +state_labels = c("S", "I", "R") +flow = data.frame( + rate = c("infection", "recovery") + , from = c("S" , "I" ) + , to = c("I" , "R" ) +) +sir = mp_tmb_model_spec( + before = list( + state[I] ~ 1 + , state[S] ~ N - 1 + , state[R] ~ 0 + ) + , during = list( + flow_rate[infection] ~ beta * state[S] * state[I] / N + , flow_rate[recovery] ~ gamma * state[I] + , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) + ) + , default = list( + state = macpan2:::zero_vector(state_labels) + , flow_rate = macpan2:::zero_vector(flow$rate) + , N = 100 + , beta = 0.25 + , gamma = 0.1 + ) + , integers = list( + from = mp_indices(flow$from, state_labels) + , to = mp_indices(flow$to , state_labels) + ) +) +mp_trajectory(sir, time_steps = 10, quantities = c("I")) +``` + + ## matrix time row col value + ## 5 state 1 I 1.147500 + ## 8 state 2 I 1.316046 + ## 11 state 3 I 1.508417 + ## 14 state 4 I 1.727685 + ## 17 state 5 I 1.977228 + ## 20 state 6 I 2.260727 + ## 23 state 7 I 2.582154 + ## 26 state 8 I 2.945748 + ## 29 state 9 I 3.355960 + ## 32 state 10 I 3.817384 + ### Model Structure and Bookkeeping Structured models are combinations of simpler modular model components. From b3f4f54da611ac5f9cffeef02e33c2d1b9bc6f38 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Dec 2023 10:14:11 -0500 Subject: [PATCH 218/332] remove rownames on trajectories --- R/tmb_model.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/tmb_model.R b/R/tmb_model.R index 34b31874..5fc7d64f 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -317,7 +317,7 @@ mp_trajectory.TMBModelSpec = function(model, time_steps, quantities, ...) { |> c(matrix_quantities) |> unique() ) - model$simulator_fresh( + s = model$simulator_fresh( time_steps = time_steps , mats_to_return = mats_to_return )$report(...) |> @@ -325,6 +325,8 @@ mp_trajectory.TMBModelSpec = function(model, time_steps, quantities, ...) { (matrix %in% matrix_quantities) | (row %in% row_quantities) ) + rownames(s) = NULL + s } From 4ecc796ccf9b75d54c84c268d0ef11b02c52b636 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Dec 2023 10:16:53 -0500 Subject: [PATCH 219/332] makefile fix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b9a76b02..1adac661 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ png-readme:: misc/readme/*.png readme:: README.md -README.md: README.Rmd misc/readme/*.svg R/*.R +README.md: README.Rmd misc/readme/*.svg R/*.R NAMESPACE Rscript -e "rmarkdown::render('README.Rmd')" echo '' > temp echo '' | cat - $@ >> temp && mv temp $@ From 3f487c4fce580161fa11175d4210dc4196dbf9c9 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Dec 2023 10:17:01 -0500 Subject: [PATCH 220/332] update readme --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6ee78d0e..ee704be8 100644 --- a/README.md +++ b/README.md @@ -306,16 +306,16 @@ which is what the [model library](#model-library) is for. ``` ## matrix time row col value - ## 2 I 1 0 0 1.098000 - ## 3 I 2 0 0 1.205169 - ## 4 I 3 0 0 1.322276 - ## 5 I 4 0 0 1.450133 - ## 6 I 5 0 0 1.589599 - ## 7 I 6 0 0 1.741573 - ## 8 I 7 0 0 1.906995 - ## 9 I 8 0 0 2.086833 - ## 10 I 9 0 0 2.282085 - ## 11 I 10 0 0 2.493761 + ## 1 I 1 0 0 1.098000 + ## 2 I 2 0 0 1.205169 + ## 3 I 3 0 0 1.322276 + ## 4 I 4 0 0 1.450133 + ## 5 I 5 0 0 1.589599 + ## 6 I 6 0 0 1.741573 + ## 7 I 7 0 0 1.906995 + ## 8 I 8 0 0 2.086833 + ## 9 I 9 0 0 2.282085 + ## 10 I 10 0 0 2.493761 TODO: @@ -432,16 +432,16 @@ mp_trajectory(sir, time_steps = 10, quantities = c("I")) ``` ## matrix time row col value - ## 5 state 1 I 1.147500 - ## 8 state 2 I 1.316046 - ## 11 state 3 I 1.508417 - ## 14 state 4 I 1.727685 - ## 17 state 5 I 1.977228 - ## 20 state 6 I 2.260727 - ## 23 state 7 I 2.582154 - ## 26 state 8 I 2.945748 - ## 29 state 9 I 3.355960 - ## 32 state 10 I 3.817384 + ## 1 state 1 I 1.147500 + ## 2 state 2 I 1.316046 + ## 3 state 3 I 1.508417 + ## 4 state 4 I 1.727685 + ## 5 state 5 I 1.977228 + ## 6 state 6 I 2.260727 + ## 7 state 7 I 2.582154 + ## 8 state 8 I 2.945748 + ## 9 state 9 I 3.355960 + ## 10 state 10 I 3.817384 ### Model Structure and Bookkeeping From 922a98594e4c4ed1fe00ed3d18db5e4cfc6e8026 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 20 Dec 2023 11:52:11 -0500 Subject: [PATCH 221/332] wastewater tmb engine --- inst/starter_models/ww/tmb.R | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 inst/starter_models/ww/tmb.R diff --git a/inst/starter_models/ww/tmb.R b/inst/starter_models/ww/tmb.R new file mode 100644 index 00000000..2ec3a564 --- /dev/null +++ b/inst/starter_models/ww/tmb.R @@ -0,0 +1,98 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience +computations = list( + N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) +) + +## absolute flow rates (per time only) +flow_rates = list( + SE ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , EIa ~ E * alpha * sigma + , EIp ~ E * (1 - alpha)* sigma + , IaR ~ Ia * gamma_a + , IpIm ~ Ip * mu * gamma_p + , ImR ~ Im * gamma_m + , IpIs ~ Ip * (1 - mu) * gamma_p + , IsICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , ICUsH2 ~ ICUs * psi1 + , H2R ~ H2 * psi3 + , IsH ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUdD ~ ICUd * psi2 + , HR ~ H * rho + , IaW ~ Ia * nu # or * or N? + , IpW ~ Ip * nu + , ImW ~ Im * nu + , IsW ~ Is * nu + , WA ~ W * xi +) + +## state updates +state_updates = list( + S ~ S - SE + , E ~ E + SE - EIa - EIp + , Ia ~ Ia + EIa - IaR + , Ip ~ Ip + EIp - IpIm - IpIs + , Im ~ Im + IpIm - ImR + , Is ~ Is + IpIs - IsICUs - IsH - IsICUd + , R ~ R + IaR + H2R + HR + ImR + , H ~ H + IsH - HR + , ICUs ~ ICUs + IsICUs - ICUsH2 + , ICUd ~ ICUd + IsICUd - ICUdD + , H2 ~ H2 + ICUsH2 - H2R + , D ~ D + ICUdD + , W ~ W + IaW + IpW + ImW + IsW - WA + , A ~ A + WA +) + +## simple unstructured scalar expression +spec = mp_tmb_model_spec( + before = computations + , during = c( + flow_rates + , state_updates + ) + , default = list( + S = 1.00E+06, E = 1 + , Ia = 0, Ip = 0, Im = 0, Is = 0 + , R = 0 + , H = 0, ICUs = 0, ICUd = 0, H2 = 0 + , D = 0 + , W = 0, A = 0 + , beta0 = 1 # Baseline (non-intervention) transmission across categories + , Ca = 2/3 # relative asymptomatic transmission (or contact) + , Cp = 1 # relative presymptomatic transmission (or contact) + , Cm = 1 # relative mildly symptomatic transmission (or contact) + , Cs = 1 # relative severely symptomatic transmission (or contact) + , alpha = 0.39 # Fraction of cases asymptomatic + , sigma = 1/3.3 # 1/time in exposed class + , gamma_a = 1/7 # 1/time for asymptomatic recovery + , gamma_m = 1/7 # 1/time for mildly symptomatic recovery + , gamma_s = 1/5.72 # 1/time for severely symptomatic transition to hospital/death + , gamma_p = 1/1.2 # 1/time in pre-symptomatic class + , rho = 1/10 # 1/time in hospital (acute care) + , delta = 0 # Fraction of acute-care cases that are fatal + , mu = 0.956 # Fraction of symptomatic cases that are mild + #, N = 1.00E+06 # Population size + #, E0 = 5 # Initial number exposed + , nonhosp_mort = 0 # probability of mortality without hospitalization + , iso_m = 0 # Relative self-isolation/distancing of mild cases + , iso_s = 0 # Relative self-isolation/distancing of severe cases + , phi1 = 0.76 # Fraction of hospital cases to ICU + , phi2 = 0.26 # Fraction of ICU cases dying + , psi1 = 1/20 # Rate of ICU back to acute care + , psi2 = 1/8 # Rate of ICU to death + , psi3 = 1/5 # Rate of post-ICU to discharge + , c_prop = 1/10 # fraction of incidence reported as positive tests + , c_delay_mean = 11 # average delay between incidence and test report + , c_delay_cv = 0.25 # coefficient of variation of testing delay + , proc_disp = 0 # dispersion parameter for process error (0=demog stoch only) + , zeta = 0 # phenomenological heterogeneity parameter + , nu = 0.1 # something to do with waste-water + , xi = 0.5 # something to do with waste-water + ) +) From 51d4f5d7f0dd1b751da0fbfb313fd194e0a93aa6 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 09:52:01 -0500 Subject: [PATCH 222/332] move time steps and quantities out of mp_trajectory --- NAMESPACE | 4 +- R/index_to_tmb.R | 16 +---- R/model_def_run.R | 8 ++- R/tmb_model.R | 155 +++++++++++++++++++++++++--------------- man/TMBSimulator.Rd | 3 +- man/mp_tmb_simulator.Rd | 29 +------- 6 files changed, 111 insertions(+), 104 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 09c352c9..76d9dd0e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -38,9 +38,9 @@ S3method(mp_labels,Index) S3method(mp_labels,Ledger) S3method(mp_reference,Index) S3method(mp_reference,Ledger) +S3method(mp_simulator,TMBModelSpec) S3method(mp_tmb_simulator,DynamicModel) S3method(mp_tmb_simulator,ModelDefRun) -S3method(mp_trajectory,TMBModelSpec) S3method(mp_trajectory,TMBSimulator) S3method(mp_union,Index) S3method(mp_union,Ledger) @@ -211,13 +211,13 @@ export(mp_rename) export(mp_report) export(mp_set_numbers) export(mp_setdiff) +export(mp_simulator) export(mp_slices) export(mp_square) export(mp_subset) export(mp_symmetric) export(mp_test_tmb) export(mp_tmb_library) -export(mp_tmb_model_simulator) export(mp_tmb_model_spec) export(mp_tmb_simulator) export(mp_trajectory) diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 7a204b33..2552dc39 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -10,21 +10,7 @@ #' #' @importFrom oor method_apply #' @export -mp_tmb_simulator = function(dynamic_model - , time_steps = 0L - , vectors = NULL - , unstruc_mats = NULL - , mats_to_save = names(vectors) - , mats_to_return = mats_to_save - , params = OptParamsList(0) - , random = OptParamsList() - , obj_fn = ObjectiveFunction(~0) - , log_file = LogFile() - , do_pred_sdreport = TRUE - , tmb_cpp = "macpan2" - , initialize_ad_fun = TRUE - , ... -) { +mp_tmb_simulator = function(...) { UseMethod("mp_tmb_simulator") } diff --git a/R/model_def_run.R b/R/model_def_run.R index e5403324..a53ca260 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -4,8 +4,12 @@ mp_library = function(...) { } #' @export -mp_tmb_library = function(...) { - model_directory = system.file(..., package = "macpan2") +mp_tmb_library = function(..., package = NULL) { + if (is.null(package)) { + model_directory = file.path(...) + } else { + model_directory = system.file(..., package = package) + } def_env = new.env(parent = parent.frame()) sys.source(file.path(model_directory, "tmb.R") , envir = def_env diff --git a/R/tmb_model.R b/R/tmb_model.R index 5fc7d64f..ed7b2b41 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -133,8 +133,16 @@ TMBModel = function( do.call(TMB::MakeADFun, self$make_ad_fun_arg(tmb_cpp)) } - self$simulator = function(tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE) { - TMBSimulator(self, tmb_cpp = tmb_cpp, initialize_ad_fun = initialize_ad_fun) + self$simulator = function( + tmb_cpp = getOption("macpan2_dll") + , initialize_ad_fun = TRUE + , quantities = NULL + ) { + TMBSimulator(self + , tmb_cpp = tmb_cpp + , initialize_ad_fun = initialize_ad_fun + , quantities = quantities + ) } @@ -174,6 +182,9 @@ TMBModelSpec = function( , after = list() , default = list() , integers = list() + , must_save = character() + , must_not_save = character() + , sim_exprs = character() ) { self = Base() self$before = before @@ -181,32 +192,48 @@ TMBModelSpec = function( self$after = after self$default = default self$integers = integers - self$simulator_fresh = function( - time_steps = 0 - , sim_exprs = character(0L) - , mats_to_return = character(0L) - , mats_to_save = mats_to_return - ) { - TMBModel( + self$must_save = must_save + self$must_not_save = must_not_save + self$sim_exprs = sim_exprs + self$simulator_fresh = function(time_steps = 0, quantities = character()) { + initial_mats = self$default + matrix_quantities = intersect(quantities, names(initial_mats)) + row_quantities = setdiff(quantities, matrix_quantities) + mats_to_return = (initial_mats + |> lapply(names) + |> Filter(f = is.character) + |> Filter(f = \(x) any(x %in% row_quantities)) + |> names() + |> c(matrix_quantities) + |> unique() + ) + mats_to_save = (mats_to_return + |> union(self$must_save) + |> setdiff(self$must_not_save) + ) + s = TMBModel( init_mats = do.call( MatsList , c( self$default - , .mats_to_return = mats_to_return - , .mats_to_save = mats_to_save + , list( + .mats_to_return = mats_to_return + , .mats_to_save = mats_to_save + ) ) ) , expr_list = ExprList( before = self$before , during = self$during , after = self$after - , .simulate_exprs = sim_exprs + , .simulate_exprs = self$sim_exprs ) , engine_methods = EngineMethods( int_vecs = do.call(IntVecs, self$integers) ) , time_steps = Time(as.integer(time_steps)) - )$simulator() + )$simulator(quantities = quantities) + s } self$simulator_cached = memoise(self$simulator_fresh) return_object(self, "TMBModelSpec") @@ -241,13 +268,13 @@ mp_tmb_model_spec = function( } #' @export -mp_tmb_model_simulator = function(model_spec, ...) { - ## FIXME: don't love this name, but trying to avoid conflicting with - ## mp_tmb_simulator for now at least. ultimately this should all be one - ## concept, and probably mp_tmb_simulator will be the best final name. - ## using mp_tmb_model_simulator for now so that we can move forward with - ## the readme file on the refactorcpp branch. - model_spec$simulator_fresh(...) +mp_simulator = function(model, time_steps, quantities) { + UseMethod("mp_simulator") +} + +#' @export +mp_simulator.TMBModelSpec = function(model, time_steps, quantities) { + model$simulator_fresh(time_steps, quantities) } #' @export @@ -289,45 +316,43 @@ mp_final.TMBSimulator = function(model_simulator, time_steps, quantities, ...) { #' @export -mp_trajectory = function(model, time_steps, quantities, ...) { +mp_trajectory = function(model) { UseMethod("mp_trajectory") } #' @export -mp_trajectory.TMBSimulator = function(model, time_steps, quantities, ...) { - ## FIXME: this is just a stub to get the readme file working - (model - $replace - $time_steps(time_steps) - $update - $matrices(.mats_to_return = quantities, .mats_to_save = quantities) - $report(...) - ) +mp_trajectory.TMBSimulator = function(model) { + model$report() } -#' @export -mp_trajectory.TMBModelSpec = function(model, time_steps, quantities, ...) { - matrix_quantities = intersect(quantities, names(model$default)) - row_quantities = setdiff(quantities, matrix_quantities) - mats_to_return = (model$default - |> lapply(names) - |> Filter(f = is.character) - |> Filter(f = \(x) any(x %in% row_quantities)) - |> names() - |> c(matrix_quantities) - |> unique() - ) - s = model$simulator_fresh( - time_steps = time_steps - , mats_to_return = mats_to_return - )$report(...) |> - macpan2:::filter( - (matrix %in% matrix_quantities) - | (row %in% row_quantities) - ) - rownames(s) = NULL - s -} +# @export +# mp_trajectory.TMBModelSpec = function(model) { +# (model +# |> mp_tmb_simulator(time_steps, quantities) +# |> mp_trajectory(time_steps, quantities) +# ) + + # matrix_quantities = intersect(quantities, names(model$default)) + # row_quantities = setdiff(quantities, matrix_quantities) + # mats_to_return = (model$default + # |> lapply(names) + # |> Filter(f = is.character) + # |> Filter(f = \(x) any(x %in% row_quantities)) + # |> names() + # |> c(matrix_quantities) + # |> unique() + # ) + # simulations = model$simulator_fresh( + # time_steps = time_steps + # , mats_to_return = mats_to_return + # )$report() + # macpan2:::filter( + # (matrix %in% matrix_quantities) + # | (row %in% row_quantities) + # ) + # rownames(simulations) = NULL + # simulations +#} TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { @@ -418,7 +443,10 @@ TMBSimulationUtils = function() { if (!"after" %in% .phases) { r = r[r$time != num_t + 1L,,drop = FALSE] } - r + r |> macpan2:::filter( + (matrix %in% self$matrix_quantities()) + | (row %in% self$row_quantities()) + ) } self$.find_problematic_expression = function(row) { expr_num_p_table_rows = self$tmb_model$data_arg()$expr_num_p_table_rows @@ -482,13 +510,28 @@ TMBSimulationUtils = function() { #' #' @importFrom MASS mvrnorm #' @export -TMBSimulator = function(tmb_model, tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE) { +TMBSimulator = function(tmb_model + , tmb_cpp = getOption("macpan2_dll") + , initialize_ad_fun = TRUE + , quantities = NULL ## character vector of matrix and/or row names + ) { self = TMBSimulationUtils() ## Args self$tmb_model = tmb_model self$tmb_cpp = tmb_cpp - + self$quantities = quantities + + self$matrix_quantities = function() { + if (is.null(self$quantities)) return(self$tmb_model$init_mats$.mats_to_return) + initial_mats = self$tmb_model$init_mats$.initial_mats + intersect(self$quantities, names(initial_mats)) + } + self$row_quantities = function() { + if (is.null(self$quantities)) return(character(0L)) + setdiff(self$quantities, self$matrix_quantities()) + } + ## Standard Methods self$matrix_names = function() self$tmb_model$init_mats$.names() self$ad_fun = function() self$tmb_model$ad_fun(self$tmb_cpp) diff --git a/man/TMBSimulator.Rd b/man/TMBSimulator.Rd index e758b60d..c4935b7f 100644 --- a/man/TMBSimulator.Rd +++ b/man/TMBSimulator.Rd @@ -7,7 +7,8 @@ TMBSimulator( tmb_model, tmb_cpp = getOption("macpan2_dll"), - initialize_ad_fun = TRUE + initialize_ad_fun = TRUE, + quantities = NULL ) } \arguments{ diff --git a/man/mp_tmb_simulator.Rd b/man/mp_tmb_simulator.Rd index f56b0b94..3ca2f152 100644 --- a/man/mp_tmb_simulator.Rd +++ b/man/mp_tmb_simulator.Rd @@ -4,44 +4,17 @@ \alias{mp_tmb_simulator} \title{TMB Simulator from Dynamic Model} \usage{ -mp_tmb_simulator( - dynamic_model, - time_steps = 0L, - vectors = NULL, - unstruc_mats = NULL, - mats_to_save = names(vectors), - mats_to_return = mats_to_save, - params = OptParamsList(0), - random = OptParamsList(), - obj_fn = ObjectiveFunction(~0), - log_file = LogFile(), - do_pred_sdreport = TRUE, - tmb_cpp = "macpan2", - initialize_ad_fun = TRUE, - ... -) +mp_tmb_simulator(...) } \arguments{ \item{dynamic_model}{Object product by \code{\link{dynamic_model}}.} -\item{time_steps}{An object of class \code{\link{Time}}.} - \item{vectors}{Named list of named vectors as initial values for the simulations that are referenced in the expression list in the dynamic model.} \item{unstruc_mats}{= Named list of objects that can be coerced to numerical matrices that are used in the expression list of the dynamic model.} - -\item{params}{An object of class \code{\link{OptParamsList}}.} - -\item{random}{An object of class \code{\link{OptParamsList}}.} - -\item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} - -\item{log_file}{An object of class \code{\link{LogFile}}.} - -\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} } \description{ TMB Simulator from Dynamic Model From 027ce4d32413604255bed98decf438c1c45f1209 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 09:52:08 -0500 Subject: [PATCH 223/332] update readme --- README.Rmd | 24 +++++++++---- README.md | 62 ++++++++++++++++++++-------------- misc/readme/plot-tmb-si-1.png | Bin 25993 -> 34593 bytes 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/README.Rmd b/README.Rmd index a788393e..f9cbc8e8 100644 --- a/README.Rmd +++ b/README.Rmd @@ -77,9 +77,15 @@ print(si) Simulating from this model can be done like so. ```{r plot-tmb-si} (si - |> mp_trajectory(time_steps = 50, quantities = "I") - |> rename(prevalence = value) - |> ggplot() + geom_line(aes(time, prevalence)) + |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) + |> mp_trajectory() + |> mutate(quantity = case_match(matrix + , "I" ~ "prevalance" + , "infection" ~ "incidence" + )) + |> ggplot() + + geom_line(aes(time, value)) + + facet_wrap(~quantity, scales = "free") ) ``` @@ -113,7 +119,7 @@ First, `macpan2` is meant to plug into standard R workflows for data pre-process Second, `macpan2` uses an engine plug-in architecture. Models defined in the [engine-agnostic model specification language](#engine-agnostic model specification language) can be rendered in a particular computational engine so that multiple computational approaches can be used to generate modelling outputs for a single model definition. This can be useful if different model outputs are more efficient or convenient for different computational approaches. For example, engines such as TMB that are capable of automatic differentiation are great for fast optimization of parameters and for computing $\mathcal{R}_0$ in models with arbitrary complexity, whereas other engines such as Adaptive Tau are better at stochastic simulation techniques like the Gillespie algorithm. Sometimes an engine will be unable to generate a particular output at all or with sufficient difficulty on the part of the user so as to render the use-case practically impossible. For example, it is not possible to conveniently utilize differential equation solvers in the TMB engine, limiting it to Euler or simple RK4-type solvers. Being able to swap out the TMB engine for one based on `deSolve` (or other similar package) would allow for more convenient and accurate solutions to differential equations without having to leave `macpan2`. Third, TODO: describe how the model specification language can be used to build up models modularly (e.g. swap out alternative state-updaters as discussed above but also add in model structures like age-groups and spatial structure to a simple unstructured model) - + ### Engine-Agnostic Model Specification Language @@ -142,8 +148,9 @@ But it is not convenient if you would just like to simulate from it, which is wh ```{r tmb-library} ("starter_models" - |> mp_tmb_library("sir") - |> mp_trajectory(time_steps = 10, quantities = "I") + |> mp_tmb_library("sir", package = "macpan2") + |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_trajectory() ) ``` @@ -226,7 +233,10 @@ sir = mp_tmb_model_spec( , to = mp_indices(flow$to , state_labels) ) ) -mp_trajectory(sir, time_steps = 10, quantities = c("I")) +(sir + |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_trajectory() +) ``` diff --git a/README.md b/README.md index ee704be8..78d88a7f 100644 --- a/README.md +++ b/README.md @@ -154,9 +154,15 @@ Simulating from this model can be done like so. ``` r (si - |> mp_trajectory(time_steps = 50, quantities = "I") - |> rename(prevalence = value) - |> ggplot() + geom_line(aes(time, prevalence)) + |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) + |> mp_trajectory() + |> mutate(quantity = case_match(matrix + , "I" ~ "prevalance" + , "infection" ~ "incidence" + )) + |> ggplot() + + geom_line(aes(time, value)) + + facet_wrap(~quantity, scales = "free") ) ``` @@ -300,22 +306,23 @@ which is what the [model library](#model-library) is for. ``` r ("starter_models" - |> mp_tmb_library("sir") - |> mp_trajectory(time_steps = 10, quantities = "I") + |> mp_tmb_library("sir", package = "macpan2") + |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_trajectory() ) ``` ## matrix time row col value - ## 1 I 1 0 0 1.098000 - ## 2 I 2 0 0 1.205169 - ## 3 I 3 0 0 1.322276 - ## 4 I 4 0 0 1.450133 - ## 5 I 5 0 0 1.589599 - ## 6 I 6 0 0 1.741573 - ## 7 I 7 0 0 1.906995 - ## 8 I 8 0 0 2.086833 - ## 9 I 9 0 0 2.282085 - ## 10 I 10 0 0 2.493761 + ## 2 I 1 0 0 1.098000 + ## 3 I 2 0 0 1.205169 + ## 4 I 3 0 0 1.322276 + ## 5 I 4 0 0 1.450133 + ## 6 I 5 0 0 1.589599 + ## 7 I 6 0 0 1.741573 + ## 8 I 7 0 0 1.906995 + ## 9 I 8 0 0 2.086833 + ## 10 I 9 0 0 2.282085 + ## 11 I 10 0 0 2.493761 TODO: @@ -428,20 +435,23 @@ sir = mp_tmb_model_spec( , to = mp_indices(flow$to , state_labels) ) ) -mp_trajectory(sir, time_steps = 10, quantities = c("I")) +(sir + |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_trajectory() +) ``` ## matrix time row col value - ## 1 state 1 I 1.147500 - ## 2 state 2 I 1.316046 - ## 3 state 3 I 1.508417 - ## 4 state 4 I 1.727685 - ## 5 state 5 I 1.977228 - ## 6 state 6 I 2.260727 - ## 7 state 7 I 2.582154 - ## 8 state 8 I 2.945748 - ## 9 state 9 I 3.355960 - ## 10 state 10 I 3.817384 + ## 5 state 1 I 1.147500 + ## 8 state 2 I 1.316046 + ## 11 state 3 I 1.508417 + ## 14 state 4 I 1.727685 + ## 17 state 5 I 1.977228 + ## 20 state 6 I 2.260727 + ## 23 state 7 I 2.582154 + ## 26 state 8 I 2.945748 + ## 29 state 9 I 3.355960 + ## 32 state 10 I 3.817384 ### Model Structure and Bookkeeping diff --git a/misc/readme/plot-tmb-si-1.png b/misc/readme/plot-tmb-si-1.png index 1249799a1af360c09cdb01e968ca8a78ad270f35..8126a616ffa4fcae937e8c7f211dfa45755e08d8 100644 GIT binary patch literal 34593 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 literal 25993 zcmdSB`9GEI_cm_twxYq5xEstNbD7G#GbFPp^HiA$nP;06Qjswk3n631%o>FXnP(N5 zGla~Zb#C|P{(N8G*YgiNKfLc(Z!XuquJc^yTE{w$W9=|ab;TnGPaGs6AvvOSSze2T z1e-=ef*B>-4?ppKFd|Gsf}^mJlhd@5Qz&RQFQO=RL}E(qwc3OGCwDALZVmEA+m~L9Y?>WPw&4)^xs_qV zB~5#g>P_}&^yseP8%`G6+_`YR=cSs@ad*zFwQVRq`$kVbQz=wZ9sSy_wqYBUG2N$K^ z-@N8ib86Bx=jHkK2A-_W1S_hMJDzTDB%43}mftEnXPR`-&0j0&@GnY6lF=XBd_~@W z*%*t|Y`8;P9_m_)HZFy(z5YdKiS_%{X)H+$x?{#xh;$^o@ z`6_5XvP<1FNjmKM>8#t<%7(jcast_pC^7m&Cmyg1=#(wWehSbnuYQ^hyA^*bKTm=EuGjp&nXEgMcr9D z#Tq?!_BcyAa};Njw)q3|c5!72k;XVy7xO=r?VVm2Hm%h2;Wgot9v46ESeZW=Ep7dv zoO+(Bx3#y`t~s{H!9?88K{veml1^%Mx6734X{!^qe6FARJCmJTci);$^c0y+oHsbU zbHIOJz|4S7g~*KXXPtw7RgQ-575_-7n@eZ%U9-PllKxy@L}vrb)I#D{=~+!?wcl`S z>~E7ylwH%`C?<}8pTlF)-@m$~I;Y+qA$&3(<##r9HoeW{lB#Ii=`}BG_q#Ct)xlpj z@mB^@Wes}yB+r=Nm3sT`t+|TEZQ2-oW3u9&I|2Rcla61rX+Km_3o`|@8G$W7w-NlQvHR2A6*vhB}s4in)(V$ z)5^Scx^sLayza{EVyE*|aSO$d2R6y2WlyK?ziS%%dE;QAAUECN@rJZBGgXt-r`lgo zl*L<*lN9*~UU=)Mw6je=;FW3NlDhxG7N^8tgK{y-(Y$?ao7OF&1goF5^CWrFFFaSZ zJUQRr7{OlXy7TUhuEvShT&9^7Lr(|xVUk|oT9e()ZEwFn`#MKRUZ?Hu+hzAW^xESA6yI#sKVDx&lEV@g#)4NkJMJ8Z)`k6OF zs{ioh*qHZ&`}b+)rUcHNYq#UfEzDlYwop}9FZa$iX^fZfaQGwcw&W4=i)6#Z)wwk$ zut3poccLX;pra=_ou)16bi~G8dIko*#l^*h8-0HF>fV$liQO))3aopxtS~?3-MM6A zEMCdDQkxy-E-M?`3Zk#jCf5t*-FU-;wY zGTq6Qs~gg@I5qLIjy^fboc@d^zLX+=Dd8Q9XQ639Ug&>oGWw8&Jo~(D-ktybNg4d4 z$MygDYw-A^ZPA<>&6F}*kt|>9cQ#bw_zXEhD4EpHYcL5IS6y|L{@?H;2;s1CgcK{jofL!6}W`==Iql zZ^{3{k*X*QkEdy?-`z1}^ctg9x_tR^R;k~n`})r2yv2|AVN0_mt;(CLa~8ioCs-yx z5VjOuf0vZh6*$eh>?@PT`rfC>= zQqS6Uy<|Um-XSI<<8)uGPenpj3q6mH>e8<-JU*+lO4Z_jssDQ{`!kU|ROfW_4Z3E3 z4LH`TRaEW!V`^Vgqm71PN(azF**%%W;o28R(bNbu-LrJftwbBRl{6>*36JM2X-1uR!TY7H# zo7YCt&Qw*wW>ZsB&R^r!tlF;4jjR?6_g(nSnsY$G+@xf_OHcl=#%AxzR%S)PxKTyH zdO@T95VybOpN_9CLmorZ;^NnGb<`|BKe>Kl`SVf#ovbVqOA*r3m@mA9&``2JH=Mro z&_~yO67Vybn5j`Q-O4wt5EW2QE)2`zIo{QnqJHJdmFEFsefP394=*omSWq25ZZo_P zIP8(ON<7LNx1FV?H|1uuWS%68Px%-Js3wWIe9TdZF{<48vzVKDOFen{k5T=^FxDp=p}ca?G)KVKXu1 zm%P0CZNj*<(xHQY!duJ3X*sF*@e$x}DZ7a4SoF!=Q_fk+UbAG2eyV@Xbp_ zwZkhCOTDjd(l9YO(kS%Ig&pPmCe=M*9Izpko645r8#g(lv%6d^Hs`nPa9b*CB+KA$ z{LYj|-i%mnVM}#N$KRUa9^4g2;|PJ;uJw&1o`lc8Usb!&oIGh4Wy874+}Uxw?CZW2 zf7fxxgszL4N88|(HbZdpRlCm#hD}(d`f;pGcL@m;Y^=<*JXZ*pHx$#&(_3&y*c| zil4dFziE@&G0_skb(2Q=7VQn$23;(y>dKde_eV7@_Y@dsYCe^B>+vYIJj)V`I<%rHo}e_9PUK2o)_{7O1?b@0|Lqjv4~ z`}6`1JKR^Z%^X&)3QHMHi?gd8#rIVD$MFhZWEs7Sa_HwD9e<>j=beu>O^jdlq(G&W z`3ctB+Nk=-V3k|$@qEXD@>?8JgVi36(;q~<{hu@b?(S~BcjTmU<0FPl*Qsy6y!We} zEsH;3e2+X(leS16d*c2=%yD#|h7D~0jY^9ay1r7IXr&7xQ=Rp4AruLtWs74^?F!Ci zMV48&zg_!kdGF~@F>x0oL!%An1%HLHp8J1t3zW5ORc!O8(S+KbchDK9Gm+}n@TBS7= zyuhk{wR`^7)}Pj2^YNzs=ZqJvb1gsIKc*IxBlXISSw|<<442-_V_-t?2#tH(x(#TDfi2hK-x%BG*A| z91Sz3?5Z2pVH6F7LY#1AXRu3C6#LTLhi7RkPF3m(_i4}hsgDHgZgYI^>{jF7zZ5Gz zaIrxOp7(E(-a+EJ?Z@|~Lp=Jo5@;Ev&LE}{a=t6KZJ^YK?Mvc$dIq-$e_@7Q!8C&CQFuE2 zyVU%|r+cD4L@!`sAEhq!IVCEJRm8S4Qrd6hNk)lQrt(=90^u&;F+L-);Cl!>EXZJL z`;92`1J=HWEN%ZCNt6~XyYpAq#OuR@>@2ne=iquN+-3aS6%F|?xIQ?qp}@G>915YJ zT{mw#b?8goLjLqi52JkMYjuD8_#xYUH;B#53f@|xhw&APWr|~MJiRM5>bK^$epk<{ zR$TV(+IUAPV^5Ko`Was8?K4lUx`79*EeXf6J zkZ<4;Ngy+MBw;^!$`C6Gm{9xPqBlvc5EZMfVy=^#UJGvyum|jHEx?}MfmLBJs-!)F zp5tZ1uP=#eKXQz@V!5v=Uy5Mlqm*1AVWZJ6dmDNP!*nQ;u<=jK7h({a^A`bB@Y-JK zvH1S>u2!DjIj@y2-ApLeG{=u8tSf)MNP=mEhY&AN=AwcHHMMjCs^0@XthR;`={e&W{KW_%AhaX8Qd7ZDcg&Vj~(#kb-xc(|44A z2$$;Vm~C&Ye5-IY?JKqjU!H82gA~kh_Yi%#eGLvmA`s5Uks2`{d05|5Tu#3S#k_t# zJU=$%F%o<5826LK{=)hlZ!!O^*K92+&mbB2zyr_E=^aH8K3b2p>&|%>8$` z)+dp!?Z=036=!F`O9;sC$I!GLJ}ntYeBy$J1R*Q)u}_Z$ZN8n((aOBW~EBNo#{|ilA&eg`Mfn7i!9ib9oa)`aHQ= z2!&l8)#yL>1!(*BqrE>Hxc+A%v0GhgM0<1f^t!7asrD&c5HDogv4duRQNoJFxsE-j zX1nynMJiFmj^hwLe+&(~vL2WG4T#PIWKLwmnApqI=#x#!lEmGbjr!{B`vehIKCa$D zKVWeZZqks69{>IV zJ9+46c%H#yr&<09M=|G4r&s4&DY-rt?Yucx(XzJqlU8ywNXit8)K-o1VM`NMuCB_$oou9>w`ig)kcjYDr@;`k04 z&Ao2eY(=y7kp|@vyW`0w5eGAF7H+z1)bDP}*z^@=b`}^LmJVdrc>URM@t7*l&(GU9 z7X9SOliyyPQf>1TeChQw=bl&^L>NeC1$azay}YCK{IJ0B?e95m|76co4C&VSt_!~Z z?)~gsAN6q%tWMN4UK4GF!FKC2rxp3sL(!=E|F~*~>UcGYu_4&9WXCEy4(d`;B z2vhe{(g~?UkLf7hYz8MhM6uOPqgUaO>^j?frJ#1zAmQB2OL#-+eD;gN5Sj{*+jzPU zLJ@={MAch#{f^Cg=XVjSFHd8~uNR$~y>|`a(L!`sU))qcht(SkKsJhkLHu%ppv72v zgfxv5-(7OpyrQQN&t0~`o_oi8xyG!v-?(+9JFXSB$fuHBM-HN8i)S=DJaS9rLpQpw z-J#{V0o|ST2`1FpW;%U;+f5NS33VX@K2WY|gG&-ic0pluu3{vvRc6;{*zd1#j!ynl z?fVL1@FdT$r-D4JKWOnn;4okqM*KRnuY4=9(x{xT+my{3je&Y=&x6O>4!mS@f+xqD zx5V*rEZTp|2-})5-3`K_XTA&1%=a>k8J;Ha0k;2~XFR{Lyu`sCxp{GAI@E@n9l(6W zl*#c?v%&vy3L{WF?5*|j5c6C}^IiNvZ?xziMoCGHZ|H~c?k-?@j^2@<=j@I5Wb3~Y z9nJeoZM2@r-fiK37mbqCHPQ=N@AZrBrD@!Os)qXd@`#Bi*yvZ=kBcfAEx;vpl2U z@)a(_@;vMMR8{nG#-tYxbQXvx4w5lbX<@^{!oHQ+>9&6tooG!J@cQ%XNY0YB9>9$_ z__&hk=x^{Ba-{ny8F^8K>npKRnfjK=V4!>-YQ-~FNVVJ|YGUxJ<=FloVi3<>Qj5qv+}YC&PdWRV^*F#IELP= zfh;2<<5P!*4Bz9&j|H8_J~hn7)S`IBP>oXgl0^2A!itYRBrv+0zPfQ&HCg-`G=;NY zIP?ynHOhdh6qiRABuf&gNxuf4G&UY@8drX6K)J;7i)`|pB{ryr0P7fZp3%Y|%nCqo zkxmU&SI}}_{bb~6zDsiji*})wBxJP1XMqgvNmfOmD5-RxGgx2y8Nn_pDmuAH1=)&u z)DB~S_v33`MNfT-%zp6om0{1HM_fMm0B~_BV%JfCH_JnjHdMtT2=_UMkcvv7Nv*2l zpWTg~dfB{XT6(DXN(8xuXIBSej$v@Td<%f*S^$HMm$XVpPfl8^G%7ox+^vqY#(W85 zoI!9-nx^f#=m=)vJrUfu;Sxa>c&}wAER7cjAYxCD$UVZGpy>VBw ziHjS4{1RSEuIv}rc}U8a@SB^REaxGzZ7)d`tD50Zs$Nqf&}quNDb`2Z#SrLNzpdl? zK7As^N1glH%dxNAyoh+~H31~LdhId>t|`EBswbVjS=&=+YMSEtgS?vV&TDjrBjBh? zaoi+D_c@QR7n59KUA3~Tq>faD<#d?2lheBm$fImJn)Y@y$Nc{9e*_pW^ zsGxCS#Rq@2J(J5+iso>hNcIrV)Qi~%_gGMdg)QvWLn(1V)ePXL^V-7o1PPAuCkDM7TDT&Qz3A3}s27yf48sZsgb(aU6(KJZ@Kg}HcL-nksqxIExcx^?oQC%6 z``h%km+s2pBv^d_G}4DoSrXIsv9+ZZfpMT1V#$wkTpq6Ra;XyQMy2@_3;s|x+sntY zaI%k)eoYc%le>5LG2HA^?cZU$4^(038OiSBH4X89=LR?jDjYR*7mA9`R(`*Ca%`+A zdTDdMJ}WV+m59gOssE91&zqbWd~jT7mPYV4icbtRnJpH;waz-Ur`m_AJmyV=B$HQT zE4){1+bQP(NM?QI#HeMC0>z+bWClQL2|A?an-NkR$9Qx~JxhZT9Kh)?XOmk)izUD7 zD(>++X+6zx)CvrMKZi+^%qw9K=%oax9(9qL!B21h7mO}@H4KpG-I^r}N zFBL#(5Vr4algua2F8BQUR73>`1ZKDCPHHL1RCn*O$BL>yUbc1>7!N$iPjav0{0s2m z%PJtn*(Td^1D);CB!DaB_XncQ*jQOr5q>a;S7%h>)GgTc749ozTzQ*5-V)yeB|6id z<7e%R&y}nvPyW#``B84r{d;ASqP+X2vhw|P7b`Cb{`Aybucfiaz|~9Ko7eel@Nuxx zMWTw)c;uwPSG+1D;8y4JahLHLNf%fS(eXw?pYURTE_t^4(_nw-*aWS{Vq&NuDTbxw@Fp9;n#ouLGBUvH7%MVcmx9 zv|nvH=cbPYEKX~wJPIVMgXnyRT%o)bE0Qz z9UHxUAx(Vw5Zk}#tFveq9;FVL&&^gu87ijgjkwr5Lx&3adZBDZ8QVo9x9FEzU+L&Q z3as744fEqZ_V#(!g~tF3>hoJoBs{%2HSy47j3Jb(I!;H$^I!D`>n&xZ%JesVWf!z7Z!1F!IczTzZC&{xJ3;O+E@w@;) z_Tss<&?2B;E*{+i;nrLIrAm)ZiLr{8KT633{l?!2 zfhSxs6`42orS9w2#Pb4jq2AZ3Q{*z~&rEswJ&h4WM_%XHKhnOhr{+tbMbSC(_%-^Y z7BZX^=8JkzT<~o0lL!**8#=uz*PJ~4l8>&{W8xlCz2{y9pw!xoy(n{_v-!e%M2?tq zrWr3-N1W={y$xlv*f41w-iZWDPs%%jRpujp6x|b{q*P=@P{1+!tvnzh9u9;IANI)Gb$9!3$5f5b%NQ-lfFw{@_Jw{afM*uN9_3J_ zVt5*hJX~@u5Zcqri$6Y$KUQRXWgGe)a1~jvEL=d>B_9a|%svp}>J!wfu>`A> z$Iu|)Bu=fuPLlN+z(w6%iVJYjnLuE!6q@4&1a1wKAL-;LHld?>n0r55?Yp$jLd}V7 z=rIl8liA%WQB9H5NBw@8emRQm;5=Az7Q+=24mz9(rBkJkPQ;|%CaJt`lZv}~a z-$&MleR_nd1Qi}qI)0-wNXvl+?Vczuli2T_fucfru8$U=xeD{Sb5|}sJi&f`#J6P7 zwL|SI_o+-2)Io5OHPvFD2rPl28E|REE)(hr7j8c<+s7`BqR>EHps#l0ZDU(QBtSrT)VZ%RsF>#LvZTqw~PR4zF|h+WET*B!RQI(venZ z*Ah<)S6d{Y?O`5GhFc6*zM_LA(PS1A(;xQVyk#@^`V};s1A*)rm(iIKfq2ZnLMac_ z5FM3Xsr6&~fpWEt)j7LBYcdQxhZ92mE;Dga5RwP5;=eX0+tbHCo^*^k>G1l^9T9!R z9^xCcp&MDb`d}Pz_+zZ8@8Vti;TrwrgPt7lOA^fP!8Yu)scHEm609hdQ<;A^^w%du z?XB;Gk^}OhDyo3zTrH4!4*SSN#tTxTsKekHIQUC%x_fc(A_TlU?29GmdtZ2-8bZn@ zDncsJ9BQXbbq*p@Tkj%(8=g+=Ik>b9SGO_E(+4f1FO;l{;eiZ@Rd~h$OO@SqVH}pq z73)~L5_c7(F9&F1TwDL3O~W??z-Mf<#_<6{|3F%|`C9^5F;nyA7%sYqm<9A2R^;#+ zc^er3ghm4i<^DPaM*2@h?0&s5;JuI5J5n3=)P?)g2AnFDKqmjSi$E-~ALUe!0q|lN z7np`{X*H~cZO>CS2Z)o$=qA~k7lCXqICU!|7X2P4Ne_&Pi%>WGJMj?W*O!2GX?}hC zr}cKJmoberoI7oJTc7TeSJ()63bgHZ1(lZ!%Iy>G1-ek)QKn~teF0d#j zJesnCzEdq(%BOo~TTx#AGB^On05TAnjp^7rgRh{b=7vnW`5J`uzjTB1LgYird=273 zuq;BNK0Szn&66|UozxMK=mdT?M#L*JdcB`n^L_O^cSS@@tdBmiWK8t=^XKyQH)xI? za==L`kXQDMhBUPH=LNCdD4n7e3DFHMvaB)Z{^)jWvr_zZl<@5L&2{fqVIh2N~MM6mP3)?2qfe{q`wC=cc9>%wup zy?SCUzaFGV+i%Zyv?hX(zR}5F^3djOV%K|?wLI4+fD|85_g1=OA&L^Psil9$g%@VD z#BTo?9I+!j4R7p_1i4l7dzj*mIH0U+|1pCA^z@JTs9%$;wjfmwbe;RCTxLOx0U7m`&p zYDpinDqu@>-HBd?$XFl`b;LgdQ!sGI7c_}qFA-HYP{&J)j)&8Ict&m8C}%if`!RDT ztU#T2hWNIZ^*|jFHdz8+jeogOiBS0C$fCw!+k`am_z-|X}ygo1hN+m(Diaad!1al zB*n|i`{Y@XBTA!zs<=UAyvumAg@L$6vUu!up@6N$kc9Z_`!!H8xJcF3-HIAV5N(2r zJls*8XC8e-FeJCWdNl@koU5Sha^~t5iVk$?wCk5xCOD`+J@5GYYn|SH_}GQqhSj-& zao}*{9fqou%*``Q|9<#L%t$r#PMfT!8Pvu=ER2Ur7{=KBt!@R*Y9`>kZ=hq&vze)( z4629)Xj?F8BMWD8xt*?WPqUN34XWb{YnGJtK}+xjEVTr*$o=yB-Ff;QTkC9R%h8j1 zvN}z+u^@&+q0u)YENs#ITHM}}7ZC=?vp;6?r9M0%5(TtW;@Oq6l0*uOG!S~+gg@(DxX%T%FHkoK$MdTbKu3bIfO7&F)bm3Q;Aw?idMCj~zAvlX!R zbBNx1#LsgVQDjNXqwd(3^vqM_J4a3kpB;Y$Nd&o0B<5?_zzX0TL~4Q3@B>~^51Vjg1ns${rA;0&Nni(DruKEK8KfJmEm=*iQ+`4yAN)OM8ydI|unQPI&B zZ-dEy{mF42Z^jG?RTCw=Phk)n*}w6Pelk9*c`^LP21^sI0^StXLsf2wJy-(nbK>;= zZnOlKm_T>LG)5S zpaVwI{UhUF101LvbbcQr-O~Ur5(q;mD9X&d8MECNjIz{|JDnZ+iWQ(UxB6(J?iWSu zVNyx`ch;?t%shGK7Ge%bu&Xpq5}0H_{;qTV*ANT+EEr5FaexCYJySSQkq#iAEK0O! zj(JSSXRu~RZ;!APzCi=)z?mlHy&MTDM(bghtdO4_>iyVelsj?C_=p-_(>kL|5B|BX ze&ArIKM}hU2#&dJBEmqVu6VqMS>mim50F ziUD3k$rEL0G>?X9W7%96PooMy!Hze~0y5dQg+d@Dxk_K#rKnQp#! zQAWWrfx+DH4eelEgQMY%sx^hDz9rw4Rg4wls=gm8AhTHLPJAaq+5xk zuK+0+e^_AYt7a*$oBj>~oemyvO}8Q61uqf|MLrR?9IYY!7@SNAi%4Gg2m|LH!^>zQ zgOo*Bm6HVduoAW2kiT~ZkAdim)}{od$VAawQE+IqSLa6r#CMAQ$q%0p{&XtNbT7>g zwmvKNf=p)-2_g-oMf+upvO#_cJkNi4IjtAb>l3KSPdWW1tE#6*O%fSsE>XvBxswO=#w#nnZxiQQZLG6HYJS zF7<&AXf z`fso3u1{s9UijOwD58de-X#V%SVv}qiYFvO72q;X^YW@XRF2cN=V}d<*>QqGO~EeI z40sYli;9SmTi3O3(0h^mjy2xfAZ^&sJrhOF8qNz5W*G@*T8M2A3K>Q4|==W(u{=j=^tC%OT-rf<6{yphD&_^Eemc$WXW7H76 zhKh$92vlOuW1*+n*%R~ute%y-ZL5;$-ZA#26bv@-s|?79;zQHviV<XQLI z9Fi(3X9}u6f6$ns8w2a%1$DflzJ&Yi6F84wZd;^7OAVjH3h743@^d6-s_#;9a=Q$4 z@FwhO{mr+RC$F@-)avBvT^CQ_*Duaca`KoTP8J_adGtEwyrT((d4!SQN>|Rx3%(4X zdcs;l$3$O7cSZOhXFXHdgMM9iSJ%?mTvoVZ;$d7n zsDi0k2Jyd>+@G+0W-iDRd*na-TH0re;l8MrrATEmf#RRD4W~URrIsVbXaR#e3rS9d zQnShL)+7L)aJS zB3F*Q&{i+ME^4+IpssEdW73jUcoH0Oajh#>i}8uN-~F~08;fr?Qmlh=d&)GTpeIjP zQ&g{M1w6u=S7cn4HI9Oh&LHC1!qBr7`s^`Yy~1OBsohr0FJ_~>ARR#G^=W5&^9#%E zThBUOikFkVnACbF;DGo3z62etr%_6CEKkeZyN5E(-UL|=LGb~jnsx?+xUh|$i}Hw1x)SgIrXLEXP{KtToL!*^6bxIa5MF>Zo8;&r18}FyI`ZY3m^m1CW z5<%R8UPHdzLe;gogHr%g9s|u0YT|me+Wl7hyKe97!Pqn2Reu4RC;@a;RqW`WWdJf= z0F0r1su7G*=0IWoqN+{tc}{-{xuRT1OIuB)o<6@>|8IIQgp61Uz3cPjk27BUPd#mA zl0i}x;yVT~V9_84@YZvB(J3+Vr-UkwpOa;Z8nG^kDzJGrYfgLGg#aC@EQHZlC%R!Oz)_2uJ-<^rR-7U^_)!a9J zxFoK$ZH$0^@q6Eo7q+>tT6HsL5>>^c09LyE*Oz6}Km{2(oem)QzO`vG5gVwJF4HpA z-lzh)Ol*jD25}T*y?9isCGK|fN4pNX*E(Gm_=nr)_JdVO?P06rM6}|`mNt~dr&rXF zWcYjRYfbH|>kO#;ksH(?$-aa<|76-vX1NFq`8WDEQ|?9=M{=f%XGy(J62 zM^Gfsc0ZU;e{j(A=R>VmHx&9_-%bZUbjX#F>4Z@X4RZ9P(I20V=!4^M`_(gn>wQ2Y zA#q+a?$VxhjU-4I4gEujoGO1J@kR6dzKX2w7cnp<&+xXS%IvW}^O?hF%ER)w67L zHX})i=N(0j&rXplL02a@bcR&e)*V1sIelAe6qvD}xHR7~{pa9bdVh@Cg-5NvAbmAM zjI#s5@Zi3%bsKa0cj1+T-5^`|l-{HRT-tW5jk@z=1e2q8dz=kC z&SkM3EZ$$N*_)99-mxJDCh<0{^ip7X*f0!r9^hSF>eSX4x0c955+1Xe#{Un1D4)M^ z<<*T389+A_1L^M*my!+T{|pvH1#p37f=}*ZpNzy_i&H=_3>p33#*U>Q>ct%wyioQ6 zuwUjJ3vl2<{T)zKp1}9^53#^d&^zUl3UhNSBMfp0ZvYsCrCsOB+8@O>s_RwbafYXXH|^g; zA?3O`VQ@TxSc@En#1V!u|NP|Ke~(;r4=uCFc%AdOGBWFrgAaoC*0Fs3k{9bGm#&0D zx1YzKmY)iOO$>M_4WzFAK>ZcfLOT4Nb=S*l<6XL@vBU@i%otPg71Kqn0u{z`WB0Hzw=(qDV$^MaK7d4Jtv=Ayu72Jy}Z-maC8(xF=MLH0NzC8Z3T>_83%2JhiDU>>Hsmv~X{z&vV?sW_^6 zYh6`IsWNZ>go|<9T~2EJ=<|kWav{j8iQocgR3se4O(P41mseGNfK11AI~#WT_5QWA z{6>1%OvG} zRor!wS^I_RW2o&Rai=nY;ISuN7PmsSqUBx z3&?B5HXR%;dTNa!J#`mhf`NfXj~33oHc>!0Uj*Isci(+pq$x0uT0?wmGVI%d zBm5W&k6J<1H1|FlMn-#TLAC3gx@}s?;;bk73yeAHvFjfLxcw6fMsjcA`YKfic9M9PC zHbe%a7`2TIlAc9V2V9|-dmdnuRjONIYZvn9?`PP@XFO-&ki+yzBmjiF{8(i0L_2{N zy&-U4!s@=K?la5ePj&8Wq99>(?WqiICKyt0MIRwHA{TUlg`Vi(d%#N%k(Ff(sO-sM)b`$NrJ>F73>P**#%v zv%Y`GiNK`60GqVWHh+Wp1n`ALV2ngn>T{A$z_s_#T(|QvKj$cW#S7?aaMgU#u1eK+G5aACFNk)*H zqTvGZLZ=vHzQH#XB3U3d{vq3_YT$=(ddMxug)A?W5MuVM<%QVm^qUJRi!+Sa*NXsc zy=yA~mN&)n#3xC3e4V+a=l22F|8{2%c6NQKtA#&1#1BI|3q~BD+9NRj1}41D_K=`+ z?4W+{NJBz9W9qjOYbqQ1;DNA(tG5()inqAieQ3P5?<=XQy4q5Iv3ePY28YI7HatT7 zUx=S!Z(c}gNtUR{d58D&nB0^GgOJ>*0?t!iSnt{1bGnt1bzkBIQo*@=S-t%GqWZp? zAP%ec_V%dANUBk(0}dXyZ{L1y>aAX&2+(;POgsAyBwA{~^Tk8A(-cH{XzQ0z8XogU z?rqb`6L3qXKaFPkYV-0#l*gfC+~zbT5W$9m(x$k~k&_z=Gh<9D&}@l36oLu8I%eSY zg|CPcY4KUe1Y!QeNs?ZI%x-Dqjygvt2$q@eByn)g(@aYXYgeB_i!JZ)`ujcCKc2)= zP9RicAK7BuD%yAAG#KUU}_t;R5nqwX{!$?hN7ZO0SufLpO9%x+2F!AZ@{wLALOVSrVCa?0F7lG;Y2qns?GAK8k5p#n3JL|hhM6Lq zc!};4=h)yt)J|GMK|lm1mr0E$VYS(T_#sLBU}#-F@xqsE43Doi{&pqadv*3d$p@+$ z=+b+RF_7*VzP2WbX(NBRcm^vXOJPOpV3J}b_B6~!T*m@iS2elz=_!Gz+e4;Nfd??P zE#||WVl9IHHvuTN<>@XJ=!qsGwXPf{E!)x?!SVtuS!I$$18YosFtFJI^w{~VBnb(lGFrU;xn?_Cy!ah>XQ+83|vnB zVjjI52#j>->SiCrtIJLn+aV>GO@NL5Uq_s;KpGn(;4Iw~{|%cF$0WYJDmQu$-udKZ z-XMZD4(X&IEfX;NP6NH6&P6wlZb4O)0GE8>e&9sCpaUU)0t)z~wPG@~|L#J0kP}dX ze?&Uz!`>Kd4}K0Z#6pSM_cFWu`urG@un$fxh8X#eXhBka+@<`C>au_n%=#WbcOzIB zyfVT(>dl-XRykAtlXIERL_ulz34<1W|02egPfazF* zhEWOHfN>b}bT=dC+UsZ%Wi-p&kZy} z984r#hU2oY%F&%jTqAM)bQ+P&OC&$d!O<+#D#-?(sd!M`nl0Op5=WcxXOOdx);2-# zUQ7dM(PMziV!$7)3c9WA%UN3J6yP{9BKJgkN+=Dc0#|O|#|qq;sGooL%WEzFn~m{@ z^q!LfSC(IF*Tat-eW%66`~QKupydyYs4Js*D)7tv#2DY+{2R|x@LD#-Z?&&Q;C%^k zFUj8MdUFi(TnDKwu5dY2OT zV1)P^g#NvaJh=O^%bgfu21R;nLGIlZ7*|0=ll)OlsTfZrZNet=$twIqrl)Jxnj$6W zyXIsAjW1$?s{(0`?S=lu&n(kFp(F4>K{ zYA9JIk|lfhpjMVT1NostD#kkVstJM$%lA!_!C-*E|JlE_>qslt2?q+*(#jOkK} z)||WS1G@4O*nblzjCO|ucKwi`?d1uBO)9X4#w9gXUCm7ZC3I){E!cYwwec@{pU8^S z-~`4l_dx;A;EG~D5Y^^jsM4x(wb5wet*ps?@JurTXvcDMhBek~&6kQ&2L^JYyVLzm z?VitdDz!|GF;N$Icg!E@@d%j_3mB?8K`S)i-}~XbTHA=o?$)1(O6worLzv>K=j#UE zdjofh{C-~TsWM>TF1P0b2S`*DyNaa_ozS$(3!##PXQroTg&{c$M_7cBK!_82M9noJs4?-mn;yKXD zMiqqxBkVa-U$#pR$eNb4t%)by=O9i5)1~tgI@?bxU-%ejM z^tSEJn=%MC#L1eB&J@Dj*RJ>fg24%aTP8@Gv_J zZnMxGV~3Z<)#WunEj`x-;?1uhx=)~jx0Qv;77J6U$E1SNhT;~7$;$e(YT(Ebwn>A$ z$%Yx~KqLJ_IzvI*68xY6#2WLZv8LyDE=-b8>?wR?y=o0h@ba6ttew5Hte-I)g5 z-Zz+T2>5;f7P0&@kDd=qV+{IhnA3X}jX8#|mbMy61@t%hEs2$GoIyktp&SDsKOt@!BF@nEuE_J4uZvC&hSBi_X#IA8uWzEp9eIp~Ki z?}e|dQ}ib?L~Fwt&4wUqm;fT?zTrdQW2$AaV;!Xr4A|WqfpjJBvQ`VP=HGYV zB$1LrBg?1L-gH`xkQuQSIgUu9KoPd>M8kD%|23+khXFNOb+TnT5gI_r%K)D&Vg%4g z9rQd0V(zmN8-M1=D+FM(o-Q25UZnPfv0HP%b%H*t_FOQ9p?SkBi}+MzI9Y%u_%kJx zxMJ$H#YQFtNW`=L@OD-u!eIL&0)h(IjM|xqNd{(wv|J`@fk*rGmhAv-*#CS~CRFh_ z$=px@z7g+Skq0)--D1B362uTG8UXc;7A^-_J{{2HepA) zuxk2cq-$Cm@tK|>JEymsdKe$;0`#x8Iz;(7J80E9>tcvi?} zQtd-G!fOKlxbya_x||xm+He}cIp|wWun1>hChOTHyw|@_=GZ9r+SP-ePHv zXqYW&G-{`7q4_K4KlJ9`Q_6#y3Z-HO96$9M)RpnO^d@Q5nJPw}|9+ne`I$l(y<%Pf zc3cT2u`@twcC8pYU@A&HpJtBm=RQ45?S`88=vL%cg2^s(kWO7I%-@K!o<`X*a1GZ%HZm^|*RBo>P%97F3@_rl3r!sjq*Y?MEuvZ> zxj)-GH6+-0xT5y(K@AoF;DD5#diNR|qIP_8ibHP?fd@t+7gfT`7il{*Yv2r2UD7YN z*9V9EBU(6Jrqws9_qOQ*vd&U>U1kYlFM^>Ro1TJXBbP5C9P7Y)1r*6%uiv|@7rv~g zQ}mibn$A%@Aa9kV!AF5#vMFn3VI-z~6&Q6ksPZQJ`VtGO2yw72Q#lq5wnafH%e|*; zw0EEXth_q(XjT{Qqg!Af)e3&J+z?gYf3H!7&(i9;cvgqLZL9>2C-f%A62O`?2G;m# zmZMC2F&Kx2*^OeUBCnAmQ8?NcCIFMiI$p-g?|oJ|VrvvcsLhK=fw{AysO|w%2THEz z&YgQh5w^Xz>t=?qZ38+Cfd^!f=ky*W6F-RBan--8T1tK4@6JY>*QI#MO#58nY>^hio8G7n3;Os;A4_+uW9iVAgml`$GYbAB+E8$XE7R%QpcY47rM87f_(F;tw?ptS= z4mG{?&3T|*Ktc_lyk&5084v=HIHgJTg;@Av7nqy4P{k;+E(z`%uB^hrymAI9@2tWu zd@Yl)OtFxQ(^c%tqG9y{RVmJa1^1CaBaN1+F+tn)d_yuT3)a}F^`nrnM zkLY8_{CTQvv}S*At^%p_&e}}`agi5@4tVnH;`ldm6nl(42@U9lRHph)UyMJN5A6bD8I|h_?Dn@woi z2zsiE_wVg}!2^_@f7{5BVk~?o5&Dt}WTHTg9q0)ByYsu-GVQ0A7rwuXdzjd;w`62) zu)LZ9q`jqEQ1jBk$DxOu-yS1=72p@T3}42>4quN{ViQJ&=Di`?x1kiAT;(pWJzgLixTt%|Y6CS%-l5xQQWqR;=qsFv!T~BKo)>!B z`)X{-!NUR^4i2_NuISs`|LNt*|DoL5aHO=Hw4IWQQjF|nsSq<6TlRf0wulgwHRh!# zEkcy+OQJC*yM~aZ&PhVkL{lWRBEv`;j@^5IjXLjN@cME1%#7!GzW06I*L_`=}c;&QkT+rJ*=3#)dj1sQ^?++`kgYp3P*x1Ubrgqr+14`&e@qR48SAiuJP#Ey&u zu2;|s#tt@6NVW#yQe6{nxm?y9c939zRI1~TwC?<=~+H7TQ<0i)A7c z0Ib?|PS*@#LjLJFM_-{+?e}BT*UQ|Fi_2d<%HHLrERsDo0@OwS%S>sgXE(on)9&g5 z>%WOUfbbXbsXS{g^>8s{c=!sqMY1-MLF)!#geV8`3F=^>@YXp+>Cje^k&Wfcvsj^u z=fztLy1BJz(gwa+Q}*4;YN);b*XCN~85*exS}Gj( z7Lbqa%>I8Acvbb=9)374ofa@Z^I;(gcxOGdNCcx<{|2?pXHF|)DZlE372J=+i)yy3 zk>44G#?%WL z9cz(BDMylKIa=SsP0g3;$Y?Y?U`{sdvNoVqTH~mn=(sE0zw{+Wik>{vO$J zW-)64-K}IuGO4SLX5Uc{oh*foORrc)F)_@T0<(IWmIb4a`S%dGg~;qy190$iM;i^e z=o8ce!@cSdZ~}`YQRPp}8IlIY7%H539Vrk6#*pD&VSdQilsf0VcR~3Z@KHcrBbaz$ z?abMku7!SZmbk%UcMMjg+c;77o)ZvF^aC-%&hrr29q3&8VE*TyfwA zu9}reE?ptqV?}r;MlEkS2Xe>Iu8O)k50f$d5K=UlbElfVcdcvpdDsJaKt>0pt?r*F zwli_KhJ>oz!3#Z}mrP+1CJ6)tkN=zMN=Me4f$4#o3*ZiA<|DE^!K2r=Nf`5_5NY4+ z^G)wE-KrMF_vkG;(S~4s1u{`2IHNPcF?poc{?>qyL;WXEOtj-R_QH8sIL4sn3>$k1 zl2wg#n+)(Frw*LvqI$TLx={5_HCny3q`xb`Vl) zRWIBA`wqzP7)WGyo#_MV0A19x?(sAI*ATTO0n9)NLYVr;H-aONd!3v^A{ICd*C+cs ziii>pH30_*7uN4QYzl@h59&U+a|UCc)a|0%mTpODbhsN42jht>01m}4+e**ZIyFIT zDQLjSTH4z08j}wH{@3~B_BuE7DS_PzoduoIwv<3`KObLaTUap|f|xEy;wxug?!Bt$* z{Pf7Ibz8n4>s##N8;@Te@p>1qqNFG%*`19xgdUU;)j|G@ppg;wJ*WPz77|eOXAT{o z8fRKL1%ncp7FyW5avOxdIj&6(y4=T}D6Gn=%or@O>4=k;ewOk&g~Ly&+y^>+8A^Kj z@HB9A2g}Ip&7I{>NJ1&|;d;i#sNMxvuNW%H`>+FKo&AjIHj?1oiLW_xM75A@C+o6p zuom)k_1eu3`t6|eZ82ICNNdCyPCJml$GyN9V6vxO2)pN;gTnMq4Gkm`98-&T*4TGR zO5_)7Sz~1&5KyoN5u400Go6`+i+Hj~MP4wrYi4}=ni8ubQ4IonPBl>b44FjxdQ7^j z#-ibT7&vWj9w0(l{U+}z&{XZHxv1t?e6S|Al%80d?>7OVO7=bSo~=C}pIl-W7=HbA zI+yWMd6SHAf!A8lLjA{=rprHGSqw%0dwSU`WI|SH6iGFov|G271P4cALa;QH*Xx3d zK02V>2RI(p-=dZOlNLFP7IWuZg8n~$vmwaWdHToTv5%gd_P;oe^7W; z1juNOVSm6|COz>$9jrA8FTu=r}>p2wn-(aOFshXGQJjl6vM7)+7k3JUE~ z z_ob9`37Enbv|fW>+(NmZ%GMgL7Lwk911OLPyjyzU0*^{3(M-nK-PIOgQA*!lI-&DP zb@p^3>5nP=D2$sob$5j^Lw|U>^}Wm__J;m6N{GpOyYrlk^x27F+mKI}1;vgWu?Imy z;&w{V%ou3WQx0`Q*WTy)EaoOaOv=lF8-aWpa~um4S)p{gyHo5g(t`qQ=-|^+x1(g0 zM{7OOm_ddbh9L7P8L&=H&p)F=>6sbI10^f?q^$3MLpMQ$hJ7`qe44d-lW2 zl`fNL#_Dc@&5gwl=D;wzu?&;zIt9qCA1!uQg+X;N-~PY-tV!4_vnohqtbEnO0lSxTmJNO(hxOGJrHg4K@*L28QKH9O z6!uP^G9hXQ<`}>JVZ%DAr4>2o-5XeCO{9(AUc<)TBGD|Z+=ODX>NzjfzUx6+n>gs= z&QE^g_je+ERy=5++DflYDSHlgUTQHC4J?Do8mt3}e~q2vJTl`StD40BMRWe%Tc@V% zF8#*!xkNTZW+qovRWV!IW`H|B7WP7TXpLo&ofQz^NurNI3cyX5r5IaC%7jQjQt?x= zuo#$A-=lfg|E1ixM>DHMAs-@4Jm1_;6pAW@P+fOh4c7-^54sK2h4a9H(cjIWJu!>( zR4xo#_}q5X-=`H&0zEE7ojeGW;{KyVstA65kC;!yF}2SvjrfZw7>rF2Y;%*uWx6-J zHm8!zYXx%e7s-=)Q;nIq;ssKz*_@_c6a~uIHT(>3WtQd-Clnjo615veNl zIY&e>-ew}|f27oyXG>D*7b@?p?>EJt;dxV#TS4to9-M>3)b{as;*K?c%GxR6GaX58 z73-q=I^)OBn!%JzPppJf<;TMDGIPW3&mx!)`ohhnA~za9vPN%QPZHgIr)+}S1RjnS z&Q3o_EYO41ls2&yfdWCsAX$WM5$iWQ>D!j2ZJ*#6ULYtoXoi7`gxBDCBYkjO@J`l$ zg1~doCY=W)ABj-sdkW9P)*pf=HW|EZQI#GSfLk&zaa7-tB&MV(0z2=)$^r1WC>FUD zn_I-mtseT|)e(qvsZrN;3LH)jQVmJtN_WcMmS72yY2IF&YCxp)lK@aD6*R3=v-d6VKhB$oYJw@mgXntsFGB?pgPpRyf$ji zqy9(mkCxz{t#v3O)xh*oFRDAIr>L>}r!(_)lLN}l)vttN zvx^D(5SFd-k0PNsp3&?eR@v~Tz<0b`^I1;iIVmDjKeAtGHBc19f+d zYu&(j{G<9^$~<)e<(lfZ!X53TkzeN5uu*_}erGw0;BXMxA(tBGQD~gYS1)=Mxi3vZ z?hW%!jW>6=UmU|P0mZ#?(-Lit-folvV+%L1F0wW}sJ(dZk0B25#m)Ik-fZwpKa2Up652|- zwn-Iyq3{Jrj~KrrO9XB3hOLJKS+`LfmjbVR?dU~>9g!DJ(IsAkDm)Bi#TQt0ZbdPD zG>2RE5Q_~(24=! z56BX&ilEG6Mu4P>x=~PvQfoldPxnvrU>$xBDF_LmWS}}V)T>}=U@cMz9(DGFDbIEu z9O?$LGcf4L9_kIw73#SMLQKrvH~3kxyHu@i(AuQK%Dxw~MLPvP1{-0p{t1!Gp%e+pWR8(Z^1r0F#UbwC1?ILgfw4cmKq8x_yMxy~7gF9zM6 z{_TCoZ&`Qfcadt`>DxQ-i(?BBCYG+8v{fL;$&fOInJ7RzIyTlQZ0FXkNhBF@}63G4T843CFA4ccf$=;?vb6!S=Mzm-%+%L zz>Sy;LOEKlMH>}=9UW&^6!zxZw^8-?E58L}zfk8vq_zD2{K@1(J?liPLWYFkGWa>B LZLC$S>3s3Oz2Z?y From b2aa648c0be0f7f3e24b354eb49f32251eb9fc26 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:02:42 -0500 Subject: [PATCH 224/332] update readme --- README.Rmd | 3 ++- README.md | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.Rmd b/README.Rmd index f9cbc8e8..41dfd25d 100644 --- a/README.Rmd +++ b/README.Rmd @@ -58,6 +58,7 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World +Here is an SI model specification, which is I think the simplest possible epidemiological transmission model. ```{r hello-world} si = mp_tmb_model_spec( before = list( @@ -74,7 +75,7 @@ si = mp_tmb_model_spec( print(si) ``` -Simulating from this model can be done like so. +Simulating from this model requires choosing the number of time-steps to run and the model quantities to simulate. Syntax for simulating `macpan2` models is [designed to combine with standard data prep and plotting tools in R](#modularity), as we demonstrate with the following code. ```{r plot-tmb-si} (si |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) diff --git a/README.md b/README.md index 78d88a7f..6acf75e5 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,9 @@ the following command. ## Hello World +Here is an SI model specification, which is I think the simplest +possible epidemiological transmission model. + ``` r si = mp_tmb_model_spec( before = list( @@ -150,7 +153,11 @@ print(si) ## 2: S ~ S - infection ## 3: I ~ I + infection -Simulating from this model can be done like so. +Simulating from this model requires choosing the number of time-steps to +run and the model quantities to simulate. Syntax for simulating +`macpan2` models is [designed to combine with standard data prep and +plotting tools in R](#modularity), as we demonstrate with the following +code. ``` r (si From be5cca56d4bdea8d3949aaac063414ab52101144 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:04:39 -0500 Subject: [PATCH 225/332] update readme --- README.Rmd | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.Rmd b/README.Rmd index 41dfd25d..013aee68 100644 --- a/README.Rmd +++ b/README.Rmd @@ -58,7 +58,7 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World -Here is an SI model specification, which is I think the simplest possible epidemiological transmission model. +The following code specifies an SI model, which is I think is the simplest possible epidemiological transmission model. ```{r hello-world} si = mp_tmb_model_spec( before = list( diff --git a/README.md b/README.md index 6acf75e5..0d21be4f 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,8 @@ the following command. ## Hello World -Here is an SI model specification, which is I think the simplest -possible epidemiological transmission model. +The following code specifies an SI model, which is I think is the +simplest possible epidemiological transmission model. ``` r si = mp_tmb_model_spec( From 575080f3c9b36becfca62df6a504b9e2184795ff Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:09:34 -0500 Subject: [PATCH 226/332] update readme --- README.Rmd | 11 ++++++----- README.md | 9 +++++---- misc/readme/plot-tmb-si-1.png | Bin 34593 -> 937726 bytes 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.Rmd b/README.Rmd index 013aee68..213fa1bb 100644 --- a/README.Rmd +++ b/README.Rmd @@ -58,7 +58,7 @@ remotes::install_github("canmod/macpan2@v0.0.3") ## Hello World -The following code specifies an SI model, which is I think is the simplest possible epidemiological transmission model. +The following code specifies an SI model, which is I think is the simplest possible model of epidemiological transmission. ```{r hello-world} si = mp_tmb_model_spec( before = list( @@ -76,17 +76,18 @@ print(si) ``` Simulating from this model requires choosing the number of time-steps to run and the model quantities to simulate. Syntax for simulating `macpan2` models is [designed to combine with standard data prep and plotting tools in R](#modularity), as we demonstrate with the following code. -```{r plot-tmb-si} +```{r plot-tmb-si, fig.dpi=1000} (si |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) |> mp_trajectory() |> mutate(quantity = case_match(matrix - , "I" ~ "prevalance" - , "infection" ~ "incidence" + , "I" ~ "Prevalance" + , "infection" ~ "Incidence" )) |> ggplot() + geom_line(aes(time, value)) - + facet_wrap(~quantity, scales = "free") + + facet_wrap(~ quantity, scales = "free") + + theme_bw() ) ``` diff --git a/README.md b/README.md index 0d21be4f..72332b8e 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ the following command. ## Hello World The following code specifies an SI model, which is I think is the -simplest possible epidemiological transmission model. +simplest possible model of epidemiological transmission. ``` r si = mp_tmb_model_spec( @@ -164,12 +164,13 @@ code. |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) |> mp_trajectory() |> mutate(quantity = case_match(matrix - , "I" ~ "prevalance" - , "infection" ~ "incidence" + , "I" ~ "Prevalance" + , "infection" ~ "Incidence" )) |> ggplot() + geom_line(aes(time, value)) - + facet_wrap(~quantity, scales = "free") + + facet_wrap(~ quantity, scales = "free") + + theme_bw() ) ``` diff --git a/misc/readme/plot-tmb-si-1.png b/misc/readme/plot-tmb-si-1.png index 8126a616ffa4fcae937e8c7f211dfa45755e08d8..6f3d318b1775ed41d0118765a405557bd6b6918c 100644 GIT binary patch literal 937726 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

    `ZpFsK4z~LdatlqdL-{^XFe&=5LP$! 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

    +fHmSLIUD9MJN4aY`IU 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 z zu(!k$gWjU)Ay!=I*2#nwH-O04HS`tmXF)YueeeyzBicbTjunmz8tu?8f;PnOu%vfe;^!BI=G_^&{Gk-viHRrFnL-90B$6&{tG`|;H!(S9}vCQ zgNz?9P9wn(5Kl=YUbdiXZ&T7cz?)hlf>vxdz+wTSAtU}9kWJA9WN)q?>BaFJ;}(^s zS|*jxxW28Ka=pG$wQU8?@cFK%?%6Z{)$7+M_gie+wjsk@o{cIYK^IVpIWGqWX3xul zC6@cZd1c4nxQ&im|o&K&nyfeRbXiiFHcLYw#)1|Mo#g6n5G|o@*-(&aX&!TcY0v!gPhP z1Btbp7d8QmP{4LxE0S#xFxpxTjin6$$JeNqReG$yw=)9fd3wy(so+9P+GJ$VTVAcz z8$}!ud4~S>?dIenm4bIL7WIUUra3F5HC4xSl6CsWEnt7D+PgDmSN0zxOeKb0<2WW` z)aaacx2iet;UWXF$tB+w_9xU}hVa-K8yT^YVxmj~>5$HzT7+f7AsLE~1NLlu>x%si zO_T5C%a^x4eE6WtFD)%SI{!TKC?%^(2R<^ow9&wFiyuMyiDrR9H3LGH+-@av6;qa= z_fLNQK&J20O{r1n_)BKh00GqiJB=m~ z6*}MUsa1U+zwYeJ58w6bai)vP$&p|z>tP-7j)%2EA(BVenc)SW-gfym0`FpDynDAm zJT-R07F}{|M7QHjhY7lUwzf^nLT!K6(8i8`3;TMPt*j#`dEM$CVrAz%h1O2SPX&^% zi2ReVpNPTX?r%5i(8%<{|AZ<5cds<14PU!N)T8d=XmVEeaJgPjjGiW}gUhH{p2l)` z6)QI2FMi{Zk(c-OF=tP3wjsfJF}H|_J0&3AA`NHuT>EJ(*2nb?p!fB8B!})#3hwFm zWFu6;aR{950k3I>`%<%%1M^JH*|I#8KR@Bimm9B!giP#s^x(lo_Y89>+-?GrWhHVV zC<@wQJ{eir6EDiMyu7Xk2lEj^Q8F_e8R?Yj9B8HFTPTSS)IW^vvj>MRcBTtHbFVS? z{O)aCb)yq@1LRwExM#%J-o*i(<9TrC)HWM{t7IKkkc;{XGK_I2NS+otjpv`e5oYCF ziKNRGLKzr7%)frDNfaq2sp@N)Yj^76fwv2{{St?iRyXDo} z@IK}xeaK(g?;e#|lnSTA_N*xS2pV>otC9jZsj z@6@s)^%oQWcn;8Pwase$XCsm7+)@qj*EOZ3Rtbu#shY5Vld{s~hpEvVW;9fIel+2xl*X1vCgd=4WJ1Qj zVhGN%61GYYm-Il1EY@DTiO7aD9^qu?CgUF1WUz)q+4=i=?@6z7#3I-9m7p7=Jxd z%?|Rx5Xy@DP+=l6(x}?a7%WgiH_Z4c~+|Wi!30;dT@*B3m#c2=;Hdc^{QGIRaX=t~-F>HTW&7Bo_Xe zIg>$fmD25wMZ^V{0#_7hX@iG#|DpI+J-c{&N^G-t7tre%fap>*Qyi{Q`}PCU`W4Q zIku1uiN-!w^g2Cg$=Y9zA8Z=mgYn3Ct8rRMzk`GueF zcb@C-q3N)C3SE>Nu6Ep-A`ar$P-k+2JCy8)h7VO?hY2w;dua_)>2pS<(nSO4;(R63 z{c>~GZH#ygG zNekWhs!=(aKv3gY!>)V?QzF<+UHvCPIXLCa)33x?WX=Be(hn8u4*5%4pJgk_<6GFseVB0n+E-c)pJMQ1xwtUOYJgNom&fAl^PzlmoTZ_S6O2J=qGa zLu_~+Bb^iFN3PfJjJk=1aOK{Jnxgzwy(-8RA)K!Hmn9j;6mt#w(#GIXRk%$94~CZs=c2?d`qu$B;ai^unr|BTz@>K0gf! zHteH~||aH~clE~CG)`^r8`eDc*(4#J88ua9p#A*^Uf&n9XH;x!QoEPDC3 zZyBHONX}0Y#VrM-R6>l#f%6{GBmJcz&Gz}Vc|G?1@7II6@;k0V!p0j}b|CTF@2M^e z1*aG0rlu2E*ZrU=0%W@EX^|G`>jY_l=XncCAQj|BWJ))eRf2Ky^HrWz6D$_8>deM| z7?N+=LI%^SChrccWYJAo zvSdjEsBL(%1$dQ9`o3v|vthGn3!zEOFz4=FTuuPp z1uUZnlD%m6n85i6uNr1RJ4xc33r?XTYw!P9!cSsp7B{KDi;BxF32bF8ZzA39XrE6S z`PDZ=7F|cTAN#iCK}OMg!>Eeh@fD1&>A}8w_3V4GrHyRZt;n0`j;~$4O1fhqkz}Vv z!&Lj4)yY0&(-V0XHTmus)xZuM6J2lqvGM$O)cYHB*kQ#Hj_Eqs-}#PTy-(oVCjf?C z4}l`FYM`$Ll(u@Mm4Kx*(IaER1TiRYS>l!a*gq#HXQH+%xyXrUE6!XUcu{Nc-Ls^J z-4;Pk+Vpd?l!Xay7$t$TuD(8-3yNir5)*|9fe%3oGdna@nM?cH^2wfN;cwV!Z83mk zVDz`oMPziUcw`RRRPNc=YvR;4Ep}L@WB>77nUYYByg*!*`}l!-Po>?nteNX*)-HkET4cr3E8|5U6@Oc~~~tJl$>+a$B=lTUnZ zYwa^)78LWB!@R4z8=`|K71ab%fU-~uazk9+cXnaxA<&!Z>8a+?Tak4N&2+Hg`w^I% z9>&m*XU2}7bPIxEqGw9dEu7r?DBZhIN2iJKq`zpq$T&W}u>fZrC8mNaK56j8U(wOg z(5&Vc@4l7vX-wXNp{qgzGOgB%)BRsZcws*t^Ebm1bA<}zoA26Xj8iJ;q9>S8q1ukR zDQm3H%~qXG)QnCH{<9br|E(HQiaG2h^rN6uY+NiENy3VtoZy6wVHu9lZ>yWr+u5w6 zquQZQ3~h>qQ5(xR86&=TJmj`|CM5Zn$rQrq(3bs9eW3MRi^*9rb>hSv6BSE~LaMKW zCn}WqOWiqWU@T6FTp){*g?^x9+UUkX1!#uRLTA+hCkBoCuw$)Lky8;G)BLbs%l7GUES(UsJ&xU)i3ddEwIxn0v0l?t!?OPt!2Y}ijGGqlkI2R|92O| z|D#VqZO4#pl+|xQt&;vaYSRdoh3?7?{oMOUH>5EcUHR$$D+p)hU4@qit5#sz22{1a z7;(>qsHo@ysE?&9k?h`z@}sZYc{X+Uqec|s|FNaQ0#rr(_>DcKlZ#S%4neonNK8Ku zCGzi;Q zU;BD9+D|WlLr_vu!bbP{|8DyC|JbYrr*Z$6uEA>|!NFR9ZvAo4qi#IQ1D{3*{#4XK z3MfP$&|%YL_OjV3t_n=*<+N5tEzD)`=8@;6-@aKv1?xiO1fs?m_FIUD9p>Rg8E_yI zuXPVn!ryca+V!)nD_*fg7Tmq>D%pasLsORW`Q%)F>lARsKkAOo75^JDN`{ZFky`d147(C%dotUL_b^~Q{dJ9pl~kPES4N>m-y z6gG{nrtlECj)wr7e%T?vgDlVa9nLwUcd|8UnY@BA4M9(v0_n8UT$(2l^8eIyMp!-3 zY1Co$5)Mi2Ywus)uTpuKoZJ;0+DkeDQNT8$EQSAaCsmR+eNqY)HFwWV8Y#L&MCA}%638vYuJ%*)bgC`5ao{5E{ua-`llM81REX}sPP#* zgjGIh+>%%~%-sq)pfx1+^{^UaB?Ln^sBaclY<0rKl06MH!U>VemR{lbRToaWl~~TL zU4T*(eh7)+&s*s_rzE>~Agk25@<72}FyEaXKjyH~wO168b+P@_LsIt>^oE*N`0sDt ziu3X)255M{q{;amRL2aA4anpFM6SVKx6l+<*unjzSPc^44f0;yT0|-^xshDdEbDMB z>N@Gx=1m4~ZSFB$oa>&uo(hA`4FIjp#jm3q!|33=`l`X+G za(Kt>CJHEBffKb(qQp$00aR`_%4huCOpLT^<&krlH!A0{?=n5Xx)c;_pl9QoJ8CAT zrXPz|coWV?VpjTGq$)SOD%rhmR5{;3sT5IX>OY%xn`1 z&I$E8CRUet?JZg~0O?Ta?}wSj`y#?3RS9`kp;mbFn&i{0A z+ThM+^Q_?H*C+Yy9HazhOu4$IeCGFRjAH%9PJoSSQuv}@-+Ij_eT>yq|1TldYA?P77!(1fKxwY?S^_Yh|e5Mi%h zq$HJ3tm;N!JiDJ?C3Hi?Uxsp(_2Kloy#@xc-6{N}#1xU31euLDr>z(?&&_MS?PORM)+PR?(#uY%NR^ZR()SEK zU;b{EL#>8o8pI^pUj2<0Yhv-RRo~{tyEIoSE?p|L@+a!*hWXb>Ard>YFiX`m#7L05 zvQnJRf4Pmd|I@-ZW^9S)m`}J1X1oM2*#P4%o&F|#-Mh21l%Hha!<=vP&?G6{z~BUd z@T_hVB*>66|G|VcQ`b)3e}Z_U1o@(bO#W-o1O@0e(=j2DnS)YI9}AK4{=|Y(p(afE!8p3?jFa z&PfTY2svY$c(wR6cBZ!m(90%Ba5RB^dCVO9~v1IWiL%q zT}&7YbCF@0s2$d$3t@<$8L5vfQOQQ6rf7U3&-49wjDU08kBnp2nRm9|EN$#Km}33M z7AK2i_qLPsw6CiKCEB)2R}(IY$@jrJ%0ZV5rE_JgNbi~<4bXm71XBLIQ{a8BgPY9QmdsyQ)bQ z+p97UH_&NgZx~`x+Qx8nfU6W+Sy0tL`nVBwu}vZ!F4!M%4!RP@!Bpn)5&s%WXG`Fa zR=?=8NQs(wJ%(ZV2^)JLQS7dpq zJGRtcKO~m{F%yvjOiuo7KIo!~>&fjTJ)*Q8`4ND_R~RRQ?Ru7#s=(T6z(;9-MK(1F zZ7TdkPDfrPH#@wAw>z9xk}WWUJMpKTmCwz{kf5=+&nJoq{99Ti7)HGnEP&I)Td8RpN#~nevX|WC+BBrrKeho+1ADDXH-MNP76l1IUa&;!lbUk@N9^J^i^3>CdjB5OR%U7yRbr1S|YPp|4uM z?ReC^y2m*HK)TPESR8W+>^!M)NZsk|}eB`&`~&}Bnk zOdsSCjjvlrYEGa44luxWEdW%Cmg(dGNWfJb+Q5(5^$t9YiprcZhV}Vya@;Wm^eEE6 zlQJMIVWg|^E*P`tISGMniPNbhFAiHXCE2lIv+=YC!rf@r>>VT{rf(p8d-Y_X{_L8T zKinNao5L3~!N|um1BUqa0dl|_(~;LPz`?B+fV!6zOIt%y6(=PNo1C+KW~qCs5IsYB zHp;oy1Rg$I)!5OKTA4E`Jd0-{Zv+uf12Q&4qsHjN3%Dl8OU@$WzG}NLrLGBMtXP77Q zJoyj94%|mj^0Aj^>0FN>KLW|nah>YYY;JjbtGU}e1_%nlv*R=reEz^DA*r;(+`ynQ z_4Qifca!-H_me+j(3O0^p!E^W--$1wjp2gMDAyCvJVW$?W5CNQvs%c88NusK0opK- zeV_sYs9`}z*pUQhYZv?L8QB9tDH?Hi{0>^tL^K*G|Dmzt&;9mRq2&M|X$s3cn&RcF1G zq2a9T{?2u*17+F>7t6W7Dl-HPS((=WEGjYj0rs<03cmZsGUnGC!Kz?DCV%nbg--hG zKLxWW(n6-3Rt}@{_{0Q3ypge_rJ}3u4E?K~bk&@Q4ZB1NbYOH8Eq`*|i(jrHU}T2O zdP72~xk-PvWO`bB{CVh>yKWzF4)2~Yc`fdC1S0+EFlKNbRr1shKw}FbDkKUB{NrzO zDBSSkxJGK#6qsr>Enc`Eig8!J;n|3O75VYq-Ns_S+%po?ei6Qgsq26}`clDRP7UdS z>f?riO}3}B@u4A;lMZa9A6cr-P5#Xs(5CZJ_tYV8GvRUCLlr!YSROBYE@IU3 z!_66;Z()`%n~-kA(}M9`MP}d-rw(#1S0}zoTnfl5ppAz$nXwQ=|74pn85@y_g#M3k zlvtFj3{3)rSvGPbn-^1G>5x4wN)^Z8Kxfvwj~`8|Dvnh{DS|(VtVY*qyk=lZ0H5tn<2zhF5l?~M0 zbmQ-!BQv!c4_QzXG4YH1%lqiC|7KUr5ePj z8vhOkt*X_*MeE)`mdURubkEG=#9bUDvLR>PY-nJ>S|1Z6QDHU*VOmZo6N_D#^)jEN=l4hPrEm4xYO4b5PoyJ9_P(De)qW>X$)F z8W1qwn#7oGdkuO)A^56Ha1sQ~2PW{;;^=<<#T2*+r zgf?+97^MQ z=HckwqR%y7QW6|?eM^26p6}l;$Hv{^c~$fRt>CLf4yGfp!N)0_Y&=muw{aA%~YKigX>v*a{G^ z33|u_wHl2F8ZL_r`*;S12)`n|x8kIQp`EOuq@FnL>^Zzj z(c*zj3QTX^P=%EQcSa@Xj$E&=s|I`aG(;4RK}LFhBVSFz?1)FE!a%f{3;sN zpK^T8b$`yY1NHRT$CMy&C+8}xSP^vR&YjiKfRuwqPxQ=^}0R)J*{eW?-HsiGY=m^S>Dpj=Mng;pgu>ob!*IM*kKdxflT%c zd3H*ee~m!hn7<+8wbjk#upQd#hZa^froA^H9CJOIUr9&c9BjB37$pfJsG&mz;5R6c z$fdWkm0CJNzlV%!!f6*Ab26T4I9YTT9k61eW4_|BVqodceMCt+-O+d{?x>+_6SSq? z@EfdKmbIZdyXtMI~)5~7}D6y*X5UBqGHS<)y zfT`4X*HsS@J2A*q;uh#i03Dd6)XHSt=t2${KQ{eR(UB2Q-`ykUNH7bcHz85C5vFq= zeF9`uV{`LK3HTt7g&t+xI7)ObV=K`)hU349ID1MLfTUc=?D-b$4aI>Z)ck1&n9V(7 zx8hUpkQ3dgt&PxH3av(M|7B?~%+G+JT2_Go$5>ZzjZgquY?e;&>EJ$E&O_7L;jac> zRhdwK+y^^m``5-1>Nac8AWZsZ1A{+z_P=s_bk%r6JDc%mYZyRnfa;gut?76_n2Fm@ zod`y&gx)%tWEkmQoQz(Y6$CK5mLdcW(p5Q{P8!i!NIwOuI)68EuM67C4%d!EP~d0& z?Opgvn}#OxN<5=S(M_YVj=`fQY}!!~XoG82QX>SFAf{dFG_63n6S7RonF^@Pc#QWO zSvxS3*kQ5qO`^k;U=M(<&Q`lM#(MiomVG`(TYwFhx_OY5io&vGftmfk$_1Xn4wkp# zS|0B^f};>3x)^0 z1lE6MLH8Ip7Si-%8rPzo)~oz3x?EcjxyH{?PMG^bu&=d7(BJ8x^&Ee~vNU zhU0xA>{a|$)HZWaCrITh)NTo^%{oy((v=GXFmp}2*op&lZL2a>iPbh(%4|69$g=5v zg$Gc7ENtgugD@BlfG>MrhH_!cjM!73Iw0juc^Yvf-YPO2YOX0QdG#2_xK!)^S@b*5 zjxnlc*mx%{$m9e=I5MM>%4S%XheG8K&xMGWM=L}1*z$GF)!5&NDkJ{f8#M~={ry$O zo?-QYuAP<#VQHlfg#uZ zfB%N+$Hz#mSxT!>d>q&`njC1!RE9!_=~LpzGm$=e4>U`eg!)I5k12?xXNee;@@&iN zyxe)m;=A0bUs0q{I+~d6y*meFZ?w?(h+PbO`OtW^FS|Kq|LooS`7U(g7`gOFK^ zAYT`JEhJQ2SA? zt|~uZ_}Rl&lV%JJEr!w0n&Pp)5j{l9%gbZ*zt=x`vifRhC_fu7!QX7+b;`EOHJc!Z zC8gT?{UJLU`WPMXo;p^fkTN;C(g@7W5$0!Gg4)T=YS6_9To5>#A&B?JsA)F+22885 z6J=z9@oY>wKyt~#lo-vPkYwfw=>%lJP2(rcB;8B=1-Jy;TX6|E4#8862{ZVfq-PxJ zd^=vuL!dH-Eid$|7SJ&>*^~rb5b2EuaKsb~+JPfUQGm@QM(w>~G-aR>S-_tY#^)oa1K28}iNd#)s_}pshkCy?!)py|EG~ z`5BCcYTbXN)RR7+q;I(ej7GR`Xt}A=Qs|)6>|w9e6{Zd@dr$j;rC6FFhNph#LfZr$0Uq?q`t|~KMyDq!WC?~1 z4coeuMic+K_Mk+|M)kgyHI@?PiMCqJS%aSJSonVMKufZMNL_FWC8%%OBa=yEjm=3k z_7fFV<|~^zp7^CKgnb61@GhN&5)}x>iS^(H_fJWvuX~vjGa8)Sz8Rpab`GAJg-f0! zfcqC)a;~c8k93fgobKh_Mq%s~-g{7xXp3n1~bDneF=bZOxb)da!Zf7H-Bl0^*nEDn@7}*zrhGi+eVh$#1HUrBz zRyc=t1xP1}_XG@#jkiH)SQLb7!kI!Wgtfq90%qhXxa#J~0>$aitOl7X*!wW>X&%()Ly~XMXeCa#5tsl2t#{PwPZ+68K<+5qP6^cD)>JAE|# z?Uv%=I|i--&CPCZj^xqwCUfTU=X**84uBUMs}SaiY)h6bDe38PYEFSf$#13D-*^%s zrRX%&)6#j48@1q1bRSR%aQ57fBr(W2+%o}SqTY8)hta30Nr=FT*Ao$O zI3m2j-d&W^*{GOe@1ETPSaHQ|9Cl-DNfhW=EREh7^-xKgxfJRHFybNwx_B}MfX@Z8Aw>n1{G5sG0fS> z>7dw095kiqo)JC6CZl?AsgMQq^t-7QfHM-VAyl_w_;=h2AcFWImexQFK5;2A`%KX9 zs(L?tn#By@);l7Oh?}Z52H)1EEoHoeso3CdT8Wp0Vj>4 z4jQ}U!svW{MmA&SMldMX<&5Z!Hk_GU28-0R@*8GuB(0s8t~}RIa0RIKYMD=C^o_W) z`5^#`+fYnfi=8Cb$x8bxj%D23m9O93o!L3sw~wBr4Mmrv+!RCB^Ny%se(1QXZ_3kxTY4T#7( zz#!I6Z6?8iJkGQF`_km2p#jw5PPVR^;3!=;7In4Tgzv3sAC5>UWZ5B9aMR zE?&7~L#>_MGnw)&ijg@Nj_{>B5OlsbsV!ik>aLyURnVS^&BPGnBv8%fTW~R$QBOJ% zr944JW~L)WK4Zt+2R{ZeQw%rvJU9}0N0T_Q&Wrh?Zv+^dNtKaw2Y&)u)eHmGM()RV z#N7GR>MlfI&x!paI-y2tmY_`S>c7bxKslF$7MPfX9|!#bZE?ERVfqO)atxa+apsF| z9so|!bboGP+I7FLUKc4#xlH4Pv2th&RA zSM|N8W@7*gIUWR=2W!d!6GYHb*} zYAGQI6S9BK)J;Ywn-o8Gl=WTu4|nCIiL1_uV}MAt)@{SoFv8jp6A#|ynYj+txGeu2 z1aj_O3V5AE@6O0R{QuZFE}<2t`a>8XRdGuYigD*oO4ZTNxDiIbbQuXpsQ2GzpON?C zr8e=|g6&M?7ab6!cQ>jGXSf&6?;LCjk3!^1*R$K-+*^q9K)9emf6g~ePf=XEHZ(9W za6Puch3<5b$A|ePVgtgbOcR*%BmGd(U#E5)t~}>W7nKL&sdPTWic<(izk21ORpn3- z6cQTB^&@zo0KwYJ@SDpM)lX=MRxXq2dv$aZ-}Zw$h35vgY&MFy8s5lDTAq<~(rW*@ z^{vlxGXJ`-+##{`!iO;Ly6xLHyEHkiZ*LoOy0X!+quZ-pIsEBArxz(>pA!Gv)K`K{ zh;|T^b8{Ektvo(4A3(-z0A{7s_hC1Ypw5=@M?wl@LOm7ZFo8<(da|;f>W~Ud-j-jE zuL1w1L{X%vCddn_Z5wK+j=+Rhg);J4bPSH*7{Z>RvXx48@iMwggwhRxLEB)c$AX{i z5aXMSSUG2|m>7?J>b^X`B=6B7CZ>1r1$%)>I9&JoJoape1zcwX0{m^N6D@SH9E7a* zmnS&4mOEHQ>4x)6JSYt#-@&3pUht+9S zIMSR8hQxdK?l~M;d;Ig}!ud9)^qWntU`?^D{Fl>r{p~ntJ&1srZvGR`V#T;rPO=CE z>?u^LJEgQdD4z)?VF@`U2z(09#hB20cJOFd1)2|2s-O^f)!U$l^rnfR!~&#Vx`65M z9{B=ohG-%(mSvd!6AZSB*v{tX@1GZ%)mLR>mFLIJ#9)tKu6B)kk7b%7sx}rHpbi;TJW<-nLfF31+$VA2G3@y)kP+Y9X%t;i4 zfnhvmVPk=PE21*WXh3G{j8nkEYl~ofA>=n|Kfx z*e3)px}+S(A;<@H$e~khB6ci0HoBCahmpM?k`gGhcgJ--_9JdE!n_COo=jec(kPvs z4r^4IwN1!jtsbhP8(M~x<-#)Jt5>fa)J~o}i96Y6Cn;SY^Gk<%TQwupJqP4WvRqeQ z7QlrPzxHM%sKY}V9Mk79WkY|u&OcBYbOa2F@olZl_3^<;QP+Y5P~SO4wOYi31^dhr{4BlhYR3MkGH4y0 z5qNf$QXaQAPas^~O9e9m6#FHZI}+s7yb+B0k;d!eOZ@!&n4zCGZ%13_aX=UM&;^+k z#L0GbwZ%hcDq=uy*ROHdR^DGM!0D@3hFX81Ze{XQhl8(DKq9Z+HO*E@Eas2N|JcRf z;f5O+{$uaI|L0i<7*E5svwCrg*+{5|Sv06ZCaF6_!KMVDRqTKykWCJ0vmDhUZxZJ3 z>U;EP1M1zxPT|092JV**qv`aA1zq>|LQe8slSO!E=aPwz=NW3NG*lH(p!0^&O?KFj z3^SKq=FOXj8_Y`(W20V8nM@7h!bpVEisHf($&TsOgC}F@j*sC|8xgwfwy$50P99+| zDBZwH)i&tvAx!tV4|@^)N5gccf&{ux@&#W6>)v^`TqE7p(z5xvOExYiggb(Rda$~g zV*mV$b8Hfqb;7AT!Q9KvK)8=^O5--?4S4x!ws;5XyF}rB?E87E2o6ArM#;zgptYvs z!@eIK2m6Dv=rL?MkL=qQ9^3|NJuCXRBYv$i4nSt)1Z6_SDQARabx_|q&Kogh%NR5o zfNpv#N5@t1aWSD-Z|9fmCahpZ-lucUNLRiU6x%c7eRg^R(CDH5lC&NpL43hV#ETa% zb|!*?hYHFV&B2*^RZe-IeGl*Kj#(d3m+2zwH8$8Z`8g|L9IvgDUka)pC@U^5UVS%X zLRJ;_gW9mgv0;C^T2D8@d2K$K9{E@4*}_!GqY)(9m}# z(s28Vps4istLP<~fe2k#48iApCt+(GmR~@mG&fti?!imsg=571_vW_4v&n@?O!y15 zazIRBsq^gC&HxURx6Y+9bA^RN+5=lL4}ljXf*+iGe(}DgzSzIXy=5sB=SlY-b$5LW z12b`B040jh1v{vS&jIaSqa_jPA7!CjJR&!8{iVYIOa z|Acds4)Zg@$x@;;GQ6m2)>2g+~>ctS6t6IA3*-diq*U5)Gg zOVB0%qvM%OJKPO}Tah8aN-|rct_$Lfy)+m2ZgIPo7dk4WVBo32S##&k-HcL#580sm z1*c!%%_i>GzCAM0f(s%1Ccwm2skAmtMPB~(gXLR)_U<3zPn1SMr0-5WdS-w4HFF+T z2rw?ayr#@wTZz}1p1%qA3}(9Qrx*q6Oqg7yTRbHRIZ{;~ZFaFHnTkhhGLman_2b-N z`jdog{8_ZniNO<17vzzE=0-qohQC)hdpHO>Go`n^f zUn`U0EHuS+N657DEZk4xwr~V(VR+&WdIC4Jg_~FakCeKyWwm?YWBQMgh1eABYp)4P z8}M1Tfz$VAq0hv>^h3O;{{h$zK%Xc8sC<>Am;;zPM-b*P)=0tu)4|7xP*4{EpjhWI zymv(4<&ZW?S}Jhnv4a+KL!9ZKwmucbAi3FcC;Ro|O~asYU-9Ir2>SP6uW>NR6i+CN zR;-h(AH=?usLVH4TwJ%mqmH*N>)`W$lfY==Oi&N!w(a{xKy#f*n4vqbHES1r?h`h&dGl8|B>JX>0mge7S_jvH_nJd$xF=%p_&=6< z(4>C2BUjhrDk)6SDI&5c7^7}=r`1S|UVk#KNwFh8h&mo#qF_$s%3qyp))^92-$L+hAfp-Qam7_LNwzYUqUyQ}a;F zvX9=yiA@X<;yGzlK>8$3@SGNQ~A@Yif zg0)2q++iZv1TTkYMiXN}G&j?;YXqff<;$;ZX&l;)n`(*^i{fSS#?mO=DClIJ=Nz+Za!1`Q>i~#U&vVJ}Q(P+% z&oRz3!b^^tj!@SgWDkfOSVek%Hdvv}x03(Xbu^`dl}@|J0j&Q#-k-J?4~pQv1apdBRGr)sQ4(cch09Z6 zSCBQZ@?U(8(*%*MJo&0<54s;8of@`gLLC1nJTRG$LV@Bi{?$ z-<+tqqVo(~WOaT7dwBGH=5VQ*)CFYt*HymnTmWBxZ*&pWi+cv`^N;A z$9=93SsllaSt<2l*$C%^|Fez!hZ1Anz0&r+Ev#z{UH?|{QP|^E-b^^ zCe~Fzv2C=8mwt#n+H<1LH`A`-b=DbkCjj@@cl6`_z))*^X=&0MTY2zqaj<$AO#{g_2s zFEsXG|IX;(Xdk%QnFymGB7oEC)LTzzg=8i$me>m-cBoZ#mnPO!Ln3Nmz||e*?FRdt6K_KKait`)xhN;*ke%vC=;N7rV zuKT;sTjPJ{@B*;@7WYS#Z2H^Y1_`b(9tgFm#j0Hu>VB_%#`$wuC6*&1fzlPOJiyUq z%G=eJb(ZRCB!6zL8y|BKo;%kv{fRcLGl{>s@tBy8*&Ce-FBVo60n1)D-EPu&MQQXc z5cVW+3Omve6OpNFgRlPEO!VU^EO9yFiGPfJc`9BCG}dq14ptbG)r<_k_Sg~T4AhqvI{LUF!==UB{mk{)X$S>>T5YJNg%`K zy;f^>)q5f>|Z%oV$UxBwf&Qtp6F($?j#v z^W0xsFD>ybZ>(fx?>T>>L%TUF@ihlgl}pvNQ=;f&OxP6SHFo(K)k(sRmo6z@{__Ok zJX=VeQoF~@W1c4-lZ21qCYM^Q0}~A(Z%Y@Id65FnMZb z6B}j~c)=ik26!Vdd-Zl4kqCJq(LKBNRfdI3Rfcnn_-r@9vo`e43zla8y*CXnqJk8J z&qc{*;4-odHub-Yl3YZk7)YRabYu%LV_GZcz?~S{bOTpmN7##iu`635Qik{b_x)e( z=QdQ<(xcdpyT9-zi{_Q!XbSkPsG{Vs@bID02PbFvxV;sje|oHahKD8CgEL$%(sY{( zW6{XG=uT8zv*wNee(WIgM39)O&xTdFlp*#}=pKOr6XzeYqbfm|G0elvLECalKRaxA z2VXO0x!3_FX*of5N`I?SEo3cZIx?{_agyL0;5BY#lW8YG(+5x}-hxOo+2ErfUZTFg zLXhQ}!|5B?#yxXpQdXV4lBA?$o=oD*QjXaYou??k-)=p&x+ec=NXtQENrL$jmNxbB zq^g*~!9$rvEBAh^0ZpS6v9#rkb-5K4?HIepMNRIfz${x`s6OLvVhfKaWCke2-TC6C zD<#?wZow{cO@*0yxzlnh=qrq4t@4Vsx^kqp8%*nb$cHs=`1a=;mfTZW+P?m|zjx0> zpS=!dd8DJrDPF%nGQVfo85m|p>!yri>$lS90E#8!vnF%*PM?81@e+ul_R1U$z&JCO z3lr>WGLoPy+^P3dnW9v9&M1H&z?D>z<26jmX~Fe<{U>AuGxjXtCn)VphqTA*5pUgi zGd5uQ>;oj*HKwI{BvTFL1e~N0FHaBZg1q`x-shEXz}uo*M=wqx{=r!dB@fUoF&?t- z(|AxW;OACAclEXYfi!0TV#yMx+?lC^M;jwxf{tT)Do4Q`YU)2XcYL%%U6!|1ZM?oP zRB30o$DM`ojTL6z4dxqk{H72k*~C;`FM#xPe~75d$_5_r*Pq7Uj~~8=D4pfbArA|1 zy>fY%y=Hbiyr%c|Z*1Mz4)Gp&8WF`({=DMq$36n*cPSKRklczk!c!D%sP`B5-V9lu z@IOBUCCBWE*8zxqd_RWQl35AoOTL;JUrMoup2P&v7whU>#TVU zy77whOY67eP2ldO7_J;`gL=y-ms&TAAvPw=WI7aB$)svUIbcnk8I?2w zT+}o#2BBO%Dsdn6%UesghF_XEbU^D-B6Te=YN4!86DQ6c!aZUo@DItnZMsQ6V5d^Sde@{1DJq+ed4Et^gpWKQ`%Zf)x zRYq}r1s-9>vAdRsNhi{?Lg1DR-~a47pc2Ze@{6%%WmE=%C{sN8+qFRh@E1CyvrN5u z%MjRQCRwnCmHPN4+>Dp7>~Z+bep*T`UUW}#qlm-5h7fzTyb}D5Spc=1620OtEg1j zRVMXw8-xZvf#3x1OU*ODcZZh3$odDROb#ZfiMzI$TsEOdYM7d9iM9oMZ`Yq zV&Dpsq0ga5?n3F8lu3qDbm8*UNu54>Pb9*jl~O=&ldQ|`FeRut2$Yi(i#{lzTCQl_H4rl-I{k2_K&ya6( zs$I>NrlRnlY-zA5T%tNYiG`xm%B!ZNPh34q^O(h#SPH{KN1HUp|7eff5sjJTYcyYW;agSHpINBd6vj*8#i-Z_6tP?#c_$3M8vBLc-!qU+X~^@> z(@@F;&&&zfH|p$L02`pyobPX?`CjrRaVDozIj zM`{ndOjagIRGD-J0O|F-`;8OV2=<_s$ZW5i46K!Q|6V}izbFY4^W9(E+4t5y3iSj1 zGlT%X-qs3D!}$~L2HkeM2@G`R@#aPBS$+lYl+kSAw+}rFfOQTkOI^Vpo0qiQ%9R&U zyBEkVHivTpqQ=K(grXqZp4yhtB6w}h(ft{F?_y2xuaB`=nOge+Ht*Ch%u-^iq3VMc z2-(QBD_bxo1+qG8V5+zKKA=FrP$1r?SIMD!}Um24YU>qE?KVeCA)sF7fgplF8! z$fHJsPngO4|6;wUv!?wUzBd;SYqtR&Wn+I3N7P7n*w_swMt;lT3eW9qEgcEPR4C!K z3Tl>I*n9GpwPMGfKg&knv?XFk`)~zDcb{c|$QWwO@15A8q*>p=b>>jzUS^%_unaU5 z(>u!Gp$I+*4iImO-HMTai6)ktH*YrZFvzeG&$A(VTd1md+8S=`3IwaD%t=z#ZIYGs zx}4B790dWEzsGR>NnWN*{Ld=JdahH_k2RG*AKv;fC7*l4#A!2dsSyJC7!gfg&4v*a zVKYbu$^c=3=8@9gA=H${3^Oo=bX%9b%weFj!Qnut;*l*75;`H;hcQa|HP@U7@ z+SLMB8{@U)YoW$#3!^Gb;-wpbLnD?@&<8j^3)(#rD{#+A`w_K5nqbOfyh zJ&m-)JU=-cT~bk{^+Mz1gP?;-R@bQY=Y9f@`6-akJ`qWr4&d<-UYy4%x z#wWVlg;+cLwth^5-|>#xFeqOY#alSMWDi2~v5b#J3?{aEN+xMe4)S#_yPQKf{sx*p%9K{$}LEms(-^@eXRC62>#kRoBoy{)MAN>*NrK{xx@_`k&F>cpq+T=hF`X{qH}- z5#S*TtV`UfjM??#_bdQ%ZF~RzeZvYW7bd-7J&3J+=m}I>5ycQf4;X*g$%=YT_6ER@xt<6@>cp5TqlXh&r< zb|QB3Q6l0sTr1C@1}#HysoiLfLhLXuC&#Fao>gps&8wlKO zKFO=jOidgS^-lzLbnjHdSdQYG-x^cY?%a@}zo9Ph5|wIQ7s{Aq$nUc44$?ACcU(N# zjOhghFr30GP;ebRn4QjyhYTjOJiO_y4gF#W67Rv@9~wDv8AFm`L`{NNflDl-pOoVa z4XWi_)mV#0r_OYmgLeJ`KM}I<{!(hp#%qYueI)!k59Vu$V*F`t-G@~mtD6?XFn<7z zI?y3md;2%~q3hLYXuido-g9!>hrkwSV2*Q248rYPq8{&O(CFPWl>S`u_S#c-`Xu7MPg?NYc@NX{`g zZKlUMKRSp>>_fvBf<3@BJ{5ips?G5a^y~W^db6r?yw-Zb(BFnt3L_a0z0`A;?zqvb9n%N{beFkMfOg}EWwW98lULkCT5zXXxLJVU^f|cX4ptXT7S&6#XEYzO z#wMMbZ4u+5!!$P%p8Jl{#c|M3ur>>-A#suKe`XDTj_``UCTf$g_wEjDxM?vXc94mp zfdX)W8a1%D_7tMxtfkQ08;U2inaYw6wYOFGyQ;9xJpP<$ULhBM91_ zKHdOi=zxW}>k%Q0eF~NB^+5U20)--+W;G0y%s`)5lj-8$@AKucP-#KIHoVJ$Jtz8` z!90QuVB3LbXqKnh$YV36K)`myKABk?WrukXB98!dBkgOml)XM55a+@pFCY2S0Jbaz z;C1~BG=QLq10uZEksdmUn z*IW5=!%v?)(eKK5i9J)yu7NC98KV{Yth+rF6jwlMhtcg(z$bW6G?VDS4q41M|gfB)7(^h{mw^qD^`1p8={} zo0&2HPEp{#)(o0DV|UW&0tM)BN)g1Q^s-wO??eY8*Z!l@aUAE>uRLGMCoaexcP>8c z)7Y2t)($UDc*8(!2UIX_xHkGcW|xUP{A+-EJcLBzsTqPRL>qN(;Gibrm06aDC{<{1 znRRm23**Tjus-qmO@y#cG4GgA7g-V?8F7 zjbtD;jRz8Ayd=h6qm1^~iL1vlmsF-R`ugkZp=)m&TyR{Xy+mG5iF0SqGSiGo=tr?` zc89(N=JW_N*nv_=URn8%oM;9>V8RX5Kyb@IQUzdqq9>WbW6<8@bc>|g0HVO`r2yhzf=(d( zY0+AVuLA27k5qox&Kb-4?R0_wETMIdKXpI!b;vM&33`Z4($A<&GI@H5MPuLrZwbti z3E4K9RDB{j+-LyP{EJ^I-#|31RU^3*TVF&lf6QhCA@8bH_uut9OJMedk3X&TZf1#0 zYV~Z++`-1smXASZ`2EIw2)oD9sT6)))miHMfu1lLye@2KC|>(d{7Oobz-=h%=4mb# zzytt9L*m%j*yTfpb8r5J&lI?m4BZT=;{_J#mxhK$RFdS#*xkTB7+>@LsT$jhAIt8+ zl%$^^cyL`)KDH#gr(|oxRz}IMm<4)E=R9nJ*3(HxzK)3ctLODjUEfr3=ow-{8;R{G z4$7>HZ^s&bMZw_UVD>sYU_C<|6b!6s7b4K-jROoLcEatTz3QhBKh6#wRjxoo;ZkJ* zz-8j&#Weu8Irk5jVTH8#3Cl-EWzS4h-ldPla1BtPtpBto8e`Dt%q8Z*!FiR0db@|A z5JaohxHX00pB#7C62Dp=I9>VOS?AUsQ^4F;=ITU`_H;=oKQBm4YEG;$^^nPixtC|k zm(lIk1S$jrGXDMH*md1u!fjGPjCwpC`|e`V87F^AW=BpaIN@o3FsP6Mek%x!J-GA2KXoEJzz_`!~hBND7{}@zU_wv)jfC^}h1YAI9#r+A3 zCE0tDz@Dm&4k_qjejzpx*?w^~Rn=<{uH%#a=DpN8i>8pWpDZ!MRv9F-d)pypQ*-?7 z5vF7oO~iH{Ut2yOBVomUNS;7uxz{>kr)rQ<%dhbE9at!W_LwnG4Sh-+=nJI;&x94< zSW6!${Djm8RluJ?WaDA>ymWne>T;de4GfF1{ zjfj{mi$LHGs&@xoS1O}~6V6t!x^DtyBDs|bV=?}!jrGuj)&c;{_12!=-U#S>!ee^| zUPJ)qlb}YRcq8;ml$Xd}XBS;Qxpbhp7}0CEFQPKs{+wrxXLq61!GnvKq3&_t3@aCN zuQbA~Tod`FviDJK=+XC2@MsIHS~~w>(CyzqNFu0SR9u&;ZfdQ&4Kw~;j~{4L_7{>f zCXQoIx`@Ki!{ZPB#&S)O6WO3VDGjB1m}%Rhy&MddBZl)%eW=7|>RMV0I@J3zvmDE? zR=foMko^3N1161-umxS3x{6Fi4z$viuz>~TM^xOp4!WiShRe)@eWho}vI)cL+z|-} zn0wii-Qgwcc?l*skx~tad^v z4O8amr&~Fldc&VVISiB(03VTOn%mazTmZD21PyK;?A6}GZB{Ub{dzt^N)~FJpS%%R zPxbfy5Ea!P9Uc8&7Z0XSTzg|wF=K?*T{wgC%C)<)p+ zVaCy`Md(v%ZK@sCHQp_Vf^Sk&jHky_Y}$D1VswA*WwL$@vqET`(m|AK_A7xyhbeho z4B)?gwmk+_n#vNhv=iFDB4LVC##Eb_fv$O&BAY-1gz21zj6NPhp_dQZ3m(a9@jnFM zpL&XhTD51i-o@KG+GqOu;bs}Q0^M7^S_3g(X0Q?J4tb~(u(*HjlC(({!xT=`c5f6{ z?ya4U$$sH~kMLFF^*2oZ-|Pirdn3 zCntGkpjCHNYEkRik%48&(KkQ{J8&*_Ml9n(k$(B1>ze-{O)LDp~HYoz1 zLelHQLUCryGO-pRd;PWP=W%+^hpM?%fNrk&LrzYPJkV9(Pq$49h9C|&{)T4p%>umS z;+x)W%-ScY@QhQc%e;$Sr+^?r$NByJ{7}zop4&Q%8&6h4D8=y)&o1`azQ9)r(n=$( zDk0^~*iqk>TBif`3)RpJ#YBZ$aU#0!KgU$8!3rQX2FvgETpa&N8nZxiU)JO8)mI^Q z5>aV`sXN;cNQ19W#<|V(VP+vGBAqhHc2BN+(>nwmX3EUMdL0PX%j__1!fXYFlM^QP zozi6lV^$fFb8|Hg84Bhc_zhT3ej!97AmqMd@dlPAp{&6sC`D#iHP-Ob} zR4n!yVl|qx#%sul>ih^Xh)l_CNCfnwcU9$R2u+;Xpt%y7d5}>7)6`BVp12JFkk09` zaZ6M4Xubi-6w!1J0!vb}p^Z2RoN@d$VrdWkaeO)Ezq6UkJ7E;!EvQ|Oe$~}qcJAD{ z?2fCrY#c@&(V`whY4qN0&v4v;df*GnF-K zg5kE#3HNE{$FBAF}FLcgPArlI^RN&A4s3AfNuZHl`<1R%$zHvX)lJKSPnt( z_>RmM#R8C1h=OduDieJ8@Zt5Dv1YmIFg_1QpnZbS#11Lu9NXUzSm`AWV2qt0!g&1R z(_N7S{h%ik=dTj}hTT0g#SnX&vL`Lj1IFGjU)>aOe1&`1d@97lBncSpF@u6UdHPgH zDmpUI>pi#%)*+s?|N+1Ph*up4Ov(nUT*FPZV^TNHWDmQ{YFj#Q$7c+L=xFpftryehD){(eAc)2 zxn2zmVwhMc8(Qd;r@$IT|7NnlSD(7}#jE@De>p+$=Rl6?IiOpaEY3?Np{!nZ#K!P| zbLV7?ZvMnReLsHm!wDBa{kd8UusKrpBncEQ0lF`!BZvj6RJ2jpcjb3d`RYVOexd2l zQ<(a?I1yT`^uiWk_J+Z#2`D!C^xPF&0nTT+uKuaU*iov3;?EFJ>81iILF(Xg{|=y1 zAn*oy3NnBRyZR}&S58Z0Xi1cMZ?vB^o+grZTffTAC#c5M*L`vf`+x*4gsfbHCUTpq zlp}lbXq*#?BI;6o5k`~E&KT(FaZ-WdiOdu{&|XfE(K{#T zV}e2ut(*}mi+yTi+j1}q+F9V^sD?q3Zo&KW#F!?6JPY1^0Mto*9XihGrpRvQ- z(^Iv&0z$P?NsYnhcvqOtgnl(h4w|q~k=bLo%U}8;1u&HosL--Zg+}eD>({U2ojs_O z0~n2a9#Gb!)>l((s))f4(%4iAH^`^c`?rzfVqft-ZJfwqttPSz$Djx$RO;Po?BBjQ z*ZW*u-)IPPv;vnD&2LR1z7%|O?|>Ez1(~UUIMhr%zdXi25ZyI3QJjcREdq3&LB#v?79k)lGK+B!OZsm~adNU4~?CE&o%z@5SE0=-2a%SF)N zUfY@s1uvjl#P>Tg+ExujxL#yEK*D}`@eK->Y}N%tB3l&!(TQ3oWg5QBwpbLE%2aQL zSb+)5mTmzBs9)NDJ|YXi!S(bHMztaoW4Njs{~5py1+ZLazJLFMYhp0t24*F(xh+F8@3{n|8DQ?~2uQ9R@0l!TB+JCnZ>?zEItR8_Tzu@wCGX}vpBe6uWsJcKAX5Y|a zjWe8E*tm`8yL(K!m0!Hvx0oEr&H%`#Xc2r6B~@Qxf#(BBLX$Bkh|>g zXrGGt1~^2>NJ1x9HdhobBA_+qxG#@HHSeWYMTo$&5vl9lbxBEm2uphKpRoKeX9))Z zG?n=y;vD^k18Dj!@tD0W$_1lwP}8vfx?EA|6ETpFC!D)+Pz&a}=${doY9WcAkQb#f z+9EF}$A<@vu+Ijc|7nnP5~W(f|4=j-9a}Z6yw?652mQ|hi#BQ|;K=FE?ubCl`;&xC zvKJ>nt(imKg&V1jt--Mv^ja^HilC`M&JzYMUpEcT}W zpD?vfoz%pAjf`t6+KAGYU*cMEkM~0n@c&7;|1Id0@-U2=SjN>*eq1-(u8=;cfQm(! zVp8RJy9&cE!3WJ4`%W=AN{B`!di*F5aSfHH<_Opwx3!gKPAZqc8uqU*$6ZQX>{(x) zh_t`}bFN*|S(lpzAF}CV-b5q`z>HPSes7pNEI-U8>pAgW&@7klwxpc0^3__X)pXB4 zqb@EgYLc`GYxeM)Pmn+Z0(R-(3Zy%ISb&UWpFfmWL_zKfug;K!@a(xMt{tjm&!gZ5 zB(+Y^raH+mj;sVo-j1|aCYi^fbjzg|+Zw`1ELVVt1#T_Rp?!8x6oM}M<@}Eh;fy-P zfbbcRM!TfRS3TE!ckV_BDif#F^ml_3y!)dIHZNM-0!r^ZB;w>lZ{dq;;q~PH;iGHm zUuSSk|Ctg(6z9kXNsT{|Q)gDkpsfpQHNZj^lOgiuUcU_ES^$NwKpKqqRq-6?1Vqi} zWp`GCkuP>*?NP4Y;XRZ6?KJkd*^U@Ra$gSYzl&dg&{nFsEapAHp!|=~dT+ye&uxju zuXolOhk62FhgZ9k?ggMRw=hcgoInf~N^{poma+p`XSlcU?Ts|6#Q%J=DS#n!?Ce}! z*Tg|)h^bG@qn6!;TksP^AbnivJhqCR{-Q?DOHASG+?)0GokAKb=8wgvkuip{SN^@nu|br% zFB=+;f>1H2u%B#Ip;HuvnyLgc2!~_LMOnlp5Q^BaY{k|U66*cFrErwg6-QGUV31%- zQ9zP8+1vu5f)g>0+9S!z9m&s~$CTG3EHKwHNe>m}FJKW`VZ0eS1-8P3=xu=f>6W~H z{o2Flu!b3oHz*vVysodW@6}Mssk?t@YgML;eHBbB`e5(}t^P}AQa+EDyA8LcYBu%s z_eaeTbH&e%Vs+O=(HQi~_fwp3@CII{|9b_Bo&fF?bNg^nUar0(NQT`O%gFcNd%ugE z!R)CT*Ao_A1h)iQl$~ynq{3IF6sJ-z&~ahKBlr1 zB7%#vfkxo+au)FH81DBO@9%+XO;os+;as@lnyTBX!m+`C@qqv*=*5jP8!a=_4j-;= zLNmrgYPQf(wgm1%XW>SYZ85RM2KG*=lp2@1Fw`?p>(yTqDWV2sNAA64dr%*1$V*NJ zR~&x-zrW%XPPupBX-R8*u9#gcux2Ixg)!C){6OMiq(qyx0{-2>9}KPUX2Vq#CD3hI zo0{^P_wbBVFQA*(9q%<8zoI_cq8nA@GuDkrEw_4|9i=ss#V9$&y>c1W)CLmF&CMm+ zpc<e27|%X20vVC$u}Ew9E!;uijiOIT1W@2=Gh^H4Ej2 z^ZRppB4ka(9aE}IsApp~_+n3qO?-Ar)t;m3G(xYo2q{!e`6QY zMLGicz3xC9lDC)16blQuF>J*f*n93%4g{zLdN;XvONOa?WgMA{p#7fE+`-W3D^q72 zDLs3JW}}K{vBZsZ1gI9Ry`dF^-KDCn2=bEM6}@Yc_$#q})<9osZWwZe9W=uS3oMxW z0s^XOQ;>=z5qP;q@-#*?hk|?tgPG;AKN$7F+SFjk{QZHb9)ziAvd`p zk^#jEkt8_qNK*ac(D1P4^!S`1YZaGvRw)(AAFYW^BC=|&}=Gf z@7MC_(W>b1XbN-j>*J#}UX=ALXiN!Em>Ax^)n0{ibpUHI2)o&(7l9Fgv~TnJ#m*IQ z*>!D|`P60bbKS>3rc*W=rJ@49Ynrac8R1BNti0pT@h_xQ4UgD+{-JjCB7%ieRoQJR zZ%c%7I(=axYoMa-8$Fi*Uaw}&jySKr#PIB~QK|T>f5m)8zVv8}cWYGmCuHv_rYND~ zH5>i3rxYot`bYz>(gi)*N;BdU1em>ld~CVDzyD4%v$|_h>NEoK^`wOPgGCeiipEEZ z&IuGnhlnQ7U+xp#Lu41WoWVl-NW{TB1<|y?^jrp&whX@J28;Ji7YI-Zyr}=Wr^08n zq6I2kw4K+k-PY#f)9+H6JNh;^!VGROGlw*z>~o0TZLFSmOv0LlT<2Z)a36Y7y{qi&+f}RG5rZE`v@YP z9XkMFtO;>^v~FBmHmh@eP4A$fdf&Z(2%n)SpA2<_w#HCxj8x7HhP!!(3rL;nWkWzNP;sXs6oeiy9cyGF#s^oaP3)$7{>{QT~0D*4E;ri zeZ+NpVhp4=-E?Gda2S#P!JkQ;_l7Yd)^!fh=9fx`71WJ1}ePU zDoW}`3hQp|wM&n<*wt@6)@MK72VFX}-F8HMHj>ZSpwrmMV6h@Vd@aNoV(hp+_o*z2 zEQp{OO-*4xaNJ=82;lLsHI1>Nxm>?J5ag;|+mb6vVCqJmHj_zQBLJr3*i^T()JS&@ zybp+iCa;(VK^Lfg9{XH0CSBv+8pmxBB2x`~Swp27qouDaDh7OOzx4^=uiDR5X6<}$Hm>9i|6DY$m@Gx;pvjRp z3G-3DmYGf;K#htAF5hV8fOB`chGMb0fhS7BIsL$fKhzDjrw14p$PL?)U_^=dHl&|5 zyGD>n3ca(3HdrCLquaALP|%CiG{~q-?hF*|qy%4Zrj3vA)|LJhBDILcW1zvmltQ6Y z%Xndv85qGtTOpkZ`j@Zsc?wIIqyy0Tp}I{!*)eqYw47htZkd)-_ONc?T-|1%{<6<| ztJ+_E4CdP2X>g}QOKwYoL)I!IGX$gq8&A)nU)EF>ZuYNbFY*q5;Rc2>aNK_!5z&tS zD>KMvMV?XYm*N#q6hRUGSxYMxK~J=<`ryI5r=t&LN7lQ4xTqz3*X+*2x}mr_Yu6ZH zmyd+$*-FOobhRSg99h6^jED?K_R-fZz$u=2R3^tg9FrL}<8pccc3YEaf>Q6!j zIHCO2QkzwXFdjiYdu*t+A&1#?J~Vs*UUoxrE18S!u>VE1-q$<1UHTr&das0j*T{yU zh~?GU9xfTEbwzj_Ru58!kg)l<}Y_BH8ufXBp?O)dThgfFi7`RwNPMCjDJ1&y!@uPY<9 zLra>QEj5$#xA#cpc1dyDX9B0HpAFsQ5vVJpwgaix1>&D(J?gS_OuYUZSPcnb1i2)fq|RqAD!msws)%Y>W|3DDI1QzczF68glWMvIECua^|Xpy zAOK=r_3YWR>NZcDj-Gbjm~+Hqx0jjoFUw<qrTpaU8Es zJghFiS|a1BR~dG-*~>r`?Q`%)z#%xKFz-ew(Z zAn?QHeESc1Dx@Hb31uUTwa(A`Gui0K*hbwSX^Sy&kAAU(&Te(QBb6dSY*W`wJ;(WG z?uAh41ioobR?0EWe>^t}d;X@yCrq(QL&dqosmf~hQ~BsH%gC@bf36;tE!`G+wLt!~ z^73m3Sf1TDvGgeKKPye|NC>U_*#6Me``BmRlNmwBNQq4u@vGlEyzF%=&3VJs`^sBZJ*;VTVg@{dqki zVsU(W#5H+HGGzs602EbwOq(_qp?qX9F)<0sm2Mt{*?gQK_SnZEm@gvw`S~Sy4t8CP zKIKA4-7v4{`hKV)G-HC1fdEpFJnGM5R_p-~9xq0Zezj*B}I{HnoU-aw@G=~9}LruiBMMg+=p#8?5%6Lk0R zxOu*$x_X|9ipmXU%I}GQoPN zs~>_cpp(356SI)eoR*#vON*1rkaSpB*ej+=-7Goe&zG++hYcg7v!NO`INt86l<4SX z#%>?4xD_hU@lpkG%_%iM(pHrmB>NH9!(slKwXrB%e95+8zT{d!a38{a!<)fjyXiX@0L?Sg-kl(8ph8KD;y~Cc*-$TLKWUGdm$Rj9 zPhgNzFzs=8F4Eg}GFic<+%xICBv$r`ujW|V+3~=Ae$KQ@DR)rYiHUw@TJ6tZTXl7t z6A{{Lc)$fc&s2tG(^f+2^;iEU!4$4T0?lo0+`jV4miG3tHeQM}IYKZjwZMDO`9#Ei zgZQSC$(Fcj4HCS>l#~=CLK{yeKQJ-IHdA2C4$byMjCGQ*lDiyKd;Qj}Th-*&Or!fN zqg~f~50^>V0QW9+;=Sb;bfWTrnypKH4CbtMk!%*wetZQ7x=Kn){~Eux*2OLj9B{6e zc%#@QP;4$^wygVCtHiG3z*chbbeq!~_S^1L#+AU+7?Q86(MyIE?{16RK6fgMCI@7n z`~mP8pJzR=vqDnm+FW4G{}SE0g7!DyI=FlH?#0NN$~lJ%L*_r7N&83Y_ixffD=5+! zAF;e_j$s0!6;Rs!`1*vz<@~NdU-}S`JyDY@(`{a1YxHsJC!C-x<+4mrd_fLeNFeth zrb90KoWaAjA}H$KSDkj$tw@P3-xb6vZ;-jDL6_Dsg-0L@vjs{U&IT{+#+J3f{pdPA z+COQHm4Y+Bc_viNrp3WYVzem-~i4=MkcfKElo%BoE|O=P~B0iE-EU@R;WZ* zR-;Tzv?%hc8);N}H5cH~)A-~CYu7Be8~3LHFh?d|L0Gik36{GMZ8(aU7H zx@}3uz=j30!USj?g`ZsSXH!m0x49`}t;WK-WqIdG@ev8sk7 z6#2kNK1>4hlz1`JV<9%qjbGU9AEHs7<*V*_C=9}>p7M6|b_c!^BH8p&A zeu*tCX5J-PATIx)h~_Ik4GWyo&8{o3HMm?Mm>f$+k}H(Aulg~R=yC`toRAcZShcpxRul&vX2dzH7*1Y zKIhthEPv@0{T5O)<-~+<4wUR^hje`pWKw3Ba1f>l3e(BEen0|KkVT zdEoAp9qhYYDz5Li_9g`E>@7lj-ol*TID3Iz!!Q6i59}r9i(UI2+cmk4HN~VnjWta4 zlU~-pPdrb-Tx6AH{Z5v2-qq%V%LIi$X^1EKLuQkW{+&Yni`7v)W2cRHRwDEq|Mk*! z@B`o!Ln$4Bftn<{8l>3_kCpRNK zq%H!KW@L%?e*N}3E-h=m&eMMf!RnV9P&~9B97NJVAO|VYt55BHXJFoWyZkxHs3(+?wBv=-;!JMn0&5 zG^60la1HUqxS12`2+i>m1UONwbw_3GTBdZ9xP#p?L7#KC&1?y|J1o3vJ$kMHByk#b z2aGibJiM{-3@5z{3lc1w-LqrOvBrXkAhyxU*RSVPRaIf$L|yRT0>O$$jKJE6+<2G1 zq#H6k0eC_W+RdSHO}_5Lt{_6Y%K;a#N+!D&T>P7XS;Zga7I1ovmhFoB^91EK2P)6v z2vF-YSl3z@ymH|PolL)Xo(OFew5UeSZ5A6_&3l?JJ6_< z`v;`Hxn2~7D#?2G#+b=0JwOL0Oy`GEpd$02?u_G*tM!NJQB4XJi@>DPZ5zds0|mwf zph(u~s2xAZ%v*W+862-EQ&?#R^u4nu*)V7rn$M9 zT}DP`(oe9t9{K>RTzXxy>ajX+ijOW0j}{MIz3<_v&ae(oNze9U;lpp7W&b+{WScsn9PJd`-fvtXQVd{V z#=qHDV@84EuNFpl%mzt>S%?YD#GkpSgm9V-)MI7BM-INZR8k?;kKE1Q<;{|8N>Dctd>11_WfLuFd0;chlbeBj+kI{+>Uokzx>#@wCj@*;lL-f+l@d@v(scebhlce7LlwDV zWyVLKz_a1HzZfT^`sdkNRnQ%ZOegR%Z?xD0)7OMWykZs$<0j56-D3Xon({uFLBHJw zImTI#A=+*@pH|0yDBDl|LwvyrSWFw_?QYwCIgac0#eZt%!up-L{ts4eI6<&t)gh_= zftY&|AYAxMDOlcW$3VMFt_X5B=&SW0@`XM38tV=hw{m+=V*{u^K>U{j9P3jM%xJ}j z|AxNLLK6p2BA}-ckj;M>h%fENe!X6{$P^oY;*JHzU1C04EwG?=mc@H3%!85FUHNWm z;j0e4@GmR_AS%3K#ONxbXhJuQ^G8%FrCEO2#z_gF?)gujR-w?u6$)lbV`u2j{#(2!+9UGsHa|1|tGUN(u_TF;LiavjX+jNa)x1w3IdgE!*}lP@Ch+!M|j~ zp?m>U^Md9e_8d1GDhcuon-?d!Swe$YT^Al8rl#`YLv=ro(cZoO^OWEbh@NbY9SSS! z5^Zd4XF^kJfr|*jI^-ZA=rB#sJ;~kMsWx=|ps^&uy!TUC12oy5{_B~7L4N#%wBJc|O0@L}rpyyuUlKrD%ydXVuq#RJ?G1NKSKIB-}`rNDN=}9m?kJ)>A zdQPFN?z6>r0bVVx{`_>vpA|wIf+XE{F=p^KlAm&a!Eq{LLwMnzj!EJ$@7+I*1Dwe zWl_bPMwsiw1I3ZUk2bgE7AU;A^F&KiGcV!LIeB&4w3pM*+f%#Gl2`nVK^`+TkA#LBw`W=%eEq z?~)y#9l7o5>?=aF)oTQGmh{|AKn?$ov@Z{YYJdN~SCp+NYgBZVwa|i6m=>+JB4pQU z$x;9;v9r<3$_i%{Xr5&XCwb>om~A5J%n_2`$YJnhRn|o*)axohziS4b>0N2r*dCIewBoL6ER=d*9kQJSdWG8n+vixNQ1A04cF^F{LW zhFbF+l2s=UzuP~~gK82+6`Fj|zv4Mg>({R*NG>azvAulx(iR5Lls?~m_f0d4UFAnO zwk8^N1;BOUQLA+`B$bsjnD*z?*M-;i@v-eRpcm92$es#yvMurNfZG@0O=mxbv=8ot z=7QH5cfLg;d<7V7*Tx_<-!EYzc=5qDaotNx({@QBmzNrgBdA~4p!Mo(UFT`*fgJ?f zH1^36jbosQeLUc_OM;F4@mbntr5wSbq4r@-R9gC60)Q6%2f%cJ%W?iaeM5ZtqT75+ zh~ogrK0vayIvQmnh&3XCG#N$FpnISIWg09^7hgF;%Cr^EhX6a{dfOVCGn@n9XiOXK z(jOL7w0L09aq>x!xJYhKT8ck}xgxmBehYOg6hN%Kqj4^A`7~xB^4P&oELD8si9a1e zEkE*1^Z9|oqVy&d6avy5E7y^#jy0hG+oG&zuJUAdUm)BFguVm6bLuSsXU&I>a#7)h zW`Z~|x7MH$FvdU4{iPF?iS7HhUM4d3mi=@H?$L8HxSy`mgO5=_z zR%97;rg#91Jw|PlbTFxsA7wf6(&O*va8_KSxVX5Wa4BEkufIH@Jjo(s@j(3)ElwCv z;bdgR0El{o+|8uBckfoUfqy^V*1*2UD5A7=&K}k9AX0)pwzU_n>c2ga-~;sTnY!kl zw}c_g*r!K+w@?l)%JQ@}Z2&N!L>@v-S41XAK_ld zrIg#d!!3h2Fp~`dBZb31jGaF7GSEBJ?`(-i@|l0v6kkD53UV>)a*nX)8CP;R`4c2C z07ChvcsD6rgY&OnqXOV~X@sEJC4(Cndq*DZG zzfE%Ju3z?>q}a6m7LGof_!&mjT89@_)BXrLsGEL>aC=|Uk>=64jAxe^If8NDlyDWc zE#z+c5-dz;8F<&=e?wa3;ohezk^q>@oeph5=B~ygKSF<`n1!fn6j}o_fJL=@y$S4- zfjkBy;I)+oKi^CV00y@}C@Zc@nagy8aFWPyTn1toG~N2cQ1Nw2$>dCr!JpV;?l1P6 z!Ob;Ag9RG^hQYJw<*PXTs5#J1HvQ!*hu$HDh*4cb$>G}f_Wr{D4HHOIEDfMAGe5oz z7fI7b$PAc5h_gPwdjR@g#R?$}EdD`rI6fzbNQp3igKRqTwuI9qFfrlgmKH7q3K!|N z^#oa)ss-5i0>H{Rno?TE?N&uF-C%(yHPh#MQBe_r#{n+9lao`a`7YM|krt$FL7>*a zkn@&PcNuaq<0NU^lh|AYH6^~=;Q90f6AVbJT_Gq71dhmS^-_xF_ZN5%|9r;>nKeb+ z9ofnS{#$b_0+FP;Hslk8Ch!!HL^Z4GANTMCLOpIN5VL*fo(0HdM9b`1ZgE@%#ajR4 zZzv17Ao;)9eRC4Kye!>IhlvJ)7OTMB^8nOtfsl#C7Tl$X6$C$|^!dB}Pi6!A*<1p1 z0qF>+&Sz$2ZMKZ;X41mHTNAgS2aJO(0NsHl4u{uORf*0xxz@;-LB$jQy@4F|ag)Vv&aN`Bj2A6gH*7b$>dKam4k7ER-G%b3HA=GImo@El9$_i_Bgj8CBIpy0%- zuPJm5*dIIRo}Ke|XIkE83A7`7epBqat4k_QfKw7MbIBfFl`l-}#|#>W{%}-z4mXV( zg>R!}*cbb; z)WbJTg;4%X^^N#=nE~*Wg9!BU?1*o&H*+2N%E^@A}960Rq^IfdjR3y*; zEXj89ZWM2TiXO+dpL_j!_BKG5p&Tw9T_}h2Zo2scSKG@=_UaMoT}65j>=MFyIwwXU z6ZB#mqjHs$tgo4=cBRn-N`pX@wt^bjwy7?c{f33j&z`o1Pg=uA zR&7f*OOuDbjsaa-M|Vfvie-wPEQfD1C}LD=wf zQ0BwCmXOeN+vb5F3Dxq(@+Sym5`)d_8z4`2By>{ z-N0j8eb1i2a|2hI-eYC4|B=uZ^GKAa1%4BPz+~(J#yRKzsZ*>}0NL-;L6DySx?yto z!Vp1-p#JIakv8zBACCVJ<(2hNlXU- zzBQzojE+@bCik&B5HAE4JsYpV6o_fUnj9G`KLo4|%Wk?>oYc?t!52bu~AvK8UFnV=xM#%Wnv~dM^?2=x}922#DRs7ID8u~=Xkja$2l0q@ZK2>v#DO<)zz<5!_ zUk@8zZj=x;R5H6Ki&PK6B^+7p*!n54;2PfSnw_eJ$xmMf{l>jf!+lZmiR)&L|2f$p z8jR+N5&8P%3sLq(e!eSE9t1+JaJ`cskP#sw`(FhBriUBfK7$~Ss)%4*&f9dg)( zSCXKlRBAX>_(2#@3LN4ZlU4IKF?^{#JNS16(1RQ;qCam2K}b)DWj8Z3%l~Ko=IHd| zJ$DH{!MtT-?_eGn8Yw;tQYW4u&7^SY^3y>ZZE`-bnA6D2rZ`d~MDZaA&6t$PCQ{TR z&RY3>d~u}#+Bb>NuJUs93_%XLO5(WXMzcdE)6KW=qFe(Er4Awmq`k2UGUg5scpM}R zI1mIP`I!7ghDf@M9wj_#f6#cA;HJR6_+^Nb-THdQdxK-k!pre5;XJbu70a4@)$Yf} z?$*g%zLfZ5V;d)FpaovUtKuBD8EmC&(7uZx52qNP5j-OR4z4jVF>$*>oS=mJfTF9H zdlW2l2ghlg@nS`|^qqRBk;xWLwDgl6+4NBw0yQmkUp*V*n)8mS9~oSKPvNI3CMzH( z)ir$uv(Rz`1eyZz!@r$+gqfnq6il_UdYCtMmY}2M00!lIrW3U zjPwB7#mO}yIL!F_XV)zUb<7OFZK$de&9!ixTu?%gA#LV{WYnx=@CAu9bQxPxQj+b% zV;rTs|3AXuhF|&}n)w5vX7lKA)3{4C_I%@(1iP|L3Y5mH8g767(LBpIv1&rO>r(MU zhYu5}pjUsPTW5KS6(kM{tL!f8O|n_rjX74v)(v3^VA{2+D)H@D&*oajeKHit5m z9x#1whdFvBB_;f%%9uvM5OBP5Q>z%y%7DJ{OeWoWKx%mjK`9Zowxv8i5J2MVLJ*}b zYUF3tP7krU z@UTZyh{?LkDqXIBT@dvw;T+kXkyD~RW(Pn;v_cWiE*bowQUSQK=_9voGZWmD{$n6n0FzdxeHDSDNL4d3mIAI$YC2CN9L$r z*N{YlZsc!%Y2&UVpzrf9${9u@NH_6zj{O?uAOO-^{q=88+()r30!W`Z&Yy9zq^xWP zfnXVh|Ca*0*|zL6D=DLj5E@`Q`0qQ{iuuItT1*sGFfZFE1_`Z)NyYepi!QXMXwVE7 zUHMGKw3A75R|KOqPMfuiA1rt4$%5^;r^E}&$X3!tZ`*u2%NL|-wJXWH@?yv(ew-C8 zjgx9XxB>*mA6j_Vr%V(}#nAR@c3weI+4_*z<>dla(D}vU6{9_cDu}~}jcZ;!fPp3X z$2fQbL=Q1_H;lVl|H*tdI}OBN<3 zWSZ-J#H|V10||5o)4_~8VC8I@fd0xw11D_P1wZ?s91}p|j3&HYQk(z`pT37z!~4l> zcU}!UJY}q;K>qV6G@+=mDv_aWe_&t()$8 zwYZPc&LbKmUYMXX>wx!lZ0%o*$^@A~U15R%k~edAch`;FUW)sMu{+H4tAAh)A}G@_ zUoUK_+%5BTMS=u2^NA!FLlknOyC{KyMh|}VZ()zmiSha1EJSy<^ z=bJQX(kt(kX{?xDhpp(3(`yf#qY9L6q&bO4#TozrEUxzi8pjy+#UPlDz5NPoF7qu8 zzEmLyV<{>;-aub8)x=`Iw$VxC>Z6t|QW^NMYXiYJbQA`}NBMeoJY1P99sF$No-25J zI!?$({xU%WHrb&)GI1Pn4*d~CSN+#GcjDDlyvbm9Frh*jVgXcJ`v0h{tz|XP)aO|_ z)Z2J1J^e@dq$5@tgFSCMui7&W&|=Vi^i8GShfThj^fMNC3mGn@4Lb(I-u)qbbw06u z4WIkr!_$ba+7$X#i+{?x)zwFC4N9aHes6kS07<3LPy5OlmcYDP^iCSWZ8+5`F;OGX z^+HKyrCR>2hk@bY;RVocPH1;!Mj}oDjwj4_Wa4*>tPl_ZrG7HcleTGD$fo%QR^YoC z1m{6KC;Zbi-P!W;$|DE%SupV>8wj9vKHqa!W&b+uv4q{k>l=|phgCL}$(9Y!re7LM z5>Z`o6qc#t8hjC7^8KI$Y3T1!11GPXT>D>pkyWp_1dC2PlxKvW9LX!y0W>>#4KUeh_MXQb zYao&@&{t;a?M|44UAlDXyyG3=pNK+qpApj$j9h7=J+EJk@(;CubiH=F^vU0rk6V~X zWtjxnU@x2R;^-ph1i)2}|8DH|f>ek`nj7`6ES`_RTLj3Tw0Wr@dd&v_he*zy8v6+# z?nm?5!8g%9I4}4aK^BH-r4*_#QVjlC*Vo|`gQODVD1$4rA4)?U9Pshu$ApCfLaxFnd-oTNr#pdUAAdIF)Ak>|V4kc>Q7*2#yCZ~>q#r5YpI2w4%Uhm#E(Q_M|W<4dpWahQQ% z+52U$)`}WICKn3JoMC5g&!u2|>u=3+=tX4>h=tu&q*p$HPs?sH$(*Vf!pH?UU)qX~ z->;$gz2vymnSId6ePU(+?%)4p>y|00tDEDK03GS1uFdF0vblxqo82*B*411933pO5 zl^}#%hbFy5{KNs#t@WT~ZGl90&DnEHOWwR$PUK5w3ZRBdL^)YhfRQkzqZg%V1_C=p zr$dS%iy)7PlTi?7%na@5rLULFa65*rLd_*};8fL`O*KUzPW^sm4^yItn#>NZ$-vQu z)Y7}5o^%2q?l;aa%~a^=An(WLN_n$<2FP0Cihq!`a7(Yi+W}rgq%b8XviK1I(ow3g zY00bcST_1b0`(8#=%6Pf_kRBTnOytC=ovh|ftq17`%iEQ zfh8j4DN~XVK*-TuCx>yFM`=&k(HKr>E7Ur?@SRBG;~*YjK6vJyc?9>tKd@i73ke-c zqq|E_!>1{Jcc8K%G{B{;0VsdhcX2gmq9WO-u#CQN(1o$x1o588Y8?wwoyl6l3kG~gPFDSq|pl@LK4`ZxP~Mg@BruC`uc zIoax98p6UXtW#0J2VxT3+5MkyHn`{gglQ+l&Dx)}ZC>5jR=S>$^NIK_(>18*PPSr= z9~R5pnhm-VNn&;!j6ej$a`qSEv&ySc>W;TOPM7ETrG=*%t5|d+1gmT+3@Kgz%%@~M z0JurHC8M`b*%(!Qy0*Y{J2B^+?$-p>nNdqO`hqHqJg`SuaeHuXLH!zV83h)W0=xjx zpD-saG0*9y_w2TSS8!6jXryAn5}Ofro6axz;SY2`(nVX`^H zv-0PcyA423R?0m=*5GsqlG^QRQQ9G0x2*pW`>F-m)aw% z8C?UKs4xQP$L9g6rvPTR2omI*E5a#WUS3@n5P=Je3V?%N6nX;sqX3Y^I~&RH2ZsUs zQPLilhGd zX~R3)>+)jciv#(zxc08q7QX0^W;)$S{@CR|_Ix-^)8XDXT$=pq-RD0R7s%P3lQfEz zonu~{DyVJrVctBK&if80*cbmeasG7z;YS z(b+$lo&|5_M}1vvnh2UODljzkBTpaTcT<0mcudL)9w06)(l4%K{2|IJj?dkmy&sVN zHv;Qt-g&4p{Mt~burVO890D{V^PpQVR~rZUo~`JUChkPRKN1%4NjwtKGC=r zjcBN;Vh}@rJNm8`d4rp;^37q4VS_O~6~sTXk##_P)dh0^0=(vq`^pJ7^1she1voT; zjM@zls~x2<)sc+UP_oc=e$_7-!foe6c8D-S!4I3Mc?hPQGJh9{8A^z@ptic%-@xUT zEABKKEqhEm?GW_Z-k+FEe5>2ny*hQt<`(k(CBPBn?d))p1!tS*XPs^F-Z&G2w^Yf9lV*rQqe# zKopCNjO^Gh9<=TO1MBtgmWS-4x^_L7`%Myl1N(cIK}C<3S>xc$C^B{NoV`i!Bfigz zz&Vj_4`?MN24di5M%}ScB)C4*4Q$}g@kyu^ce*ODa{g|oC@?Wuq6xYYHUh~5Hz}JH zkI1&=^vjkr9xWx12QlqMtZ-NKE`(tUCz!7}tg~42=f2(tQQqG-_5;;*K@O8xGkcb! z@04(%SsAVE?fSsN)xth`?{Bo*$UohKDXnIky?5Z`%jO2VLz-EyF(ef?AfhY+1AA&7 z>I{kn0;+l59{?`T+Rom~qE9IUb_y}gbz}u~9um|RdvzX0nQykiJagt~S`u5X=?H>Y z@T917C4uY3s8txmwoNg0Z*im>5XN>aLOD1xUIeB$1MF3BWZd0R6+(Z3O%r_=gB$sQp*$p_n^Dbruz}Bylaz?cz|@A< z8}-X^;0{V4H^SDUd5~_I0OY<0=8C||3vNTx)PQbuL#UFqqI|?b7S_ul3?o!GrDT8K zWQqnrlDUm;VPI1ude%k+GU^03DqTN0dvvg`4HztS3knLZO@&OAl2grtMHQ!*9#dNc`SL(yVC<&(2^a0)*2f@Q+|sUq2X_NL znvTS~Bzhd!)%0ga0be=5S(X2{ z8XS`vbt@{&buI=>^w6RtXfdo|R@7r39+B<{H1#@ObNeTp7Iji|9qjL3s;tbfsST6A z2>eEDm>Y4o*#iUK+_Mc04NI3T6LrvL{)GwO!4A);g2_)FS%}b-lwYg99Vt`a%p%-u z(P~q=^?-);1_6X~Au#*wzCu+1_DFFF^A7oRB-kI1)@4vjBb)Qz(y^Wyi0-Q#K||}@ z$U0HG&}HgtQ5R>tOm;hw6IC||R3=wYF_B0D^0>QtdWQPro|(YF60SW=Ecy)o6T|+N z9H!sYxV0gqhY4@puFRHHZUzQ;#3S$uxFfg_6RfY0!%+1?ds}yd0`K z!%9YP0^tj6+6-hdAU}b;2K5aETX1{MxM{z1Rj>@ql>e|ja|Z4ol{d0!4fXEygz;qg zCQED<>c}v~9yRk)9bVm73Mq_m&8VkSLKQ7H!EEC2r&}FK{O;&;ImrNBfIkxSI(M>S z;D*FtkZXkmKn7sib>RIEIIIc%juW_m{13L<023jacBbaH=%Kz?&|`KJGo1MU z_7Uuq=v`#Px?i4dbpKLSYBlA)IipG>*!FeQuOyVh~9jiVjU%{ zXtC_EnkG(nz<&LWBzRr<#ZN~_UF^{(h`UPxFWaRZMi=Rw)9kk+=VMtSwcwXugafNp z8=z#6yCq@Tf?~4|yKt?iGztN~4AIEy(M9}^7dB=*;JV3V=Ph_n%GepOg9x1JxEO(B zCA)Wl1{AJW^di)S>h=@HRDWQ%Ie-{tMURL;Zx@sYT3}Y>RobS_|6_p+>Y+`lxGQ!dn2FVq`Pr|4Nh*G-xGIr$p;fV+7GAgs zQC|Lh&gC8UsgK=wI|=mZs5fFKL3gsh0B)olL5hghim7Mf(cy z3uF8|dPXc!P}#;wKCKq#yzoCrqJc(m&f1gB(RAQQc8WK1_Nycsaq9JQ2d$GLvTzei zMY(Q0;Cb{=2fvE~NiR!K8!lD!O8ZYZUFsy+0TfK}FvVhJcKN2*8O{Xh<#b_ahRW$a zX6}t9p-#FU*bSVe}`qtIr5FhKIVFRm^@WIKb%#6J-n1}9K*S!c_ z+ltOXy47s}7X9)Y+ivC)lIhz?2i3^=s@M=GY_AU<(4UGvWxX>7nsf*34fu-6FpE)| zloPIvvRNDS-)?8jtk9MUCsejJ)FNkI1JNWCL^R5aFz6JG2k?R3zHd`+0uSK%I_QxX z*X%|E;m72XNbbg-qr{3n{rAoS2#(V#u{IKa>l(_=YnrvW!x@ia9xz5(tB_4i=^~xX zB3XZy^96wHx`AspC4yD&;Ol*wOqBOObc^};=}{FeGS4R<019`9o-tnN+zk*mu?0;zX$nJ1ZN2g1uZntblmyiB$81JBDQ~G)OWoFt5r! za?mW*W)&&V>y$J^kEM|hIvJl0vzJBn*etO=|2kkzvL^bjr81xs-dZR-bVH{~*t(%l zxUA!Eww7NK(i_NypTakX*JTA+gSG_>+FzQ7_ToSV<;bHAbT?8AB}%bSk07S{I3We& zr4mCaPgzZFyKhD#bL@9L1(N_M*is`h=HNOq2i{+%;duGt&e5RDwHz_R))`C?lV66R ztEc>j6IlffWXYn#celrX>u+ztalPOG(?vhR#?ddze0RXG#r>hXgq@y1ldBME^ z){05w#xOb@Sc{}Qen50l&>_pwXSOHH-NJ6 zK9BH%4Wz4YTfiGo_#Eu+%xGkkry}0?E5KYYP&;+@lVP13g@&og{)5!*jC|z`Fpeq80U9Qx+6DgRB}$C zt92nN_b%>gWywOyx(w*b@>if~9K>6UsCSos->em_btG~=4700qb)9kHR|( zh&;~GC62uK&E}dV3FmeKRIq#vpN1e&U>qz(7YBowQci-5?Zo~d&4?3mSNg#CnFjN= znv5m?*OfwwGxy*GIgMumvLTxqPB$GPRq7pGG?vJZfNgKlK%efO9#SYSYBS1Y4C){4 z#Gkul$Ka25@r_T;1-4rdORCLxc2(yZ(cG%_5;pu9Tav*x^ilhhrnwBRHLctj`4KHpx4bs^GE7CmsrsHA(N&c; zslTv(&PKpi^H{sNx+;@8$I(T(Z$*M_~1!1kY(;)oCku7ouYjjg6@QmIy)}rPKlSf_Sg588U7S7pyf8bTyX}#WKI~0p+mcQ~2W0 z3@jxVG~S*>ZL=4pv#e#a@!uM^8hQUNcY&02DWG|X?l{Q7g<-2s1btMhVYR^NDns|J z?!b-Q3|!o#kWGI;X-zcsqdpt%+?02-K_T9LcxbTU=Qd)QeQ=O|r@pa%r`QpAJ@*Tm zhCogP@?ecUH~mfU5<6QMrjV}%eVLN)E=LH$kaYdT4j6^QaPTYA7n$JVG2HhtdU#9a zIsh6Q3zgW^XP+yZwqF|Tf2@F$LL!F;ONE$i;`pIqT0865M|bllP&t5d*Q*w za0qed5M~tG=eV7k5o)8>{oT~GzexP-$N|!J@fh8F)iM9m%kWSu?{ANY#0)Q)FhO7& zlG%mX`0~ovRFoHdcMSl8MEd>fE!5gxzNC1iK<>uKPXq{4=ARmU=dunxrww%FLA={L zaiEnEmBDaCC7H0bN#dDI()uR`)vY*SmC?6ps@h!G^u5Zu_aftQf(^7W$ef+juT<~ zu@y?FF8kx1pB8FXaQ6-VirWOjQylrs1Ai#MckSG}bfpG!m zUDZ||4?4h%5yEj<3L!LKQdxx!mHEsv-Rzt9fO2c;Xeb~BsTn=M%e>fyyy6FdQtF6> zuFRR_l$RPT_VC@4`nbKoHVUKhJf7eO7nl&Evn7=ev7sRO-2zr6;hwzmcNg1Yh>UY} zNLG&}UGKu>QVZ|JlLcMgMK2!%1$@aiKu<jU4&tR*t<; z=0aI)?+^%#;t;Pssm#X%gw6=p^~P8Gbneh{eSYSf%<7A+p-&N4Nl(6~#T2CAxXc z6Q|Pa@3R3iDX%T;n=oXEzvX^sMq<#G3Ycacb_#=G^>!w}o1}w^Y%JLZiUJ#)pi@ix zqCMMR7q+D_C|81qMdocM)H_utpE`#Z-)91JZxJFE!CadOQiO>@mthXOS)Q*?Zyd{& z#4Tu8Ml6V(L0xTa1cK|53*H-%a>xyG!7q3tKWc^Ql?Pu=2lWIKRcI4MP`nuak=1xH z0W?`qoIR?<0VIrEn4o+g?%U2RCOdU`9w4u=4%AR{31nviB zi`>{dviE2(pr8!cE(<1azvRyhS0`|2XuS~F2DvoC)NY0?rD|gU#exiAzR5(smpi*E zkTmChL%HNUn6O6fa*y1sTo%EYwEg2CKg(1^;f1cry^~tIWKOC?XY4s7k?BfGw3v{ z*@D-kV=^;x9MBIr9b5(J=v`Ase9iwnEdCjyvu5!glG-wX8pu*WsiHaxtEnQ&5;oLM z;t5kR!#4WfeSI|TovQ~4Kk+icz)FXdKf>)Z7R7=3s-WGer}J&x@VldFanIOsQt}68 zFJ%A;W3MCN(UcR7I&l;0G}DX0m_P6oP>6HDz*Ft^VU-0u-L znK~2DFx4-=8<7n};iu&HSLzs7k0Qe0PnSa@d5Z}iQBDFI!*jpxq&%uymE&}Jxge`t-#$n%dZ%$Xm}rWKnr zC%2*eWKIOY3FJdI37GE_VtRJkJt2>;8K24kOTYT(PZ-ErhUP($a|8a{J?tdsm+Caf zzn(v~2;TkXa@0>U-B*y01^mg%%_jJ8ct}PW5SjPUT(>Ya7b~KAfDRv|a2}UKMygJS z)rj~(2<=(0OgZ9Hyg5I8py+6t`S)ICZO?RA%5q?$b-80sk8JVC@x*XUo;-P7p3`S& z0d{uCc{C0q? zGqB?AF=tS0^-|-`Tgibpc)?N6vBlX4)Zj@p_(%W<_MkR-SLnsDUAg3k3v-e2g9t>5O2$3*oXRx{o-JZ#<~9M8KJP%j*6+wC{LSe z9(o?Ciq$-nYYf_oYQUEUN$B_bT-q&Q<@mq*L!iyp5nZ{D@%Z%TIxd(~Zifss_(dx< zda8C38)z>S+1E9~$ip%;7K{L8L3EZZS-mYU+6EIleWO;IAp0 z(USj_VOlZ|@3d)Wecx8J9>}Pv@I1{3UqYf&y#{d)wV*+XPCcOd?LUZI>1BnPTPFYk z!>vCef}_tct&kv4#OVXL0N6(`8IDEI@x^iE&+VHd0Bt#^ApFfVL?F4NvVr}E*b-P_B92#y|i|6$5KA~&hg z+lc$@85DNR>Q2rGMfG`rf;%YQVZLlsCw7<}k?*D9*<}Yl2N5uvX@f>rK>Hhs!kQOX zmoMr5@gtA`7CZXU(HE3XkX#j)IK12Qm>*b^L>tRb`WPh|N#8G#XHtsTeL+*mm4rdN zpI!Sq-=9vN5QD5CgGC*zzX~~vZzP@j|h7na~3k(W$$rNCcHv+5iFMw=Lzm>z? zCv2F@8|D(VR3sYOo;#2+izww;ZVjLX4tszDH3YGoIRW(kb*SJ6JS7QPVBZ0|`>4i< zQF4!h^8o*CX%F*S4qP07_z63G3$Hpj`R!KF9pOlp%dFF8t6%gT`1VBkUm#T>!tUWj z6E<8D;T2!#;*scMLJOJQO;853CluN?1PJpeW|=tzy7H!@Q4RN?1&Lg?&kLa5TB$HU34y;Gq-Cqn8Lk(fL6H#b$FO&i_c^(BUrgE|O^qfdTfeRqv zUjibV6i}W0J-X;^7a6yi+QC+tVy&Mvu%q?DpN<4fE2rq$n?M9FDhGZSay#CF(PCjn8-AUO;_uPl=6Px>^0MYMAaFIm1?oH@byMn{IqhsNN)R9+Grj zHSzikHHZ0qj_L2k%;Kl7eKddVJ@M^px18bI3dl?1s-k*{T!p3K zR=|Enj);&t4AipS?@D+XFNw`sZbYS(4Guu5mo)(FiN1|`fTO?MrY+-BuOzH-3noD6rp7eq6eMarriDxd2%%(*R}Jb9J2) zUN1CjAzEz<>)R;{Y_dK&FgxHyq{a$WMH0OclFAggT~qn5+v5&@!wfW%qU-RJAaRk; zt;9^#Axb4tlO17BjwhUBf;amLoZK@$JmVFctW6(Iff{dOJ`KU}=9dPBh5`g!pagFH z#Jk%S|F9ZyiarKXn&|9(%xzco9@u8ZjCofN7A=1wq0@^-hYNE1pNN@C!tWVb$+|F_ z)VY;2(5R`LHfUxc8Ujf0=#%w;!WX-{6jz8BT}0>3>ar49V9HM}+CMC_J! zITv#!eq>pU2lxkeLj)-Uua)iDo%v~%R^lZ0b{Ue~qB0%EC$L$|dGG%H0I2k6w;zI@ zeImt=eV&e(UBg-ALpDmI01q@8o6t1})>>p>-ul|2^yo3h1u#m_uV=;n6zq^Y`{%z) zaY=PiE;b03PuG0}y)T{&v#kj=S2rgNXkP2`vF}q9AFzh9L_JSc=lI^|jhe{1K9Zj9 zY-0m@R@_woE@K!12j%hc%HHT7fRq`S=n{YsKuco4?VV57IoY+>fC<{LO9s{%npvu^h% zpBv5E9E{H6?`$zvLAUKIp^c=pyLSJR*|uOd$sG`e36EtA8Y7&5O8v zy(}j37W&y2U(xA`2&oKD`M0xzT897 zX*>?IW~=KTgyA~ujUs43X)_bo!?$JwtDFYyoTGygKzdJ26q ziCXZ>J!?Q&ljGX9afSIb;gyJ_qX(5~*~aVf#ElO5+UDp2Ap+9FV5v(Tq#g^);$O)a{nQZGF0koAH6rRvD$!pgfs>WpCunv}e!{mvG-g;1?JrLpYQW_E$wkWiFD4I|j0Z zF8hQMQ7DWK&;xRnF>2qsQ~Wf*-IFFYmO@h51FbkR=?ZSe+c>+Rg&s~#yM1?QwDz^+ zWa3p1!k=#&R=H0Ge(_6$l||WNKRV2wJ-gI!@7*`#rU%jLf$CrNLGB(L_n(jw1X=lk z62782z#H+H=*u9gva_@MyRNy|Z>SSt%UIL88EhFAikUD<;7K4PZ3yo%^RY}e0oV3n zx5|pqOG|%|;DLE-O5_9@>Mfs-hJ0sBGX6V+3$R**o)p#abgS6o4jmI#zI{!s$!!K3 zD1?UFPv)GiqZ~YVP}WD}HI8SO&_kAtfRl{v%uG1NXo9YU9k)e^;)@A`HhDwI(C6ABn$9&3P{$_G@iA^4+Gkol#OJEL zkBxC(E2MyC3tM_QIXPVhPzjL&hx`zdlBpux{zQV!{9FIMV{n8GCS}^9DJTB#4!?D} zRGYw&LubWx2Es~OY5E@7^=c~W&s8Tx8|vNKfh+# zLJ6FlbNZk7Q}3|*!dOy&ea*gkna$*r3S!`l2Rwcw&uB}i@7aC7owh!K2H#ZHR#g!m zvgi>u!#Eu-5o_=LU5}#xe!|)gZ>MX8dUTOV4>6NeUW!z0(A{l}3WM^9>te2H8DNJj ze?Ur~&`Xjsq`L~)EE|UD2z7-Zf^H81Fy^?d?Or*-oEauH1Ol)IxnU*C%yCdTqpOt7 zn>V|?gyu+I5ewH<#DdFB4<)l%^=t~KBdmKbz|CTKte|CsT-0IHjNBS^M+ICQd1^XJ zr-44otOEeUdZHxQx@4H}e6nr;W>6JC4*|)o`6X|Z-JBl|WHhgarWLAgT$1W%sn462EgxQ~`9_=7^ zp8sj44SBT8L#=eLmovj^t9p~aF7Nby9|&Vdt+mVavx?cH*qps!E+19m=OJ6X3<1KOV86tt%Oz= zmWOg)H=%aTe28W=iB!QZ6fJ8!L(+_uAP4MToiQ#im?(mVQzd@`G~6^aB%Med9Hqe& zV-W|(M$NK2Fl?;@c$`<^NZ%MWIHPI0VL&yIJ%ReoJol0wv98?3w%m>@B0v(Z? zC4h*yfiL7Fkzd2#t3%bA`r6FgyZ`{Y{cnthm?Z`0*^Qmw4GMA?+jl^+uHlloGw}~F z{Wb47&|n3P&f6GI@w_^Qrnn)Z?9Ai|Vx2#*og9i#0aV^ycyT9RNH_Qk7gg$P7{BOu z;viYe*5|F6NrM#DiMz#`D@ewWE0I+@m$eNSLw;sAGc${Jym9j;k6NvpA+c^q;kNJ_ z?9TI){pL9J^$f$A`1?AsKx|d&Kt1zn@4S~}7LoTFHIdg?Tvs@DPUuERZlgbWBtRCI zpMfdEgil^?!U2_c-)@8Vl~6YOW;%)Sff?Oq#)V^UJZOl0Cat=<+7)J`5nm0)zX7)_ zKp!H>hP&yD8C|puK{Ruh93d;OwzOu?3AKXWJg{hbZp%t<>xQfP1Eqnqm?J;q7coiS zxD~){gK*Wd9tf24QRRSSihP3oRiUR-l4IlIq){kIyuWMNkP#78bFMk57tvKUH2AXs zI9mP6_D>$SuHOkTG94|#9?;7|uNI$vK{}#rKzVYH!6Y1_d+S_!oc08nFlqhBS%jP5 z`WFUyNmt;9j%+rH-U+f%y6iZkp8c(Cba5k3^1XwDL%vgbtuWzlFg>-X6%ypicb^#r z*`^8&xh;4cBp%ahn(eWl(C4C9vt7}NODXq>gq*HVq&targOT3{e+QyL#qV+9&fK4( z{Co*-HPh4Dcp!hr%I_v4_6xHW@#t*`ENCbQjA`xdwjS;#jKi>8c7QE70b#t4Zrdyt zB=`V^NXii{XJ#RJ4dIJ6c33Qd(GTI|y@{UOYWTVJ(18cd(Wi+`W&e!J(5st}nAqaD zA?1@M3|lLC{rYw55g8Wb!HoyW1KZ{(t?ah@Z;5}0>FWxRN$#+oB*b`CFk6AD94~h| zbfMB^-#+3IyUVW7P^uJKPxs%k&4~mkOHSlL{0PL~D2om06hEi|Jxn$2;LYHSAsswKg zh;Y@pX0WVb{}h(&#z0rRR+|Y1FqpwY9>)7ad=+@}kO$#6)oA3-{&#Qz+V%AhkJ|zs ztJF~mSgwdSHW8g<4)Cj7i;0nH+4`6B)+i-$AS439DOpjB}Ub;ViYFv>HW#NEa4|}p=wb{St5m&VVqj_O!ils4 z)dB-vyaQ6}pSZw4A!wkT?bC+)BBG^^9P)W9`aqiO-uI_VIY`GDluWeb%5DQ(pRVW( z?9!!6&4=`W1`?$hnCCphBNhr>2AWXOC3|!-Zc8=(`0_>o1`W*$5hBz2HFCX;>#qgQ z`CNI7yr#Im-p0*y(-!>JC>yZ>ME{JH!_YwA1lePn0oR$?D^wA%O7Qffg!!8@w$@Eh zW{|f=9bye3CRSW7xqW*OyCPVmAEi-ctx%X#w#`4``grEvS$%z9`k>>Wz$Ev32&$Qy zXdau*s1abBZkXBtWlU*SIKWaLK+iSFGHuHYs3cleu2Er>S_rPKJX>N*s;k9v^mgvt zN$e}#LODX`RAZ<#Y5b@W$htRy2f`aeLyZDxN-4my&ki81f-*Fj(Lj3DwIL>L>)o`n zsp*@8s>*sipvj{EO(!va&DENilr$AudNmrBGaH5`Zs<^(VhaP!n;xG%3k8z(QC>KW z!{iB|hcxJem~7jS%DqX9hSA^>I5A3&1GkT_4-ylCyr=t@xy1zLi1#EV@nJQeUvEJ| z8f5N_^d@ML3(%)lY;b}zL1v`^%*!O>r>N~~lNPkmyCQkE+qaMRUGHPHd^p@G`+|n_ z=uEPkj<{s)dH^$Ihn%-Z;@w5pwMde=H8eCz#a_OANv!%Aj}n@(y0`!dnd=yhBhG@F z?81c$OFQ4W7EHB;Nnk{I%0Jm|vq2-)?NZoli4#BSyp$|}@TT-2Cv#yLi6pIr^Okj< z=0)f0InV;T$~v{;fm7f2Cfi4+gbHq1dO>JzSL`b5AG>&9EOx}czdM=MdqWUe z%b%HGY72x+ET)rl`!_^^ac{|zmd9y^yP&30g&TVf3{-pJ<7bf|hBpB6t*w>! zfK0gA#Og9$bfE9i522RavMY#&)YCP5nOw*F(@FtZUo#y|!)^}VcnaP51@5_>5gpKN zKJ+`yht*4L%E*hl>0I{>O(X2l+!mDuPTkoi8AO% zK7nP7)44K(PykA-ktu7I`(65cvVJ`_We(@N!LROXyu5zaFWma^1scKr0IG3ot{KQK zc{ALQl$10W^)C)I4!4GzE`{5QFE5g^IP_{8@$JCN2j&Lp;7diV(YSs#<$5;gN#q>N zIN1f02i5CUt`LAGD9cgsd9~q5kWXJz=$gHIKka|Q#GCkQr?3X02kp5(^Ywkd2cy9W z+5T5;x)DFO*3Y|IJm9TVaTQIgn+uq{;@2;5i==pRpyBqnl8 zFib!;r}(GR@OPzBflGBtiayEvd%nDBIHi!xDIlY_^7}zB^d}G+2h3ebY3X#7sX(>2 zINk7ku!hmE6<+?xY4>U0kOO+mzGBwKHB+X|;%v|PVO7?^9EjXlN6)Xuc16D2W@0Hs zcztjo5GVhJ6r?|MRPo|4NgA=rr-DUl?h6YGBS!8|-U@ZtuA2|5DX(`ZG8)#AACk4M z2RJ()SO7nB-;UuKKtD7suX9D70L-!ymDagj(mD%@=<1%%fT~#^87da4(OZAK;oB1l zt+Eqsz|X3`DXL%~SgS9%gEoneo`hZXm-ExNE_s%Ro%SniWQxYhq&fhh6o8!KOzMjE z>?I__7iSc{`1s%l225Uy8mVo8t+n)^ikcc>)s&O>ItsqK+(4sq2J%TvOG`_e&{k5i z=|hDA{dxnX+FxB-vN`ceHLyF)cFt(Jqzw6n1JUcBAa=P1{oyYC0!#N=p|3E`r{iwa z;Fso%dHP5~&iQu5bcFi#g>DR3A7t<3q_P>u#VJ{q3=uYsQ{d^R&e9c#XSe_{k^nTjYt}J> zyEb`KJruU4!nChxfWO(T59N=d4uqD2(a4!!ZZX>_`a50L_0MFYcu_85yMU!zsdj_Q z&_@^KDm^dkR_(1hSgcVtFR9Dwo7P~tR!Fz+!9+RpmVnwNdtNj<_504&ueO1Tz8(*7 zXn3HMQ>AAlReirf#`&xMerTQ(D%Q z$A;VetLdCKcv!9e??=JRYE{$~+u!)u9{}3xip_u1wZX51uAD0j76?^Y1p?&(R&4UE zjOyE`)zs7i{f0jIO#?=*&$k2h`v-uPRiou{)t`WuIT;OO%`%D)OaHSNuUUV&;0%OEko9>uucx%4LQp(}`B^2EW!YlIKR?z4 z`@GMHc^(bj;pm^+QGc8-pzUqCOk~4m>;A31LqGB)K;4HfGyF@fIr$T0F*LIqf?Ms%tEADjEg~C+lA^lW zwee%MXk4gTqvyKx&9x;(%cN?c;qTdGalH2h-mlHybX54J-vPb$1H{i3=!iGXQuOit zHbtvst<$&u&0RIfr9l_-j6lWQ-uDQ4Ei*^UVA600->~vG zBS4#7>zerz^M;lf=`iPk-f!2sG&82)9*{yU;{XZEe|UUKO!@`V0vj?HPI9+}%r?eQ z43kC0sSpMK#{j=Urj^k8D_;gzQ`*xL;43ZcjH2+LW;3`?Nu2ox7()a;`TlVCQ>&<` zn1>!MCG+zQWk7HLTRyeR>m}rib?@~wg<5Ma&^&o+3100;8la#=VQ1I3HSyrbCqrb> z$mdCLcSaISqXx*}5DL?4=cmnOl(3FrHt5Uk9TwTGX%0mkQ7vEZD{=({Prw^QH^r-e z*T&bxh3M%TLh}Z8N0tE>Ov<$+Yg z=A}iFjO&50DvaZBd);gp7Aj!*eztqfH7=ifk$52`-9Ox$ z+hbbfRU8|BwEb#uP5vD#!SJ2^;8{zs${QGTTvquN5w?pyv{7cu?GB3%6#=b71C?9T@neb#9_o zCdv^u=n2z%PczBufA#OqDLd|?u0-|QS6H?MpN!1o#FRG5D2yfNzB zw5&$8+=*Hk@#^QD~p43fveU1o+CHZVZ;5A9;i-b}J23irGQf9NWT+awi{)t;JuV z4L50M{E4ByhLB|)^u?@kN;2Q8J&B+tfXPV6Z=l7mm2ci5hif&NC)C>e8c-DDvyl--c~GTvUf8OQD`d4b-V>FMb?BS@o^3YdZJwSlt~rck$Xppn#;{U4qM7eJImvgWH>}oEC zselLa;~XJ|UEo;vP|V_nO3O}3hdStYM1#8p7A}W*DQg>faVa@)qqo_h=0tGu&nULT zSW~{3bwPyqX(rj^G`a*nX~>}*Bi#H$J^56}%H&k%+RWgytxgm}LqnJNS-7?c^hj`` z;09|T-O>;nYIW+yO#@lGRN?tDOg9L56mYhmt{&9u?McrY%y<|pZZGlHEvYLzIr;0s z#(+jzPup7oB`+Z9lbQ4OEe#=V07}|*xc@<;*sa-58aNo$o}>YW>Ib~BKLU10fEHDi zC)0Tie*~tmUfU3db!pyQU%FmmD$L^h;HU;d8-HmWAKgZL9*82@4UT#biXPn} zz}xjgeJ6xg>OXI3oLT>l%zCzC9FEqlUK)OP1m5iH*|p{>v3NqurWe?b zd=M_fG>J#dSAcFFJGh{}^zUi?$d$`0UiSkp=reJk#VvsPFCt+)X7H&7+^M3^X>W+ND2KjO@$!7qsX%>W;RE=4)uKs zMD5_O3^TWXh2*E$gh{f`)0<(^2N;cS0Zvq>->g%nCmJ1Pl3ht}zbdS%8@#y#kQhW-gs?9^X1>t;Wt@KDuFMl#S3gmfzik8wlTeDH)0epqrnw)jF3R3)K0+4EqL z6_Zr;D;Rk4eL(F|k#QLreH$kAHFFV%24libzs~Yo0j6*&d*3M>#aNUG%E{*CT9iSG zz`@Z`OY622flb+}h`NqyZIVnE#Cf|H64+p*!yXFD`b_SW(dygyemANYD?{Ps-$pO- zDu4~XO>8`?e_44h5Lz@f--Zfm0GOr4E5ARLYJgP7a?h1L8@;@|Omn^aE<_>K@7)>x zj@KI4x(Y5Y+PNeCBGXUKlIeg}i$G}0ob7(=s>qvt_pdfQCQv?1A*azvuua$;fRuGN1!C+3 z_!D=rMx5sTlkEu?fUFv=LOY`?_Q->3 z>WO`PgfJwuMZ`myb{nYFZ?U1a8OUil7)?dZ+`tBRYC=!Hi3jod)WFm4cKQHlJ9$TM zm*eTqwRAZM1&9ui%&ZrS9BkAYYNV=|tT&&Tw*|Spssz;3i3B44L>(QJ-8?mLde`9Sx52HaoV)=1&ggqXDVbYAc2jwBZ!g&M%4PSO~p7MnQm z|9~trZ@+qzqU-DJd%MNnfLDrIsJt}V*B*dTsLq$3Wt^Euq4J@@m{!G?iu!VD70wB; zb6%WVQrR9Bta}$`+knM9a>up^G=BYcHc07zJ@f+2i-4XePmOnPPf85;KFNttZ~2Du zx{%PWewVH%zxxlYa7J^h`^#sCxAy-q(bawVEMU1lC^1bAy`J`zvf(G+P>|#Y|Cbe) z(zkiIM}E-Hj1SCg7w}K}`Gxov_xf233SE++Q0LK?!UYpTYrQmN)*OiZ1M}r^b93wR znudrhsaoyb5F0$ zouGs{JbQjRMG$xxRS8Va(Jv>?D1oNr*Y~`m(r>VG_crUyD?g^H)fgDZr9Xs%Oxw!z*SS+nF$Z%{5&bPk>U-gK8gn1pwC$YMsGRt$FuAXkQLn>s;H3OUtyso zG>^^gbgq!!Hax}&v~SSEI^Nzf9@AQ2lpnZW%4sOicuCx~#DoNna)VH{GjKEApq4Z6 zwg6JZd^^0BI5*5_gdX@iy*&^XUIa=pr*-nf1u&<@9o@D1z}nGc^mQia4!3K4@+N$} z12|6Zs8xYr^nnisnqN;sAf0$J0LGHP-O|VR4~tj)c3e{DoCuJN)T5D;pO!#+7YR+v zJ&?Z1r(DIvF--)S+-Bw zO^K~EeePEmQFC=4Zk>801MM@oSw;vkk@sS)vi?RNol`_LI+P#0B5SqN06~;j(eOa) z?u!8!firQm!|43usDe?75`|@2%H{LC;zlBR%nK|{_)gE+eEDyg=V7I@gA+Ej9BotI zd9o$HJ?iY)AGO+T?WLzmq74_Ew6OgUz%p$vm*$*#Jg?IBTj+L(u&`KXjW!h~d97;3xHCuqPqGizYLX|~Odf)!1d-+L!C%AwvdQ~~I=_Rnx?(XYMo=+Pr|)QOxM zElwosH6DBQ>XnRLXWd|6)==PWdHe4E6UTPxy?X+YOq(RiG^!Z5)5Lxve^K6dd~*i# zlpE{`oiZE6aexDK0kF3KFnfM`)AN8GbGJn_5wGAY$lm!@IosRYTWiy$Ey09XaGSx% zIcR?s(vH?EsnB~WfO*L8qcB^EbWz*%SJsB@j~>TVDBfIEX0Wvn>7=cG{ORj}HfFFS z)@2?NtX?R}Jg;~yd8|e!3y$k6bhmo}d~dyWW@TT2(uDl@Q0T!C%ntm7yOfqdd|k5=x;uhM2IGOkshd!%Ikv-AI!V7gxpg zip)-u-Yl}&-TR|N+*}{35!av_O-@uCHOpunQ}=Oil7ECtzfm4A2!GvuuaK zyYKBk+_-TA@hX0K&2M=X@XifEwfbQX9*E@2wL~Gn&|S;gOh@s`mLW9mb&2a}SB8-- zEj_=;Zjgb7qcdvmFM)!$=X6ea>9vID^}L0V802z4AD?|RCKWx^P$rn$MP>&m8C z_&+yAL*h`6SBqnVo?XNBov&R--?+YH6%O*YkaYpY-Pjt~qege^>x9r;**6e#-AHe8 z9#Y)Z9Z4u|)H8s=47|d&Gz=+)@}yALaeD-ywSRKu%9VyFoMH;4R7@H;j2ogC#NFC0 zjC;>0+M*~(Os;q5KLlaV#e(tEX36ZyfKusPr;uqmA|D2ectjw?SB9;<}U^2-aQ#U89EO6QLgKy+#cTwfOlj%v?q>MPC(7d*q9qKHrJFcEt7ZD*Yz(qFt$Qg* zn*mvqjZ5tSpk=Oxjy4EeK|-M8*;wEAkHRpMg3q6m-LhFkOsqmtu>HYZ&*KlGbYsu9 zRh%6;o$|Uru|*BAMKwXuwuU)}Y2-;zi3t!%ynwXWb?O+T)2`cly`VEhzr}9K4`K7C zVPYdL!NPUS&;#kS-lhED#aEEd2?_!~j!r`s>M^5;zfE9Q8W^*nBm|Cyk{4gP?X71F zm|FGxvZa9Q0zu@gYxXHCD_1$RL|eU=dD9T|K?qf8Eu{*a$_D}7`1K!7#YQj3Bi`4iszlgwyesSX=6$6_OYpwDml8$85K&65k7`6`uoheqRzS5{vSs9md->Wkvgb1HoKQ5% zb0WL`c)ohSy60b%#moyG7kBGDyL&5?m21~Lv(|*jCucKw*RE`kn@0M~BHOR`sodfN zG{}E}TlBV3+c9b*xJKbm99w0Yp%8IWCG1IA!0Ui6xuV7Nw6EQNseUozCXwR{kM*00xPG%b^wl(&`$b7yT z>BtK0QA<19!mBittMR4$u-Au;`YaU4-tIIvH%DnI-GsoQ#bpSH2*?+o{m!cM#-O-w zs#+~}e`FN(b{e&)s;GP^+{#(q(zhkuyDcos@Z{|a>4OMc5+D|L=6f+*^Ohxkx#{^wj4T(* zHRcCb7Ji!%dvv`2Z1FwiA{d%8qs0J{A8pj`Yse?x{IsiaBFm}HWNt9DLT{qt_<^MG zNsfQ8E9Bd3kJK2SLmMSZ;$MI2DllaTOB6vR2u$D3JybOBCN?K3QvRGs6dlR^T z4g!rD3tgz6L0vO2>0L;o2zVMd)SaJxgUZk{xCLpsE>QhWU8j>e(6u-+Uik*h2_=%{ zh>OlRB+pdzmi5$b@}Fq8;<-iklQitFIGuK`t>58;n*uvm>%>FJ{$0?v3<7C+L(Mb^7|J8G3ul7PQV7>@*p8oqvG;+4?j(J`>vUfi+&nBx2% zQ>79A`s3H+4TXcx>{&i5Q?bdwhx+#lhDZTnjf%d0YNy#9mw=hZq2zZclNje^v6E| z{`nwFs+)%ZfjbwU-IxD)3;QB)Y^}1V%{)@w|MY0y)0q9V&Q*gSmCjT3)+*&wkDfkV zO)$&}MgDD5&m-*6__v2CqI)A(*sedxq)ad`Q5N&)thkWo z{{6mHnj`&fccY?KZIq&Ye`+|3Y|E4%ycxXRUJ7clHUb`phITPVhOFYoPeV2`QahoD zm_vDp@}8N7am=XAR-o-wxn=Gz4beyNHi%i45)aqp5;+m4X4_YrL^5VDyX#?l(4 zx17j}Z`iRqt3$`gCGl4nbuO07CItPy@hfMw+8TjRfs$}j)@lI0;fH1~}HuEZ-1OT0f zm=KLnkri3xdkeSbwvB!>9zVLs`t4GpvN#X&1g@d_!Pq=O2nnt- z%Hy|Fz)}fYjsR_j*G8Mh6%60*J1J|tFP572fL&-zKj5`Q;mducG4mj+*9$h9GN0oW z5f%MBv~9WV!X(%h**9F5F)Ivfh%PHDV`$O@4NYLLx=?x0 zs?5++asU$ zYFtBoAH)4^fr--|Rx?>Ij!Ud0sZY_{)J?aS=v12MQOX^=Hecq&$c9`8yl8{wWhkRC z&Q)R}^zdSg{U9%4*&Qsm7P(VZ$_%r1`n)0%5^6Q$2j^&J`9|{;G;hs%GRK8yWBd6brs;h(AgJx{{p`ZCkTLPMcvS>aCFp zOh@Yv|5P~11z-gPF|R@B0-c*D#5}HC`9jT621O?)Z)i#<=b$*}c(TdG$JME4E9xG9 zZ-om~rC(l}v;5qKN(a{}=!*k?dl}&HQ^rO-m0YbPdlY)=zkGdt&0^lQbQpfB+wrwC zhqEA6n|8`&VLvmP*V(W#vp*r$ZD8(=AtDJl=B1!)Hp_nq!0tfeDn$G5&pBIJVYNV9 zJzc^AAb@X45fKr&(@W88weqJ&RpTKu>do4RHO;@2KE-(@>K#jlV$s(U9peu}OQ5E6 z71eT3LbEZbT7Uvw;Ic5ZIXddp7wS{oQKqFjZ>(i^A~6uQpI(z-_HD*iG^0?t_B|s{7fve zj?*X{{Oevq8QM?QpbdttT?N&e_FY+;%NE>)bh{|z(r%R; z0=Y%ihN$E7Xm9CB<2ux69Qf(w1y=5Wn|w4N8yeI4^IE@kMgGmi#!{@H#2-YgKig26 zX6C$k^NM1EC&n~p@L-Og>JYtUr%anZ<0UlxhE%Ekm*3XE%6n*WF8`_IrO4bV@X4O; zf61+JHFRRM$*TFz#dVq`!;*avW3{L`LjyTV=tQ*)?5~H>Z_kXN2JGbt;~9_9yFO-> zB(N^I9J3b7$$A2ghoM7^QRU==EQ1&NUf6}4#m>e&xz7Z8&!52TyM}5%KRZvA2GXG~;2xE?^ko>%-)MaV<}h8~H!ColT<5&*Q@K(}7x^Y^y>9uqADTtric6^~3zHFvI zDk&++4g3zi$kl~w#c(|X!B8d@RaKwLTqlv9CY@5 zMWqnPa39gC9##SRiP(nthrs32u&=i26$HTo&b?ZtdGLE#+#6~KXCC+|o4DMH%mTS( z`)Xf`otzDxDGCxhoT`$oiogrN6$gsbTy!F!iWDD4Krr+g=3@9zlMFZ-Ebdu0G>$^E zhSi&V;b(Sdpn&}n2HaMgmBqA;>oy>Uf(^}K|2%E(_Y(5plk$WFv2@FDqoflw;ypEb z3hJ&aA=X)<#Q{ezQEgEjs0dGBX%lATW@5A4Y&*l+#)Y)%<#)I@VTb>#b@+#5o< z)jEuPBXGtbG5HmWFdC;G|CfK{%7dD(N9$%PAqT&4+iLUTd&z*7vUwN(`b&6t_y(A` zW>cXDDN6Oe5Bo<^RR?P6{(aUUs3#o;4NI@lHUctzaRrda*UzS*YPa5|d)49W^`s5_ z_gCK6z{&V5@(@!4nMNAf940DosR)7zaF41v)6UV4+Jp+8PMBzi{|Emy_xsizGsV?@ zX3I%%usabR;(qll@RyGI8Q$6(xt1>!2p%?`?0^RIs|ZbbfA?Mw9cX$}8ZPpBY7_}= zsWQM|9@RpZ5;&1sZ4!MxSm9^Z2z?C?tP@9UC-gV15A&dPcPbw=Yy^>*F%W2+QCuvn^2k@yx z?aR0x7uZ#A=G8f=s5v@_#Zz&(83ml-;2_>N&=*kzfU9*yBN~GO2*^B9AH7k)iL4BP z413g95zMyFE%C>zGCZF6bhvVTOZz>Y7&U zsZX~0`URS1^Ud=Rak=_G1=f0_6Ia&cltAe^CMXY`D}&WLIz5i8tR;~gptTkzzf9v zBebPjh`mS#nni;2vY;jJD^w)kiGI9wWz9cd5gMu?{u_8MFHx`eD?khR?UfAt^8R9Y zElhl}Y6`YJws-t~0OFj7SahPdoRdZ^kjL4N%!rCuGJWRktB__=Bvz5vJWqPXT>k79 zjkEZe7hwp$LJ3Kw<}eC3aDf?_>_PU8{#yI?@%VFkeCR8WH{XlKW&_QEx5YjM%tlVi z7n?TXDnW|*SoH+xs6_x|_C%!QE$_$0RX;oNQ0xN3g zRT~~Dd-v{LY3Yrz$wgseCXBcEH>^hM_^rdK|B(EXwnf@kF!GwRFJ< z)=fxm4G#anp739kb*lABJ|C@!rB?Y+kn z5si+VPTGxd+D{-axx?=>6fDz$ISHk9UX?*A3p$uxuFD!otT_-96%|!9B0714C(pQB z70QhgeMsFOf^_*qtBrU%iAEB@GZB$2%uJveN2?b>EmY+PZOnBPmy*J-fs62U1DCdnZ1ystGATNEUdYC_~u-LF=y%9k0zC z>*o{|kTxOrHWw|QE!P|-A}Q$!O=_~h;+;9x0xim(XpF{pMdJ&oRlDt<>P)-VP1u7J z)SOQx<3;TQDW5NM3lp^nI>;h8x1YrdOSGm*MZ7txh&2T7XXL!`T*(F%e;2%gN~O4U z5lyM7a??xq)E%MOZ|XutL@3)mcwa$Uj_py?r(-Bj`34$_z5>6MhT*cr6QAco=hln< z*oN&3Sy5N8RmpyxwR^tN^o@oyp-zy$eFWkuYzYE}@O;pe6%W&QWAvN1Ta-6r5cP+=E_YS2=G zn`#Uzq{dVFAuyi?5IIY6gD$AGZQ(iqu!5!WpQ99v5Q?PwY}t0mjy;R1y!i^{j?v7R z4yUF~YPvgURfky)328d$h!9Kp=MMx?9|@w%_*p=kPZ90tT~s^;G=McM_GJ_S^8{og z9Fx8gFnb`PQbup(#CIUW;cWWE_{dOAqvCfO#$H4C)XCgOYV=*&=N1-Me1ITG;85jg*Y98uFC@SP& zBPUB+rdi?%hYYPC`ZEDr)2;M9jkbO28^7ITzvLFy0WgY5I;w95;!+A&rr%$Mav zWugR7Pz0n0f)O3-p0+TTpO-#WBUs2r6-HYvW8+Z9FE&4}wAXa-{PfwE9>=7Lp=2Qt z4`$8*79jKC@M}?pkcUB(grmb{S)S12xvlXDO$QT~$O#Ix#=M3>JU)oa>o$Y$nksh4 zmTO+cNFw}K`2=-J9YUq#@;`j=06)blvX5#bpbO{LCu*V`fpt*=jggILcm_e`hk@39 zK!V03f4MT)xISwEq@lF{fR8{O&#J?K;=l^<;Xt(&G-vsatCyB2ILKC3@X`XMAlVH$ zuM;)t8&j@>JH*H$W6$v~@P$VCF&YzsRkC2uLB5AOfdcSr)U$jQT#6x)1CwcUg#XJG z?Ae?MG(Pq}K^64w(9kOH1?6ikYto%gJ$dDrH=5ZD6JC?YEWm$$V%PNr0pk)~jS*C9 zL=-F_x)?HV7ICAR=6lC)P&W7=M^|bTSw%rjz3WF2=*4~ZVul%_DLMtM%p^O&bvky5 zVe?qzM>}(J(;$YwrXDQEz#cpzH##RkLGBGuGTdMR(gntlH3S7g@Lo_F&&I@`r zO2R_U;nk!4j}u)BMxlkXu`TD*>OTKF$Dz)7`boTcT8j_~w!c?Q;k8TG!%o1BCp zr%wRXZ3Y}-ch>v}rM)UYz1{x12;{P?=NsKJWbc!mot;lWOmCn2{`^`x8X4M)h8_TW z7jL0*fX=+6qIypEG6dFxoCmI|qHN(qWvZ2^IEMxkFt7(SBJuMj^d2KHV0S5$-uS_1 z#m}hIy&%_HmicjwwKOQv@&ZJ)m%9mN_j+4DufbhMSRpgmA1)xmS3UM1HWuISr2j(d z7L|&i0tM}|i>g$`N~X4$8sv;KR`(W6J9o=BT9 zK!41orKcD0J`QvXbkN!p+!(5C$FHLR3#^dkckivUFN4NqSaYY5Tqh_@Rq)Nt#IA-i zCW!K4Tis_}&f!#YQfjS`z?jUR4*_)2+w05kLSpFQLzD#u8rGjsGyKD+m0pO1qYTs5 z0lpzHJsKIpv~!9TVt9x_nl-~=)oGAi*uQD;0OAevO9^pA+mD7BcB|zgfwm^#O>Y|OSaql!{ z6RBcrvJq3tJ4DGA~m0CFk*n1b|#72 z!EhQ+XgKX~S(>*7RJN`*Ej=gpxtKkR-izG+>qGqIpAG}R^Fu8nf&Da68Y{>I|7B=| zohlS2wd|kD%HBZS;$yiF;gN6=h9rr zB`OeW-Q9ujI!s;D&ri3Cfx3uUwuDAq@N%JCduARaWf$HLKBtGTxhM)$As*VoYs$Ck z02efja1bW&#yg7;3*7ukxu zsG^R@XNM&Hm83<*#8hzW(SmWN6a+vR%18qChgTq*1L<)zSXhS3hIG0w^*AAa6q24t z+{>jXAmI?6*MvRAY(*YqKMw&MEZ3H?$&0h4C^`?n^j^BP->C4dFX*@(Vg^JC+CO(F zdN0af+aFeN=)Hu%T*PW?EUn5A6e~R3K*P1ue+OalyMF+v!~+pK;_Dzlp8(c@11j5n z*LN;4uY06A_4ucr9G(6MJIjWwG&P1xff%kC@;vxLNT)WkLzqBo_*}8Uewm2?8`Zgf z>?NR7BXD~~3m&G}%pgp3dMUMY3Gj!dgKGOQ0!FvYDMA9_v^1zB)T-L;Njx@Z>R$;r zu;vIYoy`c@1bm<_Xw+X3Yrs%tgjrFRZgt-V-OdZ8s#Lcd8de*{Y@kUiVxMR>u#KS~XDAYhGGIBRy6D3Uj6slwV2*^~f78e&YQ1aK8xK92etua&=rRA8 zUE(fv4ThViLK67&^~YGb`T0Cq-LUw0S^I0kA$H54t=O9p3}DJv{(g*vDB^rX^K!eP z6_aSvhG`)xgDzcMhTRYH8a6Znq6_F5R>U6A(z=dsO9UM$kcc~CoUZx}tj=MGp3;K) zhy-gtUpHQzil4o9_It4q3cv`yZlO2i5jegl(^#zltuV;U+9x3FFCsI=e;O*$hgAs^9Csnb~jXh%qW22zsYw!_Bkd95em` zZCzeyCI!CgrvF~_KYCkCx*KwHlfFZ@K@bdTz%SEIUDi(}e+>*@DYcDhsyYo@zyy$) zLoKDzx#EmkQ3aJv0OY5&XU~nBA1Xe4U=CB4ThP# z)J}3>*xi+>_6H)d4n%_52Zm8UvYQbY*#iN#V>KCT3PYsXHp&B+A#$Dw8d<7*w5n2o zqElGPc+Z}>)TT{5Di8WCe+z+s!ik%ou$w?x$%2S6)}5-loxX(;{P!=)87h?dBEp{Z zG!iz@(w+PK}nvX=lFQz*j~89?7qdGeF&_QQc&90cd~z zkMhYbzWJ!M)>0;Ak0v@2&50sKQm#XSJ8J23E3>y)HINv9QINr6FH@V(0D-n5ayi3( z8WveY6DUgLdi=5Iav}9Q3W;N7qg23n$-tOmqWbp)gn4<|DvL4F%e9LO^;e<3J=R>M z9kULo;tr=MTtxx~UNs|ApyQ#!6bbdt{ybK*Qz*ys3iw@p{Kg(<4L?TNMS>TAoQUq6 zcmxf3-wH&#u;SkM{O=H*&k|6S$5*f9kIZOHHk4V_^~^v4qN3ToDB@#O%lSw^mv^f8 zDU`vE@4*{b^RL`b?ka;7H;8x-bv6J-u;KQwjXm^v0t#@vKzI}nI7RkgN9kd8pf7D0 zDg86$Mz&#FWt=IGFyHA7aR+n0JT@LXPIR*+VY9!5N6@5V8w5L{PXV2u&8}7hJtL#c z9#-7MM7b1INHRN?d&$p*{tcozRiwKpjdPQsUUwLv+ptni74pQP z0Ll<=O{3=;R)Le8e}rZ+f}2o=sFiI%(|0Jl5qLfn46JBqZm61^a*KC9w7mm#RCp*6 zi1mo>6V(@5L_$zeG>6;%At-1i)Zj%_H$MiaGv&%dOmTA>NtY!ZbY>Uo3-oxW;0Mhy ztwVp~#`y8cWCS|{7a?~bnlXruX0v|2b0Zh<+gSVDf)RwO1OeuP-i#Ius^f@OLeq`u zvo{C=Pjpg~2VGwZ+N?NZYSpqM&E)ytsGLmj5>YjR>~?C?FjQn$NOeU>Ig9S_T;xx5 zqnCkrTnU&psZf3?(QV+|r(VC3VXXM|>Y8xqkav_i?MAPD{-W^mAtqR!dGmy-Z4poa z@P*f1ImYJc^w@2>;M7x zWvD5%)^YQ49YDbGKRoS^xAt4S&Cn@?AXh_)aDV?uwt0}3v|eZlq?Q?!3F!QlU2~vA zYP%W@0|G>=5aFX}74oRB(5e|ktF3$#6o8LFy-yssu7)Seu#G>Nv=KYWwKoV@1F#UH zF8)K%&=K!)u^kwc3}g;lzo~q^e{t4=xfd6oJMh&_3>(n?k|HZgv78UblXJL&o6ipRmmZcs9Y94!!ue$bnxM3M)S4+f5)5Cj&KN!~A0tapB?1 zMh^MXM>K%odyoyexNxXD0a{ys_ zn)kU?;Fh1Csc{tE@NEh&;LapHAt=~16fz@92(ObB>y%IK4f*eEfR6Hkj#l3_F0&fS zp3RU}AgFlAjCL9nhlAgm&q|{|bg~xuOW_t}0ke)0FE&7R-~qisM9W}QD=3Mar2Gh{ zqzVtoRI?--y6#21bLBD zz_2EQfGX-Yfq#amTW2n@6Rhm6{_(5)TiKRQQ%qoRclA^ts=9UK9G1oGts@FK-a3)3 ze}Ij*p1=CYE_@-Q+~P(B_Duh27P)8s#$6kZA@Keu8-@hf zJgCWtfBMvVz$YJH+Gz}c*=8gVT$RySfpBF;!*o$8jsvdTPKJeVSSli#Lvs|n+S57` ztBzG)ruhb(rT*L$Hg#%mKZ+VNH2}N3;m3o!_&6=h+NdHDuoVQNQ8OOcIj8IoPkQ!Y zF9lwh1GDSg+gS(bSZ&dt7OrD*=M-21;D42xR@4fFUvIw%Y!qt@ zFfhlAMiOQX*bh=B3TiT3ckZw85O5`hlSzjj@6^{Kn>$LrveDM5Kd_y7`>{{j5xgnP zpM6|cZJH)!_qcRP@H8ep-|(G!dR_-AY`TgconId(k#xZ+u+Q*hG)Y;}NBLMm`qNt; zM>?7^271z$xE_+R|GhygRkEj9a0|65OxYE9=VB`3h@CU!@RbqM^8haU-b!r~81nMH zRNs;#D_$?!T_4*eh-`*uS)0;~Ojx;~i?(=hW(#-zJQ->f%|76W=I;`#IEr=^G*)@h`sxpsXe zIv=4DaxSKFxzR+sZB`(N;*Le8gyfg9gMEWZbkU0tY@I+tO4=#80zk|!S`46Al1$e! zgtkR`BPn8!=;#+lCGGr`#@Ox^NIWzkxO#-Obqyk3A8$2!sg3`L>yB9zCI-LhxH6ru<+Qe|~!DLW$NPj3|LDBbLpI3`bkLYiI()kJi>% z4CGm(4hBoHWuGvbm*2)ighdHTDVcuvf9+o%wA8%ASWjAykgKUNl(Qk@yuHOecplg=WeA z2))Rut)Jo0_uvgb&WX}nSO(Ie%8rnZ>6soupfLB-l_|%_Q2$f^CcB9l+C*Gs}_iY$#e;!+dy-``k(^Zcj^I@*-r0N?& zs!#qhq(Sb%xY<8ZeXetwJ31!vk`9kQgDC*c6CfU=(RCjlP_%HKJjun_;xaIK@s0l|WvR6&M^NcW;<7m&`o^TQt_EQQ^qKBvMXP#QJ|`IWFd zLc#p{=Tj;+hXGxGHPj&MBTD~Fc7-)Uc0->D$5up;MwNctyN|l3P#5lC^H^G>m zjTX*(n(BaUZ~b-8KZt5d`sHvt0%hz+G`=Z=uwPWMH!LelDZMi$CMF&lZwM;xLNla! z^~!r_G%u5S(1zjU`dnR=9ND))d1cG6T5N9+^Iqa zoCRNzUs?P$$sIYMYFP!_B_0xPbg@iTm~~y zvF*4cALh|KdrSuZS=r}wR99Ogh9(zG-NZ052~9T&kU6}=?sb9U$O(K?P3KyHu#sEX zdj|$C@@<6b%o(t5gs&O?O4aLt!0$T@Mq@7+u`nLdO9j_72iFi#4zVKqM>TL+)hp}# zktQqU{e67bVT`33numpdd~~XbTNF&&@^wmD@4sx@Qj#C^5Q#8JoORw$cM&VpJ{k| z9bY5ksWVFHTq+wUIyR$E|9x!q=3y&ri_e|*V2ofE_L7uFJ{J_nx{kGU%9q6$1|lpZ zqje*K6LX*gxU(IlPe2-uZ^t|hPm%rFph5?deaL+jvhZm?+=qXWakiNiw)ts8h972| z1N8ov-mN2{0udlKIAUM?kua!J9p1>#&rI(Fu2Ps_x^wp}2!&Uw);MB4UUO#k5P5$@ zFxWYY7(x&W41-)l^it%bvwwbac;~`}3)u*2P1t-hj-M$bn3%^Qb?iT570ez|ui8^| z)pWaSuv3WD#5)l=AYz)3&w{aX_`Yn|StgZ;J$5oMPfi_prWxev>GaXhCCeAI=qb?Y&DF4hzEL#NW(yww?_W}X)6-2h!~~WVz30@1vu41}SNDhygsg^zCG`+C86713-YC6eoHkG#AW z0N?Jlx3}YO1)VaK{)~3Fb#bQ)#;lo%QR=6chNF6e3c_pRhX{p#lTg_$C+ptI?zPg= z-iemAOO)3VRb-l{DG;Y@YOBfElzs)+1R`Os^J=1cB9Yvhv|g=0DubqRT9%APiGK$+ z85SjbAeJ?UF`D?UG`WS<0KjkN5FXdN{aKpi3#4g7fYpZEh#awpm7q9%5QM9Dten{f zbG6{q=8uOdD;VqED)F(vXuV%&Wu`^3PFZ9GOg@zal|8zGSH#g|^kD?W zp>P&TJh>Z6&%}ts?K9MH74c8Wd%3amMIw?<{G&26MAZzVDn!CQd(j3id<#ftRN?k% z%2Pted+&co1gAON#=vzP0ArBwLs&ZPWHtk(N^&G`Psd!j-Kp7#<*pZq=Px#CF*28w zQeHI9CSZ-Jy504X<7@HjG?;Vy^(|p`V2sPweqWXMcx4dX? z^VDx+GY-20veC?8nAI@HW#WHm;67$#;;sWB(H1$YFWU%yG3+?miU}pUsCi=HDu(%b zI(Z6EM%x_E--w;|?XwL0%Fr1zXXbR|KV5}?&Ssp5+r*vE0bt|t5PcE6I&hwpFDwoA zOF~+q0{RhBM+D<1@*42XqW#b17+_40$sWKg9Pn`Y0Fbw6y#%p<7R;|FwF5T=P&ttR zr*+fO|1*(T+6hJf2s8|=KKGVNL@$i|TZ@0E=_Fs)2i&v(^Y!nsu*j7Lb-=8pK6CaW zmMj>?xb{Dk?+DARi7x@oIthB$BIPi;hVIM$JzJ4WWJ^m+GiCa(r>=ePRfiDS5VRq-#u7vIZyBmH6*2=>*1DSjT z^wcXN9w(Yy2>WlAKU|xUn&VCT>e4{ijqX1kp^|@m`N)tB2 zXf!ubsn9}Fku2VbMSgJb*-eZMd_?7%juhyj{0P)H_)3S;&-0+^d;&C>21lsjZL7+?ymFI zF;b4r8h02Rf<&isaBwibbdnv}A;%}34Rq8WVL$`HGz}7v0M)Rhr1cye9B#t<8F^Be zD~0qec@2V0`P;s!n!E`=#cC=7yA_>HgAGvyzo5|+{S8|^%%jOZ%YBOLR8}Jp^hKcmfb^hMHpyC zG-xy)bg=Zc1U(Ld(E(CSODG7^27`{)$ln2E@IAy+%-}0%UtRVYQoi=K@2_F5@@vDN z$`_c~2U9@#o;`DhLE41Lof$OS{6jDh%iPS2sLn?!aU+1!Vl9?I{8Hk97r#s%3X%ru z;ux+qEQB*)7PV5N&~DRB#_a#E4_9b}g-kDKy_D zF*~mycHBry=N5hZD}vcVs@ngI&|R&Yj<`^TcT(9&pCTb1x(cAq9JK!*M{(WJ5BRaXkzn{i~VvX(PLN*x9 zM6sHKIvY_QLP?yF5xd(62*Ngg&C7J}&uV1GS-Yn>I1hwV`~7N+Dpi zP=A3flwA<~LTO@`5j*cw>Mn2nA=gX=F4c(qmmy(Gn28IPs}~Re!ZHow5;q+ zgftH+18hhD;?N|hn>BgB_{}?d|2?)?@PY)R5LLY7yLTlU0~NL&&{#6CrgaA3(+?RW z?MTlA9M7CL^RY|&k5zvV>KyGUxQ_w>`G-h3L!T{CAX+#AqKRxe8iIK~7q1;m$oz;H zsM^JSRem%#GjdphRKxa(1-F!5D!b_VJRg83MDfZ7SK3GasJ>7Msyi_(h?7;kYBYH)H|u+QYH2Bjb32L(Rx33{H$rxBh!CLCY=LbZ*rc zOxd2L(Of&;R$JWZnu&26{{3U@bK?I2^H*`uFB&tb>g1}^z_Jq$Cmlz1CV;J!(G_gs zMuRZ*QmnrXxd#mxMQd;Ba9&Tyn!(M05qOEGsskW*rQ{}jlM#x6PSivKV9{3+4>w}3 z&f&RK?L2?}{JlM&pDUVm>vGiMse(cllxTBl)o>mDcm`4x1n&?!;mb%rk3Izvj({NV zB0=0qI7(BPZ90~Tkr?Tfl+4UbnQIfRbpUQUQ|mn7+<&73ck86vNig|!mQoK?(9K~+ z2|??N4xM{>XJzg9y0*{Hgh!vIwWo?30>X{p=fljvqZ6<2^q0bD@2h%sFo8;K0!Hqx z{m~UFkPH)0KYji@G?eFpsWeP}C_9-A5g|*!6pqpNdeICjr&AT>q_j%QkTNnd&SpH= z^Y9ZvABhL00k&7l^kBdc6fFpL6@NT2<-dLO^JmF48djR0SA}B5C*qe^Z~AB0sn_f< z*nZ{7&rgL$!x7nbH4IZAy1$pu{(gz^#}e^f6*>Lm)5Bgg^J+u8!E^}cyicWmkF=>i z7mU5UzqahF7y=0wYy!**ASROV!D-p%r(^U& zT@dNw?8Pd5qpR!nPo)S+9Eht6DzK%cn*O==Yg{6}D)nywRJ?{`)+|Or49@Au?x=%i z6lLfw;#87n=P1bG_v1Mji2nlBayoq?$?*W)+?f_zJGsgV}0LIr5c4d+@9 zKzYgUPvjV!FZ;pg(D$nfm@T4p;O)Gyx7Te8s<3+-g>bQ^8HxcOkP7$?6(If(nul@r z2Rf5tNbG!)yUPr0)CT<%^dtxd7jO|wMww?Xk-y#FUd`{)IgmM!aRy$PD86bLN({VB zGi}GnaVIkch`A%mVDd40*-poUK$i0G@nehloNJgNsFQjClrlmr^*s8MoMHALdvAY# zXSh|*S~Jv>-{J)>F=lwg}7Bdrl*TAO7s%z zq*CUcNI^l#UWJLsRilUSoiCchrL3F-g5h)Gf;mQIk;kasGR!bLJ-8VW4xz!y4BX`~ zBMOXmzmHtqC2PQG2aWN7ZX_Y*_@+2H*@J9|w0>-SAd6Ee9=e}UyAV;6(u_>X@azC= z0ey5onh2o`!?5a&U6-`1*S!@10MsYPUSqWPlPy^CdCF?x6K@f5A>>U_(R=8S%dv9vC#G>raet4zB8M_oOXVP=Hr z>xI#57X^CJ3>)N~5Dd5D{|5I{?;u|^Wpcx){z@?<1@}VUO0~+V=fn2^ix^f0I{vC3 zSz%_>;*HiTVBMDKkzY14k_622VF$Rl?11k7BS5A}-zGGkt|AJ^h+YQ9TKltd%cMC& zf=v!&2busOWEAE(6|TU~!kPpzLsMXCuWziA&?xkH{XsjAW)?yaUo-$JeRBW>5f$M5 zkQOKv+ho3Y>ZX|4AYVC-3`qLn=bSE#YUA@Qsh<74>7_5u5QVCIlIR5D`Fp5YYBUIq zZ+8B!YD^UUguk);?4zSiN*Y7kGL)_6hj$W-{5!*QD*!TYV(F7<2d)Vz6neUFn@QmT&Zor{Q0{!ook)nA!@&5 z(w5MQ=@eZ?{nb@L=?xoh11ZJo7Y}S|GrwFB-}7*v+`wP9xR;vt)A82sdglcgE<6dno;{zbwZBn{zv*kRt2Fn1r$bA9h~rRWwbkm zz9#!6wK~hb{$#~If+o691&KvObv*A>6&!zuoFO3lIoawk|07-6(MQy8-p++8W03X} zUcBLS7)zckHj_71*I*?i{zCQ}0oNdx-n#;BWltgR(JM94EydvMXoU=VtU*B>-*g<) zv~RYY0;4(Zfr2vXf|18TsMO0Kt|u>N=9MxEk_4WnPVpZ@BOg0eiFG$XpBFn?2*OCn;wbQ#XIm05Ro)9%lJpZ#DBsrzogG^`Q;mVbT1hs6SnxZHf8@-{ z{Sz4J>s8=j-44uRLOeKn&|GNj(ko26my>ZeR*+o5FAkXCn7UYUSRC?->r)C~`ra4} zK4p|~+kF9=duC@f!Z4LxC~{z60;gVxUyGO@oDDhO5F;4GSHz*1_W)sE4Xty9OjEwj zC9-a4!Gqg2(w^Zv)u#z7BxS+mwqhyQp}fw8?wmS-K=04MX){^I1Vh6YxO@QnTHIbu z6o`#vdxvmgW#y(+O4EEO@a0>*IXd+w;k@k6yU`{NX1TxGQG9CeJNXVXDhrJ>E(<|J z5SmV@7jLFDxc`qpS0+a&@UIufUV6%RE?S-m1}YK;$BiwrHN?%_V6QDOV`L&tQJaxu zJ@x%~wCjKw{<)3zfTGjh#s-XIi(f;<;(oEtI$d5=9ickW~;7YKtjcoupw zcUxu-9b@okINzT=1ZSxG^7Hev4Cm**)l292s~@1%lO+ z40h8boLMrpIXoT#TP!U8B;k^s!o0+x`OX>8n>L*D>(y@^U4Hd6wz(E}`@ka}L5kXW zpawHAL~|Dfh9OA@gR>!TD`oYd3`Cj$eKV?uEA$bX zMG0m-$dAzum)MpMPkoodLsTt2?*nM*Xrl}{1_8}v*o4M9qQo$RKf_uD>RH!$kkF9H z;h_T8@mKtJ)2+cViUd~StJd)c83_YMiBxDndbTa82~&QiykKjs%~Il+;2TQ?{HG-56o2ig12XO}{y&Rd z%B&u$7>uq&XH_gf{o^6(W8fNPA7MwWq0Z|TD>@BY^1U9c zp3W)n9l5)!CG_>%V;cyVZ*m~(Am6%^iS@Up)^sT0Y@=^nM|a9uH|(+-21>N<%|3A< zc)jD4V_aygQflW5_N{vtEL^Ap={VxNDzNe0@)INMheyo^ZDNU4&bT&YfnCY(_$rBc zJ=x31ZMKIQ!~sWyA~#R!=Bamya+|E9ql1dAtHy1%2j*-Njz6 zqhGVeg$?;bE7O zNBa>~!IHg)ar-`hf9}^0lyk^gPHRs;i*c3(NhoLWV>m1q7>xXm6@(aimjtZDUC0hz zC@U=IlzVdtYSl2Qbn0{PgnsfN*}G6lZ#fjX$5bM$2^YCL*F#xh?86IN%$pQmAoaNv zuE=}3(!f^~H$oPaL^Xvj#K!<#ufL1fgRZUBj<^oRyJ-gc*-^{BgUg_?qqhPlx@e!suA za~(XDRXggUbz6FkW5$|R+k)};JwX>gJH~c6lntlLkJz`y4PX$GlY24MGJX>)tg2dM zonp}2uhe^o(~Zh}jLYISRh3YmB!c$Fw{mfk@l=y`)0?cSs`>@WEfAp##UECkdWU|0 zGr5cebp4#ZzE%^h_Y)GK8c}mgtM^oLW8Pb+>+3*=`mQ!dECO{?nIl; z(lQwues26{kd5bD8i|a^eCjxY@kc59*v|li4=`I5SpcCb(5{&&;HK`)=b>?i8`|PO zhWjPK$dG1SW{lr&`|l4F>v?9!kE_J@c@|_0V?1rV!=VaY*a!qaEt$U8F(P>S67vn0 z!h4B{{amr--=;cazt8Ek850bw=W@P-ItGex`^WLV^qUAOnZ?1Rl;&tJ82OCYL{kY$ z#7-m;Uk#H?r=H`@^VzlI{Yhv_W3#VIJ-cxy<9+4ZWu09CZDn&qoclk=TNYv5%-?TD z|AY5JiU7<^x|5Q!(Ln*2zX`rBIR$y%1|8|nQ}u}8H&Tj0{!Fz!E?%p={yn?jn+1j| z=v;;QK^O~C#C{;l#rX_T1~PIAtL)mf+b!+idM`Z)-NZ!%F>8#LsSHh^P<|JXRUR5k zke|BglY)PPb>IH|Ze3sB?9}*jKEaG=Z}`!bor7P!E}}{54rM@mG8-!Ec4vqYH)_U! zz>1e`vaFpdvAdp--(1`+r+8xS$j7eDKG3%M3Ll}(a`Qs=1y=6xvK086IB1|~CPs4w zFd6GP$pEV7K6w^;2ruI^0!>{Ky8Cb;!6K#9*pFa5?JLYcI6pT!CNon}3!j>XEXsmP z3`Kb-PMw+!D4DASEjJ67$V^IxKt1mB`ofDFhE;=bP+I@P(Rp`gO?qmf|X|z6H#w)9@uidyc&lL?R)T9m*-Exnr>%ETP&AvXhTD`8wv8F) z#cY9rf4x7TF{!H)Ks(zB=XiU!Q#}7ExwT4nZbrhLtry*)l(K{9hZn^)c3@4wy}?;LzVB7h5(JOkIR|O=o{#c zWrN9w+DQ@qx#nG$5&hmg)R5U+;`B}2n04nOt8EPQIQb!4ko7y!`g=a=SB%EHzvbN} z42v2L&ko=2G54eeam70gj6{zu)@9$bjCxarz<@JS-zvIwXlhGuH|mrF;OXd6f|mtQ zpHi%&KxHsH3z`YSlg?CGy>Wwq8^T0C-#Ni7wt3R5{uE)#A@=zA_zU1JZI?RS+gw!t zH@oB|2AW*tECo7~w&}n9X3gl`F!zGGGs9Q1p)vlpnK+bk@4z5{qD915Bbc^8DJ6>; zqu(^oP{5eF!L-rsv3Frm>ptyajVd!Tu74{eb-Gwt*zKeGCJ6@t1h6-=uplgkT+Qs% zY+W+2aNkWT&T^gTgj~UL!uzZ-tyXbDgFOqYeXlbii)@$@6C=sUA{%o(c=+&g&5k#I zBn3!V5VTEjQ=B)~K`nk3QfK<#2W<_&LSA8TjC7Z7bT3%siTF-_%+2H3YyxJo9#C8! zncV<^S)eZW{T>68Maf(yeX=56Q+L5c0i}D$$@oqjC!Q-uFCHvBtKxunqI0anHLLE_ zgb6Slj41VP!mI=QOAI!_ZX`wTlcBr;e(^8LZ zFk5tp5*ZoUEIxaotO$%n-^oD7_pSxa%$*V8DK$e6Iq$^0T1eB2vLlGi3-=JD%fcRHaep$XF29XH(Ve=68wx^Z_&=Xv4aTYvIqvaF%np3H8k_^4v;1lRYK1f z!Po}mIzlY&RCQ`+>$8F#QWMZkbvn8AX&zCT)8xEXNR zzy@DmzSAAGB0K0m)`^{ssKEbVo7s*$`@AhL1SYOOc=Sj`_97vEvcD(;5X-e?ELnol z8`(?ePWr}~6+i;Vp9y7IM%KS~KbkYu91S!i;^1K@Fgg(IjX|0^VkMQXVYJm$$-}50 z2G``kL}$X6VJ1|*D=3EYQ*+SMNExf*Ahi$&PGR%oPo<^q?^Gsq`8h=Jy!EgGw%tJ+ zWYBOR2iGAd#Z+8IdwS_4g9C_|eF|%8mSnWXI=5L+gIZ!v{a=*n?1L+Zr>EIrJz@u4 zkFVA##BrYKjS<;6Z0}n#Iwl;rv-gOt?_{tWBe zAnhHtj|q99kXo6SQKc%C@yXQaZXYqyNjX@xUX!8`CYwev;(FWEbLT6&!(msrkB9ft z%rE?9tln*@Q#BsmGhgP=wq7Hf6a(Vv3Q?B$#6w%G z4$C&-piK3g8#n(m!wrOkkvX*HYOc+4y$$0edbI^m`DnN$mXR)}`(~DVh9-!K7FgXZ zdRHqAJ^|V(E>`hFR_0Xejr#0y*>Id(vnmFMe7Ak`Poc ze_5s1Y?)HvB8%On)=|wWYW{u&q1s| z&7b8mS?r)SiqWopTcN3|y8uS^h9`|S-}Q2D3UyT^dbDK0XeuRXc!VLO4-R(LJ-^(@ zH8mYUe_tp6hk;atWy8#(92k>#*E9;!A&ZS!TCr3bzuz-e7`7>OeW&1EnBlS6s7n?j zUi3md`9CUq>tIMSCrE@&@0Sr6kQ9^p;}HYFqkANIfXPYRc_8R2rU*b;!i6%>vZ|19 z2{)QrkNf=qiZ%CORg)qgP=9l0v;m-eXA*8s=J)IU`@=mg6JY72reV!XN=gnwP{img zX^kkQ9kr`Ds_($qZ}Juj=w&ykY3(fNhVs9n)96n`WXwNkLe{RU^YU5PcQn_E2_F z%l@O#s^As1b+FqnYY`e3#o)9x+evHJt}SAUP48Ag<;w*O{*K;1@}5CuIID-D2cAan zP-^v_vJYW(=Y|AYTIwzawkK6iHL}(Q>R}d?*C9^S>%CH`ydR@ zC{kSjb6f`@Tf^u~>7F2B0AlSHDEhB10wo&u7p23w!|@PfPk$PTZ*_ zDll_NmKlB8$zMG;&5b?_Nd&@|(+wH)(`%S9n{j5Z2^@X>D?a1a#^wReO=Gmh~+tLTOJg?5I1KuU8aww7f$=$*i7|$nzV>`YC%dxg*M4s@<_F3t_C@E) z58nCeolqW%THz7-&N`yw5bC_Csf9A=ENa=Zef#$CjW015j!vmee)mhzLKl%=#IvL@SYL6ogUDj`cqb|LHUoZDyezQ+50-#vcwmq)|BulISM*IA$Ec>pg% zISgC``XbS>l`Yg|j^Uazb?RzE!w+Lj8p^lA22Eb6D~DEf|1p*Q8Gnk0n-{yCJ9}C` zaWCl;D5$lgUTEanGVjp9x3^LTZ#@RyYqZ8 z+hhLxBOPBlG`{|-?w(OrQX(3tV45+s)t%V!8(rO4BB0;JkV_Z`e$!baL;`O0t0_n1 zHPEBcJh=Z4HtW}uO3xXqg`+Ib%pd*DP3kKg{jc=G|HK^bDyO z9!g=Bc~d*AJpIwwN#AgPJKprMN3*t${)T?Os(*iwzDLigGgbo1ob=O*1Ni+=DUnrI)p8VqxQYQQM6S_Yr^fzpE48jRDLxuNu1)s{RBD zjk99+7hhZRYU$#|l*Qb6_Lxm{%VhO_x_{oke@|+rPVllY zw#$HEk>9IpYDi&Sw@Zy4o2reppu^8BY+6~THso})>WtJiu|)@7-(KL9XTT+XY^fYt94B>9?)-vFTi<MrX#(_4^i>xfZ45rfQ#6D&=%)T{4dYWRhH@2Hc?O&oED z|L603btFi?+iZgaQif?o;2L($0;UJhoPO4>8$ z&~sP;4a4>7E#0t2;!mhfx|20Ez{)?{y{K4-Mc{*7Ba)KVe@)=}*@p^My8pp#sm`7Y z&9eb3C%J~C)k!qYtk~6A9W*4p`hYG`Lpq%pB_$=8XRhh%FGY3wewHlycQH z)4kZg)-Hv5)-V@K5*pKSJXQymz*Dz*3TQr*mdqjQP2MV2f(cx7p5=|VH%B}M(oxde z)p>;pbTX4OI&Ubf)y8FsC!w`Xs{j8H_fwvg$N+rx)O1gcAyfOO6V(5ithkrDym9U* zeV^rCHhEhxkdD825QwoKgUz0pKZ^)H91iCgp+eY82>sfWT=6F~QoSiF$>|9XV9k|z zoX284W5?~wzQ#ToY%5h5{SN&bwEX+**HK3rQp9wL9WsXB1Xk(H9jcD&VvYb!+fG23 zkzO@tlgj6&gLadkiqrY@ml~2fjQq<=r$l6Rg@XsoUAJ%F{-F0uoTEZ<&$3oFh}T1bwgsn=&b0ooyKraHewxJ+`nvnYrgH7(JpNvjBF zMzvu$fDxAy?|vjh{2w}tXy?wIyBZxW=bVliCJf)ZlG?*^AuRtat2#aBnu721+G~`t zXK-$qEJH#rn0Bpr0C=}07;d+?lned;==*DaB*Q6l?`t|!@{R-GgWkeBZrZX18!s)A z=%pcvHOvW9F8c}Adov%O;7?Wj8g7n!C&x2Ba}zDae_qm0@xGh|hS8yHY~;VU7Jrql z!80HDWB^aRqS#WU?`!;%$@{KMAggvmu7I61g_rN&PeIfn+3&FxJy4`Y0Zd$$0E>3Y z-^3oza0e92DOh4Nt8&8>IRH`0{X3)2Z2tG{8)NxLKF47xBBgdRkBtc#-!_HlS%`;ZH;eON zho=GG!M`PlPZ%!Dg=r-VqHzOhCB_GtI9rga`06=Od=|MI85yC_h2)575RQ>r(El7| ztk6+pC;RdyQlQH&s89fu&b4}KMZMjXG5>wi(f&mk8$Vf@V|9s72Bz`Z-uN>m%RL7B zb@Ut5RtwGY95qKjbQ;T7*JNuuzEjUE$4-3Emdz?NF-*Rc*yB=5_32K#le4D~Q!ny2 zK)2;~jd!yhFlK|hOU%MqlGYcBg==CQVU65XKr@orXy#RsWmtfQ@|K|6=3js~FFVaF z2wCwY8XSPHUTSwIq-(Fz8SKqTqLF5kxQ+uTxX6I$4z}NTsZdl}dU5}G*z~}=`s6qb zQe@Mg(R4}wGZ~c#IzR$!s)_Ay44Bk}p4bdn@zNHfeWb5V3|J@*wdYD5aY~n!+Vp;P&wqycA7g*g8vOk)oe>2=1 zNbA0b>bX_F-O)Z#==xc)Sj+@Sp7-0>kTpYzmiNc$N~I7$gwEOt!IKy{K7lrf(IKh- z{_bf10?UbDawargmRPHu`Hh99-H~Fq=b9s@5)G_lLiCzfCY0t`wPLam;Xm)bR0@*h z?tv-a_CeK3R@Uv~7U57z0;iwn>h}tp{V0`Ym;nhzhG2_%sukq7-d<$est}G(A z@7?pIV;?Xp@?N z5gN`|jfv1|n`TeqEtckGAP*;k;fgH>UhGp90!r01Qc*VuV#>+?4!GnW3=Q^?mWhZi z4xN9k98LuK7S=^fkdc*rq67eX((fQ146a3jnTlsJ|_Ta?0l^1(L3Mm&(RH zwM918t~Rd~TCD0ZEv4o@<2g)z%=Dek5O8ljz7v5={159wT_(Cc+T3}9>fsaU_V)I; z-t3ynVh;7Ck*Xz(sb*IE5jGB>>$;V&F?W4JlDEL+&Xx)_(gA|?KGakMUE4(__6Al-r4U-^hzkh-4Y7xRBg<)yYhoua0kxyEE!mj!K&FpO2t7 zpo?*64hSGxk4j&j#Y>k;J}dz4jL85+2_>y?T-P)MqxI>frouUs*Y6W8pTMq4%1K(S zL~GG%ffKma&-snK+SgUWxS#-RKsjTg4e9np*g{YCiHuG@ z1ioga!(_)Nb+z8jWWP94YT;>Iaqq0IjWZX3V8%g;+HDI=& z+)1aGf#Q!58Y@P7r=eLsMs)+ZFhhCE3!|2xv|U0;nAp9ccE#QO^jE>MM$ns1V}tau7CnUj_@ zd$|kLhWcB9-E)ES)nBMo6?Jp7Sh+u6wpnM>2`M-Ktt)mLs6OsY6}XF!oO(c8o;fuNM-o3jDx*DCkW;oTHXE`UTb>QB6bU^OycyMUv zwebZ&Vo5SmHS#*rNzT0%13m;U8&Y81x_E{_CE6 zXO9`0BE44W$rfT1s51_&-kP8gSbBLl?+oZ4224Kg9)7$_=Cb9}v- z)K$?NcwKv6h5ac$B-V#}5KFhI|N8>TzZ65_8A7EG9W8exS?PD)!+07xeAFEakn|ZS zw|H$Qf~3RWa^u8s?149T>l?Ri+x8D2Vo2k+8*@yYvsLK91zk)e#7*)!8+~rUW#y;now-?ns+)1;R8*-R%oPz3& zqO%ZddiEDGL_Blm9OtIQx7U>HpmK8x=FpVLq8p;-V&>(G(}p*?%XdsB+CgWn%0R<| zyomAa><=6Gy3eh2E(tmk-ZBn!D8)lLy_Na90Ody42GdGpR!lf!8I!)0vNuUbFW8cP ze-SYv?ewHuT3WJvQ;HZT*IglqxGu~N%YP|5OkzThXPSF=YQ$2I2n@u)*&$xn#YLwzIW0IE1n^I4&PKeVNCXf z_=>|0%BRG@vK5Il?v>m8xl}!8=;mh+r4<*w>v#pF*MvuQ&uUq6V*eD)W6Vcf51nI{ z?|9qR4;VS|!_8&y-U-G8Tw<2qH-7^BWRnsbaUoCtTaN*~Ny`qIZBvl3W)zfrDGypd zhVsGI_fr#`X~>Vlh=iRIP(AQ-PW7igfW0IYq8PS z+4xqW$K_3mzg~qxWy=fJBrCm;|7c8u(^kcaGj%Q=>%RIp?W`ny<|d|#)|JbizcE5Z zT1{TZhu%>AfH;4F9gWLkTL$}D1fbb&dq;7HhXN1$HTSly@Yk`&y0876%f-dB2L9~~ z-5=ge1g-p(^MzNtM|&iPt+?-#7ld>~Nj=k}6xzP4a>iJaz_>;_lKNCyfZGwv!|byH zLo%&ULqd|CcGIxtu4){q+$i~~sqGJ9YC8N`^a`;D_1C;sqEp<{&lT-yK9YHPzc+4*Iy+rOnY0cB zv9_4ep}!_6JM8cL+zk8$R52b<7fAr_8%#Zm|E|lw97L9JjlAJHQ)ok{(pQX$yQ|#s z+n@@U{%dJZNn6IoJ+r?|I<8!@QB*u=Gw&23A*20&DMrjX&Io)Xvd!ww>9~k7$y182 zPZf?n^Wk>=j%$h%3pZPCk+HpVW~$`1b+cw(WrR8@3W?R%ep^2NW6QS|-5ahgWgSD! z%QG$Sq@^8E8()%Ft*S3U^pkO1<-k9T=#gZ&9XHImOn_Ob4ds>4 z1)ZK%7{|6YVzk=+j~BHq37qOmLEJrBzB|)>pbBwtb|!2}Ho7`t2-ZHK-au6|aSM9O z(m>jYRabMLTW1b*-3OwMB~BUfA*HYY1<-kt79T!XCmVNuc_TYd7CLPVZN4bOWO*17 zR$Ja#+$eAS@adl`zP1*-C@9}zmZjK7^mJ={X+x5ZKh^qMUq3K9!PDQM?coUkd#R?I z(3CAsjP#l1*ZL2SV9)ySt5;_Y_T;F!ce;0of#LV53e(4@eO~)s{ zavRuIh@`PN2B6WQb$1-2YM)A0q3e_t4;>ptv$EZAi9`)wlyT}(&!JF_<(^^R7I&-M zy%4;Ji^oBg0h=D50#{jSO}1A><2?7w9H*;V)W=o_v^z9&YWSnKYNyv&@#FnGe4%lJHf z+&0(Km&UI3cwyi=^RENP6RQMX>APDsh>W2%(6YuZT=Lc|U(NERwd^c?*Y)iPLrV;R zVP$4!d>ohT93$6f*7!H852^4ZFB(*h<)<~xE7}78qPB1G{ey)oYTk3nsE4PF?N>_H z9A6Ur%GUdbaP1G?dlX#gY!G1q6tP;o!ZVt~-kU|28s)2b1^YFXRZE2)-Sri2_I;>m z&pnNy0B}HEz91r_&G;NVUF7oe-hEn5<$95lMa5j6L;3#i&9q)ccpp}ZK1Fxp6IQFU zxcA7QZK&eFy<9D)=J$7b1xrh4SuFxLm+t8J*lO*TtepKVxbKLzeWpbHq{w8xk-`Pa zHBlHP5ZeX5I@eIzJ*)=!DK-`GkM6VnV=YW*#(9=0y6jX5(IQCvRt+$zgVtXozC(G^ z22tKK(!2j2`2qiPa*HK(To-hMWqn_I$1i)ZT5WI+=1_hx_&L&~j(VHv-+6Ho01#Y$ zVuJ!htpfJ}bRkv8J-Wph`H|8JW5E72y6ejk?T>+kPXGB_AdWB$c<0&pc-`MJ)-%ik$a7;~H+&|hg(JUo_&Ide(Etc%Qg--*b*`-ZV2LA) zf#*8?^`AF2>KEr}(~l>%r|x5?()QzSPKUe$xR7%@l)_43y~L!YR;8bxloLz)gcC8= zapM|*bzlRhsOaUHSzBArd-Z}MBjyV(WFX#=df2-CuJ*D||Ma{J%D8YvjM|`Hct6+T z9kQ7&d)Y!yX??Ta&IXA>YpW{YijM%BcP?ZRd$=`x-;)HTh4~ile&#EettsA9IMiQw zbnttfX2?N`t7-PkgpTD&B;t+JRR&iQl}3p@7dmlL;g1Zye^g|r1=nDa$>4~PM837v z7N6-VyMUUK!})`SpIOA}&W9%4huzjv1eF?Ak^jVk(rOIgfyjX^>gUs1!G|sIUjF%F zP1?*+b>u%ESvwC7?o_T6Ni2ZTy=b_@&dWad@}bX{_GJT2S#g%Q)+*MZvVW`yNpr`$?dayk}xCR5tw33C14poomtEvq8!^E9>oPXZwH>B}W{ zQ#6fR(KL2u>n|mKVDQ8pi3hfqIlR)+3X!*PEqh=qk*|1$)V8-~DY!2j{A@FL^Zr(e zbGTy-NEWxfH1el-_=_}LxPjdRCtKJd=7lvF%7S;|2f>$j7k(i=aO?@@c%?GIEJ@2d zIkQepo9NMf#3N`*(5O2S;jxBf0V)2_3{|BiIVKq*wd!)um+b}w%CIxh7M28a9L1f+` zQ0yG2Mg^uN1JjArrscI4$fej5&#Y7JPFdDlA2HOQ^u{9{ptHMRm^!+E3!@ z3F?`kU%sRv5rYdj*FzG6A%szoRiHSU`5o1Ku)F9_;u~$v zr1_}!?(hGim${*&2N5ZCSL`>Di@{1B0mn*774Cicr!?J7!Njy12~m@I{Un-zN~aET z2IP7*K)NiP=~6>c-R$A;B`GbL9|^8N=Pw#iUlhL2NAQO9Qhj^kyCA3?BWYsko*?60 z(4x()1t4l@PxXwUi&XEJ*#~}BZgCCi0h*FN$VZ-}>iiYZ`B$u7L3~E~eAA4T&6z`k zyU*M+PF)AxKyqO(mH?yj=Jg%X9LVyX^aOc)4-&1rM7X^pm74Is=sAKt`8MuCIB3?s z3r4U{ErU1f*YP*eQVqVD+d%ZQnrXqtp z3N5<8L~n&@;?`S+(_*5dt!=$|seQY{I0j7h?H>62Eh@gi6G-`woXuy)9usw#@YL`S zO)?<$)b>=iZ6fX5xUmZ_yzbTe<~e&wjTL<>4az@f{k2t7?!1!oR?v@|3&t&ky4d^p z(g3P;AErKQ)(Z2T0IVGKR7oG=m+d732~!EthVoTr;#-rcKu)Py{v8KnV6ohOa+OJ# zy8y!6z?+Mm+#zOw72`)1JC*YiK95{*T*@brymS(@ZZ%9^Fd4r`0D&R-`(>9pmy9N4 z8F>%G4ioS^bSWX}rMf^DZBlX%mb`@ph!LYnqQc4XA~j_(WFdh!p)dk_?sM%okz8kD znDN7qBZ42>7?!hsDO?;?+?SYtbMx;4MzY-0W?(fN2h^10fy9Oh^?(RM65bmdtsM%0 z3VK3J=Ad&#-8gE=F*qTlic0HxZ<)*g4pu5u15{0ma%$~_`ckfvNaU(iK0{x|~i-oqf)0)m7i^tH}r%FU$r5vpp|iU7fI+kSTx*e&yux`w9yTo`Rzr_`F0@hwLlZ zULw7g7krRY_iJy08rILHb(h&|BTbn`c@tpSrek@TR0vRyavf+i^??}S){^tu`O(qQ z1Fv01-}^g4N1^{QiIxf7oNAl{_7{tXpdlN$AN?s?#hYjqolKiNhn1JHX(l$oql{ri z+#<*g&vOvBjE=PME?6aSV^hVh^7um4sZdv}*i|#{rpX`3*c@>agT!~*8p1% zApasZq5sCn6M|r+z4?wfq1sTy%jFKMPP1Wov;PqTtEi=rf|7?@>nC6Wnq~r7f8L%w zgqKI|K?F-TuvltvA}99^ES8+2)?4gD-2vT0QM=eQogJ1AgHIIkb0y6M%8gN_Q2LEO zz(a@!h|*$|@Qs_aU?!k*GE1^mgCS1oF1A#lcJTck3{VWLzWR&7wuZ`5Ac~=`BBP#L zPW}gwmx1{SX2Duxp79A?SzBRT?%+pgcRbr5Q25h(;wpiXGR0el0Bwo4J~JXrr~!#;${?G1ob#~XA&_VqmLE&H|f-m1fO6;E`S;U z3z`n@L28;@2_)PA2-gNulQZ(Wm(tMo$vFH<+H>0q(qx&w!S_VUPZ3EZTFHSCA^#(e z^;b`2Pf(wBo;)zMo`p%K(Oy`x+y;(9f>eb@i0zr!(VFw>Re-p0w7UvbzZHK7Ous_@ zHS953cst~nk^L@KrwISP?KDo5?P@Md(NCSM=I%-^>4XzZY=;>?yzb$62iURX0+&owtOyCwYA&Xf!?i2`^6DnRpC* zt8x6+^g)TU7_22?0ScZCG1n!dX1Ka%vAaT5Y@t9kT=K|fwo|Gggj~+VXq5j zK?qy-pzyH9a27-7=6M0+3zlLK;&?;mu=kB1SukZSfiYms79i-HQNMsO>STbFADWS=R3wCgDOd@39+UOy%PBws(_7eQ+e(_)s9P?bCB?8 z?u_pAmMpN^VMa+n9KT$aV(l!Lg%I&k>Dpn?Gqe@DK>AKQ2?ya|RFqGFAqlBg{Xpz2 z?vZD?baC{wusG;e)Re~r1d$Nsrd-l_ZJksM&eqU_@mOw3+h*N7Hy zn{5kF#jG)7r?sA$pg>K)3J|oFLGoP|0xiUM zQ*@OJSsr#IA2e$c~eZoA?{9-$A zT(WHowa(8Me2Lmi)DiaU7VZyuLrLzz2Tg>$N=KD8!@Xzc3qV6#K7!?Rg`|A~MzkDa zfwK;#HU+v~f)J%9YUT+0MdE|x_~*ltODM^v1WGMcjJSDIlJCSSMa3&r)#VoW&c1|G zY}*Ly7tr`c=br|gp_G#f)crhMJ1$pjB(>2D>sdw7hRKK^ZodJzm?<`CZR;qzlAGL1 z50*`Wgx&KPL230m+Y?dvEK7Zo`r#q&Al8AG(7y{b**$y~v2iaM%Q~qH>*EvL)diDL+6wZM^O>;P3`} ztFAkq%)`Y-bAgs(7brK#1@Pq6Fw#n{Un4F;>1c-^@7p43UYpIcs+ThW41Lv-<|s~b!%r>!B{J0XQa*lB1}!K9LNQ=1(Mp0VV+M`=q5=*OB5(d zqS736L^Y|EP>F!rr+n*|$e8SqBjuOUynl2$HOLK1moN&tz1GyVEvpCCv;|O22`Y!U zkX#IzYD9B@V64j0w9kNCA4j|_HPHCFmkNbKm7tbU7yJ&}g8w-pnN`Rl(T41RmI``Cwrk1V^vJ)R67}7M?T(7C zfi1Fr>Rrf=+@jqFfQDR~CXj8PI=hN(_pNylD`8EClY> zC=ncQBkzDI|INfLMVNd>WAZu2>d6#NX5{C4zYxO2^`z2EcRXM2Suyk-2+OZ*T}3s( z9g_7OoICp%(VWaI+Dr2imGe|8KRPGH1VA*qV4nfG8We5jf|_s)vg`N-#Y30{{zRMf zCbQdy`Y0Ak1DI3C=6%eGgB9Kg;>tTL}eD>B0J^#L+$C zx6Axx2IbA%56Ih9^=Rx2s&1$LEDcw3V$;Xk4aRMYVF?*!cPZEt4;IBEu$7I`5so~^ zXXV5~)jq5CHxO*qEdzd?QC0nidPne%o4#@xU={<@qNs<6i~{La=q4@$_Bt*YBi zKE3lL<(L?tltpIN_7hr&>K%KI`=-|AK*ubAwB|rAbb-9i>oAuAd%Tw5h+U1uK!2T@ z52SqLW|KX@c93KMYu8g4c8g2u_ZjN(X%q8k6C;n~*#BB^W~&BzpgW(I5EUTlJ`=kj ztghqsQ~-cNF3w5KxdChA7nB6_Ft~k={PnO|UoK^T=7o7%cLCBP*RUDdgGm2HHFy1u zkqvk<%Z*u+*v@?(ci6_=bAW6=3v5*g2cfTF_-|64hZXp=1i(yCOePowvsum=qN@vu zAJMwFw**T_lLZy*W{PeOmvYI?U4&xz7}^JVrT2>F(+D?e+`!ohdCS0F$`xzw=Nf!z z1J9M+0>d<~A50@kfc*=HUp8aYEF3^f-(ZW&{eUUkh&o^q*_9zGA0B-n2ukz}0_ zEv*A9HiC@oG)hD;Rzt>T^6;VP{0}A3bFF^`*k`IbB4b>M6?ATX(cRYeKswZxQs_p# zch5BidnOA-%!x+4G>`Vy)Wa>U^QES~9_O;Bg9C%{``B#Y2n$Yo0T{f6urM}=Vtl2O zOA9h}cY@i-Sx#P13)y4<%u#$R|fj9IGGs$$lL6C zJBkb>y)BNOeDGtHOGTeayQm(t%^_PQ57*gM==L@SuK0mvQE^F1;44b4lRw6hHfh-Z z7I^lYb(^4s*>sgUdXpMfBha*{f;$Pavv9Pe^%f`wu%6+YS+oxBT{tLlqnwg<$lX@0UhUUVkeT~E5o zgu?#>U>MMA7YR}@o!(vH;XyIQr{CaR369Mc8a{iiEn(#{;+pr77l(=Z41qD-9u3Q_ zYBp2wen%zJU4SpyyFDG)93wQvJgz)nB|Lh;nGrWZ+FjBefHixgkH88ds-@R}xk&Yb zXp-_RVXAJ|9zIm2Zo{{5tvsJ8EMZTyT19`*6w+E(^>?TZ+3Vf_9&<@Bj}OShi10El zTtWkQkBuf>tXKgC)+vjfr@u-p^E5=YN|r0cOpfmS3dC~Mo#*IkGx0&?-~7A4l;^&I z4Bp5-e+l`uhpp`hF>Ub}>eGK725nJ_=?qLR)Ucucx@y>(m`|SB5~T}fhOq^a z*lTmA5t4hJ-Fzztd#Y_=*nju-<<(~utH8e$kbh*}b~6C4!zK0$$^=9(@-a~rc?!R< zN6SN)X^yfsf|Ruw+A8EPmv9|fW=47~;8V6-U_-3aZ?c@ej6+Mg3YtbzLj@BMrHIb> zM`+Xf^2ZP(V&{we)v#OST2@yjO0E++PRKMv9$EZMKmPZ_4MYUrz2HQ=v+IR%?aMA$ zrFf=c$O~8eWVD`8*iC-n;p?G77bS?v=X$A&)kor!)QXERr|S3iUD^+YjahvJ;y*=mb~7suEnbLjPE^(^RNT}*?# zeX!dW#-ym)<_nSZ`^yaq+7MctF*MLpN445~j2?**qDW9KQSkdinEl^7sv)L0XuLoE z9Y!x-fw1-RIcN6Kmf^pFS*@P7>iDIq`wFg5Y>LAsw+FyrV-BxZ|9J*3fBluV=gguh z5N#Pm?14F=;bR!gLXtb+CAL_*>-inGzOHky?De42V;GEw1QC5vk$@J$ZqE@YoQ37;!ZNFU(OGZHNn8f%^$w zW<_|$36e||5)_ps$^D7PY~&Jy*`2%`e;N!rEd&NhY#>z2nttDK#Ws6#{)>PiWZP0~gPAF!O;jQpCv^h9s9f!~&%cY|VxC8BUiM=CggX zT`zHE@i@Z8CW@fw4ENxdm_eD0@A)>8{I0+SWBbwZEmc$Lu$y6MlfJ~nx#32X1c-h& zDL$5XFY(cRvB>Bh%BBrVg?2o2uqcDj=<>Ow<|Y8xRYOd0(D<1Z+2MJ)A#U_fYy=v| zoZfGZR8a@@`(cjpDK7WWf8Lv|lraHcFLk-s@POPSq=@F5k5#Q4e{2WkFT$g|8&Kok8_**m*0TQsXz@3+gyI(5bpY9v?X@#nG&K z0R6Wog#rHaD5yEra)7cQ6DnQ106L|wdH2qpT+)I)@pyQ{!sRd+;9t9haNX)VX&rTi zK9Hp^e}1(zJi2suK3PSS=Yo6@c&kav?RF#Npy1-di1V9Q6JH&fG2+V8G|LZOf){^} zTrY&4r(@i|MN*e{ktpiPiEC2`U*>Rtfty*O(-inuMTT4ajKSc<-*%E*OPy@mP^b2g zFkmR%+W(w>w32bOElZ5D!2f0fQBoB|jhjq10Ok?9q15xcBw+|2<(9l5G}^2k} zZ$$GbOaofxb5PJ%u#5qi#kn!R|H0|{sEXovHmsrb89UJ=ALf2%Nf1|=a5y@bJ1mZ7 zVeS;l0VIRUO7)HdBZmPPNNYKLM?&}I_I?Je=h@v9^TYf5lRG29(k*7NRptJNgQ?a| z*Pfn-oJkocKPT?HOg4s25D^G&fD1eSNK95jD^TtG+VfZ}gTt%_!I-c@5X03_(1^Zz zSCSwe5jiCO1k!i+^NBf7gsp!YW6B{OGSg&)s<>psL|kg^8QrshFO8rI^(a!M!G@4C zK$9b3nuf_107ofabvSPl&QTa{)w3pe9+^%5h5}J;-B;u?EuFC@su8D_o3mVT2L}Hge|s` zB{8O9u9px^ctXsC@-*WyW{D`Y94+}H2?c9<77y+>y_eRW7d3z^TU>@#akdZ2$l#W+ zj#v_SbK|9nz;MIKbSKO|1dZ{Wg*nq+wLg{Ii{v|>3&Sd&>OUcQRRk9a>hxY4*EajW zhEYicvs)pDJ;7R#k8+IE#8{S(70tUxh!*MZ_@j2*pA8+*fJ7fKlxtx4gTtK-cVNKu z$OYAu^OxQlA2EjL=IVLHlcQ@H`_Xw4e3&p7FUBt>|Kj1%HkT!-O-Y6vzG=i?TUkSG z3AS7J66Cb)A13gw?Sk+?-z5bb1v=84?ED6?NsIUExHf2-%BaM?dx2O8n7v4NcRQKz znQZ#PCP0w=zl|D&a9Dk`5bB!0U~>Y%=6u-j-jN_HX+s7;{N<5vIB^h&p;W!0c)`i& zG$OLD>q$6_Uw?e)6|6NvMx&nHOqLhCs-i8tu07ksU}w@-C^G$msToaY^!berJ>q7; zrBs05QsUmjFLybJ0)YLXDD`kP>=-ywg!F0@n~_DjG%WrCLurj2K)zkhLdBUwEW$1U zhBeTLyia<4SdL%_^EOpNY0--n9ekt^9lYei>E%facdbEeKdc0KMlp8-f-B%`?Xod} zuF&IBDF4_8%_dkqHr*m91?FQ6>)CMdr8^H#ARc@(QfWzUyy%?dbp;$&@jAXSv$`}Rn% zxD{a2RzTus3=3fPp5w{QV$!=jUumnUq&)jY-?W?+`2nHVvRi9@y-5QcX%LZVTE8Wx zeO6pBiB}DLv$+N~E#>LefIrCN5Su5QW5PgL(Dr(uP(1^c>zU+f7F*`c5N4balDhd+ zH4hs#z$h@owX>X`@cP*W&8|Hcj)QTjJrop0STM=3mT&bB>z``^1?#9rQIDuCR&k%% zzLM~YxZayq0z~i}v>_Kc&BE1zxN(r;MVq;{5(bTWZMvGdAKq<$F=NQzm#g(}PfhWb zprlRd$NqlPuuSt-w70o<$fYl@pFg;8IPbI|jhF9_)pVZCW_)8r+T!LPy;FIkMPS?FF-xL4fX zFTPL+mc16ntx+r37`k#fRA^M)zY<5-ISEb}1($Uw-~~}|(K4+tZ^cU33sNAuP=|D8 z3n|>09=?wol-iFE`M}U5UMHQ_VK>(g=fHZW(vVMKll#6GdZ@xcTi2z5!|WD}_=?z` zBfJQyS2|^PyAynul_O5EJiWU;(y!n{S=lsbwrFus)y^o^Y+P7hS=}NrH%J?+;jv#( zHO%4+G@;Wy`rDe5cksmyK1u3>D)Cj2kP-=eL}xBkvpDM={$GfRYzcD>tIjkL_lFr8X!nhgXR7q^j4 z?+cdN(E!4`8W&_XaKR#74T?i=#=OA%nF9#Ahg0I-nX6B;U4uQT%i{akjT-q);KMk4 zRr;dOYlk0-sVjyHA%RwTVTaT6=g-X{qq#&rSkEk(0JCY{z@7X;Diz(M$6%euB2vUR z`^g+64nZS03!>S#Z+!EoTOK&z85b9q@2xm|9QHpL`6wrlB;=~e6?UIt26kU$+U9g-docFvSmb{{Y1G0F6kmSbo%CPfyU>9g6r&|y$h1h`W z{%$QU6<$#GQR48l!+j43kF@z&le92wsNK?BQhoF0&4cHZs7;%Xp`DNh;mtNwSC<1XZLplx+qKJHyP~1s)PtIFr9*NL^;l(Yvi28eaM#!HOOEQLL zXo`K#hU+-qjMs&#(0y1eibugus5xJ3usm=)X`_?~L57@^pD8K`aUp-W3~{8mjc5Lj z*qO+yE}^yTZMZdH$To3sS5|HejA6ZzOt9k&PK_XDSCQhKb)8!TdrP5Z@?Bj((M<*3 zbTtw&D(*Q!W+g}XUY-3Y`S&6VXAmTx6wKm_hL(`Fas>>~3PRnSlg3-mDFTw2+tJZl z%HN?q;YSf8XvZ4adu!q%#~T52X!9s?ZvDnsH4*w4zV}Y15G+vac5bNBPk`;`8M)EqIp{!a6Bmx`wm&u#{ApWLDjgZT^D zT6BV*>?XV+D4GIki4QLP2{{N@f7^oPb#po0(`Y83@FpQg^~<@)T6Tey1c(qw3D$pP z71NX|Ton~p>Feunyu4J-o+9lsiG}gvhmBwBFhFpEVv9)1)J;~zZzNuNx-CDFZUe!n zztaSyfQQPMEJcZ@>zUy=HL|4fR(8J9_L;T%Bqsq8WD7^2UqzYk*L&(p`1jH(+Vem; zFQn>jSP5`%J6z?#Ny{G#vfyehgUv0AwD$s?&O+$KQ1eWTa@hrdSK1N3S8MOF_V#w% z*zlPuYbe*Kfq|&d1K28gEj?YuexU4}q~g8;PBAW@w%OOx`x^cafm#rIUz!{&65&!6 zrE=-Un1~}~yg?^yX%I&`KMqO52xBRH4ZZ2n?Jo!%l?*w_R-oC)0(%}QnSBH*AHSRtc$+v0c-4k)B%~y|<2n32ql3ykv{ePT+q3s@T^= zR7|$NA!rC{&OCrK=a2lMJDV`7Rv1V*y8s^ zD|qp|!!S6-DgE2Y-~U9de>H-iFG+nz4hh_UOi}DF#c=O%lxe1F$Ev<>jbgC!hoX^% zBTN4Q^=QveGMDBMbXkK2c*%2MWafRqU?5`MxefDXkv71E8BCb35`J<@Ks&er*3bD< ziw17UwgNmp3%K$wCNKd@nT_*89NzAT3w%Nza}U#59R%*8nfhjcfVvMj&Fjjzhd0@# zf|Jy3d0qoP&dI4OSr<@V#`i%Ds&+)_6Pi(k%gJstthO`mY`v8Y6}pSxuOPDH_1m}m zD2C0@wA{fJ=fS6QKdNXYAzrS3LyO7iJ%CIMx!4o^zhVMU^-4&x&SHVF;~)DO#~f<| z^!8Fa!-bnnkD+c4R#QLW0T?r+0pk>G6O+sU4c-0e;H_uZlwS323j{>8lo zb$=u2737i}rO*5N%^L}=9$3_R8XKS-tP;VXNy{;CnFr7@Ty)y}`yb;@w^ir-g5WRW z!EZ+Hxkos-gQgh(g>7GHDLKk^G(2KO2Ed{pR^BxFDnB@lP8BU@x{K8`XjEU)OMy5D z*EUh}?{o{^nr4@D0QLHdZr9TjL(Us&z8RE}v1?%9##2P|Y%GChwZ_AiC2x0F1M2MtW2Xo~ zjKiNl;-u|3x^HZf=}Rf`Eiyd5)QVw+4l)59D7O7A?b1 z6qiqI3{zFHI5&3sQL}A&dYl4z*yseIjB9^>MZebPwSWfC+OB6DKrJHFxk7`wwuVpz z3=Em~w0`1{rxETXN<$|K3mlCBiZ%qHs{RN~fg(^XK6Ay2;LDdIpu@6af;qwevMC?K zWTuXuUQj|_V@#$hY>eP!nF#Md*oQ@-hPy=lC09DN@9s{QF==(v|%wz!JuON8dCaD#ze7Y%@3+~K?DK531Xog3Hvuw^i8(B zxXxRvhu^ek6^_gPg49Pp{Iid{U%r3e+g3fl0_nf=^77(1oZMvQq2je-647?$X->t( z#pkI-gW;{LB`nAk=qIVXC-1`T+qWsYG?q&RBv16kn9vl$TLM1!q3>Zslc8{8@0X1` zR(syR%?RXh9%*ZajeE!C`%v84V^Y&ms5S$!R20Sji65)j*sES#yjnnIF%*NY{DOBn zjNWO0FPNB)+U80GVMws|3|1eO+wbLYl%f?}u#nXp%EcO9zu=gXm=Nq&BOT_;xwo_~ zi>CCMS`)4Ur+A<~pH{y1&E=Sk#9^UJk6|G98xGb>M%Mu3)aS3`e{hNlolh~SV02g`vIPKx?dN1Pgh429&f?5l^ls~LF zx93J0Mn14F0gtyQ{4s1_(zltxKB~4Fe{IR=3|RdOowG0wnn!P$I7xBu#+Rpd$ib2j zfXEi77DZ9ubAfmnuS0*my4nVV+OYoIu%|$08vF|a2Ox>T9V3lKg212u8wOasfj*)o zWMG_}k#iUmm{g_#*v#PbiMqi8TQI8>69?fy+{_BO;#_T*d6<`Sa#((x^<7#7ZsvyC z4F+R>z6yqk5v&0Qi) zIM9gFBimQg=VASKgWfBH;o!1!A_Ipfpu8E9@ZRWn68L|Pnl3sCxQ{2|xXU#t{!tS< zGf4#7vA~gZU1JdD6+VTB$wG~zT^YXACvPoqbrevs{F?A;aYcg z(ga|x8>-)#oH*d{E_}g+)Wj(c@&1 z@lQ#?4@uEXV7{-00dCFUHNZYm1#DaA$_8S{^O@mng7rB)QOAzxxs^PfPIzN{I+b8C z&cKEaX^Xun0!re5cIDK9VBX?}O^#DRxx&grx~q}O0jHbNa;8$DJ%+v@2+AC&ZO-=v z$DM_ORjewCz zj>nc|2yI$6>yPnMUcd2`_lY)2S#&V{TZw$mGD>P=jc7TuauZDhIQ~QJCMER0= z$`6|=fxf~5m%Lffkj9`aJ)R&$&L2^l2K~LuN{a}A1jBl6EX;#q3*zNF0pvtl69i*4!LR4GQwm0c0gZT@g9p7i2L`_}T)BF?nx+Cm0#SDxVLAA%97>S8QdoEZ zWpQM7YN$c9;%r!iOKHdnL!Ivs(FBJ1ZzhPyY7c8SCAhp0hY|(122I-%{e7G=CLRa% zN?7E57K)XQQte~Sf%y5;QE2cDnuU!ed@h@HA>KUD9T+V>`&=P&r4wtdz{|c3ep2DY zU*YsIo*N;&nVo8ObGVfhuEY}s7Z2yw(_ZVx?_CQZ1&&tWJKxu&4+m|bpH{n~U$Gie zu4^}L)Kcv4!W&q!WJ&S+_rVVBn629a^E#)Tf!!F&+U=A z68W7Mwr)Cu!^Iq{Ci+~sc37gz_A}5jYYiIa=481JhkN8ft*<_^E_*|BRn1;itPJ=O z;K3z0894UOa*lwjo8ETUQ1PcrVbDa8o2I{VtlIkEolZ_p$S}(36K6&;S-cE@ohUg9 zot|u1@vnTG*AP~JpoDgE*MhXbT5Lc;y7QXueC9Z{YEGP>pWoXs$?Z)hk)wR z7KYfR-fm`pX5#zv-Hv$_uGo%hL+4rrQ1~-&)n6>(8a>hpKZK;;r42Rcj-RDxmIuoF z+V8#8e493gHleAhDP$4U|Bv2l*p>qV1rYQb`~1&e(tviRZLqJ!=HSKZkHWyDya6#U zA2$wsYGoC6lpE&{#q_Ke_2JtQ9$9y-tYc&Il1Qvx-=R%iYW2)$c6Vwh^v!Ftc?m%{X1@YIJnIeg2I7P5>g% zW!ww3T&iaeL+>UY)_>;O(zuq&#-_YAwaFYf3@BrpB=0eBOY1k7*%vM`-UB=FR>x#M zq{$SU8>a_Z+I!rR-t2dMk40?YQz>E*TbmCn;kwZV0o&|g7{Q*|oo? zwn1n-(-c17`-aKt$C!Z=TYD`vbMI;%V@h$aeCxX9NjEeaWXHs0CIzH*OsqGi4ThGd z-b;QnQF_~$mZ|ez%f{D!e)Gm9#V#h-`lC>hOrnd)y5YJN?4aa22;1m;9tpVn8o2?v z%Y9G+&blarwdHL{e?<*!%xLDXQGI$FBAOJ&bI9$cs?Tt9=$4vE7=Yt^mxh2fBG`k6 zAapBx2ExLCu*cTB25GqK``8qnAB0pj%OH#oO1{U^V2WI5dSl*O`DNnUpf5{9&cd5lM3><$s(2Zf$i!Vj_fUZKcpT zc{Z@`xqK{4=HPsOyjV}>?&k7$5#Gu&Sno&*z#1iZB&+sL5fPEi+qWm?njePO6~2N7 zq_aLC^lN1LwBs6|DUAy65gwVNrw+>beSDL)fI= zOE&f%3NPG}0)KcYMkT0t86)mMgsm{H*!RmQ> zMte$DNJFXiF?t3qB{QOKFf??}^MQ=z-S$4wM|d5+naF#zd>zc>9$)g%Mv`E|=mI=t zFiQe6hDYY=1p{Pq8n|nYx58>31G)rRxFU6^U17+tK{!R=9PDASbg5aLwn&S9dUUmR zHVTgv*qi@xWBRL2E57wC_ewD<`NMMgzQ8Qk+WZjuCm!_UAwXO1R&=5+_;HHv-c{ij zx$V?Cc3Z~=S}u+Q^oCeRd-C_Px)+y(092|OoCCN7A;28n2ZG?aZzf2_%5mGxP<^L= zJhAMQ_Med48g42~;T4pgz%cr~F=~8?T}-BHrP-PO)v@DfA9QARD{AY(KdN=~6g62N zWn2lSpW&%-FMRdJUQ6%!izUfjE_L_*nKlR97~@{Bite^;NAKl|y<7%?hcI%wDk@FQ z9niWFnJr`-TKX5f!u~MaEcVbUHn7j|p=8M*0H=Lrd|#T$b@%PfY}81nWd*f-8B#I+ zsHx*B`%zg@zC!tyttkBA&->eN!Xj^ha$H5rv!0J=G>~#IJAsYxdHDC?56v2Rkx0H_ zH?aD&bpAfopO4DQdFK>nd+qaTm`T>ueE`AI5Zya>&yc^LD7?Wk;@e?tBcBmy~NnLSxAUb(`G!ClZ3p$MV7ED9+2a|mvJ zHMXP71xh$il``7StyG^%J+ngsEg!#yKOc&V2LObS)rkY&`aZjK1e!tE9cZLZYL|eI zKu}s%)(h$|*SAu4Y=b(Lu$;vkx+jeSM-F#v9vU2|z8iV(%ig?0L{<2y6Qlf4dIc*2 zLUvb%FB)UI5N<}ma51GkD5LYnBv@_7F{v}$T8X$|Bw_EE7Y5i8gf37_EZh}h!KFjh zeQ?10Y{e2_DEe!x_=SaXl*JzciFL`Pvjar&KVU+Lfa_~D93$`E-Sz4530$gsUHLKL zm)L~?%aMwJZ6AvtFDV}3$Y z>;EJG{HD$8?>WEQ6O;unIh3RJBneJha$p-Qrr4wU&Bvp^8rPpul>cnVqbpdQf_YHB z3hOlSt>L9yjhTqO_kR@b-5?aNg^^n;2Vx!nTlU!S0P;_c^_@~(8Z4IDcg4P7`6ca& z^783W45J7OJB`g69Bw{Sg+u084&;>nvKb979tT9A)-ClWC_>R{TFT{(&=)7N1H)3-TsaTq&p;-6j%5dS#SOQF zzQ@BZ+Y5wSyM_T`l)_BrsmugqrFkNtwvtmTg<&ms2wUfRnZ0jbyh2VpBiIQ^$!;>r zf6OMY6*In&)(<_!o(Q$x2#(<+yZNTqK}hX*u$suedWN+q*9dg?%=XzQ{}i8s1ALr{ z4BmsNDE zTSrhE-_2&7^~Lw0jC!_VNDM5FCq|<1-xl$mq=uM2i?{!c>A?1Bu%hJBtxnunBz~mq zJf)1R-kPSZHsFAJlPuoV*4E;p^=EF0!?$oacl-I=gddoJBCVUEYu zS$JBN<>miY4Om~1qlBZ;KQUKFS637&LK<5SP(?#Jd(qX^)zPyc063wBUm>Rp)0Y5UQ(v6*^+#9^{H%$AR|A6b)my|W5h@3_|l+P<^?l~ zdqY29{|oGE0VUi?rzmSDGN4ul=UAk`u6A|EDydf>e`EARkM(&)-g<&ovt)z_a4)HG*dB3MfxHi-NgYG^R~Ym7tB79tqfQ68ZTzrc+d6N@A)Xc;u;ar~R3 zg&v_yqg1kM<{g5HU^c8d2&}6wIHd&y2(VSQ?E7!(?AM<7I1)2;jE%)%%;b_@91wOs z80>ANHbi+t{tyr_vgpnNzX!y)N))Jcm;HhU6{(={tlP7gpcckFH3#J<1G$RWoc0xq zVd5XD?`&d;)q28xhpWVxEs6`VS#2YON5CTgcb}S*RM9DY9vMOd}O zWETNh01`sDe7^4hAtf6TICU;%K-w+mz5MgV>wAU(6Xpr!%*E6qA7L)2SXK>mS7;mL z;a24AU&E1c#laH1!cW~QvXW$AqjaM+2-((Ij*I!W_L^zP$E z=7~VOBgygHFIZD6SD>M9O>jW9t+z2oYWX%@-Rx^?y=Hg6Ag_|e_m+z?Qvp&1M%+cX$mpr*80a~B{tVe1nC|C{~MHg2-GN9GqKU!(}9 z|8_P)h9LR?X6b$NAF;PR#K*W+7)A1hhrAZQLCXw=m`_8Jaea>)OdGLqIaIifVAu-Z zaTNd+G=rOQ&jz&bsX4hHHd+N&NEPbeT{CO}^mxi(pn>JOV-jJ41+hk)t`U76AL0lh zn+Z-xK(-vPF(@vigH!LQMgkzP>sKR9>XaQB>$Qv6hT)(^r# zOkn>o#{!K1AZ(4C_Dh(!F*Fq?~3TP;*l{+@Ua!fHA56&9_iGLWa)^0Ra+F876(`pKp%!10xhW zUi(zG5BpJ_I2vvQ^l|O^44XlwtUftzp1u_S@c$qA$(`4kP*S)CEh7zysZ*xlqCaXk z_k?5-)KPF9Q%%NAD~JqjAi{`A6@X7c4heSKRv-#_y2KZ(HF?Jy`bmgVE6jqunS?=*PoZ{7~Pf+Guu$-FL$oz zw!ohv1^F)+9vCnKTG~bT?hY5#q~|fpP92sv%~)xn1r?q9zz>YuxbMy(w8+_2SP*jT zAQNYcuMk^kJO_f{!$_=4QU0@A!85OcPGh@+fq}s;=!a~i6k+_3K_{ZLRBZ0f{+XaK z{3ha{wVvk+Q~~vNu!9kClV)K%7kDRm}?v{GLq&yk`#7(mcE>p-XmF4V9(Vz zvJ3oGNcp7NpexV{`$jIC$!Nx|%qli4syC-Oq|53F%IJU@V{I3{uKDq$8uhrhH9p06 zZl<43In={{?<5nGlh*#rOY`S!%DxxVOWs6zyPuyJd!eL~VQ0=}CH3vGH)H>2qH<

    U4H`Hhc!Zqb*a=?q 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

    iQr~r$dAdlnBN7;_DOt&%v`_g9m%Z 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 From eed8f546227f445a4a74777408f4772ecaf0ba61 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:10:52 -0500 Subject: [PATCH 227/332] update readme --- README.Rmd | 2 +- misc/readme/plot-tmb-si-1.png | Bin 937726 -> 17059604 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/README.Rmd b/README.Rmd index 213fa1bb..9afc488b 100644 --- a/README.Rmd +++ b/README.Rmd @@ -76,7 +76,7 @@ print(si) ``` Simulating from this model requires choosing the number of time-steps to run and the model quantities to simulate. Syntax for simulating `macpan2` models is [designed to combine with standard data prep and plotting tools in R](#modularity), as we demonstrate with the following code. -```{r plot-tmb-si, fig.dpi=1000} +```{r plot-tmb-si, fig.dpi=5000} (si |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) |> mp_trajectory() diff --git a/misc/readme/plot-tmb-si-1.png b/misc/readme/plot-tmb-si-1.png index 6f3d318b1775ed41d0118765a405557bd6b6918c..13e43a615bd98ffb8919b4c522b640d4f656c7cc 100644 GIT binary patch literal 17059604 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

    13ZR5#{iFk>&NT*0gutQ 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:jo`+ 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

    vO*>sBC_6WtIy7qqY0{f&nJk;2hWU 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

    _7 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

    }xs)qS04aIC?kjfto6q#;R0n^mw8dzf8;QM0O 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"~Gc3vmZQY3I6YTdwLDB6cYQS(L;o^pIDh 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

    iQr~r$dAdlnBN7;_DOt&%v`_g9m%Z z0Bir2o--3*bw=MlJhg(A#09Ie`>%EaY_TQc|8ml4u(wE23-U(AjwmcV_QExu+#N4D`aQJ{zjI4|qmt=-H&c^@ex zvLpbP3F$P`MA3GoFvWqtfi_o15)xg~`i1 z#{wl-!h<%D_rPsD@W2y+H|~&s#9Vav)s~stMw{K{1UJ0D#+G=$>DXxpx;3!=a+`5k z0NeJHM;vyh>)M0N+*~))d3YPd2+&M#?7cyzG+M|*aU#_&bdAuM0jW{;+T%I5bj zmYUyl`E#bkgb7LX4WnclvTbIv0zPE_wL*wyjQ}fraFIr$!QSIt{IUccO33J2r-#b# z!uB=+v_~8cn(sl)b{Be1$z4GgKHqpK?c#{`rk|?zC$q%vfy8CNTy=ie*U!B@(o4J= z1M*6ez+kk^Vektw9dzlRp<&$$&^iGV`2uR6Fr2QgE-V8n9%O33_JX${-}HQ zZJ3`NvY&yBst|y(6J8@H49Pl0{5uqSaoER@9Y$Wn;i(WJA5Hc-&rLPoUA#gvk*nG} zL>b}{z9T{hsZ}oF006vkLfaA0XeVH8D?)a?0;i?*cm#SR1!A>2-kMYQ4s+gsEb1UM z0%Kw=R+9HNgN&VtBf{zqDdE_{`gxQ8gB8R=ia)$8&z#)P1vgt^GvQr8NQ%K+3EP{o zCr_TB9p$1scN()EahDMj6?LBOYItr*&%S*VtW)-L*nIgh{e|K=L6t~nJj!c1VPgDk zH!`CzjNXwF1tTVCZuP`?f6KZY7-%RzaK4$m5hYyiMX|Xh)C}WpJ&oHuHY{ShQhC~N zAC+(mRp#6cQ4toM0E0T`(8>0L7EAZdBy_$Y*Pe~rQY6%NU&v`Ls;#X}$Y^{?>B`?f z;VOOk>e$rG1xe24^76j9=HT)UE9u~zB0Av*B{?he%Ly$bj#6izH&{sox^fiEk!Srw zDVEfP7D)U`CG8Kd_zwjmH#9f6pkh1u6(+JG<$1xZVBVTo^Q~HyJSX<6pW>#XkCm_C z79}eT{-yUu2Jy*7e9GyBcycfjgwdPjQ+}FKLii6L({+FSA7L}`sxP<@0XQM+C={kTe{qE@Gx)cuaFM*4!m&`0Unt1`(;bP0 zg8V{3{%5kzf45oWIOc_3`$Dh%A1{|I^x7AC?f(no^)n5PgV&i~`yW}!I0P@MmJ p%-Mez<%Qz>LUI0|FV5q0rru4gTf{|6^BeppbMV-K_ Date: Thu, 21 Dec 2023 10:11:26 -0500 Subject: [PATCH 228/332] update readme --- README.Rmd | 2 +- README.md | 2 +- misc/readme/plot-tmb-si-1.svg | 506 ++++++++++++++++++++++++++++++++++ 3 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 misc/readme/plot-tmb-si-1.svg diff --git a/README.Rmd b/README.Rmd index 9afc488b..96e5c587 100644 --- a/README.Rmd +++ b/README.Rmd @@ -76,7 +76,7 @@ print(si) ``` Simulating from this model requires choosing the number of time-steps to run and the model quantities to simulate. Syntax for simulating `macpan2` models is [designed to combine with standard data prep and plotting tools in R](#modularity), as we demonstrate with the following code. -```{r plot-tmb-si, fig.dpi=5000} +```{r plot-tmb-si, fig.format='svg'} (si |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) |> mp_trajectory() diff --git a/README.md b/README.md index 72332b8e..a5a58afd 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ code. ) ``` -![](misc/readme/plot-tmb-si-1.png) +![](misc/readme/plot-tmb-si-1.svg) ## Design Concepts diff --git a/misc/readme/plot-tmb-si-1.svg b/misc/readme/plot-tmb-si-1.svg new file mode 100644 index 00000000..2adcbde9 --- /dev/null +++ b/misc/readme/plot-tmb-si-1.svg @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ddf7c9ce3127557650e9a7c882d42e01dba66e8d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:14:33 -0500 Subject: [PATCH 229/332] update readme --- README.Rmd | 1 + 1 file changed, 1 insertion(+) diff --git a/README.Rmd b/README.Rmd index 96e5c587..94d68ccd 100644 --- a/README.Rmd +++ b/README.Rmd @@ -10,6 +10,7 @@ output: knitr::opts_chunk$set( fig.path = "misc/readme/" ) +ggplot2::theme_set(ggplot2::theme_bw(base_size = 18)) ``` ```{r packages, echo = FALSE, eval = TRUE, message=FALSE, warning=FALSE, error=FALSE} From 8793eb5621f3d5d015f8dbf0525cfb87bd1689de Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:18:33 -0500 Subject: [PATCH 230/332] svg update --- misc/readme/plot-tmb-si-1.svg | 420 +++++++++++++++++----------------- 1 file changed, 210 insertions(+), 210 deletions(-) diff --git a/misc/readme/plot-tmb-si-1.svg b/misc/readme/plot-tmb-si-1.svg index 2adcbde9..d53e4b14 100644 --- a/misc/readme/plot-tmb-si-1.svg +++ b/misc/readme/plot-tmb-si-1.svg @@ -3,504 +3,504 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - + + + + + + + + + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + - - + + - - + + - - + + - - + + - - - - - - - - - + + + + + + + + + - + - - + + - - + + - - + + - - + + - - + + - + - - + + - - + + - - + + - - - - - - - - - + + + + + + + + + - + - + - + - + - - - - + + + + - - - - + + + + - - - - - + + + + + From 5531570b46e847e70dcbd05dfee056b3570ad4ac Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 10:31:41 -0500 Subject: [PATCH 231/332] update readme --- README.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.Rmd b/README.Rmd index 94d68ccd..9c192b72 100644 --- a/README.Rmd +++ b/README.Rmd @@ -10,13 +10,13 @@ output: knitr::opts_chunk$set( fig.path = "misc/readme/" ) -ggplot2::theme_set(ggplot2::theme_bw(base_size = 18)) ``` ```{r packages, echo = FALSE, eval = TRUE, message=FALSE, warning=FALSE, error=FALSE} library(macpan2) library(ggplot2) library(dplyr) +theme_bw = function() ggplot2::theme_bw(base_size = 18) ``` From f203f813ccd3bbd931edc731d3a0b580eac98374 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 13:25:10 -0500 Subject: [PATCH 232/332] convolution and time variation examples --- misc/experiments/common-bits-and-pieces.R | 99 +++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 misc/experiments/common-bits-and-pieces.R diff --git a/misc/experiments/common-bits-and-pieces.R b/misc/experiments/common-bits-and-pieces.R new file mode 100644 index 00000000..9baa37f3 --- /dev/null +++ b/misc/experiments/common-bits-and-pieces.R @@ -0,0 +1,99 @@ +library(macpan2) +library(dplyr) +library(ggplot2) + +## complexities +## +## 1. time-varying parameters +## 2. reporting delays and under-reporting +## 3. hazard correction + +initializer = list(I ~ 1, S ~ N - I) +flow_rates = list( + infection ~ beta * S * I / N +) +state_updates = list( + S ~ S - infection + , I ~ I + infection +) +default = list(N = 100, beta = 0.25) + +si = mp_tmb_model_spec( + before = initializer + , during = c(flow_rates, state_updates) + , default = default +) +si |> mp_simulator(50, "infection") |> mp_trajectory() + + +## ----------------------- +## reporting delays and under-reporting +## ----------------------- + +## https://canmod.net/misc/flex_specs#computing-convolutions +reporting = list( + reports ~ 0.1 * convolution(infection, c(0.25, 0.25, 0.5)) +) +si_conv = mp_tmb_model_spec( + before = initializer + , during = c(flow_rates, state_updates, reporting) + , default = default +) +(si_conv + |> mp_simulator(50, c("infection", "reports")) + |> mp_trajectory() + |> ggplot() + + geom_line(aes(time, value, colour = matrix)) +) + + + +## ----------------------- +## time-varying parameters +## ----------------------- + +strain_takeover = list( + p ~ p0 / (p0 + (1 - p0) * exp(-delta_r * time_step(0))) + , beta ~ beta * (advantage * p + (1 - p)) +) +strain_default = list( + p0 = 1 / 20 + , advantage = 1.1 + , delta_r = 0.1 +) +si_strain = mp_tmb_model_spec( + before = initializer + , during = c(strain_takeover, flow_rates, state_updates, reporting) + , default = c(default, strain_default) +) +(si_strain + |> mp_simulator(50, c("p", "infection")) + |> mp_trajectory() + |> ggplot() + + geom_line(aes(time, value, colour = matrix)) +) + + + +## step-function version + +lockdown = list( + j ~ time_group(j, change_points) + , beta ~ beta_t[j] +) +lockdown_default = list( + j = 0 + , change_points = c(0, 10, 30) + , beta_t = c(0.5, 0.2, 0.5) +) +si_lockdown = mp_tmb_model_spec( + before = initializer + , during = c(lockdown, flow_rates, state_updates, reporting) + , default = c(default, lockdown_default) +) +(si_lockdown + |> mp_simulator(50, c("infection")) + |> mp_trajectory() + |> ggplot() + + geom_line(aes(time, value, colour = matrix)) +) From 423f2f0677fdc24bf8440fbac85cf4ca4062a467 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 13:46:45 -0500 Subject: [PATCH 233/332] update readme --- README.Rmd | 3 +-- README.md | 71 ++++++++++++++++++++++++++---------------------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/README.Rmd b/README.Rmd index 9c192b72..f390b11e 100644 --- a/README.Rmd +++ b/README.Rmd @@ -2,10 +2,9 @@ output: github_document: toc: true +toc-title: macpan2 --- -# macpan2 - ```{r opts, echo = FALSE} knitr::opts_chunk$set( fig.path = "misc/readme/" diff --git a/README.md b/README.md index a5a58afd..6882b7dc 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,40 @@ -- macpan2 - - Documentation - - Installation - - Hello World - - Design Concepts - - Information Processing - - Modularity - - Engine-Agnostic - Model Specification Language - - Engine-Specific - Model Specification Languages - - Product - Management - - General Dynamic Simulation - with TMB - - Model Library - - Calibration - - Time-Varying Parameters - - Vectors in the TMB Engine - - Model Structure and - Bookkeeping - - Structure Encourages - Reparameterization - - Alternative - Engines - - Combining Expression Lists - -# macpan2 +- Documentation +- Installation +- Hello World +- Design Concepts + - Information Processing + - Modularity + - Engine-Agnostic + Model Specification Language + - Engine-Specific + Model Specification Languages +- Product + Management + - General Dynamic Simulation + with TMB + - Model Library + - Calibration + - Time-Varying Parameters + - Vectors in the TMB Engine + - Model Structure and + Bookkeeping + - Structure Encourages + Reparameterization + - Alternative + Engines + - Combining Expression Lists From c1ef484dcaae0397247a2be9c1b45993ecef72ad Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 13:47:43 -0500 Subject: [PATCH 234/332] update readme --- README.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.Rmd b/README.Rmd index f390b11e..1e659210 100644 --- a/README.Rmd +++ b/README.Rmd @@ -2,7 +2,7 @@ output: github_document: toc: true -toc-title: macpan2 +toc-title: "macpan2" --- ```{r opts, echo = FALSE} From 7e8b5472bab030deaab3ab3986014f9ad2b6ed12 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 13:49:15 -0500 Subject: [PATCH 235/332] update readme --- README.Rmd | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.Rmd b/README.Rmd index 1e659210..f72eaf7e 100644 --- a/README.Rmd +++ b/README.Rmd @@ -1,8 +1,8 @@ --- +title: "macpan2" output: github_document: toc: true -toc-title: "macpan2" --- ```{r opts, echo = FALSE} diff --git a/README.md b/README.md index 6882b7dc..a0352150 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ +macpan2 +================ - Documentation - Installation From 0a26e166451b33fae510f149804e3607f50fbaea Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 13:57:33 -0500 Subject: [PATCH 236/332] update readme --- README.Rmd | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.Rmd b/README.Rmd index f72eaf7e..c95916e3 100644 --- a/README.Rmd +++ b/README.Rmd @@ -224,8 +224,8 @@ sir = mp_tmb_model_spec( , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) ) , default = list( - state = macpan2:::zero_vector(state_labels) - , flow_rate = macpan2:::zero_vector(flow$rate) + state = mp_zero_vector(state_labels) + , flow_rate = mp_zero_vector(flow$rate) , N = 100 , beta = 0.25 , gamma = 0.1 diff --git a/README.md b/README.md index a0352150..6b8e6ecd 100644 --- a/README.md +++ b/README.md @@ -431,8 +431,8 @@ sir = mp_tmb_model_spec( , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) ) , default = list( - state = macpan2:::zero_vector(state_labels) - , flow_rate = macpan2:::zero_vector(flow$rate) + state = mp_zero_vector(state_labels) + , flow_rate = mp_zero_vector(flow$rate) , N = 100 , beta = 0.25 , gamma = 0.1 From c197debb820e5bffba54bf521a17d2ccb14b7344 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 15:27:18 -0500 Subject: [PATCH 237/332] update readme --- README.Rmd | 8 ++++---- README.md | 24 +++++++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/README.Rmd b/README.Rmd index c95916e3..1a694b43 100644 --- a/README.Rmd +++ b/README.Rmd @@ -75,10 +75,10 @@ si = mp_tmb_model_spec( print(si) ``` -Simulating from this model requires choosing the number of time-steps to run and the model quantities to simulate. Syntax for simulating `macpan2` models is [designed to combine with standard data prep and plotting tools in R](#modularity), as we demonstrate with the following code. +Simulating from this model requires choosing the number of time-steps to run and the model outputs to generate. Syntax for simulating `macpan2` models is [designed to combine with standard data prep and plotting tools in R](#modularity), as we demonstrate with the following code. ```{r plot-tmb-si, fig.format='svg'} (si - |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) + |> mp_simulator(time_steps = 50, outputs = c("I", "infection")) |> mp_trajectory() |> mutate(quantity = case_match(matrix , "I" ~ "Prevalance" @@ -151,7 +151,7 @@ But it is not convenient if you would just like to simulate from it, which is wh ```{r tmb-library} ("starter_models" |> mp_tmb_library("sir", package = "macpan2") - |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_simulator(time_steps = 10, outputs = "I") |> mp_trajectory() ) ``` @@ -236,7 +236,7 @@ sir = mp_tmb_model_spec( ) ) (sir - |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_simulator(time_steps = 10, outputs = "I") |> mp_trajectory() ) ``` diff --git a/README.md b/README.md index 6b8e6ecd..ee2c577d 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,17 @@ si = mp_tmb_model_spec( print(si) ``` + ## + ## --------------------- + ## Default values: + ## + ## --------------------- + ## $N + ## [1] 100 + ## + ## $beta + ## [1] 0.25 + ## ## --------------------- ## Before the simulation loop (t = 0): ## --------------------- @@ -153,14 +164,13 @@ print(si) ## 3: I ~ I + infection Simulating from this model requires choosing the number of time-steps to -run and the model quantities to simulate. Syntax for simulating -`macpan2` models is [designed to combine with standard data prep and -plotting tools in R](#modularity), as we demonstrate with the following -code. +run and the model outputs to generate. Syntax for simulating `macpan2` +models is [designed to combine with standard data prep and plotting +tools in R](#modularity), as we demonstrate with the following code. ``` r (si - |> mp_simulator(time_steps = 50, quantities = c("I", "infection")) + |> mp_simulator(time_steps = 50, outputs = c("I", "infection")) |> mp_trajectory() |> mutate(quantity = case_match(matrix , "I" ~ "Prevalance" @@ -314,7 +324,7 @@ which is what the [model library](#model-library) is for. ``` r ("starter_models" |> mp_tmb_library("sir", package = "macpan2") - |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_simulator(time_steps = 10, outputs = "I") |> mp_trajectory() ) ``` @@ -443,7 +453,7 @@ sir = mp_tmb_model_spec( ) ) (sir - |> mp_simulator(time_steps = 10, quantities = "I") + |> mp_simulator(time_steps = 10, outputs = "I") |> mp_trajectory() ) ``` From 4f74202c9d47abb32cb77d405968a545a19b3fe1 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 15:27:45 -0500 Subject: [PATCH 238/332] vignette updates and better argument names --- NAMESPACE | 3 + R/expr_list.R | 4 +- R/mp.R | 15 +++- R/tmb_model.R | 104 +++++++++++++------------- man/TMBSimulator.Rd | 2 +- man/mp_simulator.Rd | 24 ++++++ vignettes/time_varying_parameters.Rmd | 33 ++++---- 7 files changed, 113 insertions(+), 72 deletions(-) create mode 100644 man/mp_simulator.Rd diff --git a/NAMESPACE b/NAMESPACE index 76d9dd0e..8c85f055 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -50,6 +50,8 @@ S3method(mp_vector,Vector) S3method(mp_vector,character) S3method(mp_vector,data.frame) S3method(mp_vector,numeric) +S3method(mp_zero_vector,Index) +S3method(mp_zero_vector,character) S3method(names,Index) S3method(names,IntVecs) S3method(names,Ledger) @@ -179,6 +181,7 @@ export(make_expr_parser) export(mk_calibrate) export(model_starter) export(mp_aggregate) +export(mp_calibrate) export(mp_cartesian) export(mp_catalogue) export(mp_choose) diff --git a/R/expr_list.R b/R/expr_list.R index dc088d69..89d0936e 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -174,7 +174,9 @@ ExprList = function( |> unique() ) } - + self$all_default_vars = function() { + setdiff(self$all_formula_vars(), self$all_derived_vars()) + } self$all_derived_vars = function() { all_vars = self$all_formula_vars() all_exprs = self$formula_list() diff --git a/R/mp.R b/R/mp.R index fb0ca3ec..43163764 100644 --- a/R/mp.R +++ b/R/mp.R @@ -688,7 +688,20 @@ mp_labels.Index = function(x, labelling_column_names) { } #' @export -mp_zero_vector = function(x, labelling_column_names, ...) { +mp_zero_vector = function(x, ...) { + UseMethod("mp_zero_vector") +} + +#' @export +mp_zero_vector.character = function(x, ...) { + (x + |> as.vector() + |> zero_vector() + ) +} + +#' @export +mp_zero_vector.Index = function(x, labelling_column_names, ...) { (x |> mp_subset(...) |> mp_labels(labelling_column_names) diff --git a/R/tmb_model.R b/R/tmb_model.R index ed7b2b41..5ed23c7a 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -136,12 +136,12 @@ TMBModel = function( self$simulator = function( tmb_cpp = getOption("macpan2_dll") , initialize_ad_fun = TRUE - , quantities = NULL + , outputs = NULL ) { TMBSimulator(self , tmb_cpp = tmb_cpp , initialize_ad_fun = initialize_ad_fun - , quantities = quantities + , outputs = outputs ) } @@ -195,16 +195,20 @@ TMBModelSpec = function( self$must_save = must_save self$must_not_save = must_not_save self$sim_exprs = sim_exprs - self$simulator_fresh = function(time_steps = 0, quantities = character()) { + self$simulator_fresh = function( + time_steps = 0 + , outputs = character() + , default = list() + ) { initial_mats = self$default - matrix_quantities = intersect(quantities, names(initial_mats)) - row_quantities = setdiff(quantities, matrix_quantities) + matrix_outputs = intersect(outputs, names(initial_mats)) + row_outputs = setdiff(outputs, matrix_outputs) mats_to_return = (initial_mats |> lapply(names) |> Filter(f = is.character) - |> Filter(f = \(x) any(x %in% row_quantities)) + |> Filter(f = \(x) any(x %in% row_outputs)) |> names() - |> c(matrix_quantities) + |> c(matrix_outputs) |> unique() ) mats_to_save = (mats_to_return @@ -232,7 +236,9 @@ TMBModelSpec = function( int_vecs = do.call(IntVecs, self$integers) ) , time_steps = Time(as.integer(time_steps)) - )$simulator(quantities = quantities) + )$simulator(outputs = outputs, initialize_ad_fun = FALSE) + do.call(s$update$matrices, default) + s$ad_fun() s } self$simulator_cached = memoise(self$simulator_fresh) @@ -241,7 +247,12 @@ TMBModelSpec = function( #' @export print.TMBModelSpec = function(x, ...) { - print(ExprList(x$before, x$during, x$after)) + e = ExprList(x$before, x$during, x$after) + cat("\n---------------------\n") + msg("Default values:\n") |> cat() + cat("\n---------------------\n") + print(x$default[e$all_default_vars()]) + print(e) } #' @export @@ -267,16 +278,33 @@ mp_tmb_model_spec = function( TMBModelSpec(before, during, after, c(default, em), integers) } + +#' Simulator +#' +#' Construct a simulator from a model specification object. +#' +#' @param model A model specification object. +#' @param time_steps How many time steps should be simulated when simulations +#' are requested? +#' @param outputs Character vector of names of model quantities that will be +#' outputted when simulations are requested. +#' @param default Named list of numerical objects that will update the default +#' values defined in the model specification object. Any number of objects +#' can be updated or not. +#' #' @export -mp_simulator = function(model, time_steps, quantities) { +mp_simulator = function(model, time_steps, outputs, default = list()) { UseMethod("mp_simulator") } #' @export -mp_simulator.TMBModelSpec = function(model, time_steps, quantities) { - model$simulator_fresh(time_steps, quantities) +mp_simulator.TMBModelSpec = function(model, time_steps, outputs, default = list()) { + model$simulator_fresh(time_steps, outputs, default) } +#' @export +mp_calibrate = function(model, data) {} + #' @export mp_default = function(model_simulator, ...) { UseMethod("mp_default") @@ -304,12 +332,12 @@ mp_final = function(model_simulator, ...) { } -mp_final.TMBSimulator = function(model_simulator, time_steps, quantities, ...) { +mp_final.TMBSimulator = function(model_simulator, time_steps, outputs, ...) { (model_simulator $replace $time_steps(time_steps) $update - $matrices(.mats_to_return = quantities, .mats_to_save = quantities) + $matrices(.mats_to_return = outputs, .mats_to_save = outputs) $report(..., .phases = "after") ) } @@ -325,34 +353,6 @@ mp_trajectory.TMBSimulator = function(model) { model$report() } -# @export -# mp_trajectory.TMBModelSpec = function(model) { -# (model -# |> mp_tmb_simulator(time_steps, quantities) -# |> mp_trajectory(time_steps, quantities) -# ) - - # matrix_quantities = intersect(quantities, names(model$default)) - # row_quantities = setdiff(quantities, matrix_quantities) - # mats_to_return = (model$default - # |> lapply(names) - # |> Filter(f = is.character) - # |> Filter(f = \(x) any(x %in% row_quantities)) - # |> names() - # |> c(matrix_quantities) - # |> unique() - # ) - # simulations = model$simulator_fresh( - # time_steps = time_steps - # , mats_to_return = mats_to_return - # )$report() - # macpan2:::filter( - # (matrix %in% matrix_quantities) - # | (row %in% row_quantities) - # ) - # rownames(simulations) = NULL - # simulations -#} TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { @@ -444,8 +444,8 @@ TMBSimulationUtils = function() { r = r[r$time != num_t + 1L,,drop = FALSE] } r |> macpan2:::filter( - (matrix %in% self$matrix_quantities()) - | (row %in% self$row_quantities()) + (matrix %in% self$matrix_outputs()) + | (row %in% self$row_outputs()) ) } self$.find_problematic_expression = function(row) { @@ -513,23 +513,23 @@ TMBSimulationUtils = function() { TMBSimulator = function(tmb_model , tmb_cpp = getOption("macpan2_dll") , initialize_ad_fun = TRUE - , quantities = NULL ## character vector of matrix and/or row names + , outputs = NULL ## character vector of matrix and/or row names ) { self = TMBSimulationUtils() ## Args self$tmb_model = tmb_model self$tmb_cpp = tmb_cpp - self$quantities = quantities + self$outputs = outputs - self$matrix_quantities = function() { - if (is.null(self$quantities)) return(self$tmb_model$init_mats$.mats_to_return) + self$matrix_outputs = function() { + if (is.null(self$outputs)) return(self$tmb_model$init_mats$.mats_to_return) initial_mats = self$tmb_model$init_mats$.initial_mats - intersect(self$quantities, names(initial_mats)) + intersect(self$outputs, names(initial_mats)) } - self$row_quantities = function() { - if (is.null(self$quantities)) return(character(0L)) - setdiff(self$quantities, self$matrix_quantities()) + self$row_outputs = function() { + if (is.null(self$outputs)) return(character(0L)) + setdiff(self$outputs, self$matrix_outputs()) } ## Standard Methods diff --git a/man/TMBSimulator.Rd b/man/TMBSimulator.Rd index c4935b7f..d0c6f172 100644 --- a/man/TMBSimulator.Rd +++ b/man/TMBSimulator.Rd @@ -8,7 +8,7 @@ TMBSimulator( tmb_model, tmb_cpp = getOption("macpan2_dll"), initialize_ad_fun = TRUE, - quantities = NULL + outputs = NULL ) } \arguments{ diff --git a/man/mp_simulator.Rd b/man/mp_simulator.Rd new file mode 100644 index 00000000..913277e2 --- /dev/null +++ b/man/mp_simulator.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tmb_model.R +\name{mp_simulator} +\alias{mp_simulator} +\title{Simulator} +\usage{ +mp_simulator(model, time_steps, outputs, default = list()) +} +\arguments{ +\item{model}{A model specification object.} + +\item{time_steps}{How many time steps should be simulated when simulations +are requested?} + +\item{outputs}{Character vector of names of model quantities that will be +outputted when simulations are requested.} + +\item{default}{Named list of numerical objects that will update the default +values defined in the model specification object. Any number of objects +can be updated or not.} +} +\description{ +Construct a simulator from a model specification object. +} diff --git a/vignettes/time_varying_parameters.Rmd b/vignettes/time_varying_parameters.Rmd index 1e37d588..05e2c2ab 100644 --- a/vignettes/time_varying_parameters.Rmd +++ b/vignettes/time_varying_parameters.Rmd @@ -34,19 +34,19 @@ knitr::knit_hooks$set(basefig = basefig_setup) Here we modify an [SIR](https://canmod.github.io/macpan2/articles/quickstart) model so that transmission rate is time-varying. ```{r baseline_sir} -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -simulator = sir$simulators$tmb(time_steps = 50 - , state = c(S = 99, I = 1, R = 0) - , flow = c(foi = NA, gamma = 0.2) - , N = empty_matrix - , beta = 0.8 +state_labels = c("S", "I", "R") +simulator = ("starter_models" + |> mp_tmb_library("sir", package = "macpan2") + |> mp_simulator(time_steps = 50 + , outputs = state_labels + , default = list(beta = 0.8, gamma = 0.2) + ) ) -(simulator$report(.phases = "during") - %>% rename(state = row) - %>% mutate(state = factor(state, sir$labels$state())) - %>% ggplot() + geom_line(aes(time, value, colour = state)) +(simulator + |> mp_trajectory() + |> mutate(state = factor(matrix, state_labels)) + |> ggplot() + geom_line(aes(time, value, colour = state)) ) - ``` ## Piecewise Time Variation @@ -84,10 +84,9 @@ simulator$insert$expressions( And that's it. Now we plot the updated simulations using these change-points, which we highlight with vertical lines. ```{r time_varying_graph} -s = simulator$report(.phases = "during") +s = mp_trajectory(simulator) (s - %>% rename(state = row) - %>% mutate(state = factor(state, sir$labels$state())) + %>% mutate(state = factor(matrix, state_labels)) %>% ggplot() + geom_line(aes(time, value, colour = state)) + geom_vline( @@ -106,7 +105,7 @@ First we simulate data to fit our model to, to see if we can recover the time-va set.seed(1L) I_observed = rpois( 50, - filter(s, matrix == "state", row == "I")$value + filter(s, matrix == "I")$value ) plot(I_observed) ``` @@ -123,7 +122,7 @@ simulator$add$matrices( ## location of I in the state vector ## (the `-1L` bit is to get 0-based indices instead of 1-based) - I_index = match("I", sir$labels$state()) - 1L, + I_index = match("I", state_labels) - 1L, ## matrix to contain the log likelihood values at ## each time step @@ -137,7 +136,7 @@ simulator$add$matrices( Now we need some new expressions. The first expression pulls out the I state from the state vector. ```{r pull_out_trajectory} simulator$insert$expressions( - I_sim ~ state[I_index], + I_sim ~ I, .phase = "during" ) ``` From 56cd18423f4dfb5d666098056bd048a9bcbbd93b Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 15:30:49 -0500 Subject: [PATCH 239/332] update readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ee2c577d..4ee1ab6f 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,6 @@ print(si) ## ## --------------------- ## Default values: - ## ## --------------------- ## $N ## [1] 100 From b1a71d161d92a856025cd80160f38729d60ddc1a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 15:37:34 -0500 Subject: [PATCH 240/332] print method --- R/tmb_model.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/tmb_model.R b/R/tmb_model.R index 5ed23c7a..604dc72c 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -250,8 +250,8 @@ print.TMBModelSpec = function(x, ...) { e = ExprList(x$before, x$during, x$after) cat("\n---------------------\n") msg("Default values:\n") |> cat() - cat("\n---------------------\n") - print(x$default[e$all_default_vars()]) + cat("---------------------\n") + str(x$default) print(e) } From 5fc1ab7b85fb47e80950f39e1e927a7ebabee5ab Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 15:37:50 -0500 Subject: [PATCH 241/332] update readme --- README.Rmd | 4 ++-- README.md | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.Rmd b/README.Rmd index 1a694b43..04be132a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -214,8 +214,8 @@ flow = data.frame( ) sir = mp_tmb_model_spec( before = list( - state[I] ~ 1 - , state[S] ~ N - 1 + state[S] ~ N - 1 + , state[I] ~ 1 , state[R] ~ 0 ) , during = list( diff --git a/README.md b/README.md index 4ee1ab6f..9d04326d 100644 --- a/README.md +++ b/README.md @@ -143,12 +143,12 @@ print(si) ## --------------------- ## Default values: ## --------------------- - ## $N - ## [1] 100 - ## - ## $beta - ## [1] 0.25 - ## + ## List of 5 + ## $ N : num 100 + ## $ beta : num 0.25 + ## $ I : num[0 , 0 ] + ## $ S : num[0 , 0 ] + ## $ infection: num[0 , 0 ] ## --------------------- ## Before the simulation loop (t = 0): ## --------------------- @@ -430,8 +430,8 @@ flow = data.frame( ) sir = mp_tmb_model_spec( before = list( - state[I] ~ 1 - , state[S] ~ N - 1 + state[S] ~ N - 1 + , state[I] ~ 1 , state[R] ~ 0 ) , during = list( From 44b74d93962815d25594809eed526dd1269f3213 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 16:19:06 -0500 Subject: [PATCH 242/332] update readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9d04326d..1711ef02 100644 --- a/README.md +++ b/README.md @@ -143,12 +143,10 @@ print(si) ## --------------------- ## Default values: ## --------------------- - ## List of 5 - ## $ N : num 100 - ## $ beta : num 0.25 - ## $ I : num[0 , 0 ] - ## $ S : num[0 , 0 ] - ## $ infection: num[0 , 0 ] + ## row col value.N value.beta + ## N 100 0.25 + ## beta 100 0.25 + ## ## --------------------- ## Before the simulation loop (t = 0): ## --------------------- From 8ce711fc4d5bbe7b2bd6b6c6069516092e977ece Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 16:19:19 -0500 Subject: [PATCH 243/332] default printing --- R/lists.R | 30 ++++++++++++++++++++++++++++++ R/tmb_model.R | 21 +++++++++++++-------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/R/lists.R b/R/lists.R index 465c3105..7c435755 100644 --- a/R/lists.R +++ b/R/lists.R @@ -17,6 +17,36 @@ nlist = function(...) { setNames(L, nm) } + +melt_matrix = function(x) { + dn = dimnames(x) + nms = names(x) + dm = dim(x) + if (is.null(dm)) { + col = "" + if (is.null(nms)) { + row = seq_along(x) |> as.character() + } else { + row = names(x) + } + } else if (is.null(dn)) { + row = rep(seq_len(dm[1]), each = dm[2]) |> as.character() + col = rep(seq_len(dm[2]), times = dm[1]) |> as.character() + } else { + row = rep(rownames(x), times = dm[2]) + col = rep(colnames(x), each = dm[1]) + } + data.frame(row = row, col = col, value = as.vector(x)) +} +melt_default_matrix_list = function(x) { + f = (x + |> lapply(melt_matrix) + |> macpan2:::bind_rows(.id = "matrix") + ) + rownames(f) = NULL + f +} + empty_named_list = function() list() |> setNames(character(0L)) assert_named_list = function(l) { diff --git a/R/tmb_model.R b/R/tmb_model.R index 604dc72c..9c0e94fb 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -195,12 +195,20 @@ TMBModelSpec = function( self$must_save = must_save self$must_not_save = must_not_save self$sim_exprs = sim_exprs + + self$empty_matrices = function() { + e = ExprList(self$before, self$during, self$after) + dv = setdiff(e$all_derived_vars(), names(self$default)) + rep(list(empty_matrix), length(dv)) |> setNames(dv) + } + self$matrices = function() c(self$default, self$empty_matrices()) + self$simulator_fresh = function( time_steps = 0 , outputs = character() , default = list() ) { - initial_mats = self$default + initial_mats = self$matrices() matrix_outputs = intersect(outputs, names(initial_mats)) row_outputs = setdiff(outputs, matrix_outputs) mats_to_return = (initial_mats @@ -219,7 +227,7 @@ TMBModelSpec = function( init_mats = do.call( MatsList , c( - self$default + self$matrices() , list( .mats_to_return = mats_to_return , .mats_to_save = mats_to_save @@ -251,7 +259,8 @@ print.TMBModelSpec = function(x, ...) { cat("\n---------------------\n") msg("Default values:\n") |> cat() cat("---------------------\n") - str(x$default) + print(melt_matrix(x$default), row.names = FALSE) + cat("\n") print(e) } @@ -271,11 +280,7 @@ mp_tmb_model_spec = function( |> unlist() |> c(integers) ) - ## TODO: make this work when integers != list() - e = ExprList(before, during, after) - dv = setdiff(e$all_derived_vars(), names(default)) - em = rep(list(empty_matrix), length(dv)) |> setNames(dv) - TMBModelSpec(before, during, after, c(default, em), integers) + TMBModelSpec(before, during, after, default, integers) } From d373f6a91bd46ea28882ce63546777c8ae031a72 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 16:22:16 -0500 Subject: [PATCH 244/332] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1711ef02..b3428e36 100644 --- a/README.md +++ b/README.md @@ -143,9 +143,9 @@ print(si) ## --------------------- ## Default values: ## --------------------- - ## row col value.N value.beta - ## N 100 0.25 - ## beta 100 0.25 + ## matrix row col value + ## N 1 100.00 + ## beta 1 0.25 ## ## --------------------- ## Before the simulation loop (t = 0): From 6b6bf1cdfe78103a6b049a8cba8e4232c6f11419 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 16:28:22 -0500 Subject: [PATCH 245/332] print fussing --- R/lists.R | 19 ++++++++++++++++--- R/tmb_model.R | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/R/lists.R b/R/lists.R index 7c435755..1de849aa 100644 --- a/R/lists.R +++ b/R/lists.R @@ -25,19 +25,32 @@ melt_matrix = function(x) { if (is.null(dm)) { col = "" if (is.null(nms)) { - row = seq_along(x) |> as.character() + if (length(x) == 1L) { + row = "" + } else { + row = seq_along(x) |> as.character() + } } else { row = names(x) } } else if (is.null(dn)) { - row = rep(seq_len(dm[1]), each = dm[2]) |> as.character() - col = rep(seq_len(dm[2]), times = dm[1]) |> as.character() + if (dm[1] == 1L) { + row = "" + } else { + row = rep(seq_len(dm[1]), each = dm[2]) |> as.character() + } + if (dm[2] == 1L) { + col = "" + } else { + col = rep(seq_len(dm[2]), times = dm[1]) |> as.character() + } } else { row = rep(rownames(x), times = dm[2]) col = rep(colnames(x), each = dm[1]) } data.frame(row = row, col = col, value = as.vector(x)) } + melt_default_matrix_list = function(x) { f = (x |> lapply(melt_matrix) diff --git a/R/tmb_model.R b/R/tmb_model.R index 9c0e94fb..05383397 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -259,7 +259,7 @@ print.TMBModelSpec = function(x, ...) { cat("\n---------------------\n") msg("Default values:\n") |> cat() cat("---------------------\n") - print(melt_matrix(x$default), row.names = FALSE) + print(melt_default_matrix_list(x$default), row.names = FALSE) cat("\n") print(e) } From e38d52df4a9486b9da6eda8dc05535577105b19a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 21 Dec 2023 16:28:30 -0500 Subject: [PATCH 246/332] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3428e36..566d5bc7 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,8 @@ print(si) ## Default values: ## --------------------- ## matrix row col value - ## N 1 100.00 - ## beta 1 0.25 + ## N 100.00 + ## beta 0.25 ## ## --------------------- ## Before the simulation loop (t = 0): From 8121262d9f7941338da79f5491899ed1e25faa19 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 22 Dec 2023 11:04:11 -0500 Subject: [PATCH 247/332] working on #154 --- vignettes/engine_agnostic_grammar.Rmd | 548 +++++++++++++++++++++++++ vignettes/example_models.Rmd | 14 +- vignettes/quickstart.Rmd | 550 ++------------------------ 3 files changed, 584 insertions(+), 528 deletions(-) create mode 100644 vignettes/engine_agnostic_grammar.Rmd diff --git a/vignettes/engine_agnostic_grammar.Rmd b/vignettes/engine_agnostic_grammar.Rmd new file mode 100644 index 00000000..729173f9 --- /dev/null +++ b/vignettes/engine_agnostic_grammar.Rmd @@ -0,0 +1,548 @@ +--- +title: "Engine-Agnostic Model Specification Grammar" +output: + html_document: + toc: true + keep_md: yes +vignette: > + %\VignetteIndexEntry{Engine-Agnostic Model Specification Grammar} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + chunk_output_type: console +--- + +[![status](https://img.shields.io/badge/status-working%20draft-red)](https://canmod.github.io/macpan2/articles/vignette-status#working-draft) + +```{r setup, echo = FALSE, message = FALSE, warning = FALSE} +library(macpan2) +library(ggplot2) +library(dplyr) +``` + +One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when building upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. + +There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, especially when they are cast as expansions of simple models. Such [structured models](https://idpjournal.biomedcentral.com/articles/10.1186/s40249-022-01001-y) can include: + +- multiple pathogen strains +- multiple infection types (_e.g._, asymptomatic and symptomatic or mild and severe) +- age-structure +- multiple locations (a metapopulation model) +- testing processes to identify infections +- vaccination status + +This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. + +# Amuse bouche: a structured SIR model {#amuse-bouche} + +A key to `macpan2`'s flexible model grammar is the use of **functional forms** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): + +

    +![](../misc/diagrams/quickstart/two-strain.svg) +
    + +Here, + +- $S$, $I_x$, and $R$ are the numbers of individuals that are susceptible, infected with strain $x$ ($A$ or $B$), and recovered, respectively, +- $N= S + I_A + I_B + R$ is the total population size, +- $\beta_x$ is the transmission rate for strain $x$, +- $\gamma$ is the recovery rate for infected individuals,^[In this model we are choosing to make the recovery rate (and hence the infectious period) the same for both strains, but it would be easy to relax this assumption in the model specification by following the way in which we stratify the state variable $I$ by strain below.] +- $\lambda_x = \beta_x (I_x)/N$ is the force of infection for strain $x$. + +We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: + +\begin{align} +S_{t+1} & = - [\beta_A (I_A)_t/N + \beta_B (I_B)_t/N] S_t, \\ +(I_A)_{t+1} &= \phantom{-[} \beta_A (I_A)_t/N - \gamma (I_A)_t, \\ +(I_B)_{t+1} &= \phantom{-[} \beta_B (I_B)_t/N - \gamma (I_B)_t, \\ +R_{t+1} &= \phantom{-[} \gamma (I_A)_t + \gamma (I_B)_t. +\end{align} + +Each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **functional form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: + +``` +lambda.A = beta.A * I.A / N +lambda.B = beta.B * I.B / N +``` + +However, in `macpan2`, we can specify a single functional form for it, for instance + +``` +lambda = beta * I / N +``` + +and then attach a **ledger** to the model object that tabulates specific instances of when this functional form is used to define a component of the model. In other words, this ledger should enumerate which specific subscripted `lambda`, `beta`, and `I` to use each time we invoke the associated functional form during the simulation. + +In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each of two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). + +**Using functional forms and ledgers allows the modeller to focus on modelling questions**, like the design of the model structure and the choice of expressions for the forces of infection, while **`macpan2` handles the bookkeeping**, matching stratified variables with each other when calculating expressions. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, rather than error-prone editing of calculations in the simulation code. + +While a modeller could write their own code to cut down on repetition when expanding a simple model (and many do), `macpan2` provides a ready-made model specification grammar that enables easy model extension, especially when building [product models](https://arxiv.org/abs/2307.10308), and that can readily interface with fast simulation and calibration engines, like [TMB](https://cran.r-project.org/web/packages/TMB/index.html). + +# Appetizer: specifying the basic SIR model + +Let's start with specifying the basic SIR model, the foundation of the two-strain model above, in `macpan2`: + +\begin{align} +S_{t+1} &= -\beta S_t I_t/N, \\ +I_{t+1} &= \phantom{-} \beta S_t I_t/N - \gamma I_t, \\ +R_{t+1} &= \phantom{-} \gamma I_t. +\end{align} + +It will be helpful to set $\lambda = \beta I/N$ and recast the equations as: + +\begin{align} +S_{t+1} &= -\lambda S_t, \\ +I_{t+1} &= \phantom{-} \lambda S_t - \gamma I_t, \\ +R_{t+1} &= \phantom{-} \gamma I_t. +\end{align} + +Since the focus of this quickstart guide is `macpan2`'s model specification grammar, we have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model using the model grammar and it will output a model object from which we can build a simulator. Our primary focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. + +```{r SIR-starter, echo = FALSE} +## helper function to simplify the exposition in this vigette ----------- +SIR_starter <- function( + # index tables for model quantities + state, + rate, + # ledgers tabulating the use of different functional forms + flow, # list of individual ledgers + force_of_infection +){ + + ## Set up expressions list for each functional form -------------- + ## names refer to when the calculation gets performed relative to + ## the simulation time-step loop (before, during, ...) + expr_list <- mp_expr_list( + before = list( + ## aggregations + N ~ sum(state) + ), + during = list( + ## force of infections + rate[infection_flow_rates] ~ + state[infectious_states] * rate[transmission_rates] / N + + ## unsigned individual flows + , flow_per_time ~ state[from_states] * rate[flow_rates] + + ## state update + , total_inflow ~ groupSums(flow_per_time, to_states, state) + , total_outflow ~ groupSums(flow_per_time, from_states, state) + , state ~ state + total_inflow - total_outflow + ) + ) + + ## Ledgers for each specific calculation -------------- + ledgers <- list( + flow = mp_ledgers(flow), + force_of_infection = mp_ledgers(force_of_infection) + ) + + ## Initialize vectors from index tables (with all zeros for values) -------------- + # used as placeholders for user input + init_vecs <- list( + state = mp_vector(state), + rate = mp_vector(rate) + ) + + ## Initialize model object ----------------- + mp_dynamic_model( + expr_list = expr_list, + ledgers = ledgers, + init_vecs = init_vecs + ) +} +``` + +The inputs to `SIR_starter()` are of two types: + +- **index tables** containing indices (labels) of model quantities, +- **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included functional forms). + +The index tables we need to specify fall into two groups: + +- `state`: state names, $S$, $I$, and $R$ from the model equations +- `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ + +We have identified two useful **functional forms** that we have baked into `SIR_starter()`. In this case, we're thinking of these forms not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The forms are: + +- **flow**: Unsigned flows from one class to another of the form $rX$, with $r>0$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. +- **force of infection**: The prevalence-dependent *per capita* rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. + +In this case, the flow form is repeated within these model equations, while the force of infection form is used only once. We've identified the force of infection as a functional form since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these forms are already baked into `SIR_starter()`, so our task will be creating a ledger for each of these forms to input into the function. + +We start by creating the `state` and `rate` index tables: + +```{r sir-index-tables} +## index tables to label model quantities ------------------------- +state <- mp_index(Epi = c("S", "I", "R")) +rate <- mp_index(Epi = c("beta", "gamma", "lambda")) +``` + +The `mp_index()` function sets structures like data frames that tabulate the model quantity labels: + +```{r sir-state-and-rate} +state +rate +``` + +The `Epi` column name is unimportant in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. + +For the flow form, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$ and then pass these as a list to the `flow` argument of `SIR_starter()`. We specify flows using the name of the state from which it originates (`from_states`), the state to which it goes (`to_states`), and a flow rate name (`flow_rates`). + +We use the `mp_join()` function to create the `infection` ledger like so: + +```{r sir-infection-ledger} +## infection ledger ------------------------- +infection <- mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") +) +``` + +The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, e.g. + +```{r sir-infection-ledger-inputs} +mp_subset(state, Epi = "S") +mp_subset(state, Epi = "I") +mp_subset(rate, Epi = "lambda") +``` + +and by default creates one entry in the ledger for each combination of these values (_i.e._, a [full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)). However, since there is only one value in each column, there is only one entry in the resulting ledger: + +```{r sir-infection-ledger-2} +infection +``` + +The names of the arguments in the `mp_join()` function are tied to how the functional form baked into `SIR_starter()` is specified, but in general modellers can define their functional forms and the corresponding `mp_join()` argument names however they like.^[There is only one `mp_join()` argument name that is not available to the user — `by`, which has a special role that we will see [later](#main-course).] + +We create the `recovery` ledger in a similar way: + +```{r sir-recovery-ledger} +## recovery ledger ------------------------- +recovery <- mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) + +recovery +``` + +Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different functional form in `SIR_starter()` (so the `mp_join()` argument names are different): + +```{r sir-foi-ledger} +## force of infection ledger ------------------------- +# infection additionally involves the calculation of a force of infection +force_of_infection <- mp_join( + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta"), + infection_flow_rates = mp_subset(rate, Epi = "lambda") +) +``` + +For this functional form, we need to specify the `transmission_rates` and `infectious_states` involved in computing the force of infection, as well as the names where we want to store the results of this calculation (`infection_flow_rates`) for use in the `infection` flow calculations. + +Now we can use the `SIR_starter()` function to initialize our model object: + +```{r sir} +## SIR model object ------------------------- +sir <- SIR_starter( + # index tables + state = state, + rate = rate, + # ledgers + flow = list( + infection, + recovery + ), + force_of_infection = force_of_infection +) +``` + +We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for each index (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): + +```{r sir-simulator} +## SIR model simulator ------------------------- +sir_simulator <- mp_tmb_simulator( + dynamic_model = sir, + vectors = list( + state = c(S = 999, I = 1, R = 0), + rate = c(beta = 0.25, gamma = 0.1) + ), + time_steps = 100 +) +``` + +Note that we've specified `NA` for `lambda` as it will be calculated for us using the force of infection functional form. + +Then we can actually simulate the model by passing our model simulator to `mp_report()`: + +```{r sir-results} +## SIR model simulation results ------------------------- +sir_results <- mp_report(sir_simulator) +``` + +The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer): + +```{r sir-results-head} +head(sir_results) +``` + +The simulation output has several columns: + +- `matrix`: The matrix storing our values internally, corresponding to our two index tables, `state` and `rate`. +- `time`: An internal time index, where `time = 1` is the result after the first step through the simulation loop. +- `row`: The primary label for the `value` (the row name in the corresponding `matrix`). +- `col`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the outputs of this model (i.e. states and rates) are specified as vectors and not matrices, this column is empty for all entries. TODO: When would this be useful? +- `value`: The numerical value. + +This output can be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`, e.g.: + +```{r sir-ggplot-example, fig.width = 6, fig.height = 4} +(sir_results + |> filter(matrix == "state") # keep just the state variables at each point in time + |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in legend + |> ggplot(aes(time, value, colour = state)) + + geom_line() +) +``` + +(Above, we used the [base R pipe operator](https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/#pipes), `|>`.) + +If you prefer to make plots in base R, you can convert the long format data to wide format: + +```{r pivot_wider} +sir_results_wide <- (sir_results + |> dplyr::filter(matrix == "state") # keep state variables at each point in time + ## drop unneeded columns before pivoting + |> dplyr::select(-c(matrix, col)) + |> tidyr::pivot_wider(id_cols = time, names_from = row) +) + +head(sir_results_wide, n = 3) +``` + +We can plot one state like so + +```{r sir-base-plot-ex, fig.width = 6} +with(sir_results_wide, + plot(x = time, + y = I, + type = "l") +) +``` + +or multiple states on the same plot with + +```{r sir-base-matplot-ex, fig.width = 6} +par(las = 1) ## horizontal y-axis ticks +matplot(sir_results_wide[, 1], + sir_results_wide[,-1], + type = "l", + xlab = "time", ylab = "") +legend("left", col = 1:3, lty = 1:3, legend = state$labels()) +``` + +# Main course: expanding the basic SIR with additional structure {#main-course} + +As previously noted, we created a force of infection functional form ($\beta I / N$) despite it only being used once to define the SIR model. However, if we consider the two-strain model from [before](#amuse-bouche), we see this calculation is repeated for each strain: + +\begin{align} +\lambda_A &= \beta_A I_A/N \\ +\lambda_B &= \beta_B I_B/N +\end{align} + +Since we already have a form for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. + +To define the two-strain model, we again must specify our `state` and `rate` index tables, as well as our `infection`, `recovery`, and `force_of_infection` ledgers. + +We start by creating a new set of indices for the strains: + +```{r strain-indices} +Strain_indices <- c("A", "B") +``` + +A simple approach would be to define a table of the new state and rate indices directly using the `mp_index()` function, as we did above: + +``` +state <- mp_index( + Epi = c("S", rep("I", 2), "R"), + Strain = c("", Strain_indices, "") +) + +rate <- mp_index( + Epi = c(rep(c("beta", "lambda"), 2), "gamma"), + Strain = c(rep(c("A", "B"), each = 2), "") +) +``` + +However, this approach is less flexible if we want to build a complex model or if we already have a simpler, working model (like the SIR above) and want expand it with many strata and/or several different types of strata. We present an alternative approach below that is more verbose but far more flexible. + +For the state, we want to cross $I$ with the different strains to create one $I$ compartment name per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: + +```{r strain-expand-I} +I_indices <- mp_cartesian( + mp_subset(state, Epi = "I"), + mp_index(Strain = Strain_indices) +) + +I_indices +``` + +This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names get treated like any other component used to label a particular compartment (like location or age group).] + +We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function to make a `state` index table: + +```{r two-strain-state} +state <- mp_union( + mp_subset(state, Epi = "S"), + I_indices, + mp_subset(state, Epi = "R") +) + +state +``` + +We update the `rate` index table similarly: + +```{r two-strain-rate} +rate <- + mp_union( + # stratify rates involved in the infection process by strain + mp_cartesian( + mp_subset(rate, Epi = c("beta", "lambda")), + mp_index(Strain = Strain_indices) + ), + # recovery rate will be the same across strains + mp_subset(rate, Epi = "gamma") +) + +rate +``` + +For the `infection` ledger, let's see what our previous code for generating it yields now that we are (partially) stratifying by `Strain`: + +```{r two-strain-infection-default} +# infection ledger from before +mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") +) +``` + +As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join), where the individual indices, denoted by values in the `Epi` and `Strain` columns, are dot-concatenated for the full quantity labels. + +For this model, we want only two of these flows: + +- a flow between `S` and `I.A` with flow rate `lambda.A` +- a flow between `S` and `I.B` with flow rate `lambda.B` + +In other words, we want the `Strain` index on `I` to match with the `Strain` index on `lambda`. We can specify this within `mp_join()` when building the ledger like so: + +```{r two-strain-infection-ledger} +## new infection ledger ------------------------- +infection <- mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda"), + by = list( + to_states.flow_rates = "Strain" + ) +) + +infection +``` + +Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two of the index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list element name (`to_states.flow_rates`), with the names coming from `mp_join()`'s argument names (`to_states`, `flow_rates`). The list element value should be a character string corresponding to the index table column name upon which to perform matches. In this case, the value is `"Strain"` because we want the "to state" labels and the "flow rate" labels to match based on the `Strain` index table column (`I.A` with `lambda.A` and `I.B` with `lambda.B`). + +For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: + +```{r two-strain-recovery-ledger} +recovery <- mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) +recovery +``` + +For the force of infection ledger, the full join yields many combinations that we don't want: + +```{r two-strain-foi-default} +mp_join( + infection_flow_rates = mp_subset(rate, Epi = "lambda"), + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta") +) +``` + +We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective index tables. Internally, `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins to the same effect: + +```{r two-strain-foi-ledger} +## new force of infection ledger ------------------------- +force_of_infection <- mp_join( + infection_flow_rates = mp_subset(rate, Epi = "lambda"), + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta"), + by = list( + infection_flow_rates.infectious_states = "Strain", # first pairwise join + infectious_states.transmission_rates = "Strain" # second pairwise join + ) +) + +force_of_infection +``` + +Now we're ready to build the two-strain model object and simulate it: + +```{r two-strain-results, fig.width = 6, fig.height = 4} +two_strain_model <- SIR_starter( + # index tables + state = state, + rate = rate, + # ledgers + flow = list( + infection, + recovery + ), + force_of_infection = force_of_infection +) + +two_strain_simulator <- mp_tmb_simulator( + dynamic_model = two_strain_model, + vectors = list( + state = c(S = 998, I.A = 1, I.B = 1, R = 0), + rate = c(beta.A = 0.25, gamma = 0.1, beta.B = 0.2) + ), + time_steps = 100 +) + +two_strain_results <- (mp_report(two_strain_simulator) + |> filter(matrix == "state") +) + +levels <- unique(two_strain_results$row) # get state variables in the desired order + +(two_strain_results # keep state variables at each point in time + |> mutate(state = factor(row, levels = levels)) # to enforce logical state ordering in plot + |> ggplot(aes(time, value, colour = state)) + + geom_line() +) +``` + +# Dessert: understanding model simulation in `macpan2` {#dessert} + +As mentioned, we've hidden some of the details of initializing a model object within the `SIR_starter()` function: + +```{r sir-starter-print} +<> +``` + +This function definition shows how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the functional forms used to simulate the model, including some we explored above (unsigned flows, force of infection), as well as some that we didn't discuss (total inflow, total outflow, state update). The `ledgers` and `init_vecs` are just set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. + +These topics will be discussed fully in a future vignette. + diff --git a/vignettes/example_models.Rmd b/vignettes/example_models.Rmd index 9c2e093f..70e5fc8f 100644 --- a/vignettes/example_models.Rmd +++ b/vignettes/example_models.Rmd @@ -42,11 +42,8 @@ Suppose that we want to read the `sir` example we would go here: [https://github To use the `sir` example it can be read into R using the following code. ```{r} sir_dir = system.file("starter_models", "sir", package = "macpan2") -sir = Compartmental(sir_dir) -sir$labels$all() -sir$labels$state() -sir$labels$flow() -sir$flows() +sir = mp_tmb_library(sir_dir) +print(sir) ``` To see how to actually generate simulations from this model see [this article](https://canmod.github.io/macpan2/articles/quickstart). To use another model, again, replace `sir` with another entry in the `dir` column above. @@ -60,10 +57,7 @@ model_starter("sir", my_sir_dir) After running this code you can go to the files in `my_sir_dir` and modify what you see there. Note that you typically want to chose a specific directory for your model instead of using `tempdir`. You still need to read your own model in the usual way. ```{r} -my_sir = Compartmental(my_sir_dir) -my_sir$labels$all() -my_sir$labels$state() -my_sir$labels$flow() -my_sir$flows() +my_sir = mp_tmb_library(my_sir_dir) +print(my_sir) ``` These look identical to what came before, but that's just because it hasn't been modified ... yet ... diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 9af840b9..54a56ded 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -1,11 +1,11 @@ --- -title: "Quickstart 1: understanding `macpan2`'s model specification grammar" +title: "Quickstart" output: html_document: toc: true keep_md: yes vignette: > - %\VignetteIndexEntry{Quickstart 1: understanding `macpan2`'s model specification grammar} + %\VignetteIndexEntry{Quickstart} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: @@ -20,529 +20,43 @@ library(ggplot2) library(dplyr) ``` -One of the main goals of `macpan2` is to provide a flexible grammar for model specification that reduces friction when building upon and expanding an existing model. This goal complements the standard approach of modelling, which is to start simply and add complexity as needed. - -There is a trade-off between the flexibility and the simplicity of the model grammar: specifying a simple model may not always be very concise, and there is a learning curve to the model grammar. However, it can be very powerful when it comes to specifying **structured** models, especially when they are cast as expansions of simple models. Such [structured models](https://idpjournal.biomedcentral.com/articles/10.1186/s40249-022-01001-y) can include: - -- multiple pathogen strains -- multiple infection types (_e.g._, asymptomatic and symptomatic or mild and severe) -- age-structure -- multiple locations (a metapopulation model) -- testing processes to identify infections -- vaccination status - -This vignette seeks to explain `macpan2`'s model specification grammar and in particular how one could take a simple model and expand it with additional structure. - -# Amuse bouche: a structured SIR model {#amuse-bouche} - -A key to `macpan2`'s flexible model grammar is the use of **functional forms** to repeat the same kinds of calculations across model structures. For instance, consider an SIR model that has two pathogen strains (without co-infections): - -
    -![](../misc/diagrams/quickstart/two-strain.svg) -
    - -Here, - -- $S$, $I_x$, and $R$ are the numbers of individuals that are susceptible, infected with strain $x$ ($A$ or $B$), and recovered, respectively, -- $N= S + I_A + I_B + R$ is the total population size, -- $\beta_x$ is the transmission rate for strain $x$, -- $\gamma$ is the recovery rate for infected individuals,^[In this model we are choosing to make the recovery rate (and hence the infectious period) the same for both strains, but it would be easy to relax this assumption in the model specification by following the way in which we stratify the state variable $I$ by strain below.] -- $\lambda_x = \beta_x (I_x)/N$ is the force of infection for strain $x$. - -We can cast this model as a system of difference equations, since this is how we will iterate them numerically in our simulation: - -\begin{align} -S_{t+1} & = - [\beta_A (I_A)_t/N + \beta_B (I_B)_t/N] S_t, \\ -(I_A)_{t+1} &= \phantom{-[} \beta_A (I_A)_t/N - \gamma (I_A)_t, \\ -(I_B)_{t+1} &= \phantom{-[} \beta_B (I_B)_t/N - \gamma (I_B)_t, \\ -R_{t+1} &= \phantom{-[} \gamma (I_A)_t + \gamma (I_B)_t. -\end{align} - -Each force of infection, $\lambda_A = \beta_A (I_A)/N$ and $\lambda_B = \beta_B (I_B)/N$ has the same **functional form**, that is, using an expression like $\lambda = \beta I / N$. When numerically simulating this model, it doesn't take much effort to write out each calculation separately as something like: - -``` -lambda.A = beta.A * I.A / N -lambda.B = beta.B * I.B / N -``` - -However, in `macpan2`, we can specify a single functional form for it, for instance - -``` -lambda = beta * I / N -``` - -and then attach a **ledger** to the model object that tabulates specific instances of when this functional form is used to define a component of the model. In other words, this ledger should enumerate which specific subscripted `lambda`, `beta`, and `I` to use each time we invoke the associated functional form during the simulation. - -In this case, there would only be two calculations in the force of infection ledger (one calculation per strain), but one can easily imagine a more complicated case. For instance, consider a relatively simple two-city age-structured metapopulation model with 10 age groups within each of two patches: there would be 10x10x2 = `r 10*10*2` force of infection terms of the same form (one per combination of age groups to capture the options for susceptible and infected interaction, repeated for each of the two patches). - -**Using functional forms and ledgers allows the modeller to focus on modelling questions**, like the design of the model structure and the choice of expressions for the forces of infection, while **`macpan2` handles the bookkeeping**, matching stratified variables with each other when calculating expressions. This approach cuts down on rote repetition when setting up model calculations, which in turn reduces the opportunity for bugs in the simulation code. It also means that expanding a model can be as simple as updating the calculation ledger, rather than error-prone editing of calculations in the simulation code. - -While a modeller could write their own code to cut down on repetition when expanding a simple model (and many do), `macpan2` provides a ready-made model specification grammar that enables easy model extension, especially when building [product models](https://arxiv.org/abs/2307.10308), and that can readily interface with fast simulation and calibration engines, like [TMB](https://cran.r-project.org/web/packages/TMB/index.html). - -# Appetizer: specifying the basic SIR model - -Let's start with specifying the basic SIR model, the foundation of the two-strain model above, in `macpan2`: - -\begin{align} -S_{t+1} &= -\beta S_t I_t/N, \\ -I_{t+1} &= \phantom{-} \beta S_t I_t/N - \gamma I_t, \\ -R_{t+1} &= \phantom{-} \gamma I_t. -\end{align} - -It will be helpful to set $\lambda = \beta I/N$ and recast the equations as: - -\begin{align} -S_{t+1} &= -\lambda S_t, \\ -I_{t+1} &= \phantom{-} \lambda S_t - \gamma I_t, \\ -R_{t+1} &= \phantom{-} \gamma I_t. -\end{align} - -Since the focus of this quickstart guide is `macpan2`'s model specification grammar, we have defined an `SIR_starter()` function to sweep some of the details of initializing a model object under the rug (for now, though we will revisit it [later](#dessert)). All you need to know about `SIR_starter()` at this stage is that we will pass it some inputs to define the model using the model grammar and it will output a model object from which we can build a simulator. Our primary focus for the remainder of this vignette will be how the inputs to `SIR_starter()` are created. - -```{r SIR-starter, echo = FALSE} -## helper function to simplify the exposition in this vigette ----------- -SIR_starter <- function( - # index tables for model quantities - state, - rate, - # ledgers tabulating the use of different functional forms - flow, # list of individual ledgers - force_of_infection -){ - - ## Set up expressions list for each functional form -------------- - ## names refer to when the calculation gets performed relative to - ## the simulation time-step loop (before, during, ...) - expr_list <- mp_expr_list( +The following code specifies an SI model, which is I think is the simplest possible model of epidemiological transmission. +```{r hello-world} +si = mp_tmb_model_spec( before = list( - ## aggregations - N ~ sum(state) - ), - during = list( - ## force of infections - rate[infection_flow_rates] ~ - state[infectious_states] * rate[transmission_rates] / N - - ## unsigned individual flows - , flow_per_time ~ state[from_states] * rate[flow_rates] - - ## state update - , total_inflow ~ groupSums(flow_per_time, to_states, state) - , total_outflow ~ groupSums(flow_per_time, from_states, state) - , state ~ state + total_inflow - total_outflow + I ~ 1 + , S ~ N - I ) - ) - - ## Ledgers for each specific calculation -------------- - ledgers <- list( - flow = mp_ledgers(flow), - force_of_infection = mp_ledgers(force_of_infection) - ) - - ## Initialize vectors from index tables (with all zeros for values) -------------- - # used as placeholders for user input - init_vecs <- list( - state = mp_vector(state), - rate = mp_vector(rate) - ) - - ## Initialize model object ----------------- - mp_dynamic_model( - expr_list = expr_list, - ledgers = ledgers, - init_vecs = init_vecs - ) -} -``` - -The inputs to `SIR_starter()` are of two types: - -- **index tables** containing indices (labels) of model quantities, -- **ledgers** that tabulate specific calculations required to simulate the model equations (based on the included functional forms). - -The index tables we need to specify fall into two groups: - -- `state`: state names, $S$, $I$, and $R$ from the model equations -- `rate`: rate names, $\beta$, $\gamma$, and the derived rate $\lambda$ - -We have identified two useful **functional forms** that we have baked into `SIR_starter()`. In this case, we're thinking of these forms not necessarily as repeated calculations in this particular model, but as calculations that a modeller may want to repeat down the line, as they expand this simple model with additional structure (as we will do [below](#main-course)). The forms are: - -- **flow**: Unsigned flows from one class to another of the form $rX$, with $r>0$ being the *per capita* flow rate and $X$ being the occupancy of the state from which the flow originates. This calculation is repeated for all terms on the right-hand side of the recast system of difference equations above. -- **force of infection**: The prevalence-dependent *per capita* rate of flow from susceptible classes to infectious classes of the form $\lambda = \beta I /N$, used in calculating infection flows. - -In this case, the flow form is repeated within these model equations, while the force of infection form is used only once. We've identified the force of infection as a functional form since we will want to repeat it later when [expanding into the two-strain model](#main-course). Either way, these forms are already baked into `SIR_starter()`, so our task will be creating a ledger for each of these forms to input into the function. - -We start by creating the `state` and `rate` index tables: - -```{r sir-index-tables} -## index tables to label model quantities ------------------------- -state <- mp_index(Epi = c("S", "I", "R")) -rate <- mp_index(Epi = c("beta", "gamma", "lambda")) -``` - -The `mp_index()` function sets structures like data frames that tabulate the model quantity labels: - -```{r sir-state-and-rate} -state -rate -``` - -The `Epi` column name is unimportant in this simple model, but it will be key to stratifying model quantities with different features (such as epidemiological status, infection type, age group, location) in more complicated models. - -For the flow form, we will create two ledgers: `infection` for the flow from $S$ to $I$ and `recovery` for the flow from $I$ to $R$ and then pass these as a list to the `flow` argument of `SIR_starter()`. We specify flows using the name of the state from which it originates (`from_states`), the state to which it goes (`to_states`), and a flow rate name (`flow_rates`). - -We use the `mp_join()` function to create the `infection` ledger like so: - -```{r sir-infection-ledger} -## infection ledger ------------------------- -infection <- mp_join( - from_states = mp_subset(state, Epi = "S"), - to_states = mp_subset(state, Epi = "I"), - flow_rates = mp_subset(rate, Epi = "lambda") -) -``` - -The `mp_join()` function takes the options provided in each argument `from_states`, `to_states`, and `flow_rates`, e.g. - -```{r sir-infection-ledger-inputs} -mp_subset(state, Epi = "S") -mp_subset(state, Epi = "I") -mp_subset(rate, Epi = "lambda") -``` - -and by default creates one entry in the ledger for each combination of these values (_i.e._, a [full join](https://dplyr.tidyverse.org/reference/mutate-joins.html#outer-joins)). However, since there is only one value in each column, there is only one entry in the resulting ledger: - -```{r sir-infection-ledger-2} -infection -``` - -The names of the arguments in the `mp_join()` function are tied to how the functional form baked into `SIR_starter()` is specified, but in general modellers can define their functional forms and the corresponding `mp_join()` argument names however they like.^[There is only one `mp_join()` argument name that is not available to the user — `by`, which has a special role that we will see [later](#main-course).] - -We create the `recovery` ledger in a similar way: - -```{r sir-recovery-ledger} -## recovery ledger ------------------------- -recovery <- mp_join( - from_states = mp_subset(state, Epi = "I"), - to_states = mp_subset(state, Epi = "R"), - flow_rates = mp_subset(rate, Epi = "gamma") -) - -recovery -``` - -Finally, the `force_of_infection` ledger is slightly different as it corresponds to a different functional form in `SIR_starter()` (so the `mp_join()` argument names are different): - -```{r sir-foi-ledger} -## force of infection ledger ------------------------- -# infection additionally involves the calculation of a force of infection -force_of_infection <- mp_join( - infectious_states = mp_subset(state, Epi = "I"), - transmission_rates = mp_subset(rate, Epi = "beta"), - infection_flow_rates = mp_subset(rate, Epi = "lambda") -) -``` - -For this functional form, we need to specify the `transmission_rates` and `infectious_states` involved in computing the force of infection, as well as the names where we want to store the results of this calculation (`infection_flow_rates`) for use in the `infection` flow calculations. - -Now we can use the `SIR_starter()` function to initialize our model object: - -```{r sir} -## SIR model object ------------------------- -sir <- SIR_starter( - # index tables - state = state, - rate = rate, - # ledgers - flow = list( - infection, - recovery - ), - force_of_infection = force_of_infection -) -``` - -We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for each index (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): - -```{r sir-simulator} -## SIR model simulator ------------------------- -sir_simulator <- mp_tmb_simulator( - dynamic_model = sir, - vectors = list( - state = c(S = 999, I = 1, R = 0), - rate = c(beta = 0.25, gamma = 0.1) - ), - time_steps = 100 -) -``` - -Note that we've specified `NA` for `lambda` as it will be calculated for us using the force of infection functional form. - -Then we can actually simulate the model by passing our model simulator to `mp_report()`: - -```{r sir-results} -## SIR model simulation results ------------------------- -sir_results <- mp_report(sir_simulator) -``` - -The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer): - -```{r sir-results-head} -head(sir_results) -``` - -The simulation output has several columns: - -- `matrix`: The matrix storing our values internally, corresponding to our two index tables, `state` and `rate`. -- `time`: An internal time index, where `time = 1` is the result after the first step through the simulation loop. -- `row`: The primary label for the `value` (the row name in the corresponding `matrix`). -- `col`: A secondary label for the `value` (the column name in the corresponding `matrix`). Since the outputs of this model (i.e. states and rates) are specified as vectors and not matrices, this column is empty for all entries. TODO: When would this be useful? -- `value`: The numerical value. - -This output can be manipulated and plotted with standard tools, like `dplyr` and `ggplot2`, e.g.: - -```{r sir-ggplot-example, fig.width = 6, fig.height = 4} -(sir_results - |> filter(matrix == "state") # keep just the state variables at each point in time - |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in legend - |> ggplot(aes(time, value, colour = state)) - + geom_line() -) -``` - -(Above, we used the [base R pipe operator](https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/#pipes), `|>`.) - -If you prefer to make plots in base R, you can convert the long format data to wide format: - -```{r pivot_wider} -sir_results_wide <- (sir_results - |> dplyr::filter(matrix == "state") # keep state variables at each point in time - ## drop unneeded columns before pivoting - |> dplyr::select(-c(matrix, col)) - |> tidyr::pivot_wider(id_cols = time, names_from = row) -) - -head(sir_results_wide, n = 3) -``` - -We can plot one state like so - -```{r sir-base-plot-ex, fig.width = 6} -with(sir_results_wide, - plot(x = time, - y = I, - type = "l") -) -``` - -or multiple states on the same plot with - -```{r sir-base-matplot-ex, fig.width = 6} -par(las = 1) ## horizontal y-axis ticks -matplot(sir_results_wide[, 1], - sir_results_wide[,-1], - type = "l", - xlab = "time", ylab = "") -legend("left", col = 1:3, lty = 1:3, legend = state$labels()) -``` - -# Main course: expanding the basic SIR with additional structure {#main-course} - -As previously noted, we created a force of infection functional form ($\beta I / N$) despite it only being used once to define the SIR model. However, if we consider the two-strain model from [before](#amuse-bouche), we see this calculation is repeated for each strain: - -\begin{align} -\lambda_A &= \beta_A I_A/N \\ -\lambda_B &= \beta_B I_B/N -\end{align} - -Since we already have a form for the force of infection, we can easily expand our basic SIR with the strain-related structure to get the two-strain SIR model. - -To define the two-strain model, we again must specify our `state` and `rate` index tables, as well as our `infection`, `recovery`, and `force_of_infection` ledgers. - -We start by creating a new set of indices for the strains: - -```{r strain-indices} -Strain_indices <- c("A", "B") -``` - -A simple approach would be to define a table of the new state and rate indices directly using the `mp_index()` function, as we did above: - -``` -state <- mp_index( - Epi = c("S", rep("I", 2), "R"), - Strain = c("", Strain_indices, "") -) - -rate <- mp_index( - Epi = c(rep(c("beta", "lambda"), 2), "gamma"), - Strain = c(rep(c("A", "B"), each = 2), "") -) -``` - -However, this approach is less flexible if we want to build a complex model or if we already have a simpler, working model (like the SIR above) and want expand it with many strata and/or several different types of strata. We present an alternative approach below that is more verbose but far more flexible. - -For the state, we want to cross $I$ with the different strains to create one $I$ compartment name per strain. We can do so using the `mp_cartesian()` function, which takes the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of indices (all possible combinations across sets)^[`mp_cartesian()` is analogous to `expand.grid()` in base R, or `tidyr::expand()` in the tidyverse]: - -```{r strain-expand-I} -I_indices <- mp_cartesian( - mp_subset(state, Epi = "I"), - mp_index(Strain = Strain_indices) -) - -I_indices -``` - -This table stores all indices associated with the $I$ compartment.^[In standard mathematical notation, one would typically write $I_A$ and $I_B$, with only $A$ and $B$ referred to as indices, but we've taken the abstraction one step further and chosen to refer to state variable names, like $I$, as indices as well. This choice was made deliberately when developing `macpan2` so that state variable names get treated like any other component used to label a particular compartment (like location or age group).] - -We then combine the newly-stratified $I$ indices with the other states that remain unchanged using the `mp_union()` function to make a `state` index table: - -```{r two-strain-state} -state <- mp_union( - mp_subset(state, Epi = "S"), - I_indices, - mp_subset(state, Epi = "R") -) - -state -``` - -We update the `rate` index table similarly: - -```{r two-strain-rate} -rate <- - mp_union( - # stratify rates involved in the infection process by strain - mp_cartesian( - mp_subset(rate, Epi = c("beta", "lambda")), - mp_index(Strain = Strain_indices) - ), - # recovery rate will be the same across strains - mp_subset(rate, Epi = "gamma") -) - -rate -``` - -For the `infection` ledger, let's see what our previous code for generating it yields now that we are (partially) stratifying by `Strain`: - -```{r two-strain-infection-default} -# infection ledger from before -mp_join( - from_states = mp_subset(state, Epi = "S"), - to_states = mp_subset(state, Epi = "I"), - flow_rates = mp_subset(rate, Epi = "lambda") -) -``` - -As before, the default in `mp_join()` is to give all possible combinations for the indices (the full join), where the individual indices, denoted by values in the `Epi` and `Strain` columns, are dot-concatenated for the full quantity labels. - -For this model, we want only two of these flows: - -- a flow between `S` and `I.A` with flow rate `lambda.A` -- a flow between `S` and `I.B` with flow rate `lambda.B` - -In other words, we want the `Strain` index on `I` to match with the `Strain` index on `lambda`. We can specify this within `mp_join()` when building the ledger like so: - -```{r two-strain-infection-ledger} -## new infection ledger ------------------------- -infection <- mp_join( - from_states = mp_subset(state, Epi = "S"), - to_states = mp_subset(state, Epi = "I"), - flow_rates = mp_subset(rate, Epi = "lambda"), - by = list( - to_states.flow_rates = "Strain" - ) -) - -infection -``` - -Note the syntax of the `by` argument here. Each `by` list element will correspond to a pairwise join of two of the index tables passed to `mp_join()`. Which indices are involved in the join will correspond to the dot concatenated list element name (`to_states.flow_rates`), with the names coming from `mp_join()`'s argument names (`to_states`, `flow_rates`). The list element value should be a character string corresponding to the index table column name upon which to perform matches. In this case, the value is `"Strain"` because we want the "to state" labels and the "flow rate" labels to match based on the `Strain` index table column (`I.A` with `lambda.A` and `I.B` with `lambda.B`). - -For the recovery ledger, we haven't stratified `gamma` or `R`, so the default full join with the `I` labels yields exactly the flows we want: - -```{r two-strain-recovery-ledger} -recovery <- mp_join( - from_states = mp_subset(state, Epi = "I"), - to_states = mp_subset(state, Epi = "R"), - flow_rates = mp_subset(rate, Epi = "gamma") -) -recovery -``` - -For the force of infection ledger, the full join yields many combinations that we don't want: - -```{r two-strain-foi-default} -mp_join( - infection_flow_rates = mp_subset(rate, Epi = "lambda"), - infectious_states = mp_subset(state, Epi = "I"), - transmission_rates = mp_subset(rate, Epi = "beta") -) -``` - -We want the `lambda`, `I`, and `beta` labels all matched on the `Strain` column of the respective index tables. Internally, `mp_join()` performs pairwise joins, so we cannot specify a three-way `by` argument. Instead, we will specify two pairwise joins to the same effect: - -```{r two-strain-foi-ledger} -## new force of infection ledger ------------------------- -force_of_infection <- mp_join( - infection_flow_rates = mp_subset(rate, Epi = "lambda"), - infectious_states = mp_subset(state, Epi = "I"), - transmission_rates = mp_subset(rate, Epi = "beta"), - by = list( - infection_flow_rates.infectious_states = "Strain", # first pairwise join - infectious_states.transmission_rates = "Strain" # second pairwise join - ) + , during = list( + infection ~ beta * S * I / N + , S ~ S - infection + , I ~ I + infection + ) + , default = list(N = 100, beta = 0.25) ) - -force_of_infection +print(si) ``` -Now we're ready to build the two-strain model object and simulate it: - -```{r two-strain-results, fig.width = 6, fig.height = 4} -two_strain_model <- SIR_starter( - # index tables - state = state, - rate = rate, - # ledgers - flow = list( - infection, - recovery - ), - force_of_infection = force_of_infection +Simulating from this model requires choosing the number of time-steps to run and the model outputs to generate. Syntax for simulating `macpan2` models is designed to combine with standard data prep and plotting tools in R, as we demonstrate with the following code. +```{r plot-tmb-si, fig.format='svg'} +(si + |> mp_simulator(time_steps = 50, outputs = c("I", "infection")) + |> mp_trajectory() + |> mutate(quantity = case_match(matrix + , "I" ~ "Prevalance" + , "infection" ~ "Incidence" + )) + |> ggplot() + + geom_line(aes(time, value)) + + facet_wrap(~ quantity, scales = "free") + + theme_bw() ) - -two_strain_simulator <- mp_tmb_simulator( - dynamic_model = two_strain_model, - vectors = list( - state = c(S = 998, I.A = 1, I.B = 1, R = 0), - rate = c(beta.A = 0.25, gamma = 0.1, beta.B = 0.2) - ), - time_steps = 100 -) - -two_strain_results <- (mp_report(two_strain_simulator) - |> filter(matrix == "state") -) - -levels <- unique(two_strain_results$row) # get state variables in the desired order - -(two_strain_results # keep state variables at each point in time - |> mutate(state = factor(row, levels = levels)) # to enforce logical state ordering in plot - |> ggplot(aes(time, value, colour = state)) - + geom_line() -) -``` - -# Dessert: understanding model simulation in `macpan2` {#dessert} - -As mentioned, we've hidden some of the details of initializing a model object within the `SIR_starter()` function: - -```{r sir-starter-print} -<> ``` -This function definition shows how all the pieces fit together. The expressions list `expr_list` is perhaps the most interesting as it contains all of the functional forms used to simulate the model, including some we explored above (unsigned flows, force of infection), as well as some that we didn't discuss (total inflow, total outflow, state update). The `ledgers` and `init_vecs` are just set up to ensure that the ledgers and initial conditions for simulation get attached to the model object correctly. -These topics will be discussed fully in a future vignette. +## TODO +* Bring over from previous quickstart on `main`, which currently [here](https://canmod.github.io/macpan2/articles/quickstart) + * explanation of modelling output and philosophy of post-processing + * Modify [this](https://canmod.github.io/macpan2/articles/quickstart.html#inputting-values-to-create-model-simulators) so that it works with `default` value updates using `mp_tmb_simulator` + * Modify [this](https://canmod.github.io/macpan2/articles/quickstart.html#incidence) discussion of incidence From d37a8d574b67f5bea70f0cfdc7f561da2555a123 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 27 Dec 2023 15:00:23 -0500 Subject: [PATCH 248/332] integrator examples --- R/make_model_index.R | 17 ++++++--- R/model_data_structure.R | 25 +++++++------ R/tmb_model.R | 2 +- inst/starter_models/si/example.R | 12 +++++++ inst/starter_models/si/tmb.R | 60 ++++++++++++++++++++++++++++++++ man/model_starter.Rd | 4 +-- 6 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 inst/starter_models/si/example.R create mode 100644 inst/starter_models/si/tmb.R diff --git a/R/make_model_index.R b/R/make_model_index.R index b72a8843..629e3520 100644 --- a/R/make_model_index.R +++ b/R/make_model_index.R @@ -52,9 +52,19 @@ get_mod_info <- function(f) { #' @examples show_models(show_missing = TRUE) #' @importFrom stats na.omit #' @export -show_models <- function(dir = system.file("starter_models", package = "macpan2"), - show_missing = FALSE) { +show_models <- function( + dir = system.file("starter_models", package = "macpan2") + , show_missing = FALSE + ) { + + ## TODO: handle multi-engine case when it is ready ... mods <- list.files(path = dir) + ## ... but for now just list the models with a tmb.R file + mods = (dir + |> list.files("^tmb.R$", recursive = TRUE) + |> dirname() + ) + ## cat(mods, sep = "\n") res <- do.call("rbind", lapply(file.path(dir, mods), get_mod_info)) @@ -65,6 +75,3 @@ show_models <- function(dir = system.file("starter_models", package = "macpan2") rownames(res) <- NULL return(res) } - - - diff --git a/R/model_data_structure.R b/R/model_data_structure.R index 8153467e..57cfd9f9 100644 --- a/R/model_data_structure.R +++ b/R/model_data_structure.R @@ -111,18 +111,18 @@ assert_variables = function(model) { #' Create a directory with a template model definition. #' #' @param starter_name Currently can only be \code{sir}. -#' @param dir_name String giving the path to a directory for copying the +#' @param dir String giving the path to a directory for copying the #' template model definition. #' #' @export -model_starter = function(starter_name, dir_name) { - starter_dir = system.file("starter_models", starter_name, package = "macpan2") +model_starter = function(starter_name, dir) { + starter_dir = system.file("starter_models" + , starter_name + , package = "macpan2" + ) starter_files = list.files(starter_dir) required_files = c( - variables_file = "variables.csv", - derivations_file = "derivations.json", - flows_file = "flows.csv", - settings_file = "settings.json" + tmb_engine_file = "tmb.R" ) if (!all(required_files %in% starter_files)) { stop("Could not find a valid starter model by that name.") @@ -133,9 +133,12 @@ model_starter = function(starter_name, dir_name) { names(required_files) ) - if (dir.exists(dir_name)) stop("Directory for the model already exists.") - dir.create(dir_name, recursive = TRUE) + if (dir.exists(dir)) stop("Directory for the model already exists.") + dir.create(dir, recursive = TRUE) - file.copy(starter_paths, dir_name) - ModelFiles(dir_name) + file.copy(starter_paths, dir) + + ## TODO: handle the multi-engine case + ## TODO: implement proper file update monitoring (e.g. Files objects) + mp_tmb_library(dir) } diff --git a/R/tmb_model.R b/R/tmb_model.R index 05383397..edc4c7df 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -256,7 +256,7 @@ TMBModelSpec = function( #' @export print.TMBModelSpec = function(x, ...) { e = ExprList(x$before, x$during, x$after) - cat("\n---------------------\n") + cat("---------------------\n") msg("Default values:\n") |> cat() cat("---------------------\n") print(melt_default_matrix_list(x$default), row.names = FALSE) diff --git a/inst/starter_models/si/example.R b/inst/starter_models/si/example.R new file mode 100644 index 00000000..b18b4a1e --- /dev/null +++ b/inst/starter_models/si/example.R @@ -0,0 +1,12 @@ +source("inst/starter_models/si/tmb.R") +library(ggplot2) +library(dplyr) + +(specs + |> lapply(mp_simulator, 50L, "I") + |> lapply(mp_trajectory) + |> bind_rows(.id = "integrator") + |> rename(prevalance = value) + |> ggplot() + + geom_line(aes(time, prevalance, colour = integrator)) +) diff --git a/inst/starter_models/si/tmb.R b/inst/starter_models/si/tmb.R new file mode 100644 index 00000000..eb098497 --- /dev/null +++ b/inst/starter_models/si/tmb.R @@ -0,0 +1,60 @@ +library(macpan2) +integrator = "hazard" + +initialize_state = list( + I ~ 1 + , S ~ N - 1 +) + +flow_rates = list( + euler = list( + infection ~ beta * S * I / N + ), + ## expected value of the Euler-Binomial + hazard = list( + infection ~ S * (1 - exp(-beta * I / N)) + ), + ## (demographic stochasticity) + #flow_rates = list( + # infection ~ rbinom() ## need to implement rbinom on the c++ side + #) + rk4 = list( + S_rk4 ~ S + , I_rk4 ~ I + , k1_infection ~ beta * S_rk4 * I_rk4 / N + + , S_rk4 ~ S_rk4 - k1_infection / 2 + , I_rk4 ~ I_rk4 + k1_infection / 2 + , k2_infection ~ beta * S_rk4 * I_rk4 / N + + , S_rk4 ~ S_rk4 - k2_infection / 2 + , I_rk4 ~ I_rk4 + k2_infection / 2 + , k3_infection ~ beta * S_rk4 * I_rk4 / N + + , S_rk4 ~ S_rk4 - k3_infection + , I_rk4 ~ I_rk4 + k3_infection + , k4_infection ~ beta * S_rk4 * I_rk4 / N + + , infection ~ ( + k1_infection + + 2 * k2_infection + + 2 * k2_infection + + k4_infection + ) / 6 + ) +) + +update_state = list( + S ~ S - infection + , I ~ I + infection +) + +specs = lapply(flow_rates, \(flow_rates) { + mp_tmb_model_spec( + before = initialize_state + , during = c(flow_rates, update_state) + , default = list(N = 100, beta = 0.2, gamma = 0.1) + ) +}) + +spec = specs[["euler"]] diff --git a/man/model_starter.Rd b/man/model_starter.Rd index 2e63f404..70108784 100644 --- a/man/model_starter.Rd +++ b/man/model_starter.Rd @@ -4,12 +4,12 @@ \alias{model_starter} \title{Model Starter} \usage{ -model_starter(starter_name, dir_name) +model_starter(starter_name, dir) } \arguments{ \item{starter_name}{Currently can only be \code{sir}.} -\item{dir_name}{String giving the path to a directory for copying the +\item{dir}{String giving the path to a directory for copying the template model definition.} } \description{ From 76abf9ac7633cbea082db02d7a414826383cb886 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Thu, 4 Jan 2024 09:02:37 -0500 Subject: [PATCH 249/332] closes #135 --- inst/starter_models/lotka_volterra/README.md | 19 ---- .../lotka_volterra/competition/README.md | 44 ++++++++++ .../lotka_volterra/{ => competition}/model.R | 24 ++++- .../lotka_volterra/predator_prey/README.md | 87 +++++++++++++++++++ .../lotka_volterra/predator_prey/model.R | 76 ++++++++++++++++ 5 files changed, 228 insertions(+), 22 deletions(-) delete mode 100644 inst/starter_models/lotka_volterra/README.md create mode 100644 inst/starter_models/lotka_volterra/competition/README.md rename inst/starter_models/lotka_volterra/{ => competition}/model.R (56%) create mode 100644 inst/starter_models/lotka_volterra/predator_prey/README.md create mode 100644 inst/starter_models/lotka_volterra/predator_prey/model.R diff --git a/inst/starter_models/lotka_volterra/README.md b/inst/starter_models/lotka_volterra/README.md deleted file mode 100644 index 64c3dde7..00000000 --- a/inst/starter_models/lotka_volterra/README.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "Lotka-Volterra" -index_entry: "simple two-species competition model" -author: Jennifer Freeman ---- - -- $X$ - number of individuals in species $X$ -- $Y$ - number of individuals in species $Y$ -- $r_i$ - growth rate of species $I$ -- $a_{ij}$ - intra/inter-specific density dependence, ``effect of species $j$ on species $i$'' (Hastings, 1997) - -$$ -\begin{align*} -\frac{dX}{dt} &= r_x X (1 - a_{xx}X - a_{xy}Y) \\ -\frac{dY}{dt} &= r_y Y (1 - a_{yy}Y - a_{yx}X) -\end{align*} -$$ - -Hastings, A. (1997). Competition. In: Population Biology. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_7 \ No newline at end of file diff --git a/inst/starter_models/lotka_volterra/competition/README.md b/inst/starter_models/lotka_volterra/competition/README.md new file mode 100644 index 00000000..fe806293 --- /dev/null +++ b/inst/starter_models/lotka_volterra/competition/README.md @@ -0,0 +1,44 @@ +--- +title: "Lotka-Volterra" +index_entry: "simple two-species competition model" +author: Jennifer Freeman +--- + +The simplest Lotka-Volterra competition model with two competing species. + +# Species + +| variable | description | +| -------- | ------------------------------------ | +| $X$ | number of individuals in species $x$ | +| $Y$ | number of individuals in species $y$ | + +# Parameters + +| variable | description | +| ------------- | -------------------------------------------------------------------------------------------------- | +| $r_i$ | growth rate of species $i$ | +| $a_{ij}$ | intra/inter-specific density dependence, ``effect of species $j$ on species $i$'' (Hastings, 1997) | +| $K_{i}$ | carrying capacity of species $i$ | +| $\alpha_{ij}$ | relative effect of species $j$ on species $i$ (Hastings, 1997) | + +# Dynamics +$$ +\begin{align*} +\frac{dX}{dt} &= r_x X (1 - a_{xx}X - a_{xy}Y) \\ +\frac{dY}{dt} &= r_y Y (1 - a_{yy}Y - a_{yx}X) +\end{align*} +$$ + +This model can also be expressed in an equivalent form using the carrying capacity of each species. + +$$ +\begin{align*} +\frac{dX}{dt} &= \frac{r_x X}{K_x} (K_x - X - \alpha_{xy}Y) \\ +\frac{dY}{dt} &= \frac{r_y Y}{K_y} (K_y - Y - \alpha_{yx}X) +\end{align*} +$$ + + +# References +Hastings, A. (1997). Competition. In: *Population Biology*. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_7 \ No newline at end of file diff --git a/inst/starter_models/lotka_volterra/model.R b/inst/starter_models/lotka_volterra/competition/model.R similarity index 56% rename from inst/starter_models/lotka_volterra/model.R rename to inst/starter_models/lotka_volterra/competition/model.R index 5961f7ab..8bedf395 100644 --- a/inst/starter_models/lotka_volterra/model.R +++ b/inst/starter_models/lotka_volterra/competition/model.R @@ -7,7 +7,6 @@ library(macpan2) ## none ## absolute flow rates (per time only) -## reference flow_rates = list( ## growth rate of species X and Y growth_x ~ rx * X @@ -18,12 +17,31 @@ flow_rates = list( ## species Y on Y , intraspecific_y ~ growth_y * ayy * Y ## interspecific effect + ## species Y on X + , interspecific_xy ~ growth_x * axy * Y ## species X on Y - , interspecific_xy ~ growth_x * ayx * Y + , interspecific_yx ~ growth_y * ayx * X +) + +## alternate parameterization of model by carrying +## capacity and relative interspecific effects +flow_rates = list( + ## growth rate of species X and Y + growth_x ~ rx * X + , growth_y ~ ry * Y + ## intraspecific effect + ## species X on X + , intraspecific_x ~ growth_x * X / Kx + ## species Y on Y + , intraspecific_y ~ growth_y * Y / Ky + ## interspecific effect ## species Y on X - , interspecific_yx ~ growth_y * axy * X + , interspecific_xy ~ growth_x * alpha_xy * Y / Kx + ## species X on Y + , interspecific_yx ~ growth_y * alpha_yx * X / Ky ) + ## state updates state_updates = list( X ~ X + growth_x - intraspecific_x - interspecific_xy diff --git a/inst/starter_models/lotka_volterra/predator_prey/README.md b/inst/starter_models/lotka_volterra/predator_prey/README.md new file mode 100644 index 00000000..c7b1c9f1 --- /dev/null +++ b/inst/starter_models/lotka_volterra/predator_prey/README.md @@ -0,0 +1,87 @@ +--- +title: "Lotka-Volterra" +index_entry: "simple predator-prey model" +author: Jennifer Freeman +--- + +The simplest Lotka-Volterra predator-prey model with two small modifications to include logistic prey growth and non-linear functional responses. + +# Species + +| variable | description | +| -------- | ------------------- | +| $X$ | number of prey | +| $Y$ | number of predators | + +# Parameters + +| variable | description | +| -------- | ------------------------------------------------------------------------------------------------------------------------ | +| $\alpha$ | per capita growth rate of prey, in the absence of predators (Hastings, 1997) | +| $\gamma$ | per capita rate of predator loss, in the absence of prey | +| $\beta$ | per capita mortality rate of prey fom predation | +| $\delta$ | per capita growth rate of predators from predation | +| $K$ | prey carrying capacity | +| $f(X)$ | per predator predation rate as a function of the number of prey, also called [functional response](#functional-response) | + + + + + +# Dynamics + +## Simple Predator-Prey + +In the simple predator-prey Lotka-Volterra model, we assume exponential growth and decay for prey and predators respectively. + +$$ +\begin{align*} +\frac{dX}{dt} &= \alpha X - \beta X Y \\ +\frac{dY}{dt} &= \delta XY - \gamma Y +\end{align*} +$$ + +## Logistic Prey Growth + +We modify the simple model to include logisitic prey growth in the absence of predators with a prey carrying capacity of $K$. + +$$ +\begin{align*} +\frac{dX}{dt} &= \alpha X \left(1 - \frac{X}{K}\right)- \beta X Y \\ +\frac{dY}{dt} &= \delta XY - \gamma Y +\end{align*} +$$ + +## Functional Response + +The functional response $f(X)$ describes the predation rate as a function of prey density (Hastings, 1997). In the simplest case, we assume a linear function of prey, $f(X) = aX$ also called a [type I Holling](#holling-type-i) response. The simple predator-prey model includes a type I Holling response. Increasing functions that approach horizontal asymptotes can be used to represent more ecologically realistic predation rates to communicate that predation does not indefinitely increase when prey are abundant. The [type II Holling](#holling-type-ii) response is parameterized by the predator *attack rate* $a$, and the time elapsed by the predator capturing and consuming prey, called the *handling time* $h$ (Bolker, 2008). A [Holling type III](#holling-type-iii) response is defined with higher powers of prey density. + +The general predator-prey dynamics incorporating the functional response $f(X)$ is given below. + +$$ +\begin{align*} +\frac{dX}{dt} &= \alpha X - \beta f(X) Y \\ +\frac{dY}{dt} &= \delta f(X)Y - \gamma Y +\end{align*} +$$ + + + +### Holling type I + +$$ f(X) = a X \:, a>0$$ + +### Holling type II + +$$ f(X) = \frac{a X}{1+ ahX} \:, a > 0 \:, h > 0$$ + +### Holling type III + +$$ f(X) = \frac{a X^k}{1 + ah X^k} \:, k > 1$$ + + + + +# References +Bolker, B. (2008). *Ecological Models and Data in R*. Princeton: Princeton University Press. https://doi.org/10.1515/9781400840908 +Hastings, A. (1997). Predator-Prey Interactions. In: *Population Biology*. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_8 \ No newline at end of file diff --git a/inst/starter_models/lotka_volterra/predator_prey/model.R b/inst/starter_models/lotka_volterra/predator_prey/model.R new file mode 100644 index 00000000..01a59999 --- /dev/null +++ b/inst/starter_models/lotka_volterra/predator_prey/model.R @@ -0,0 +1,76 @@ +library(macpan2) + +## expression lists +################################################### + +## free form computations for convenience (none) + +## functional responses +## Holling type I is trivial (modify beta as needed) + +#' Holling type II +#' +#' @param a predator attack rate; prey/time unit? +#' @param h handling time (ex. 1 day) +#' +holling_ii <- function(a, h, X){ + a * X / (1 + a * h * X) +} + +#' Holling type III +#' +#' @param a predator attack rate; prey/time unit? +#' @param h handling time (ex. 1 day) +#' @param k exponent of prey density, k > 1 +#' +holling_iii <- function(a, h, X, k){ + a * (X ^ k) / (1 + a * h * (X ^ k)) +} + + +## absolute flow rates (per time only) + +## exponential prey growth +flow_rates = list( + ## growth rate of prey + growth_x ~ alpha * X + ## mortality rate of predator + , mortality_y ~ gamma * Y + ## effects from predation + ## mortality rate of prey (due to predation) + , mortality_x ~ beta * X * Y + ## growth rate of predator (due to predation) + , growth_y ~ delta * X * Y +) + +## alternate parameterization of model +## logistic prey growth, and non-linear functional responses +flow_rates = list( + ## growth rate of prey + growth_x ~ alpha * X * (1 - (X/K)) + ## mortality rate of predator + , mortality_y ~ gamma * Y + ## effects from predation + ## compute functional response + , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) + #, functional_response ~ holling_ii(a = 5, h = 1, X = X) + ## mortality rate of prey (due to predation) + , mortality_x ~ beta * functional_response * Y + ## growth rate of predator (due to predation) + , growth_y ~ delta * functional_response * Y +) + + +## state updates +state_updates = list( + X ~ X + growth_x - mortality_x + , Y ~ Y + growth_y - mortality_y +) + +## simple unstructured scalar expression +expr_list = ExprList( + during = c( + flow_rates + , state_updates + ) +) From a9c91f5d2fc6a09d8caeb6117e5ee39d8520479d Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Fri, 5 Jan 2024 11:37:40 -0500 Subject: [PATCH 250/332] model library documentation updates --- inst/starter_models/macpan_base/README.md | 79 +++++++++++++++++++- inst/starter_models/macpan_base/model.R | 56 ++++++++------- inst/starter_models/seir/README.md | 50 ++++--------- inst/starter_models/seir/model.R | 4 +- inst/starter_models/si/README.md | 35 +++++++++ inst/starter_models/sir/README.md | 29 +++----- inst/starter_models/sir_demo/README.md | 55 ++++---------- inst/starter_models/sir_waning/README.md | 43 +++-------- inst/starter_models/ww/README.md | 88 ++++++++++++++++++++++- inst/starter_models/ww/model.R | 70 +++++++++--------- 10 files changed, 313 insertions(+), 196 deletions(-) create mode 100644 inst/starter_models/si/README.md diff --git a/inst/starter_models/macpan_base/README.md b/inst/starter_models/macpan_base/README.md index 4938d30e..2d1ebf9e 100644 --- a/inst/starter_models/macpan_base/README.md +++ b/inst/starter_models/macpan_base/README.md @@ -1,6 +1,83 @@ --- title: "MacPan base" index_entry: "re-implementation of the McMaster group's COVID-19 model" +author: Jennifer Freeman --- -This model ... +The McMasterPandemic model (Bolker, 2022); A modified SEIR model that incorporates additional infectious compartments to reflect the current knowledge of COVID-19 epidemiology. Exposed individuals enter four infectious compartments characterized by the individuals symptom status (asymptomatic, pre-symptomatic, mild, and severe). Severely infected individuals require care through hospital and/or Intensive Care Unit (ICU) compartments and either recover or die. + +# States + +| variable | description | +| -------- | ------------------------------------------------------------------- | +| S | Number of susceptible individuals | +| E | Number of exposed individuals | +| I~a~ | Number of asymptomatic infectious individuals | +| I~p~ | Number of pre-symptomatic infectious individuals | +| I~m~ | Number of mildly infectious individuals | +| I~s~ | Number of severely infectious individuals | +| H | Number of hospitalized individuals (acute care) | +| ICU~s~ | Number of individuals admitted to the ICU with a survival prognosis | +| ICU~d~ | Number of individuals admitted to the ICU with a death prognosis | +| H~2~ | Number of hospitalized individuals (acute care) after ICU stay | +| D | Number of dead individuals | +| R | Number of recovered individuals | + +The size of the total population is, $ N = S + E + I_a + I_p + I_m + I_s + H + ICU_s + ICU_d + H_2 + D + R$. + +# Parameters + +| variable | description | +| -------------- | ----------------------------------------------------------------------------------- | +| $\beta_0$ | baseline (non-intervention) transmission across categories | +| $C_a$ | relative asymptomatic transmission (or contact) proportion | +| $C_p$ | relative presymptomatic transmission (or contact) proportion | +| $C_m$ | relative mildly transmission (or contact) proportion | +| $C_s$ | relative severly transmission (or contact) proportion | +| $\alpha$ | fraction of infections that are asymptomatic | +| $\mu$ | fraction of symptomatic infections that are mild | +| $\sigma$ | 1/time in exposed class | +| $\gamma_a$ | 1/time to recovery for asymptomatic infections | +| $\gamma_p$ | 1/time in pre-symptomatic state | +| $\gamma_m$ | 1/time to recovery for mildly symptomatic infections | +| $\gamma_s$ | 1/time spent in severely symptomatic state before either hospitalization or death | +| $\rho$ | 1/time in hospital (initial acute care admission) | +| $\delta$ | fraction of hospitalized infections that are fatal ** | +| $\delta_{nh}$ | probability of mortality without hospitalization | +| $\text{iso}_m$ | relative self-isolation/distancing of mild cases * | +| $\text{iso}_s$ | relative self-isolation/distancing of severe cases * | +| $\phi_1$ | fraction of hospitalized infections that only require acute care (no ICU admission) | +| $\phi_2$ | fraction of ICU infections that are fatal | +| $\psi_1$ | 1/time spent in ICU before returning to acute care | +| $\psi_2$ | 1/time spent in ICU before dying | +| $\psi_3$ | 1/time in post-ICU acute care before hospital discharge | + +\* These parameters were not detailed in [Papst](#references) (TBD), but they were included in model definition files. +\** This parameter does not appear in the model dynamics. + +# Dynamics + +$$ +\begin{align*} +\frac{dS}{dt} &= -\beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N \\ +\frac{dE}{dt} &= \beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N - \sigma E \\ +\frac{dI_a}{dt} &= \alpha\sigma E- \gamma_a I_a \\ +\frac{dI_p}{dt} &= (1-\alpha)\sigma E- \gamma_p I_p \\ +\frac{dI_m}{dt} &= \mu\gamma_pI_p- \gamma_m I_m \\ +\frac{dI_s}{dt} &= (1-\mu)\gamma_pI_p- (1-\delta_{nh})\gamma_s I_s \\ +\frac{dH}{dt} &= (1-\delta_{nh})\phi_1\gamma_s I_s - \rho H \\ +\frac{dICU_s}{dt} &= (1-\delta_{nh})(1-\phi_1)(1-\phi_2)\gamma_s I_s - \psi_1 ICU_s \\ +\frac{dICU_d}{dt} &= (1-\delta_{nh})(1-\phi_1)\phi_2\gamma_s I_s - \psi_2 ICU_d \\ +\frac{dH_2}{dt} &= \psi_1 ICU_s - \psi_3 H_2 \\ +\frac{dR}{dt} &= \gamma_a I_a + \gamma_m I_m + \rho H + \psi_3 H_2 \\ +\frac{dD}{dt} &= \psi_2 ICU_d +\end{align*} +$$ + +When there were discrepancies with how the model is expressed in Papst (TBD) versus model definition files, the latter was chosen. + +# References + +Bolker B, Steve Walker, David Earn, Morgan Kain, Mike Li, Jonathan Dushoff (2022). McMasterPandemic: Pandemic Model. R package version 0.2.0.0, https://github.com/mac-theobio/McMasterPandemic. + +Papst, Irena, Mike Li, David Champredon, Aamir Fazil, Jonathan Dushoff, Ben Bolker, David Earn (draft/preprint). Forecasting infectious disease spread as new variants emerge and vaccination rates increase. Accessed 4 Jan 2024. diff --git a/inst/starter_models/macpan_base/model.R b/inst/starter_models/macpan_base/model.R index a9a8e5e5..74464e57 100644 --- a/inst/starter_models/macpan_base/model.R +++ b/inst/starter_models/macpan_base/model.R @@ -10,36 +10,38 @@ computations = list( ## absolute flow rates (per time only) flow_rates = list( - SE ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) - , EIa ~ E * alpha * sigma - , EIp ~ E * (1 - alpha)* sigma - , IaR ~ Ia * gamma_a - , IpIm ~ Ip * mu * gamma_p - , ImR ~ Im * gamma_m - , IpIs ~ Ip * (1 - mu) * gamma_p - , IsICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s - , ICUsH2 ~ ICUs * psi1 - , H2R ~ H2 * psi3 - , IsH ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s - , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s - , ICUdD ~ ICUd * psi2 - , HR ~ H * rho + S.E ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , E.Ia ~ E * alpha * sigma + , E.Ip ~ E * (1 - alpha)* sigma + , Ia.R ~ Ia * gamma_a + , Ip.Im ~ Ip * mu * gamma_p + , Im.R ~ Im * gamma_m + , Ip.Is ~ Ip * (1 - mu) * gamma_p + , Is.ICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , Is.ICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUs.H2 ~ ICUs * psi1 + , H2.R ~ H2 * psi3 + , Is.H ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + #HICUd? in variables.csv but not flows.csv + #HICUs - shouldn't this be a flow? or does IsICUs contain this flow - do they skip H? + , ICUd.D ~ ICUd * psi2 + , H.R ~ H * rho ) ## state updates state_updates = list( - S ~ S - SE - , E ~ E + SE - EIa - EIp - , Ia ~ Ia + EIa - IaR - , Ip ~ Ip + EIp - IpIm - IpIs - , Im ~ Im + IpIm - , Is ~ Is + IpIs - IsICUs - IsH - IsICUd - , R ~ R + IaR + H2R + HR - , H ~ H + IsH - HR - , ICUs ~ ICUs + IsICUs - ICUsH2 - , ICUd ~ ICUd + IsICUd - ICUdD - , H2 ~ H2 + ICUsH2 - H2R - , D ~ D + ICUdD + S ~ S - S.E + , E ~ E + S.E - E.Ia - E.Ip + , Ia ~ Ia + E.Ia - Ia.R + , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is + , Im ~ Im + Ip.Im - Im.R + , Is ~ Is + Ip.Is - Is.ICUs - Is.H - Is.ICUd + , H ~ H + Is.H - H.R + , ICUs ~ ICUs + Is.ICUs - ICUs.H2 + , ICUd ~ ICUd + Is.ICUd - ICUd.D + , H2 ~ H2 + ICUs.H2 - H2.R + , R ~ R + Ia.R + Im.R + H.R + H2.R + , D ~ D + ICUd.D # where is IsD? (severely infected that die before they get hospitalized) ) ## simple unstructured scalar expression @@ -49,4 +51,4 @@ expr_list = ExprList( flow_rates , state_updates ) -) \ No newline at end of file +) diff --git a/inst/starter_models/seir/README.md b/inst/starter_models/seir/README.md index 316eb370..213fc89e 100644 --- a/inst/starter_models/seir/README.md +++ b/inst/starter_models/seir/README.md @@ -5,50 +5,28 @@ author: Steve Walker --- We introduce the *exposed* compartment, to capture the time period in which individuals have been exposed to the disease but are not able to infect others yet. + # States -| variable | description | -| -- | -- | -| S | Number of susceptible individuals | -| E | Number of exposed individuals | -| I | Number of infectious individuals | -| R | Number of recovered individuals | +| variable | description | +| -------- | --------------------------------- | +| S | Number of susceptible individuals | +| E | Number of exposed individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | -The size of the total population is, $ N = S + E+ I + R$. -# Parameters +The size of the total population is, $ N = S + E + I + R$. -| variable | description | -| -- | -- | -| $\beta$ | per capita transmission rate | -| $\alpha$ | per capita rate of infectious progression | %% must be a better way to say this -| $\gamma$ | per capita recovery rate | +# Parameters +| variable | description | +| -------- | ----------------------------------------------------------------------------- | +| $\beta$ | per capita transmission rate | +| $\alpha$ | per capita infection rate (average time spent in compartment E is $1/\alpha$) | +| $\gamma$ | per capita recovery rate | # Dynamics - -```mermaid -%%{ - init: { - 'theme': 'base', - 'themeVariables': { - 'primaryColor': '#FDBF57', - 'primaryTextColor': '#000', - 'primaryBorderColor': '#5E6A71', - 'lineColor': '#7A003C', - 'secondaryColor': '#fff', - 'tertiaryColor': '#fff' - } - } -}%% - -flowchart LR - S(S) -- βI --> E(E) - E -- α --> I(I) - I -- γ --> R(R) -``` - - $$ \begin{align*} \frac{dS}{dt} &= -\beta SI \\ diff --git a/inst/starter_models/seir/model.R b/inst/starter_models/seir/model.R index 028da131..fed9c28c 100644 --- a/inst/starter_models/seir/model.R +++ b/inst/starter_models/seir/model.R @@ -10,8 +10,8 @@ computations = list( ## absolute flow rates (per time only) flow_rates = list( - infection ~ S * I * beta / N - , exposure ~ alpha * E + exposure ~ S * I * beta / N + , infection ~ alpha * E , recovery ~ gamma * I ) diff --git a/inst/starter_models/si/README.md b/inst/starter_models/si/README.md new file mode 100644 index 00000000..ffbd4fcd --- /dev/null +++ b/inst/starter_models/si/README.md @@ -0,0 +1,35 @@ +--- +title: "basic SI" +index_entry: "a very simple epidemic model" +author: Jennifer Freeman +--- + +This is the simplest possible epidemic model. + +# States + +| variable | description | +| -------- | --------------------------------- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | + +The size of the total population is, $N = S + I$. + +# Parameters + +| variable | description | +| -------- | ---------------------------- | +| $\beta$ | per capita transmission rate | + +# Dynamics + +$$ +\begin{align*} +\frac{dS}{dt} &= -\beta SI \\ +\frac{dI}{dt} &= \beta SI \\ +\end{align*} +$$ + +# References + +Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 diff --git a/inst/starter_models/sir/README.md b/inst/starter_models/sir/README.md index 4013b06e..0c48e66b 100644 --- a/inst/starter_models/sir/README.md +++ b/inst/starter_models/sir/README.md @@ -8,31 +8,23 @@ This is (nearly) the simplest possible 'vanilla' epidemic model, implemented as # States -| variable | description | -| -- | -- | -| S | Number of susceptible individuals | -| I | Number of infectious individuals | -| R | Number of recovered individuals | +| variable | description | +| -------- | --------------------------------- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | The size of the total population is, $N = S + I + R$. -# Parameters -| variable | description | -| -- | -- | -| $\beta$ | per capita transmission rate | -| $\gamma$ | per capita recovery rate | +# Parameters +| variable | description | +| -------- | ---------------------------- | +| $\beta$ | per capita transmission rate | +| $\gamma$ | per capita recovery rate | # Dynamics - -```mermaid -flowchart LR; - S(S) -- βI--> I(I); - I -- γ --> R(R); -``` - - $$ \begin{align*} \frac{dS}{dt} &= -\beta SI \\ @@ -41,7 +33,6 @@ $$ \end{align*} $$ - # References Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 diff --git a/inst/starter_models/sir_demo/README.md b/inst/starter_models/sir_demo/README.md index ac589cff..b20a114f 100644 --- a/inst/starter_models/sir_demo/README.md +++ b/inst/starter_models/sir_demo/README.md @@ -4,58 +4,30 @@ index_entry: "An SIR model with birth and death" --- We assume new individuals (births) join the susceptible compartment, and individuals can leave the population (die) from any compartment. + # States -| variable | description | -| -- | -- | -| S | Number of susceptible individuals | -| I | Number of infectious individuals | -| R | Number of recovered individuals | +| variable | description | +| -------- | --------------------------------- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | The size of the total population is, $ N = S + I + R$. + # Parameters -| variable | description | -| -- | -- | -| $\beta$ | per capita transmission rate | -| $\gamma$ | per capita recovery rate | -| $\nu$ | per capita birth rate | -| $\mu$ | per capita mortality rate | +| variable | description | +| -------- | ---------------------------- | +| $\beta$ | per capita transmission rate | +| $\gamma$ | per capita recovery rate | +| $\nu$ | per capita birth rate | +| $\mu$ | per capita mortality rate | The SIR model with demography often assumes that the time scale of epidemic changes is much shorter than demographic changes (Earn, 2008). This translates to a constant population size $N$ over time, with $\nu = \mu$. We parameterize birth and mortality rates separately to allow for the general case in which epidemic and demographic dynamics occur on similar time scales. # Dynamics - -```mermaid -%%{ - init: { - 'theme': 'base', - 'themeVariables': { - 'primaryColor': '#FDBF57', - 'primaryTextColor': '#000', - 'primaryBorderColor': '#5E6A71', - 'lineColor': '#7A003C', - 'secondaryColor': '#fff', - 'tertiaryColor': '#fff' - } - } -}%% - -flowchart LR - - START:::hidden -- νN --> S(S) - S -- βI--> I(I) - I -- γ --> R(R) - S -- μ --> END1:::hidden - I -- μ --> END2:::hidden - R -- μ --> END3:::hidden - -%% not sure if you can combine vertical and horizontal arrows in the same graph, need to look at subgraphs (https://stackoverflow.com/questions/66631182/can-i-control-the-direction-of-flowcharts-in-mermaid) - -``` - - $$ \begin{align*} \frac{dS}{dt} &= \nu N -\beta SI - \mu S \\ @@ -64,7 +36,6 @@ $$ \end{align*} $$ - # References Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 \ No newline at end of file diff --git a/inst/starter_models/sir_waning/README.md b/inst/starter_models/sir_waning/README.md index 30810a32..f89989c2 100644 --- a/inst/starter_models/sir_waning/README.md +++ b/inst/starter_models/sir_waning/README.md @@ -8,47 +8,24 @@ Endemic pathogens can sometimes be modelled by sending R back to S, thereby cont # States -| variable | description | -| -- | -- | -| S | Number of susceptible individuals | -| I | Number of infectious individuals | -| R | Number of recovered individuals | +| variable | description | +| -------- | --------------------------------- | +| S | Number of susceptible individuals | +| I | Number of infectious individuals | +| R | Number of recovered individuals | The size of the total population is, $ N = S + I + R$. + # Parameters -| variable | description | -| -- | -- | -| $\beta$ | per capita transmission rate | -| $\gamma$ | per capita recovery rate | +| variable | description | +| -------- | ------------------------------- | +| $\beta$ | per capita transmission rate | +| $\gamma$ | per capita recovery rate | | $\delta$ | per capita waning immunity rate | - # Dynamics - -```mermaid -%%{ - init: { - 'theme': 'base', - 'themeVariables': { - 'primaryColor': '#FDBF57', - 'primaryTextColor': '#000', - 'primaryBorderColor': '#5E6A71', - 'lineColor': '#7A003C', - 'secondaryColor': '#fff', - 'tertiaryColor': '#fff' - } - } -}%% - -flowchart LR - S(S) -- βI--> I(I) - I -- γ --> R(R) - R -- δ --> S -``` - - $$ \begin{align*} \frac{dS}{dt} &= -\beta SI + \delta R\\ diff --git a/inst/starter_models/ww/README.md b/inst/starter_models/ww/README.md index 108825a3..f4a86029 100644 --- a/inst/starter_models/ww/README.md +++ b/inst/starter_models/ww/README.md @@ -1,5 +1,91 @@ --- title: "Wastewater model" index_entry: "Macpan base with an additional wastewater component" -author: Maya Earn +author: Jennifer Freeman --- + +The McMasterPandemic model (Bolker, 2022) modified to include a wastewater component. + +# States + +| variable | description | +| -------- | ---------------------------------------------------------------------- | +| S | Number of susceptible individuals | +| E | Number of exposed individuals | +| I~a~ | Number of asymptomatic infectious individuals | +| I~p~ | Number of pre-symptomatic infectious individuals | +| I~m~ | Number of mildly infectious individuals | +| I~s~ | Number of severely infectious individuals | +| H | Number of hospitalized individuals (acute care) | +| ICU~s~ | Number of individuals admitted to the ICU with a survival prognosis | +| ICU~d~ | Number of individuals admitted to the ICU with a death prognosis | +| H~2~ | Number of hospitalized individuals (acute care) after ICU stay | +| D | Number of dead individuals | +| R | Number of recovered individuals | +| W | Number of infectious individuals with detectable virus in wastewater * | +| A | Total concentration of virus in wastewater * | + +The size of the total population is, $ N = S + E + I_a + I_p + I_m + I_s + H + ICU_s + ICU_d + H_2 + D + R$. + +\* Need to confirm state descriptions + +# Parameters + +| variable | description | +| -------------- | ----------------------------------------------------------------------------------- | +| $\beta_0$ | baseline (non-intervention) transmission across categories | +| $C_a$ | relative asymptomatic transmission (or contact) proportion | +| $C_p$ | relative presymptomatic transmission (or contact) proportion | +| $C_m$ | relative mildly transmission (or contact) proportion | +| $C_s$ | relative severly transmission (or contact) proportion | +| $\alpha$ | fraction of infections that are asymptomatic | +| $\mu$ | fraction of symptomatic infections that are mild | +| $\sigma$ | 1/time in exposed class | +| $\gamma_a$ | 1/time to recovery for asymptomatic infections | +| $\gamma_p$ | 1/time in pre-symptomatic state | +| $\gamma_m$ | 1/time to recovery for mildly symptomatic infections | +| $\gamma_s$ | 1/time spent in severely symptomatic state before either hospitalization or death | +| $\rho$ | 1/time in hospital (initial acute care admission) | +| $\delta$ | fraction of hospitalized infections that are fatal ** | +| $\delta_{nh}$ | probability of mortality without hospitalization | +| $\text{iso}_m$ | relative self-isolation/distancing of mild cases * | +| $\text{iso}_s$ | relative self-isolation/distancing of severe cases * | +| $\phi_1$ | fraction of hospitalized infections that only require acute care (no ICU admission) | +| $\phi_2$ | fraction of ICU infections that are fatal | +| $\psi_1$ | 1/time spent in ICU before returning to acute care | +| $\psi_2$ | 1/time spent in ICU before dying | +| $\psi_3$ | 1/time in post-ICU acute care before hospital discharge | +| $\nu$ | fraction of infectious individuals with detectable virus in wastewater * | +| $\xi$ | rate at which virus is denaturing/removed from wastewater * | + +\* These parameters were not detailed in [Papst](#references) (TBD), but they were included in model definition files. +\** This parameter does not appear in the model dynamics. + +# Dynamics + +$$ +\begin{align*} +\frac{dS}{dt} &= -\beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N \\ +\frac{dE}{dt} &= \beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N - \sigma E \\ +\frac{dI_a}{dt} &= \alpha\sigma E- \gamma_a I_a \\ +\frac{dI_p}{dt} &= (1-\alpha)\sigma E- \gamma_p I_p \\ +\frac{dI_m}{dt} &= \mu\gamma_pI_p- \gamma_m I_m \\ +\frac{dI_s}{dt} &= (1-\mu)\gamma_pI_p- (1-\delta_{nh})\gamma_s I_s \\ +\frac{dH}{dt} &= (1-\delta_{nh})\phi_1\gamma_s I_s - \rho H \\ +\frac{dICU_s}{dt} &= (1-\delta_{nh})(1-\phi_1)(1-\phi_2)\gamma_s I_s - \psi_1 ICU_s \\ +\frac{dICU_d}{dt} &= (1-\delta_{nh})(1-\phi_1)\phi_2\gamma_s I_s - \psi_2 ICU_d \\ +\frac{dH_2}{dt} &= \psi_1 ICU_s - \psi_3 H_2 \\ +\frac{dR}{dt} &= \gamma_a I_a + \gamma_m I_m + \rho H + \psi_3 H_2 \\ +\frac{dD}{dt} &= \psi_2 ICU_d \\ +\frac{dW}{dt} &= \nu I_a + \nu I_p + \nu I_m + \nu I_s - \xi W \\ +\frac{dA}{dt} &= \xi W A +\end{align*} +$$ + +When there were discrepancies with how the model is expressed in Papst (TBD) versus model definition files, the latter was chosen. + +# References + +Bolker B, Steve Walker, David Earn, Morgan Kain, Mike Li, Jonathan Dushoff (2022). McMasterPandemic: Pandemic Model. R package version 0.2.0.0, https://github.com/mac-theobio/McMasterPandemic. + +Papst, Irena, Mike Li, David Champredon, Aamir Fazil, Jonathan Dushoff, Ben Bolker, David Earn (draft/preprint). Forecasting infectious disease spread as new variants emerge and vaccination rates increase. Accessed 4 Jan 2024. diff --git a/inst/starter_models/ww/model.R b/inst/starter_models/ww/model.R index 415f898c..e8d784fa 100644 --- a/inst/starter_models/ww/model.R +++ b/inst/starter_models/ww/model.R @@ -5,48 +5,48 @@ library(macpan2) ## free form computations for convenience computations = list( - N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D, W, A) + N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) ) ## absolute flow rates (per time only) flow_rates = list( - SE ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) - , EIa ~ E * alpha * sigma - , EIp ~ E * (1 - alpha)* sigma - , IaR ~ Ia * gamma_a - , IpIm ~ Ip * mu * gamma_p - , ImR ~ Im * gamma_m - , IpIs ~ Ip * (1 - mu) * gamma_p - , IsICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s - , ICUsH2 ~ ICUs * psi1 - , H2R ~ H2 * psi3 - , IsH ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s - , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s - , ICUdD ~ ICUd * psi2 - , HR ~ H * rho - , IaW ~ Ia * nu # or * or N? - , IpW ~ Ip * nu - , ImW ~ Im * nu - , IsW ~ Is * nu - , WA ~ W * xi + S.E ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , E.Ia ~ E * alpha * sigma + , E.Ip ~ E * (1 - alpha)* sigma + , Ia.R ~ Ia * gamma_a + , Ip.Im ~ Ip * mu * gamma_p + , Im.R ~ Im * gamma_m + , Ip.Is ~ Ip * (1 - mu) * gamma_p + , Is.ICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , Is.ICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUs.H2 ~ ICUs * psi1 + , H2.R ~ H2 * psi3 + , Is.H ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + , ICUd.D ~ ICUd * psi2 + , H.R ~ H * rho + , Ia.W ~ Ia * nu + , Ip.W ~ Ip * nu + , Im.W ~ Im * nu + , Is.W ~ Is * nu + , W.A ~ W * xi ) ## state updates state_updates = list( - S ~ S - SE - , E ~ E + SE - EIa - EIp - , Ia ~ Ia + EIa - IaR - , Ip ~ Ip + EIp - IpIm - IpIs - , Im ~ Im + IpIm - , Is ~ Is + IpIs - IsICUs - IsH - IsICUd - , R ~ R + IaR + H2R + HR - , H ~ H + IsH - HR - , ICUs ~ ICUs + IsICUs - ICUsH2 - , ICUd ~ ICUd + IsICUd - ICUdD - , H2 ~ H2 + ICUsH2 - H2R - , D ~ D + ICUdD - , W ~ W + IaW + IpW + ImW + IsW - WA - , A ~ A + WA + S ~ S - S.E + , E ~ E + S.E - EI.a - EI.p + , Ia ~ Ia + E.Ia - Ia.R + , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is + , Im ~ Im + Ip.Im - Im.R + , Is ~ Is + Ip.Is - Is.ICUs - Is.H - Is.ICUd + , H ~ H + Is.H - H.R + , ICUs ~ ICUs + Is.ICUs - ICUs.H2 + , ICUd ~ ICUd + Is.ICUd - ICUd.D + , H2 ~ H2 + ICUs.H2 - H2.R + , R ~ R + Ia.R + Im.R + H.R + H2.R + , D ~ D + ICUd.D + , W ~ W + Ia.W + Ip.W + Im.W + Is.W - W.A + , A ~ A + W.A ) ## simple unstructured scalar expression @@ -56,4 +56,4 @@ expr_list = ExprList( flow_rates , state_updates ) -) \ No newline at end of file +) From d519f5040cc26210c65193d74d66785b0fe16941 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Fri, 5 Jan 2024 12:01:29 -0500 Subject: [PATCH 251/332] testing latex split environment for github markdown rendering --- inst/starter_models/ww/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/starter_models/ww/README.md b/inst/starter_models/ww/README.md index f4a86029..629b1c48 100644 --- a/inst/starter_models/ww/README.md +++ b/inst/starter_models/ww/README.md @@ -64,7 +64,7 @@ The size of the total population is, $ N = S + E + I_a + I_p + I_m + I_s + H + # Dynamics $$ -\begin{align*} +\begin{split} \frac{dS}{dt} &= -\beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N \\ \frac{dE}{dt} &= \beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N - \sigma E \\ \frac{dI_a}{dt} &= \alpha\sigma E- \gamma_a I_a \\ @@ -79,7 +79,7 @@ $$ \frac{dD}{dt} &= \psi_2 ICU_d \\ \frac{dW}{dt} &= \nu I_a + \nu I_p + \nu I_m + \nu I_s - \xi W \\ \frac{dA}{dt} &= \xi W A -\end{align*} +\end{split} $$ When there were discrepancies with how the model is expressed in Papst (TBD) versus model definition files, the latter was chosen. From ebd36c5fcf4b1b468440f75a66fb0c84cecba389 Mon Sep 17 00:00:00 2001 From: jfree-man <90265470+jfree-man@users.noreply.github.com> Date: Fri, 5 Jan 2024 12:07:13 -0500 Subject: [PATCH 252/332] Testing README rendering --- inst/starter_models/ww/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/inst/starter_models/ww/README.md b/inst/starter_models/ww/README.md index 629b1c48..0a8cba0e 100644 --- a/inst/starter_models/ww/README.md +++ b/inst/starter_models/ww/README.md @@ -63,7 +63,6 @@ The size of the total population is, $ N = S + E + I_a + I_p + I_m + I_s + H + # Dynamics -$$ \begin{split} \frac{dS}{dt} &= -\beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N \\ \frac{dE}{dt} &= \beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N - \sigma E \\ @@ -80,7 +79,6 @@ $$ \frac{dW}{dt} &= \nu I_a + \nu I_p + \nu I_m + \nu I_s - \xi W \\ \frac{dA}{dt} &= \xi W A \end{split} -$$ When there were discrepancies with how the model is expressed in Papst (TBD) versus model definition files, the latter was chosen. From 558cf95f3c45e3d8b0badc19797dc46827fe0450 Mon Sep 17 00:00:00 2001 From: jfree-man <90265470+jfree-man@users.noreply.github.com> Date: Fri, 5 Jan 2024 12:08:17 -0500 Subject: [PATCH 253/332] reversing changes --- inst/starter_models/ww/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inst/starter_models/ww/README.md b/inst/starter_models/ww/README.md index 0a8cba0e..629b1c48 100644 --- a/inst/starter_models/ww/README.md +++ b/inst/starter_models/ww/README.md @@ -63,6 +63,7 @@ The size of the total population is, $ N = S + E + I_a + I_p + I_m + I_s + H + # Dynamics +$$ \begin{split} \frac{dS}{dt} &= -\beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N \\ \frac{dE}{dt} &= \beta_0\left(C_aI_a +C_pI_p + C_mI_m(1-\text{iso}_m) + C_sI_s(1-\text{iso}_s)\right)S/N - \sigma E \\ @@ -79,6 +80,7 @@ The size of the total population is, $ N = S + E + I_a + I_p + I_m + I_s + H + \frac{dW}{dt} &= \nu I_a + \nu I_p + \nu I_m + \nu I_s - \xi W \\ \frac{dA}{dt} &= \xi W A \end{split} +$$ When there were discrepancies with how the model is expressed in Papst (TBD) versus model definition files, the latter was chosen. From 677b4e24e29a136efe4de200db220f455a5f01f3 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Sat, 6 Jan 2024 20:59:22 -0500 Subject: [PATCH 254/332] starting to hack time-varying param vignette --- inst/starter_models/sir_waning/model.R | 15 ++++++++++++++- vignettes/sir_radial_basis_transmission.Rmd | 7 +++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/inst/starter_models/sir_waning/model.R b/inst/starter_models/sir_waning/model.R index a517f30e..a6472af0 100644 --- a/inst/starter_models/sir_waning/model.R +++ b/inst/starter_models/sir_waning/model.R @@ -12,7 +12,7 @@ computations = list( flow_rates = list( infection ~ S * I * beta / N , recovery ~ gamma * I - , waning_immunity ~ wane * R + , waning_immunity ~ phi * R ) ## state updates @@ -30,3 +30,16 @@ expr_list = ExprList( , state_updates ) ) + +## default values +init_mats = MatsList( + S = 99 + , I = 1 + , R = 0 + , beta = 0.2 + , gamma = 0.2 + , wane = 0.01 + , N = 100 + , .mats_to_save = "I" + , .mats_to_return = "I" +) diff --git a/vignettes/sir_radial_basis_transmission.Rmd b/vignettes/sir_radial_basis_transmission.Rmd index 89a836ef..062eea78 100644 --- a/vignettes/sir_radial_basis_transmission.Rmd +++ b/vignettes/sir_radial_basis_transmission.Rmd @@ -31,8 +31,11 @@ cat_file = function(...) cat(readLines(file.path(...)), sep = "\n") Before we can add the fancy radial basis for the transmission rate, we need a base model. We use an SIR model that has been modified to include waning. ```{r} -sir = Compartmental(system.file("starter_models", "sir_waning", package = "macpan2")) -sir$flows() +source(system.file("starter_models", "sir_waning", "model.R", package = "macpan2")) +tmb_simulator = TMBModel( + expr_list = expr_list +) +expr_list ``` From 13e1cd894ed8cb6ba72a0d321e65ca41356c9844 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Sun, 7 Jan 2024 15:29:13 -0500 Subject: [PATCH 255/332] working on calibration scripts for #150 --- .../sir_demo/calibration_example.R | 105 ++++++++++++++++++ inst/starter_models/sir_demo/tmb.R | 51 +++++++++ inst/starter_models/sir_waning/README.md | 8 +- .../sir_waning/calibration_example.R | 103 +++++++++++++++++ inst/starter_models/sir_waning/tmb.R | 51 +++++++++ 5 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 inst/starter_models/sir_demo/calibration_example.R create mode 100644 inst/starter_models/sir_demo/tmb.R create mode 100644 inst/starter_models/sir_waning/calibration_example.R create mode 100644 inst/starter_models/sir_waning/tmb.R diff --git a/inst/starter_models/sir_demo/calibration_example.R b/inst/starter_models/sir_demo/calibration_example.R new file mode 100644 index 00000000..06f857bc --- /dev/null +++ b/inst/starter_models/sir_demo/calibration_example.R @@ -0,0 +1,105 @@ +source("inst/starter_models/sir_demo/tmb.R") +library(ggplot2) +library(dplyr) + + +## ------------------------- +## define simulator +## ------------------------- + +# define objective function +obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) + +# simulator object +tmb_simulator = TMBModel( + init_mats = init_mats + , expr_list = expr_list + , obj_fn = obj_fn +)$simulator() + +## mp_tmb_simulator + +## ------------------------- +## parameterize model +## ------------------------- + +tmb_simulator$update$transformations(Log("beta")) +tmb_simulator$replace$params(log(init_mats$get("beta")), "log_beta") +tmb_simulator + +## ------------------------- +## simulate fake data +## ------------------------- + +true_beta = 0.3 +time_steps = 100L + +## set time_steps value +tmb_simulator$replace$time_steps(time_steps) + +## feed log(true_beta) to the simulator because we have +## already specified log-transformation of this parameter +observed_data = tmb_simulator$report(log(true_beta)) +# observed_data = tmb_simulator$report() + +## .mats_to_return is set to c("I", "N") +## compute incidence for observed data +I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) +I_obs_times = subset(observed_data, matrix == "I", select = c(time)) %>% pull() + +if (interactive()) { + plot(I_obs, type = "l", las = 1) +} + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +tmb_simulator$update$matrices( + I_obs = I_obs + , I_obs_times = I_obs_times +) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- + +if (interactive()) { + log_betas = seq(from = log(0.1), to = log(1), length = 100) + ll = vapply( + log_betas + , tmb_simulator$objective + , numeric(1L) + ) + plot(exp(log_betas), ll, type = "l", las = 1) + abline(v = true_beta) +} + +## ------------------------- +## fit parameters +## ------------------------- + +## optimize and check convergence +tmb_simulator$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + print(tmb_simulator$current$params_frame()) + print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) + print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) + plot(I_obs, type = "l", las = 1) + lines(tmb_simulator$report_values()[1:time_steps], col = "red") +} + +## ------------------------- +## exploring +## ------------------------- + +## plot population size (should be exponential) +if (interactive()) { + times_to_plot = 1:time_steps + pop_change = init_mats$get("birth_rate")-init_mats$get("death_rate") + plot((init_mats$get("N"))*((1+pop_change)^times_to_plot), type = "l", las = 1, ylab='N') + lines(tmb_simulator$report_values()[time_steps + (1:time_steps)], col = "red") +} + diff --git a/inst/starter_models/sir_demo/tmb.R b/inst/starter_models/sir_demo/tmb.R new file mode 100644 index 00000000..6d232371 --- /dev/null +++ b/inst/starter_models/sir_demo/tmb.R @@ -0,0 +1,51 @@ +library(macpan2) + +## define model as expression list +computations = list( + N ~ sum(S, I, R) +) + +flow_rates = list( + birth ~ birth_rate * N + , infection ~ S * I * beta / N + , recovery ~ gamma * I +) + +state_updates = list( + S ~ S - infection + birth - death_rate * S + , I ~ I + infection - recovery - death_rate * I + , R ~ R + recovery - death_rate * R +) + +model_evaluation = list( + log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +) + +expr_list = ExprList( + during = c( + computations + , flow_rates + , state_updates + ) + , after = model_evaluation +) + +## set defaults +init_mats = MatsList( + S = 99 + , I = 1 + , R = 0 + , beta = 0.2 + , gamma = 0.1 + , N = 100 + , birth_rate = 0.1 + , death_rate = 0.08 + , birth = empty_matrix + , infection = empty_matrix + , recovery = empty_matrix + , log_likelihood = empty_matrix + , I_obs = empty_matrix + , I_obs_times = empty_matrix + , .mats_to_save = c("I", "N") + , .mats_to_return = c("I", "N") +) \ No newline at end of file diff --git a/inst/starter_models/sir_waning/README.md b/inst/starter_models/sir_waning/README.md index f89989c2..ce40810f 100644 --- a/inst/starter_models/sir_waning/README.md +++ b/inst/starter_models/sir_waning/README.md @@ -14,7 +14,7 @@ Endemic pathogens can sometimes be modelled by sending R back to S, thereby cont | I | Number of infectious individuals | | R | Number of recovered individuals | -The size of the total population is, $ N = S + I + R$. +The size of the total population is, $N = S + I + R$. # Parameters @@ -22,15 +22,15 @@ The size of the total population is, $ N = S + I + R$. | -------- | ------------------------------- | | $\beta$ | per capita transmission rate | | $\gamma$ | per capita recovery rate | -| $\delta$ | per capita waning immunity rate | +| $\phi$ | per capita waning immunity rate | # Dynamics $$ \begin{align*} -\frac{dS}{dt} &= -\beta SI + \delta R\\ +\frac{dS}{dt} &= -\beta SI + \phi R\\ \frac{dI}{dt} &= \beta SI - \gamma I \\ -\frac{dR}{dt} &= \gamma I - \delta R +\frac{dR}{dt} &= \gamma I - \phi R \end{align*} $$ diff --git a/inst/starter_models/sir_waning/calibration_example.R b/inst/starter_models/sir_waning/calibration_example.R new file mode 100644 index 00000000..35cca1f6 --- /dev/null +++ b/inst/starter_models/sir_waning/calibration_example.R @@ -0,0 +1,103 @@ +source("inst/starter_models/sir_waning/tmb.R") +library(ggplot2) +library(dplyr) + + +## ------------------------- +## define simulator +## ------------------------- + +# define objective function +obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) + +# simulator object +tmb_simulator = TMBModel( + init_mats = init_mats + , expr_list = expr_list + , obj_fn = obj_fn +)$simulator() + + +## ------------------------- +## parameterize model +## ------------------------- + +tmb_simulator$update$transformations(Log("beta")) +tmb_simulator$replace$params(log(init_mats$get("beta")), "log_beta") +tmb_simulator + +## ------------------------- +## simulate fake data +## ------------------------- + +true_beta = 0.5 +time_steps = 100L + +## set time_steps value +tmb_simulator$replace$time_steps(time_steps) + +## feed log(true_beta) to the simulator because we have +## already specified log-transformation of this parameter +observed_data = tmb_simulator$report(log(true_beta)) + +## compute incidence for observed data +I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) +I_obs_times = subset(observed_data, matrix == "I", select = c(time)) %>% pull() + +if (interactive()) { + plot(I_obs, type = "l", las = 1) +} + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +tmb_simulator$update$matrices( + I_obs = I_obs + , I_obs_times = I_obs_times +) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- + +if (interactive()) { + log_betas = seq(from = log(0.1), to = log(1), length = 100) + ll = vapply( + log_betas + , tmb_simulator$objective + , numeric(1L) + ) + plot(exp(log_betas), ll, type = "l", las = 1) + abline(v = true_beta) +} + +## ------------------------- +## fit parameters +## ------------------------- + +## optimize and check convergence +tmb_simulator$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + print(tmb_simulator$current$params_frame()) + print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) + print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) + plot(I_obs, type = "l", las = 1) + lines(tmb_simulator$report_values(), col = "red") +} + +## ------------------------- +## exploring +## ------------------------- + +## plot recovered +if (interactive()) { + plot(tmb_simulator$report_values()[time_steps + (1:time_steps)], type = "l", las = 1, ylab='R') +} + +## plot waning immunity +if (interactive()) { + plot(tmb_simulator$report_values()[(time_steps*2) + (1:time_steps)], type = "l", las = 1, ylab='Waning Immunity') +} diff --git a/inst/starter_models/sir_waning/tmb.R b/inst/starter_models/sir_waning/tmb.R new file mode 100644 index 00000000..94150f74 --- /dev/null +++ b/inst/starter_models/sir_waning/tmb.R @@ -0,0 +1,51 @@ +library(macpan2) + +## define model as expression list +computations = list( + N ~ sum(S, I, R) +) + +flow_rates = list( + infection ~ S * I * beta / N + , recovery ~ gamma * I + , waning_immunity ~ phi * R +) + +state_updates = list( + S ~ S - infection + waning_immunity + , I ~ I + infection - recovery + , R ~ R + recovery - waning_immunity +) + +model_evaluation = list( + log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +) + +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) + , after = model_evaluation +) + +## set defaults +init_mats = MatsList( + S = 99 + , I = 1 + , R = 0 + , beta = 0.2 + , gamma = 0.2 + , phi = 0.01 + , N = 100 + , infection = empty_matrix + , recovery = empty_matrix + , waning_immunity = empty_matrix + , log_likelihood = empty_matrix + , I_obs = empty_matrix + , I_obs_times = empty_matrix + , .mats_to_save = c("I","R","waning_immunity") + , .mats_to_return = c("I","R","waning_immunity") +) + From b5c2af2a12deffe582859d1edf3c1be831950b27 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 8 Jan 2024 07:51:28 -0500 Subject: [PATCH 256/332] seir example calibration script --- .../starter_models/seir/calibration_example.R | 114 ++++++++++++++++++ inst/starter_models/seir/tmb.R | 71 +++++++++++ 2 files changed, 185 insertions(+) create mode 100644 inst/starter_models/seir/calibration_example.R create mode 100644 inst/starter_models/seir/tmb.R diff --git a/inst/starter_models/seir/calibration_example.R b/inst/starter_models/seir/calibration_example.R new file mode 100644 index 00000000..32212487 --- /dev/null +++ b/inst/starter_models/seir/calibration_example.R @@ -0,0 +1,114 @@ +source("inst/starter_models/seir/tmb.R") +library(ggplot2) +library(dplyr) + + +## ------------------------- +## define simulator +## ------------------------- + +true_beta = 0.5 +time_steps = 100L + +# define objective function +obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) + +# old simulator object +old_tmb_simulator = TMBModel( + init_mats = init_mats + , expr_list = expr_list + , obj_fn = obj_fn +)$simulator() + +# simulator object +tmb_simulator = mp_simulator( + model = model_spec + , time_steps = time_steps + , outputs = c("I","E") + # if you want to update defaults (use this for log_beta?) + , default = list(beta ~ log(model_spec$default$beta)) +) + + +## ------------------------- +## parameterize model +## ------------------------- + +tmb_simulator$update$transformations(Log("beta")) +#tmb_simulator$replace$params(log(model_spec$default$beta), "log_beta") +tmb_simulator + +old_tmb_simulator$update$transformations(Log("beta")) +old_tmb_simulator$replace$params(log(model_spec$default$beta), "log_beta") +old_tmb_simulator + +identical(old_tmb_simulator, tmb_simulator) + +# better way to update transformations in new simulator? + +## ------------------------- +## simulate fake data +## ------------------------- + +## feed log(true_beta) to the simulator because we have +## already specified log-transformation of this parameter +observed_data = tmb_simulator$report(log(true_beta)) + +## compute incidence for observed data +I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) +I_obs_times = subset(observed_data, matrix == "I", select = c(time)) %>% pull() + +if (interactive()) { + plot(I_obs, type = "l", las = 1) +} + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +tmb_simulator$update$matrices( + I_obs = I_obs + , I_obs_times = I_obs_times +) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- + +if (interactive()) { + log_betas = seq(from = log(0.1), to = log(1), length = 100) + ll = vapply( + log_betas + , tmb_simulator$objective + , numeric(1L) + ) + plot(exp(log_betas), ll, type = "l", las = 1) + abline(v = true_beta) +} + +## ------------------------- +## fit parameters +## ------------------------- + +## optimize and check convergence +tmb_simulator$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + # no output? + print(tmb_simulator$current$params_frame()) + # print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) + # print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) + plot(I_obs, type = "l", las = 1) + lines(tmb_simulator$report_values(), col = "red") +} + +## ------------------------- +## exploring +## ------------------------- + +## plot exposed density +if (interactive()) { + plot(tmb_simulator$report_values()[time_steps + (1:time_steps)], type = "l", las = 1, ylab='E') +} + diff --git a/inst/starter_models/seir/tmb.R b/inst/starter_models/seir/tmb.R new file mode 100644 index 00000000..d9b25b00 --- /dev/null +++ b/inst/starter_models/seir/tmb.R @@ -0,0 +1,71 @@ +library(macpan2) + +initialize_states = list(I ~ 1, S ~ N - I, E ~ 0, R ~ 0) + +computations = list( + N ~ sum(S, E, I, R) +) + +flow_rates = list( + exposure ~ S * I * beta / N + , infection ~ alpha * E + , recovery ~ gamma * I +) + +state_updates = list( + S ~ S - exposure + , E ~ E + exposure - infection + , I ~ I + infection - recovery + , R ~ R + recovery +) +model_evaluation = list( + log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +) + +## set defaults +default = list( beta = 0.2 + , alpha = 1/2 + , gamma = 0.2 + , N = 100 + , I_obs = 0 + , I_obs_times = 0) + +## model specification +model_spec = mp_tmb_model_spec( + before = c(initialize_states, computations) + , during = c(flow_rates, state_updates) + , after = model_evaluation + , default = default +) + +########################################## +## old way to specify model and defaults + +expr_list = ExprList( + before = computations + , during = c( + flow_rates + , state_updates + ) + , after = model_evaluation +) + +# set defaults +init_mats = MatsList( + S = 99 + , E = 0 + , I = 1 + , R = 0 + , beta = 0.2 + , alpha = 1/2 + , gamma = 0.2 + , N = 100 + , exposure = empty_matrix + , infection = empty_matrix + , recovery = empty_matrix + , log_likelihood = empty_matrix + , I_obs = empty_matrix + , I_obs_times = empty_matrix + , .mats_to_save = c("I","E") + , .mats_to_return = c("I","E") +) \ No newline at end of file From cb5e167262a868eb6b14952d9b855a9f79c6ea5f Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 08:41:20 -0500 Subject: [PATCH 257/332] integer vectors as input to integer sequences --- misc/dev/dev.cpp | 8 ++++---- src/macpan2.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 2c92bd81..ce2fad8c 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -1028,8 +1028,8 @@ class ExprEvaluator { // #' the two inputs. // #' int from, to; - from = CppAD::Integer(args[0].coeff(0,0)); - to = CppAD::Integer(args[1].coeff(0,0)); + from = args.get_as_int(0); + to = args.get_as_int(1); if (from>to) { SetError(MP2_COLON, "Lower bound greater than upper bound in : operation", row); return m; @@ -1052,8 +1052,8 @@ class ExprEvaluator { // #' as the default. // #' int length; - from = CppAD::Integer(args[0].coeff(0,0)); - length = CppAD::Integer(args[1].coeff(0,0)); + from = args.get_as_int(0); + length = args.get_as_int(1); by = args[2].coeff(0,0); if (length<=0) { SetError(MP2_SEQUENCE, "Sequence length is less than or equal to zero in seq operation", row); diff --git a/src/macpan2.cpp b/src/macpan2.cpp index e1af49e0..7e39b80a 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -1029,8 +1029,8 @@ class ExprEvaluator { // #' the two inputs. // #' int from, to; - from = CppAD::Integer(args[0].coeff(0,0)); - to = CppAD::Integer(args[1].coeff(0,0)); + from = args.get_as_int(0); + to = args.get_as_int(1); if (from>to) { SetError(MP2_COLON, "Lower bound greater than upper bound in : operation", row); return m; @@ -1053,8 +1053,8 @@ class ExprEvaluator { // #' as the default. // #' int length; - from = CppAD::Integer(args[0].coeff(0,0)); - length = CppAD::Integer(args[1].coeff(0,0)); + from = args.get_as_int(0); + length = args.get_as_int(1); by = args[2].coeff(0,0); if (length<=0) { SetError(MP2_SEQUENCE, "Sequence length is less than or equal to zero in seq operation", row); From 3c84759bd0c4331d1d350319b4a41197c47cfda4 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 08:55:12 -0500 Subject: [PATCH 258/332] integer vectors in tmb specs --- R/frame_utils.R | 6 ++++++ R/tmb_model.R | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/R/frame_utils.R b/R/frame_utils.R index 726b5878..626e2d3d 100644 --- a/R/frame_utils.R +++ b/R/frame_utils.R @@ -89,3 +89,9 @@ filter <- function(.data, ...) { rows <- Reduce("&", rows) .data[rows & !is.na(rows), ] } + + +reset_rownames = function(x) { + rownames(x) = NULL + x +} diff --git a/R/tmb_model.R b/R/tmb_model.R index edc4c7df..c72196da 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -272,14 +272,23 @@ mp_tmb_model_spec = function( , default = list() , integers = list() ) { - integers = (default + implied_integers = (default |> lapply(names) |> Filter(f = is.character) |> lapply(to_positions) |> unname() + |> unique() |> unlist() - |> c(integers) + |> as.list() ) + integers = c( + implied_integers, + integers + ) + + ambiguous = integers |> names() |> duplicated() |> any() + if (ambiguous) stop("Defaults and integers are ambiguously named.") + TMBModelSpec(before, during, after, default, integers) } @@ -355,7 +364,7 @@ mp_trajectory = function(model) { #' @export mp_trajectory.TMBSimulator = function(model) { - model$report() + model$report() |> reset_rownames() } From d183d19e14cc0a6370717ab26baba5e7ced8ab49 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 08:55:45 -0500 Subject: [PATCH 259/332] symptom status --- inst/starter_models/sir_symp/tmb.R | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 inst/starter_models/sir_symp/tmb.R diff --git a/inst/starter_models/sir_symp/tmb.R b/inst/starter_models/sir_symp/tmb.R new file mode 100644 index 00000000..6ae6bcb5 --- /dev/null +++ b/inst/starter_models/sir_symp/tmb.R @@ -0,0 +1,87 @@ +library(macpan2) +library(ggplot2) +library(dplyr) +library(tidyr) +## NOT DONE + +state_index = mp_index( + Epi = c("S", "I" , "I" , "R") + , Symp = c("" , "mild", "severe", "" ) +) +flow_rate_index = mp_cartesian( + mp_index(Epi = c("infection", "recovery")) + , mp_index(Symp = c("mild", "severe")) +) +state_labels = state_index |> mp_labels() +flow_rate_labels = flow_rate_index |> mp_labels() +I_labels = state_index |> mp_lookup("I") |> mp_labels() +infection_labels = flow_rate_index |> mp_lookup("infection") |> mp_labels() + +flow = data.frame( + rate = c("infection.mild", "infection.severe", "recovery.mild", "recovery.severe") + , from = c("S." , "S." , "I.mild" , "I.severe") + , to = c("I.mild" , "I.severe" , "R." , "R.") +) + +initialize_state = list( + state[S. ] ~ N - 1 + , state[I.mild] ~ 1 +) + +pre_computations = list( + I_effective ~ sum(infectivity * state[I]) / N +) + +force_of_infection = list( + per_capita[infection] ~ beta * I_effective +) + +## euler +update_state = list( + flow_rates[rate] ~ per_capita[rate] * state[from] + , outflow ~ group_sums(flow_rates[rate] , from , state) + , inflow ~ group_sums(flow_rates[rate] , to , state) + , state ~ state - outflow + inflow +) + +## hazard +update_state = list( + outflow_per_capita ~ group_sums(per_capita[rate], from, state) + , outflow ~ state * (1 - exp(-outflow_per_capita)) + , flow_rates ~ outflow[from] * per_capita[rate] / outflow_per_capita[from] + , inflow ~ group_sums(flow_rates[rate], to, state) + , state ~ state - outflow + inflow +) + + +spec = mp_tmb_model_spec( + before = initialize_state + , during = c(pre_computations, force_of_infection, update_state) + , default = list( + N = 100 + , beta = c(mild = 0.5, severe = 0.2) + , infectivity = c(mild = 1, severe = 1.2) + , state = mp_zero_vector(state_index) + , flow_rates = mp_zero_vector(flow_rate_labels) + , per_capita = c( + infection.mild = NA, recovery.mild = 0.3 + , infection.severe = NA, recovery.severe = 0.05 + ) + ) + , integers = list( + from = mp_indices(flow$from , state_labels) + , to = mp_indices(flow$to , state_labels) + , rate = mp_indices(flow$rate , flow_rate_labels) + , I = mp_indices(I_labels , state_labels) + , infection = mp_indices(infection_labels , flow_rate_labels) + ) +) + +(spec + |> mp_simulator(50L, I_labels) + |> mp_trajectory() + |> rename(Prevalence = value) + |> separate_wider_delim(row, '.', names = c("Epi", "Symptom Status")) + |> ggplot() + + geom_line(aes(time, Prevalence, colour = `Symptom Status`)) +) From 8a6e9b885b8ab36742a82caed46ae2cf83785571 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 08:56:09 -0500 Subject: [PATCH 260/332] update readme --- README.Rmd | 27 ++++++++++++++++++++ README.md | 73 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/README.Rmd b/README.Rmd index 04be132a..848e7476 100644 --- a/README.Rmd +++ b/README.Rmd @@ -197,6 +197,33 @@ The scale (e.g. log, logit) on which to fit these parameters must also be specif The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model, e.g. convolution kernel parameters). +### Alternative Trajectory Solvers + +Let $x$ be the state vector and $b$ be the vector of per-capita flow rates. Let $z$ and $y$ be the vectors of from and to states -- that is $z$ ($y$) is the vector the same length of $b$ containing the elements of $x$ associated with the from (to) state for each flow. Therefore, the $i$th flow is from $z[i]$ to $y[i]$ at per-capita rate $b[i]$. + +The other way to think about it is that for a single flow, $x_i$ is the from state, $x_j$ is the to state, and $b_k$ is the per-capita flow rate. + +* Euler + * Inflow: $x_i b_k$ + * Outflow: $x_i b_k$ +* Mean Euler-Multimomial + * Inflow + * Outflow: $$ + + +``` +outflow ~ group_sums(state * flow_rates, from, state) +``` + + + +#### Euler Step + +#### Mean Euler-Multinomial (a.k.a. Hazard Correction) + +#### Runge-Kutta 4 + + ### Time-Varying Parameters TODO diff --git a/README.md b/README.md index 566d5bc7..0bed9895 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ macpan2 with TMB - Model Library - Calibration + - Alternative Trajectory + Solvers - Time-Varying Parameters - + + outflow ~ group_sums(state * flow_rates, from, state) + +#### Euler Step + +#### Mean Euler-Multinomial (a.k.a. Hazard Correction) + +#### Runge-Kutta 4 + ### Time-Varying Parameters TODO @@ -456,16 +487,16 @@ sir = mp_tmb_model_spec( ``` ## matrix time row col value - ## 5 state 1 I 1.147500 - ## 8 state 2 I 1.316046 - ## 11 state 3 I 1.508417 - ## 14 state 4 I 1.727685 - ## 17 state 5 I 1.977228 - ## 20 state 6 I 2.260727 - ## 23 state 7 I 2.582154 - ## 26 state 8 I 2.945748 - ## 29 state 9 I 3.355960 - ## 32 state 10 I 3.817384 + ## 1 state 1 I 1.147500 + ## 2 state 2 I 1.316046 + ## 3 state 3 I 1.508417 + ## 4 state 4 I 1.727685 + ## 5 state 5 I 1.977228 + ## 6 state 6 I 2.260727 + ## 7 state 7 I 2.582154 + ## 8 state 8 I 2.945748 + ## 9 state 9 I 3.355960 + ## 10 state 10 I 3.817384 ### Model Structure and Bookkeeping From b60a049a7625fcc0c1f24f5f7640e780ae665188 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 08:57:01 -0500 Subject: [PATCH 261/332] wastewater edit --- inst/starter_models/ww/tmb.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/starter_models/ww/tmb.R b/inst/starter_models/ww/tmb.R index 2ec3a564..2bdf9671 100644 --- a/inst/starter_models/ww/tmb.R +++ b/inst/starter_models/ww/tmb.R @@ -24,7 +24,7 @@ flow_rates = list( , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s , ICUdD ~ ICUd * psi2 , HR ~ H * rho - , IaW ~ Ia * nu # or * or N? + , IaW ~ Ia * nu , IpW ~ Ip * nu , ImW ~ Im * nu , IsW ~ Is * nu From f5c406504c0a82bdd3b7aa5e04fedce16e0fbb29 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 8 Jan 2024 09:24:00 -0500 Subject: [PATCH 262/332] calibration stuff --- .../lotka_volterra/predator_prey/model.R | 2 +- .../starter_models/seir/calibration_example.R | 15 +++-- inst/starter_models/seir/tmb.R | 59 ++++++++++--------- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/inst/starter_models/lotka_volterra/predator_prey/model.R b/inst/starter_models/lotka_volterra/predator_prey/model.R index 01a59999..95393c18 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/model.R +++ b/inst/starter_models/lotka_volterra/predator_prey/model.R @@ -6,7 +6,7 @@ library(macpan2) ## free form computations for convenience (none) ## functional responses -## Holling type I is trivial (modify beta as needed) +## Holling type I is trivial (modify beta/delta as needed?) #' Holling type II #' diff --git a/inst/starter_models/seir/calibration_example.R b/inst/starter_models/seir/calibration_example.R index 32212487..54479e65 100644 --- a/inst/starter_models/seir/calibration_example.R +++ b/inst/starter_models/seir/calibration_example.R @@ -15,18 +15,18 @@ obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) # old simulator object old_tmb_simulator = TMBModel( - init_mats = init_mats + init_mats = init_mats , expr_list = expr_list , obj_fn = obj_fn )$simulator() # simulator object tmb_simulator = mp_simulator( - model = model_spec + model = model_spec , time_steps = time_steps , outputs = c("I","E") # if you want to update defaults (use this for log_beta?) - , default = list(beta ~ log(model_spec$default$beta)) + #, default = list(log_beta ~ log(model_spec$default$beta)) ) @@ -35,7 +35,7 @@ tmb_simulator = mp_simulator( ## ------------------------- tmb_simulator$update$transformations(Log("beta")) -#tmb_simulator$replace$params(log(model_spec$default$beta), "log_beta") +tmb_simulator$replace$params(log(model_spec$default$beta), "log_beta") tmb_simulator old_tmb_simulator$update$transformations(Log("beta")) @@ -95,12 +95,11 @@ tmb_simulator$optimize$nlminb() ## plot observed vs predicted if (interactive()) { - # no output? print(tmb_simulator$current$params_frame()) - # print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) - # print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) + print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) + print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) plot(I_obs, type = "l", las = 1) - lines(tmb_simulator$report_values(), col = "red") + lines(tmb_simulator$report_values()[1:time_steps], col = "red") } ## ------------------------- diff --git a/inst/starter_models/seir/tmb.R b/inst/starter_models/seir/tmb.R index d9b25b00..24ebf317 100644 --- a/inst/starter_models/seir/tmb.R +++ b/inst/starter_models/seir/tmb.R @@ -31,41 +31,44 @@ default = list( beta = 0.2 , I_obs_times = 0) ## model specification -model_spec = mp_tmb_model_spec( +spec = mp_tmb_model_spec( before = c(initialize_states, computations) , during = c(flow_rates, state_updates) , after = model_evaluation , default = default ) + + ########################################## ## old way to specify model and defaults -expr_list = ExprList( - before = computations - , during = c( - flow_rates - , state_updates - ) - , after = model_evaluation -) +# expr_list = ExprList( +# before = computations +# , during = c( +# flow_rates +# , state_updates +# ) +# , after = model_evaluation +# ) +# +# # set defaults +# init_mats = MatsList( +# S = 99 +# , E = 0 +# , I = 1 +# , R = 0 +# , beta = 0.2 +# , alpha = 1/2 +# , gamma = 0.2 +# , N = 100 +# , exposure = empty_matrix +# , infection = empty_matrix +# , recovery = empty_matrix +# , log_likelihood = empty_matrix +# , I_obs = empty_matrix +# , I_obs_times = empty_matrix +# , .mats_to_save = c("I","E") +# , .mats_to_return = c("I","E") +# ) -# set defaults -init_mats = MatsList( - S = 99 - , E = 0 - , I = 1 - , R = 0 - , beta = 0.2 - , alpha = 1/2 - , gamma = 0.2 - , N = 100 - , exposure = empty_matrix - , infection = empty_matrix - , recovery = empty_matrix - , log_likelihood = empty_matrix - , I_obs = empty_matrix - , I_obs_times = empty_matrix - , .mats_to_save = c("I","E") - , .mats_to_return = c("I","E") -) \ No newline at end of file From 131bfec2bd760a9439c44a264022fe45871e45b1 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 09:39:44 -0500 Subject: [PATCH 263/332] sir calibration example broken --- inst/starter_models/sir/example.R | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R index f4814513..a43062a2 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/example.R @@ -1,12 +1,13 @@ -source("inst/starter_models/sir/model.R") +source("inst/starter_models/sir/tmb.R") +sir = mp_simulator(spec, time_steps = 50L, outputs = "I") ## ------------------------- ## parameterize model ## ------------------------- -tmb_simulator$update$transformations(Log("beta")) -tmb_simulator$replace$params(log(init_mats$get("beta")), "log_beta") -tmb_simulator ## note the new expression before the simulation loop +sir$update$transformations(Log("beta")) +sir$replace$params(log(init_mats$get("beta")), "log_beta") +sir ## note the new expression before the simulation loop ## ------------------------- ## simulate fake data @@ -16,11 +17,11 @@ time_steps = 100L true_beta = 0.4 ## set time_steps value -tmb_simulator$replace$time_steps(time_steps) +sir$replace$time_steps(time_steps) ## feed log(true_beta) to the simulator because we have ## already specified log-transformation of this parameter -observed_data = tmb_simulator$report(log(true_beta)) +observed_data = sir$report(log(true_beta)) ## .mats_to_return is set to "I", so observed_data$value is ## the prevalence (density of I) over time @@ -34,7 +35,7 @@ if (interactive()) { ## update simulator with fake data to fit to ## ------------------------- -tmb_simulator$update$matrices( +sir$update$matrices( I_obs = observed_data$value , I_obs_times = observed_data$time ) @@ -48,7 +49,7 @@ if (interactive()) { log_betas = seq(from = log(0.1), to = log(1), length = 100) ll = vapply( log_betas - , tmb_simulator$objective + , sir$objective , numeric(1L) ) plot(exp(log_betas), ll, type = "l", las = 1) @@ -60,11 +61,11 @@ if (interactive()) { ## fit parameters ## ------------------------- -tmb_simulator$optimize$nlminb() +sir$optimize$nlminb() ## plot observed vs predicted value if (interactive()) { - print(tmb_simulator$current$params_frame()) + print(sir$current$params_frame()) plot(observed_data$value, type = "l", las = 1) - lines(tmb_simulator$report_values(), col = "red") + lines(sir$report_values(), col = "red") } From 8848a8bdfd9a2aae88afc014e192bfc6fc6f1421 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 8 Jan 2024 13:49:07 -0500 Subject: [PATCH 264/332] update to sir model spec --- inst/starter_models/sir/tmb.R | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/inst/starter_models/sir/tmb.R b/inst/starter_models/sir/tmb.R index 54306899..ab65887f 100644 --- a/inst/starter_models/sir/tmb.R +++ b/inst/starter_models/sir/tmb.R @@ -6,7 +6,7 @@ initialize_state = list( , S ~ N - I ) -compute_flow_rates = list( +flow_rates = list( infection ~ S * I * beta / N , recovery ~ gamma * I ) @@ -17,8 +17,22 @@ update_state = list( , R ~ R + recovery ) +model_evaluation = list( + log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +) + +## set defaults +default = list( beta = 0.2 + , gamma = 0.2 + , N = 100 + , I_obs = 0 + , I_obs_times = 0 +) + +## model specification spec = mp_tmb_model_spec( before = initialize_state - , during = c(compute_flow_rates, update_state) - , default = list(N = 100, beta = 0.2, gamma = 0.1) + , during = c(flow_rates, update_state) + , after = model_evaluation + , default = default ) From 5a89bd5a4446a9b854b9ab2abf2c1529eea00982 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 8 Jan 2024 13:54:03 -0500 Subject: [PATCH 265/332] sir example calibration script now working --- inst/starter_models/sir/example.R | 65 +++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/example.R index a43062a2..fc0b928c 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/example.R @@ -1,34 +1,66 @@ -source("inst/starter_models/sir/tmb.R") -sir = mp_simulator(spec, time_steps = 50L, outputs = "I") +#source("inst/starter_models/sir/tmb.R") +library(dplyr) +library(macpan2) + +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","sir",package="macpan2") +spec + +## ------------------------- +## define simulator +## ------------------------- + +# set number of time steps in simulation +time_steps = 100L + +# simulator object +sir = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = "I" +) + +## ------------------------- +## specify objective function +## ------------------------- + +# function must be specified as a valid argument to macpan2::ObjectiveFunction() +# spec$after shows how log_likelihood is computed +obj_fn = ~ -sum(log_likelihood) + +# update simulator to include this function +sir$replace$obj_fn(obj_fn) ## ------------------------- ## parameterize model ## ------------------------- sir$update$transformations(Log("beta")) -sir$replace$params(log(init_mats$get("beta")), "log_beta") -sir ## note the new expression before the simulation loop + +# choose which parameter(s) to estimate +sir$replace$params(log(spec$default$beta), "log_beta") +sir ## ------------------------- ## simulate fake data ## ------------------------- -time_steps = 100L +# beta value to simulate data with true_beta = 0.4 -## set time_steps value -sir$replace$time_steps(time_steps) - ## feed log(true_beta) to the simulator because we have ## already specified log-transformation of this parameter observed_data = sir$report(log(true_beta)) -## .mats_to_return is set to "I", so observed_data$value is -## the prevalence (density of I) over time -observed_data$value = rpois(time_steps, observed_data$value) +## compute incidence for observed data +I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) +I_obs_times = subset(observed_data, matrix == "I", select = c(time)) %>% pull() if (interactive()) { - plot(observed_data$value, type = "l", las = 1) + plot(I_obs, type = "l", las = 1) } ## ------------------------- @@ -36,11 +68,10 @@ if (interactive()) { ## ------------------------- sir$update$matrices( - I_obs = observed_data$value - , I_obs_times = observed_data$time + I_obs = I_obs + , I_obs_times = I_obs_times ) - ## ------------------------- ## plot likelihood surface (curve) ## ------------------------- @@ -66,6 +97,8 @@ sir$optimize$nlminb() ## plot observed vs predicted value if (interactive()) { print(sir$current$params_frame()) - plot(observed_data$value, type = "l", las = 1) + print(paste0("exp(default) ",exp(sir$current$params_frame()$default))) + print(paste0("exp(current) ",exp(sir$current$params_frame()$current))) + plot(I_obs, type = "l", las = 1) lines(sir$report_values(), col = "red") } From d30e8e61cf69b9b41ea14d250c0e5eb845f88d4c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 8 Jan 2024 15:35:25 -0500 Subject: [PATCH 266/332] quickstart in #154 --- R/tmb_model.R | 3 +- vignettes/quickstart.Rmd | 128 +++++++++++++++++++++++++++++++++++---- 2 files changed, 117 insertions(+), 14 deletions(-) diff --git a/R/tmb_model.R b/R/tmb_model.R index c72196da..78da08d5 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -486,7 +486,8 @@ TMBSimulationUtils = function() { self$.find_problematic_expression(r$expr_row) ) } - self$.simulation_formatter(r, .phases) + s = self$.simulation_formatter(r, .phases) + s[order(s$time), , drop = FALSE] ## TODO: move sorting by time to the c++ side } return_object(self, "TMBSimulationFormatter") } diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index 54a56ded..a275e2cd 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -20,6 +20,8 @@ library(ggplot2) library(dplyr) ``` +## Hello World + The following code specifies an SI model, which is I think is the simplest possible model of epidemiological transmission. ```{r hello-world} si = mp_tmb_model_spec( @@ -28,9 +30,9 @@ si = mp_tmb_model_spec( , S ~ N - I ) , during = list( - infection ~ beta * S * I / N - , S ~ S - infection - , I ~ I + infection + infection_rate ~ beta * S * I / N + , S ~ S - infection_rate + , I ~ I + infection_rate ) , default = list(N = 100, beta = 0.25) ) @@ -38,14 +40,23 @@ print(si) ``` Simulating from this model requires choosing the number of time-steps to run and the model outputs to generate. Syntax for simulating `macpan2` models is designed to combine with standard data prep and plotting tools in R, as we demonstrate with the following code. -```{r plot-tmb-si, fig.format='svg'} +```{r plot-tmb-si, fig.format='svg', fig.height=3} (si - |> mp_simulator(time_steps = 50, outputs = c("I", "infection")) + + ## macpan2 + |> mp_simulator( + time_steps = 50 + , outputs = c("I", "infection_rate") + ) |> mp_trajectory() + + ## dplyr |> mutate(quantity = case_match(matrix - , "I" ~ "Prevalance" - , "infection" ~ "Incidence" - )) + , "I" ~ "Prevalence" + , "infection_rate" ~ "Incidence" + )) + + ## ggplot2 |> ggplot() + geom_line(aes(time, value)) + facet_wrap(~ quantity, scales = "free") @@ -53,10 +64,101 @@ Simulating from this model requires choosing the number of time-steps to run and ) ``` +(Above, we used the [base R pipe operator](https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/#pipes), `|>`.) + +The remainder of this article looks at each step required to create this plot in more detail, and discusses alternative approaches. + +## Creating a Simulator + +The first step is to produce a simulator object, which can be used to generate simulation results. This object can be produced using the `mp_simulator` function, which takes the following arguments. + +- `model` : A model specification object, such as `si`. +- `time_steps`: How many time steps should the epidemic simulator run for? +- `outputs` : The model variables to return in simulation output. +- `default` (optional) : Allows one to update the default parameter values and initial conditions provided in the model specification (see the argument `default` above in the `mp_tmb_model_spec` function). Any variables that are not specified in `default` will be left at the the values in the specification. + +```{r tmb-si-simulator} +si_simulator = mp_simulator( + model = si + , time_steps = 50 + , outputs = c("I", "infection_rate") +) +si_simulator +``` + +This `si_simulator` object contains all of the information required to generate model simulations of `I` and `infection_rate` over 50 time steps, without actually generating the simulations. This more interesting step of simulation is covered in [the next section](#generating-simulations). But before moving on we explain why we separate the step of creating a simulator from the step of running simulations. + +The reason for two steps is to optimize performance in more computationally challenging applications of the software than those covered in this article. The step of creating the simulator object is more computationally intensive than the next step of actually generating the simulations. Therefore this separation is useful when a single simulator can be used to repeatedly generate many different simulations. Three common examples requiring repeated simulations from the same simulator are: + +* for models with stochasticity so that the output is different for each run, +* for calibrating model parameters to data using iterative optimization tools, +* and for running different scenarios by updating certain parameters before simulation. + +Because `macpan2` is primarily developed for such computationally challenging iterative simulation problems, we believe that it is better to introduce these two steps as distinct from the very beginning. Although two steps might seem unnecessarily complex within the context of this article, keeping them separate will make life easier when working with more realistic workflows and models. + +## Generating Simulations + +Now that we have a simulation engine object, `si_simulator`, we use it to generate simulation results using the `mp_trajectory` function. The results come out in [long (or "narrow") format](https://en.wikipedia.org/wiki/Wide_and_narrow_data), where there is exactly one value per row: + +```{r run_sims} +si_results = mp_trajectory(si_simulator) +si_results |> head(8) +``` + +The simulation results are output as a data frame with the following columns: + +- `matrix`: Which matrix does a value come from? All variables are represented as matrices, although in this article we only consider 1-by-1 matrices. +- `time`: The time index from `1` to `time_steps`. +- `row`, `col`: Placeholders for variable components in more complicated structured models (not covered in this article, but useful for example when S and I in different age groups or geographic locations are tracked separately). +- `value`: The simulated value for a particular state and time step. + +## Processing Results + +`macpan2` does not provide any data manipulation or plotting tools (although there are a few in `macpan2helpers`). The philosophy is to focus on the engine and modelling interface, but to provide outputs in formats that are easy to use with other data processing packages, like `ggplot2`, `dplyr`, and `tidyr`, all of which readily make use of data in long format. + +In the graphs above for example, we required a step that renames the model variables into something that makes more sense for the graphical presentation. Here we reproduce this, but take a little more care to produce a tidier dataset. +```{r rename-variables} +si_results = (si_results + |> mutate(matrix = case_match(matrix + , "I" ~ "Prevalence" + , "infection_rate" ~ "Incidence" + )) + |> rename(quantity = matrix) + |> select(time, quantity, value) +) +print(head(si_results, 8L)) +``` + +Here we rename the `I` state variable as the disease prevalence (number of currently infected individuals). Because this is a discrete-time model, we can reinterpret the `infection_rate` as the incidence (number of newly infected individuals) over one time step. Note that this approach to calculating incidence only works if the time period over which incidence is measured corresponds to the length of one time step. If this assumption is not met -- for example if the time step is one day but incidence data are reported every week -- then other approaches to computing incidence that are not covered here must be taken. -## TODO +We can generate a `ggplot` from this dataset as we did above. If you want to use base R plots, you can convert the long format data to wide format: +```{r pivot_wider} +si_results_wide <- (si_results + |> tidyr::pivot_wider( + , id_cols = time + , names_from = quantity + ) + |> rename(Time = time) +) +head(si_results_wide, n = 3) +``` + +```{r base_plot_ex, fig.width = 6} +with(si_results_wide, + plot(x = Time, + y = Incidence) +) +``` + +or -* Bring over from previous quickstart on `main`, which currently [here](https://canmod.github.io/macpan2/articles/quickstart) - * explanation of modelling output and philosophy of post-processing - * Modify [this](https://canmod.github.io/macpan2/articles/quickstart.html#inputting-values-to-create-model-simulators) so that it works with `default` value updates using `mp_tmb_simulator` - * Modify [this](https://canmod.github.io/macpan2/articles/quickstart.html#incidence) discussion of incidence +```{r base_matplot_ex, fig.width = 6} +par(las = 1) ## horizontal y-axis ticks +matplot( + si_results_wide[, 1] + , si_results_wide[,-1] + , type = "l" + , xlab = "Time", ylab = "" +) +legend("topleft", col = 1:3, lty = 1:3, legend = c("Prevalence", "Incidence")) +``` From 89bb86895734b9915f66a8adba2d5bd2e329ab46 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 9 Jan 2024 08:39:51 -0500 Subject: [PATCH 267/332] switch sir_waning tmb spec over to current standard --- inst/starter_models/sir_waning/tmb.R | 60 +++++++++++++++++----------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/inst/starter_models/sir_waning/tmb.R b/inst/starter_models/sir_waning/tmb.R index 94150f74..b5880f5b 100644 --- a/inst/starter_models/sir_waning/tmb.R +++ b/inst/starter_models/sir_waning/tmb.R @@ -17,35 +17,49 @@ state_updates = list( , R ~ R + recovery - waning_immunity ) -model_evaluation = list( - log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) -) +# model_evaluation = list( +# log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +# ) -expr_list = ExprList( +spec = mp_tmb_model_spec( before = computations , during = c( flow_rates , state_updates ) - , after = model_evaluation + #, after = model_evaluation + , default = list( + S = 99, I = 1, R = 0 + , beta = 0.2, gamma = 0.2, phi = 0.01 + ) ) -## set defaults -init_mats = MatsList( - S = 99 - , I = 1 - , R = 0 - , beta = 0.2 - , gamma = 0.2 - , phi = 0.01 - , N = 100 - , infection = empty_matrix - , recovery = empty_matrix - , waning_immunity = empty_matrix - , log_likelihood = empty_matrix - , I_obs = empty_matrix - , I_obs_times = empty_matrix - , .mats_to_save = c("I","R","waning_immunity") - , .mats_to_return = c("I","R","waning_immunity") -) +# expr_list = ExprList( +# before = computations +# , during = c( +# flow_rates +# , state_updates +# ) +# , after = model_evaluation +# ) +# +# ## set defaults +# init_mats = MatsList( +# S = 99 +# , I = 1 +# , R = 0 +# , beta = 0.2 +# , gamma = 0.2 +# , phi = 0.01 +# , N = 100 +# , infection = empty_matrix +# , recovery = empty_matrix +# , waning_immunity = empty_matrix +# , log_likelihood = empty_matrix +# , I_obs = empty_matrix +# , I_obs_times = empty_matrix +# , .mats_to_save = c("I","R","waning_immunity") +# , .mats_to_return = c("I","R","waning_immunity") +# ) +# From 1971a1ff8f4a6fc258ca77c191c93da2c60cefb6 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 9 Jan 2024 08:49:58 -0500 Subject: [PATCH 268/332] fix si tmb library model --- inst/starter_models/si/tmb.R | 64 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/inst/starter_models/si/tmb.R b/inst/starter_models/si/tmb.R index eb098497..246b4bee 100644 --- a/inst/starter_models/si/tmb.R +++ b/inst/starter_models/si/tmb.R @@ -1,5 +1,5 @@ library(macpan2) -integrator = "hazard" +integrator = "euler" ## could be euler, hazard, or rk4 (need to add demo_stoch) initialize_state = list( I ~ 1 @@ -7,41 +7,41 @@ initialize_state = list( ) flow_rates = list( - euler = list( + euler = list( infection ~ beta * S * I / N - ), - ## expected value of the Euler-Binomial - hazard = list( + ) + + , hazard = list( ## expected value of the Euler-Binomial infection ~ S * (1 - exp(-beta * I / N)) - ), + ) + , rk4 = list( + S_rk4 ~ S + , I_rk4 ~ I + , k1_infection ~ beta * S_rk4 * I_rk4 / N + + , S_rk4 ~ S_rk4 - k1_infection / 2 + , I_rk4 ~ I_rk4 + k1_infection / 2 + , k2_infection ~ beta * S_rk4 * I_rk4 / N + + , S_rk4 ~ S_rk4 - k2_infection / 2 + , I_rk4 ~ I_rk4 + k2_infection / 2 + , k3_infection ~ beta * S_rk4 * I_rk4 / N + + , S_rk4 ~ S_rk4 - k3_infection + , I_rk4 ~ I_rk4 + k3_infection + , k4_infection ~ beta * S_rk4 * I_rk4 / N + + , infection ~ ( + k1_infection + + 2 * k2_infection + + 2 * k2_infection + + k4_infection + ) / 6 + ) ## (demographic stochasticity) - #flow_rates = list( + #, demo_stoch = flow_rates = list( # infection ~ rbinom() ## need to implement rbinom on the c++ side #) - rk4 = list( - S_rk4 ~ S - , I_rk4 ~ I - , k1_infection ~ beta * S_rk4 * I_rk4 / N - - , S_rk4 ~ S_rk4 - k1_infection / 2 - , I_rk4 ~ I_rk4 + k1_infection / 2 - , k2_infection ~ beta * S_rk4 * I_rk4 / N - - , S_rk4 ~ S_rk4 - k2_infection / 2 - , I_rk4 ~ I_rk4 + k2_infection / 2 - , k3_infection ~ beta * S_rk4 * I_rk4 / N - - , S_rk4 ~ S_rk4 - k3_infection - , I_rk4 ~ I_rk4 + k3_infection - , k4_infection ~ beta * S_rk4 * I_rk4 / N - - , infection ~ ( - k1_infection - + 2 * k2_infection - + 2 * k2_infection - + k4_infection - ) / 6 - ) ) update_state = list( @@ -57,4 +57,4 @@ specs = lapply(flow_rates, \(flow_rates) { ) }) -spec = specs[["euler"]] +spec = specs[[integrator]] From a0762a7a4d49b34859803fe277efa6c10a981868 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 9 Jan 2024 10:47:23 -0500 Subject: [PATCH 269/332] finished all tmb.R scripts (#150) --- .../lotka_volterra/competition/tmb.R | 65 +++++++ .../lotka_volterra/predator_prey/tmb.R | 90 ++++++++++ inst/starter_models/macpan_base/tmb.R | 84 +++++++++ inst/starter_models/seir/tmb.R | 64 ++----- inst/starter_models/sir/tmb.R | 14 +- inst/starter_models/sir_demo/tmb.R | 50 +++--- inst/starter_models/sir_waning/tmb.R | 37 +--- inst/starter_models/ww/tmb.R | 160 +++++++++--------- 8 files changed, 357 insertions(+), 207 deletions(-) create mode 100644 inst/starter_models/lotka_volterra/competition/tmb.R create mode 100644 inst/starter_models/lotka_volterra/predator_prey/tmb.R create mode 100644 inst/starter_models/macpan_base/tmb.R diff --git a/inst/starter_models/lotka_volterra/competition/tmb.R b/inst/starter_models/lotka_volterra/competition/tmb.R new file mode 100644 index 00000000..33c095ce --- /dev/null +++ b/inst/starter_models/lotka_volterra/competition/tmb.R @@ -0,0 +1,65 @@ +library(macpan2) + +initialize_state = list( + X ~ 100 + , Y ~ 100 +) + +flow_rates = list( + ## growth rate of species X and Y + growth_x ~ rx * X + , growth_y ~ ry * Y + ## intraspecific effect + ## species X on X + , intraspecific_x ~ growth_x * axx * X + ## species Y on Y + , intraspecific_y ~ growth_y * ayy * Y + ## interspecific effect + ## species Y on X + , interspecific_xy ~ growth_x * axy * Y + ## species X on Y + , interspecific_yx ~ growth_y * ayx * X +) + +state_updates = list( + X ~ X + growth_x - intraspecific_x - interspecific_xy + , Y ~ Y + growth_y - intraspecific_y - interspecific_yx +) + +default = list( + rx = 0.5 + , ry = 1.2 + , axx = 1 + , ayy = 1.1 + , axy = 0.4 + , ayx = 1.3 +) + +## model spec +spec = mp_tmb_model_spec( + during = c(flow_rates, state_updates) + , default = default +) + + + + +# ## alternate parameterization of model by carrying +# ## capacity and relative interspecific effects +# ## alternate parameterization of model by carrying +# ## capacity and relative interspecific effects +# flow_rates = list( +# ## growth rate of species X and Y +# growth_x ~ rx * X +# , growth_y ~ ry * Y +# ## intraspecific effect +# ## species X on X +# , intraspecific_x ~ growth_x * X / Kx +# ## species Y on Y +# , intraspecific_y ~ growth_y * Y / Ky +# ## interspecific effect +# ## species Y on X +# , interspecific_xy ~ growth_x * alpha_xy * Y / Kx +# ## species X on Y +# , interspecific_yx ~ growth_y * alpha_yx * X / Ky +# ) diff --git a/inst/starter_models/lotka_volterra/predator_prey/tmb.R b/inst/starter_models/lotka_volterra/predator_prey/tmb.R new file mode 100644 index 00000000..8894c727 --- /dev/null +++ b/inst/starter_models/lotka_volterra/predator_prey/tmb.R @@ -0,0 +1,90 @@ +library(macpan2) + +initialize_state = list( + X ~ 100 # prey + , Y ~ 100 # predator +) + +## exponential prey growth +flow_rates = list( + ## growth rate of prey + growth_x ~ alpha * X + ## mortality rate of predator + , mortality_y ~ gamma * Y + ## effects from predation + ## mortality rate of prey (due to predation) + , mortality_x ~ beta * X * Y + ## growth rate of predator (due to predation) + , growth_y ~ delta * X * Y +) + +state_updates = list( + X ~ X + growth_x - mortality_x + , Y ~ Y + growth_y - mortality_y +) + +## set defaults +default = list( + alpha = 0.1 # prey growth + , beta = 0.08 # prey loss from predation + , gamma = 0.05 # predator mortality + , delta = 0.1 # predator gain from predation +) + +## model specification +spec = mp_tmb_model_spec( + before = initialize_state + , during = c(flow_rates, state_updates) + , default = default +) + + + + + +## alternate parameterization of model +## logistic prey growth, and non-linear functional responses +# flow_rates = list( +# ## growth rate of prey +# growth_x ~ alpha * X * (1 - (X/K)) +# ## mortality rate of predator +# , mortality_y ~ gamma * Y +# ## effects from predation +# ## compute functional response +# , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) +# #, functional_response ~ holling_ii(a = 5, h = 1, X = X) +# ## mortality rate of prey (due to predation) +# , mortality_x ~ beta * functional_response * Y +# ## growth rate of predator (due to predation) +# , growth_y ~ delta * functional_response * Y +# ) + + +########################################## +## old way to specify model and defaults + +expr_list = ExprList( + during = c( + flow_rates + , state_updates + ) + , after = model_evaluation +) + +# set defaults +init_mats = MatsList( + X = 100 + , Y = 100 + , alpha = 0.1 + , beta = 0.08 + , gamma = 0.05 + , delta = 0.1 + , growth_x = empty_matrix + , growth_y = empty_matrix + , mortality_x = empty_matrix + , mortality_y = empty_matrix + , I_obs = empty_matrix + , I_obs_times = empty_matrix + , .mats_to_save = c("X","Y") + , .mats_to_return = c("X","Y") +) diff --git a/inst/starter_models/macpan_base/tmb.R b/inst/starter_models/macpan_base/tmb.R new file mode 100644 index 00000000..d21cfeac --- /dev/null +++ b/inst/starter_models/macpan_base/tmb.R @@ -0,0 +1,84 @@ +library(macpan2) + +initialize_state = list( + S ~ 1.00E+06, E ~ 1 + , Ia ~ 0, Ip ~0, Im ~ 0, Is ~ 0 + , R ~ 0 + , H ~ 0, ICUs ~ 0, ICUd ~ 0, H2 ~ 0 + , D ~ 0 +) + +computations = list( + N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) +) + +flow_rates = list( + S.E ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , E.Ia ~ E * alpha * sigma + , E.Ip ~ E * (1 - alpha)* sigma + , Ia.R ~ Ia * gamma_a + , Ip.Im ~ Ip * mu * gamma_p + , Im.R ~ Im * gamma_m + , Ip.Is ~ Ip * (1 - mu) * gamma_p + , Is.ICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , Is.ICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUs.H2 ~ ICUs * psi1 + , H2.R ~ H2 * psi3 + , Is.H ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + , ICUd.D ~ ICUd * psi2 + , H.R ~ H * rho +) + +update_state = list( + S ~ S - S.E + , E ~ E + S.E - E.Ia - E.Ip + , Ia ~ Ia + E.Ia - Ia.R + , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is + , Im ~ Im + Ip.Im - Im.R + , Is ~ Is + Ip.Is - Is.ICUs - Is.H - Is.ICUd + , H ~ H + Is.H - H.R + , ICUs ~ ICUs + Is.ICUs - ICUs.H2 + , ICUd ~ ICUd + Is.ICUd - ICUd.D + , H2 ~ H2 + ICUs.H2 - H2.R + , R ~ R + Ia.R + Im.R + H.R + H2.R + , D ~ D + ICUd.D +) + + +## set defaults +default = list( + beta0 = 1 # Baseline (non-intervention) transmission across categories + , Ca = 2/3 # relative asymptomatic transmission (or contact) + , Cp = 1 # relative presymptomatic transmission (or contact) + , Cm = 1 # relative mildly symptomatic transmission (or contact) + , Cs = 1 # relative severely symptomatic transmission (or contact) + , alpha = 0.39 # Fraction of cases asymptomatic + , sigma = 1/3.3 # 1/time in exposed class + , gamma_a = 1/7 # 1/time for asymptomatic recovery + , gamma_m = 1/7 # 1/time for mildly symptomatic recovery + , gamma_s = 1/5.72 # 1/time for severely symptomatic transition to hospital/death + , gamma_p = 1/1.2 # 1/time in pre-symptomatic class + , rho = 1/10 # 1/time in hospital (acute care) + , delta = 0 # Fraction of acute-care cases that are fatal + , mu = 0.956 # Fraction of symptomatic cases that are mild + , nonhosp_mort = 0 # probability of mortality without hospitalization + , iso_m = 0 # Relative self-isolation/distancing of mild cases + , iso_s = 0 # Relative self-isolation/distancing of severe cases + , phi1 = 0.76 # Fraction of hospital cases to ICU + , phi2 = 0.26 # Fraction of ICU cases dying + , psi1 = 1/20 # Rate of ICU back to acute care + , psi2 = 1/8 # Rate of ICU to death + , psi3 = 1/5 # Rate of post-ICU to discharge + , c_prop = 1/10 # fraction of incidence reported as positive tests + , c_delay_mean = 11 # average delay between incidence and test report + , c_delay_cv = 0.25 # coefficient of variation of testing delay + , proc_disp = 0 # dispersion parameter for process error (0=demog stoch only) + , zeta = 0 # phenomenological heterogeneity parameter +) + +## model specification +spec = mp_tmb_model_spec( + before = c(initialize_state, computations) + , during = c(flow_rates, update_state) + , default = default +) diff --git a/inst/starter_models/seir/tmb.R b/inst/starter_models/seir/tmb.R index 24ebf317..8b8f0a3b 100644 --- a/inst/starter_models/seir/tmb.R +++ b/inst/starter_models/seir/tmb.R @@ -1,6 +1,11 @@ library(macpan2) -initialize_states = list(I ~ 1, S ~ N - I, E ~ 0, R ~ 0) +initialize_state = list( + I ~ 1 + , R ~ 0 + , E ~ 0 + , S ~ N - I +) computations = list( N ~ sum(S, E, I, R) @@ -12,63 +17,24 @@ flow_rates = list( , recovery ~ gamma * I ) -state_updates = list( +update_state = list( S ~ S - exposure , E ~ E + exposure - infection , I ~ I + infection - recovery , R ~ R + recovery ) -model_evaluation = list( - log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) -) ## set defaults -default = list( beta = 0.2 - , alpha = 1/2 - , gamma = 0.2 - , N = 100 - , I_obs = 0 - , I_obs_times = 0) +default = list( + beta = 0.2 + , alpha = 1/2 + , gamma = 0.1 + , N = 100 + ) ## model specification spec = mp_tmb_model_spec( - before = c(initialize_states, computations) - , during = c(flow_rates, state_updates) - , after = model_evaluation + before = c(initialize_state, computations) + , during = c(flow_rates, update_state) , default = default ) - - - -########################################## -## old way to specify model and defaults - -# expr_list = ExprList( -# before = computations -# , during = c( -# flow_rates -# , state_updates -# ) -# , after = model_evaluation -# ) -# -# # set defaults -# init_mats = MatsList( -# S = 99 -# , E = 0 -# , I = 1 -# , R = 0 -# , beta = 0.2 -# , alpha = 1/2 -# , gamma = 0.2 -# , N = 100 -# , exposure = empty_matrix -# , infection = empty_matrix -# , recovery = empty_matrix -# , log_likelihood = empty_matrix -# , I_obs = empty_matrix -# , I_obs_times = empty_matrix -# , .mats_to_save = c("I","E") -# , .mats_to_return = c("I","E") -# ) - diff --git a/inst/starter_models/sir/tmb.R b/inst/starter_models/sir/tmb.R index ab65887f..1db92b8a 100644 --- a/inst/starter_models/sir/tmb.R +++ b/inst/starter_models/sir/tmb.R @@ -17,22 +17,16 @@ update_state = list( , R ~ R + recovery ) -model_evaluation = list( - log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) -) - ## set defaults -default = list( beta = 0.2 - , gamma = 0.2 - , N = 100 - , I_obs = 0 - , I_obs_times = 0 +default = list( + beta = 0.2 + , gamma = 0.2 + , N = 100 ) ## model specification spec = mp_tmb_model_spec( before = initialize_state , during = c(flow_rates, update_state) - , after = model_evaluation , default = default ) diff --git a/inst/starter_models/sir_demo/tmb.R b/inst/starter_models/sir_demo/tmb.R index 6d232371..55d82b0c 100644 --- a/inst/starter_models/sir_demo/tmb.R +++ b/inst/starter_models/sir_demo/tmb.R @@ -1,6 +1,11 @@ library(macpan2) -## define model as expression list +initialize_state = list( + I ~ 1 + , R ~ 0 + , S ~ N - I +) + computations = list( N ~ sum(S, I, R) ) @@ -11,41 +16,24 @@ flow_rates = list( , recovery ~ gamma * I ) -state_updates = list( +update_state = list( S ~ S - infection + birth - death_rate * S , I ~ I + infection - recovery - death_rate * I , R ~ R + recovery - death_rate * R ) -model_evaluation = list( - log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) +## set defaults +default = list( + beta = 0.2 + , gamma = 0.1 + , N = 100 + , birth_rate = 0.1 + , death_rate = 0.08 ) -expr_list = ExprList( - during = c( - computations - , flow_rates - , state_updates - ) - , after = model_evaluation +## model specification +spec = mp_tmb_model_spec( + before = initialize_state + , during = c(computations, flow_rates, update_state) + , default = default ) - -## set defaults -init_mats = MatsList( - S = 99 - , I = 1 - , R = 0 - , beta = 0.2 - , gamma = 0.1 - , N = 100 - , birth_rate = 0.1 - , death_rate = 0.08 - , birth = empty_matrix - , infection = empty_matrix - , recovery = empty_matrix - , log_likelihood = empty_matrix - , I_obs = empty_matrix - , I_obs_times = empty_matrix - , .mats_to_save = c("I", "N") - , .mats_to_return = c("I", "N") -) \ No newline at end of file diff --git a/inst/starter_models/sir_waning/tmb.R b/inst/starter_models/sir_waning/tmb.R index b5880f5b..2ec6b8ee 100644 --- a/inst/starter_models/sir_waning/tmb.R +++ b/inst/starter_models/sir_waning/tmb.R @@ -1,6 +1,5 @@ library(macpan2) -## define model as expression list computations = list( N ~ sum(S, I, R) ) @@ -17,49 +16,17 @@ state_updates = list( , R ~ R + recovery - waning_immunity ) -# model_evaluation = list( -# log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) -# ) - +# model spec spec = mp_tmb_model_spec( before = computations , during = c( flow_rates , state_updates ) - #, after = model_evaluation + # defaults , default = list( S = 99, I = 1, R = 0 , beta = 0.2, gamma = 0.2, phi = 0.01 ) ) - -# expr_list = ExprList( -# before = computations -# , during = c( -# flow_rates -# , state_updates -# ) -# , after = model_evaluation -# ) -# -# ## set defaults -# init_mats = MatsList( -# S = 99 -# , I = 1 -# , R = 0 -# , beta = 0.2 -# , gamma = 0.2 -# , phi = 0.01 -# , N = 100 -# , infection = empty_matrix -# , recovery = empty_matrix -# , waning_immunity = empty_matrix -# , log_likelihood = empty_matrix -# , I_obs = empty_matrix -# , I_obs_times = empty_matrix -# , .mats_to_save = c("I","R","waning_immunity") -# , .mats_to_return = c("I","R","waning_immunity") -# ) -# diff --git a/inst/starter_models/ww/tmb.R b/inst/starter_models/ww/tmb.R index 2bdf9671..092bd2d0 100644 --- a/inst/starter_models/ww/tmb.R +++ b/inst/starter_models/ww/tmb.R @@ -1,98 +1,94 @@ library(macpan2) -## expression lists -################################################### +initialize_state = list( + S ~ 1.00E+06, E ~ 1 + , Ia ~ 0, Ip ~0, Im ~ 0, Is ~ 0 + , R ~ 0 + , H ~ 0, ICUs ~ 0, ICUd ~ 0, H2 ~ 0 + , D ~ 0 + , W ~ 0, A ~ 0 +) -## free form computations for convenience computations = list( N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) ) -## absolute flow rates (per time only) flow_rates = list( - SE ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) - , EIa ~ E * alpha * sigma - , EIp ~ E * (1 - alpha)* sigma - , IaR ~ Ia * gamma_a - , IpIm ~ Ip * mu * gamma_p - , ImR ~ Im * gamma_m - , IpIs ~ Ip * (1 - mu) * gamma_p - , IsICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s - , ICUsH2 ~ ICUs * psi1 - , H2R ~ H2 * psi3 - , IsH ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s - , IsICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s - , ICUdD ~ ICUd * psi2 - , HR ~ H * rho - , IaW ~ Ia * nu - , IpW ~ Ip * nu - , ImW ~ Im * nu - , IsW ~ Is * nu - , WA ~ W * xi + S.E ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) + , E.Ia ~ E * alpha * sigma + , E.Ip ~ E * (1 - alpha)* sigma + , Ia.R ~ Ia * gamma_a + , Ip.Im ~ Ip * mu * gamma_p + , Im.R ~ Im * gamma_m + , Ip.Is ~ Ip * (1 - mu) * gamma_p + , Is.ICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s + , Is.ICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s + , ICUs.H2 ~ ICUs * psi1 + , H2.R ~ H2 * psi3 + , Is.H ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s + , ICUd.D ~ ICUd * psi2 + , H.R ~ H * rho + , Ia.W ~ Ia * nu + , Ip.W ~ Ip * nu + , Im.W ~ Im * nu + , Is.W ~ Is * nu + , W.A ~ W * xi ) -## state updates state_updates = list( - S ~ S - SE - , E ~ E + SE - EIa - EIp - , Ia ~ Ia + EIa - IaR - , Ip ~ Ip + EIp - IpIm - IpIs - , Im ~ Im + IpIm - ImR - , Is ~ Is + IpIs - IsICUs - IsH - IsICUd - , R ~ R + IaR + H2R + HR + ImR - , H ~ H + IsH - HR - , ICUs ~ ICUs + IsICUs - ICUsH2 - , ICUd ~ ICUd + IsICUd - ICUdD - , H2 ~ H2 + ICUsH2 - H2R - , D ~ D + ICUdD - , W ~ W + IaW + IpW + ImW + IsW - WA - , A ~ A + WA + S ~ S - S.E + , E ~ E + S.E - EI.a - EI.p + , Ia ~ Ia + E.Ia - Ia.R + , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is + , Im ~ Im + Ip.Im - Im.R + , Is ~ Is + Ip.Is - Is.ICUs - Is.H - Is.ICUd + , H ~ H + Is.H - H.R + , ICUs ~ ICUs + Is.ICUs - ICUs.H2 + , ICUd ~ ICUd + Is.ICUd - ICUd.D + , H2 ~ H2 + ICUs.H2 - H2.R + , R ~ R + Ia.R + Im.R + H.R + H2.R + , D ~ D + ICUd.D + , W ~ W + Ia.W + Ip.W + Im.W + Is.W - W.A + , A ~ A + W.A +) + +# set defaults +default = list( + beta0 = 1 # Baseline (non-intervention) transmission across categories + , Ca = 2/3 # relative asymptomatic transmission (or contact) + , Cp = 1 # relative presymptomatic transmission (or contact) + , Cm = 1 # relative mildly symptomatic transmission (or contact) + , Cs = 1 # relative severely symptomatic transmission (or contact) + , alpha = 0.39 # Fraction of cases asymptomatic + , sigma = 1/3.3 # 1/time in exposed class + , gamma_a = 1/7 # 1/time for asymptomatic recovery + , gamma_m = 1/7 # 1/time for mildly symptomatic recovery + , gamma_s = 1/5.72 # 1/time for severely symptomatic transition to hospital/death + , gamma_p = 1/1.2 # 1/time in pre-symptomatic class + , rho = 1/10 # 1/time in hospital (acute care) + , delta = 0 # Fraction of acute-care cases that are fatal + , mu = 0.956 # Fraction of symptomatic cases that are mild + #, E0 = 5 # Initial number exposed + , nonhosp_mort = 0 # probability of mortality without hospitalization + , iso_m = 0 # Relative self-isolation/distancing of mild cases + , iso_s = 0 # Relative self-isolation/distancing of severe cases + , phi1 = 0.76 # Fraction of hospital cases to ICU + , phi2 = 0.26 # Fraction of ICU cases dying + , psi1 = 1/20 # Rate of ICU back to acute care + , psi2 = 1/8 # Rate of ICU to death + , psi3 = 1/5 # Rate of post-ICU to discharge + , c_prop = 1/10 # fraction of incidence reported as positive tests + , c_delay_mean = 11 # average delay between incidence and test report + , c_delay_cv = 0.25 # coefficient of variation of testing delay + , proc_disp = 0 # dispersion parameter for process error (0=demog stoch only) + , zeta = 0 # phenomenological heterogeneity parameter + , nu = 0.1 # something to do with waste-water + , xi = 0.5 # something to do with waste-water ) -## simple unstructured scalar expression +## model spec spec = mp_tmb_model_spec( before = computations - , during = c( - flow_rates - , state_updates - ) - , default = list( - S = 1.00E+06, E = 1 - , Ia = 0, Ip = 0, Im = 0, Is = 0 - , R = 0 - , H = 0, ICUs = 0, ICUd = 0, H2 = 0 - , D = 0 - , W = 0, A = 0 - , beta0 = 1 # Baseline (non-intervention) transmission across categories - , Ca = 2/3 # relative asymptomatic transmission (or contact) - , Cp = 1 # relative presymptomatic transmission (or contact) - , Cm = 1 # relative mildly symptomatic transmission (or contact) - , Cs = 1 # relative severely symptomatic transmission (or contact) - , alpha = 0.39 # Fraction of cases asymptomatic - , sigma = 1/3.3 # 1/time in exposed class - , gamma_a = 1/7 # 1/time for asymptomatic recovery - , gamma_m = 1/7 # 1/time for mildly symptomatic recovery - , gamma_s = 1/5.72 # 1/time for severely symptomatic transition to hospital/death - , gamma_p = 1/1.2 # 1/time in pre-symptomatic class - , rho = 1/10 # 1/time in hospital (acute care) - , delta = 0 # Fraction of acute-care cases that are fatal - , mu = 0.956 # Fraction of symptomatic cases that are mild - #, N = 1.00E+06 # Population size - #, E0 = 5 # Initial number exposed - , nonhosp_mort = 0 # probability of mortality without hospitalization - , iso_m = 0 # Relative self-isolation/distancing of mild cases - , iso_s = 0 # Relative self-isolation/distancing of severe cases - , phi1 = 0.76 # Fraction of hospital cases to ICU - , phi2 = 0.26 # Fraction of ICU cases dying - , psi1 = 1/20 # Rate of ICU back to acute care - , psi2 = 1/8 # Rate of ICU to death - , psi3 = 1/5 # Rate of post-ICU to discharge - , c_prop = 1/10 # fraction of incidence reported as positive tests - , c_delay_mean = 11 # average delay between incidence and test report - , c_delay_cv = 0.25 # coefficient of variation of testing delay - , proc_disp = 0 # dispersion parameter for process error (0=demog stoch only) - , zeta = 0 # phenomenological heterogeneity parameter - , nu = 0.1 # something to do with waste-water - , xi = 0.5 # something to do with waste-water - ) + , during = c(flow_rates, state_updates) + , default = default ) From 4798389e505f62919f21531a96b47a3f8ce6b0cc Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 9 Jan 2024 11:30:35 -0500 Subject: [PATCH 270/332] working calibration examples --- .../starter_models/seir/calibration_example.R | 113 ++++++++------- .../sir/{example.R => calibration_example.R} | 16 ++- .../sir_demo/calibration_example.R | 83 +++++++---- .../sir_waning/calibration_example.R | 134 +++++++++++++----- 4 files changed, 226 insertions(+), 120 deletions(-) rename inst/starter_models/sir/{example.R => calibration_example.R} (88%) diff --git a/inst/starter_models/seir/calibration_example.R b/inst/starter_models/seir/calibration_example.R index 54479e65..19c08c95 100644 --- a/inst/starter_models/seir/calibration_example.R +++ b/inst/starter_models/seir/calibration_example.R @@ -1,89 +1,98 @@ -source("inst/starter_models/seir/tmb.R") +#source("inst/starter_models/seir/tmb.R") +library(macpan2) library(ggplot2) library(dplyr) +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","seir",package="macpan2") +spec ## ------------------------- ## define simulator ## ------------------------- -true_beta = 0.5 +# set number of time steps in simulation time_steps = 100L -# define objective function -obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) +# simulator object +seir = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("S", "E", "I","R") +) -# old simulator object -old_tmb_simulator = TMBModel( - init_mats = init_mats - , expr_list = expr_list - , obj_fn = obj_fn -)$simulator() +## ------------------------- +## specify objective function +## ------------------------- -# simulator object -tmb_simulator = mp_simulator( - model = model_spec - , time_steps = time_steps - , outputs = c("I","E") - # if you want to update defaults (use this for log_beta?) - #, default = list(log_beta ~ log(model_spec$default$beta)) +# negative log likelihood +# choosing to use E to estimate alpha +obj_fn = ~ -sum(dpois(E_obs, rbind_time(E, E_obs_times))) + +# update simulator to create new variables +seir$update$matrices( + E_obs = empty_matrix + , E_obs_times = empty_matrix ) +# update simulator to include this function +seir$replace$obj_fn(obj_fn) + ## ------------------------- ## parameterize model ## ------------------------- -tmb_simulator$update$transformations(Log("beta")) -tmb_simulator$replace$params(log(model_spec$default$beta), "log_beta") -tmb_simulator - -old_tmb_simulator$update$transformations(Log("beta")) -old_tmb_simulator$replace$params(log(model_spec$default$beta), "log_beta") -old_tmb_simulator - -identical(old_tmb_simulator, tmb_simulator) +# choose which parameter(s) to estimate +# 1/alpha = time spent in E compartment +seir$replace$params(spec$default$alpha,"alpha") -# better way to update transformations in new simulator? ## ------------------------- ## simulate fake data ## ------------------------- -## feed log(true_beta) to the simulator because we have -## already specified log-transformation of this parameter -observed_data = tmb_simulator$report(log(true_beta)) +# alpha value to simulate data with +true_alpha = 1/5 -## compute incidence for observed data -I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) -I_obs_times = subset(observed_data, matrix == "I", select = c(time)) %>% pull() +## simulate observed data using true parameters +observed_data = seir$report(true_alpha) + +## compute exposure for each time step +E_obs = rpois(time_steps, subset(observed_data, matrix == "E", select = c(value)) %>% pull()) +E_obs_times = subset(observed_data, matrix == "E", select = c(time)) %>% pull() if (interactive()) { - plot(I_obs, type = "l", las = 1) + plot(E_obs, type = "l", las = 1) } + ## ------------------------- ## update simulator with fake data to fit to ## ------------------------- -tmb_simulator$update$matrices( - I_obs = I_obs - , I_obs_times = I_obs_times +seir$update$matrices( + E_obs = E_obs + , E_obs_times = E_obs_times ) + ## ------------------------- ## plot likelihood surface (curve) ## ------------------------- if (interactive()) { - log_betas = seq(from = log(0.1), to = log(1), length = 100) + alphas = seq(from = 1/100, to = 1, length = 100) ll = vapply( - log_betas - , tmb_simulator$objective + alphas + , seir$objective , numeric(1L) ) - plot(exp(log_betas), ll, type = "l", las = 1) - abline(v = true_beta) + plot(alphas, ll, type = "l", las = 1) + abline(v = true_alpha) } ## ------------------------- @@ -91,23 +100,27 @@ if (interactive()) { ## ------------------------- ## optimize and check convergence -tmb_simulator$optimize$nlminb() +## converges with a warning +seir$optimize$nlminb() ## plot observed vs predicted if (interactive()) { - print(tmb_simulator$current$params_frame()) - print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) - print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) - plot(I_obs, type = "l", las = 1) - lines(tmb_simulator$report_values()[1:time_steps], col = "red") + print(seir$current$params_frame()) + print(paste0("default alpha ",seir$current$params_frame()$default)) + print(paste0("current alpha ",seir$current$params_frame()$current)) + plot(E_obs, type = "l", las = 1) + lines(seir$report() %>% filter(matrix=="E") %>% select(time,value), col = "red") } ## ------------------------- ## exploring ## ------------------------- -## plot exposed density +## plot all densities if (interactive()) { - plot(tmb_simulator$report_values()[time_steps + (1:time_steps)], type = "l", las = 1, ylab='E') + ggplot(seir$report(), aes(x=time, y=value, colour=matrix))+ + geom_line()+ + theme_bw()+ + ylab("individuals") } diff --git a/inst/starter_models/sir/example.R b/inst/starter_models/sir/calibration_example.R similarity index 88% rename from inst/starter_models/sir/example.R rename to inst/starter_models/sir/calibration_example.R index fc0b928c..949de0bb 100644 --- a/inst/starter_models/sir/example.R +++ b/inst/starter_models/sir/calibration_example.R @@ -27,9 +27,17 @@ sir = mp_simulator( ## specify objective function ## ------------------------- -# function must be specified as a valid argument to macpan2::ObjectiveFunction() -# spec$after shows how log_likelihood is computed -obj_fn = ~ -sum(log_likelihood) +# negative log likelihood +# I_obs = observed I +obj_fn = ~ -sum(dpois(I_obs, rbind_time(I, I_obs_times))) + +# update simulator to create new variables +# I_obs and I_obs_times and initialize +sir$update$matrices( + I_obs = empty_matrix + , I_obs_times = empty_matrix +) + # update simulator to include this function sir$replace$obj_fn(obj_fn) @@ -38,6 +46,7 @@ sir$replace$obj_fn(obj_fn) ## parameterize model ## ------------------------- +# apply parameter transformation sir$update$transformations(Log("beta")) # choose which parameter(s) to estimate @@ -102,3 +111,4 @@ if (interactive()) { plot(I_obs, type = "l", las = 1) lines(sir$report_values(), col = "red") } + diff --git a/inst/starter_models/sir_demo/calibration_example.R b/inst/starter_models/sir_demo/calibration_example.R index 06f857bc..05fb76c3 100644 --- a/inst/starter_models/sir_demo/calibration_example.R +++ b/inst/starter_models/sir_demo/calibration_example.R @@ -1,48 +1,66 @@ -source("inst/starter_models/sir_demo/tmb.R") -library(ggplot2) +#source("inst/starter_models/sir_demo/tmb.R") +library(macpan2) library(dplyr) +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","sir_demo",package="macpan2") +spec ## ------------------------- ## define simulator ## ------------------------- -# define objective function -obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) +# set number of time steps in simulation +time_steps = 100L # simulator object -tmb_simulator = TMBModel( - init_mats = init_mats - , expr_list = expr_list - , obj_fn = obj_fn -)$simulator() +sir_demo = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("I","N") +) + +## ------------------------- +## specify objective function +## ------------------------- -## mp_tmb_simulator +# negative log likelihood +obj_fn = ~ -sum(dpois(I_obs, rbind_time(I, I_obs_times))) + +# update simulator to create new variables +# I_obs and I_obs_times and initialize +sir_demo$update$matrices( + I_obs = empty_matrix + , I_obs_times = empty_matrix +) + +# update simulator to include this function +sir_demo$replace$obj_fn(obj_fn) ## ------------------------- ## parameterize model ## ------------------------- -tmb_simulator$update$transformations(Log("beta")) -tmb_simulator$replace$params(log(init_mats$get("beta")), "log_beta") -tmb_simulator +sir_demo$update$transformations(Log("beta")) + +# choose which parameter(s) to estimate +sir_demo$replace$params(log(spec$default$beta), "log_beta") +sir_demo ## ------------------------- ## simulate fake data ## ------------------------- +# beta value to simulate data with true_beta = 0.3 -time_steps = 100L - -## set time_steps value -tmb_simulator$replace$time_steps(time_steps) ## feed log(true_beta) to the simulator because we have ## already specified log-transformation of this parameter -observed_data = tmb_simulator$report(log(true_beta)) -# observed_data = tmb_simulator$report() +observed_data = sir_demo$report(log(true_beta)) -## .mats_to_return is set to c("I", "N") ## compute incidence for observed data I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) I_obs_times = subset(observed_data, matrix == "I", select = c(time)) %>% pull() @@ -51,11 +69,12 @@ if (interactive()) { plot(I_obs, type = "l", las = 1) } + ## ------------------------- ## update simulator with fake data to fit to ## ------------------------- -tmb_simulator$update$matrices( +sir_demo$update$matrices( I_obs = I_obs , I_obs_times = I_obs_times ) @@ -67,8 +86,8 @@ tmb_simulator$update$matrices( if (interactive()) { log_betas = seq(from = log(0.1), to = log(1), length = 100) ll = vapply( - log_betas - , tmb_simulator$objective + log_betas + , sir_demo$objective , numeric(1L) ) plot(exp(log_betas), ll, type = "l", las = 1) @@ -80,15 +99,15 @@ if (interactive()) { ## ------------------------- ## optimize and check convergence -tmb_simulator$optimize$nlminb() +sir_demo$optimize$nlminb() ## plot observed vs predicted if (interactive()) { - print(tmb_simulator$current$params_frame()) - print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) - print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) + print(sir_demo$current$params_frame()) + print(paste0("exp(default) ",exp(sir_demo$current$params_frame()$default))) + print(paste0("exp(current) ",exp(sir_demo$current$params_frame()$current))) plot(I_obs, type = "l", las = 1) - lines(tmb_simulator$report_values()[1:time_steps], col = "red") + lines(sir_demo$report() %>% filter(matrix=="I") %>% select(value), col = "red") } ## ------------------------- @@ -98,8 +117,10 @@ if (interactive()) { ## plot population size (should be exponential) if (interactive()) { times_to_plot = 1:time_steps - pop_change = init_mats$get("birth_rate")-init_mats$get("death_rate") - plot((init_mats$get("N"))*((1+pop_change)^times_to_plot), type = "l", las = 1, ylab='N') - lines(tmb_simulator$report_values()[time_steps + (1:time_steps)], col = "red") + pop_change = spec$default$birth_rate-spec$default$death_rate + plot(spec$default$N*((1+pop_change)^times_to_plot), type = "l", las = 1, ylab='N') + lines(sir_demo$report() %>% filter(matrix=="N") %>% select(value), col = "red") + legend("topleft",legend=c("theoretical","observed"), lty = 1, col=c("black","red")) } + \ No newline at end of file diff --git a/inst/starter_models/sir_waning/calibration_example.R b/inst/starter_models/sir_waning/calibration_example.R index 35cca1f6..ebefb181 100644 --- a/inst/starter_models/sir_waning/calibration_example.R +++ b/inst/starter_models/sir_waning/calibration_example.R @@ -1,44 +1,72 @@ -source("inst/starter_models/sir_waning/tmb.R") +#source("inst/starter_models/sir_waning/tmb.R") +library(macpan2) library(ggplot2) library(dplyr) +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","sir_waning",package="macpan2") +spec ## ------------------------- ## define simulator ## ------------------------- -# define objective function -obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) +# set number of time steps in simulation +time_steps = 100L # simulator object -tmb_simulator = TMBModel( - init_mats = init_mats - , expr_list = expr_list - , obj_fn = obj_fn -)$simulator() +sir_waning = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("I","S", "waning_immunity") +) + +## ------------------------- +## specify objective function +## ------------------------- + +# negative log likelihood +obj_fn = ~ -sum(dpois(I_obs, rbind_time(I, I_obs_times))) +# update simulator to create new variables +# I_obs and I_obs_times and initialize +sir_waning$update$matrices( + I_obs = empty_matrix + , I_obs_times = empty_matrix +) + +# update simulator to include this function +sir_waning$replace$obj_fn(obj_fn) ## ------------------------- ## parameterize model ## ------------------------- -tmb_simulator$update$transformations(Log("beta")) -tmb_simulator$replace$params(log(init_mats$get("beta")), "log_beta") -tmb_simulator +sir_waning$update$transformations(Log("beta")) + +# choose which parameter(s) to estimate - log(beta) and phi +sir_waning$replace$params(c(log(spec$default$beta),spec$default$phi), + c("log_beta","phi") + ) + +sir_waning + ## ------------------------- ## simulate fake data ## ------------------------- -true_beta = 0.5 -time_steps = 100L +# beta value to simulate data with +true_beta = 0.3 -## set time_steps value -tmb_simulator$replace$time_steps(time_steps) +# phi value to simulate data with +true_phi = 0.09 -## feed log(true_beta) to the simulator because we have -## already specified log-transformation of this parameter -observed_data = tmb_simulator$report(log(true_beta)) +## simulate observed data using true parameters +observed_data = sir_waning$report(c(log(true_beta),true_phi)) ## compute incidence for observed data I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) @@ -48,11 +76,12 @@ if (interactive()) { plot(I_obs, type = "l", las = 1) } + ## ------------------------- ## update simulator with fake data to fit to ## ------------------------- -tmb_simulator$update$matrices( +sir_waning$update$matrices( I_obs = I_obs , I_obs_times = I_obs_times ) @@ -61,15 +90,27 @@ tmb_simulator$update$matrices( ## plot likelihood surface (curve) ## ------------------------- +# plot surface as contours if (interactive()) { log_betas = seq(from = log(0.1), to = log(1), length = 100) - ll = vapply( - log_betas - , tmb_simulator$objective - , numeric(1L) - ) - plot(exp(log_betas), ll, type = "l", las = 1) - abline(v = true_beta) + phis = seq(from = 1e-3, to = 0.2, length = 100) + x_y = expand.grid(log_betas, phis) %>% setNames(c("log_betas","phis")) + + ll = apply( + x_y + , 1 + , function(z) {sir_waning$objective(z["log_betas"], z["phis"])} + ) + + dat_for_plot <- cbind(x_y, ll) + + ggplot(dat_for_plot, aes(log_betas, phis, z=ll)) + + geom_contour_filled()+ + ## add true parameter values to compare + geom_vline(xintercept = log(true_beta), col='red')+ + geom_hline(yintercept = true_phi, col='red') + + } ## ------------------------- @@ -77,27 +118,48 @@ if (interactive()) { ## ------------------------- ## optimize and check convergence -tmb_simulator$optimize$nlminb() +## warning message, but converges +sir_waning$optimize$nlminb() ## plot observed vs predicted if (interactive()) { - print(tmb_simulator$current$params_frame()) - print(paste0("exp(default) ",exp(tmb_simulator$current$params_frame()$default))) - print(paste0("exp(current) ",exp(tmb_simulator$current$params_frame()$current))) - plot(I_obs, type = "l", las = 1) - lines(tmb_simulator$report_values(), col = "red") + + ## estimates are close to true values + print(sir_waning$current$params_frame()) + print(paste0("exp(default beta) ",exp(sir_waning$current$params_frame()$default[1]))) + print(paste0("exp(current beta) ",exp(sir_waning$current$params_frame()$current[1]))) + print(paste0("default phi ",sir_waning$current$params_frame()$default[2])) + print(paste0("current phi ",sir_waning$current$params_frame()$current[2])) + + + data_to_plot <- (cbind(as.numeric(I_obs),1:time_steps) + %>% data.frame() + %>% setNames(c("value","time")) + %>% mutate(type="observed") + ) %>% union(sir_waning$report() + %>% filter(matrix=="I") + %>% select(time,value) + %>% mutate(type="predicted") + ) + + ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + geom_line()+ + theme_bw()+ + ylab("I") + } ## ------------------------- ## exploring ## ------------------------- -## plot recovered +## plot S if (interactive()) { - plot(tmb_simulator$report_values()[time_steps + (1:time_steps)], type = "l", las = 1, ylab='R') + plot(sir_waning$report() %>% filter(matrix=="S") %>% select(time,value), + type="l", las = 1, ylab='S') } -## plot waning immunity +## plot waning immunity (phi*R) if (interactive()) { - plot(tmb_simulator$report_values()[(time_steps*2) + (1:time_steps)], type = "l", las = 1, ylab='Waning Immunity') + plot(sir_waning$report() %>% filter(matrix=="waning_immunity") %>% select(time,value), type = "l", las = 1, ylab='Waning Immunity') } From 5507c6f230e3df177b3d12c4998fd6e37ea837f6 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 9 Jan 2024 13:46:53 -0500 Subject: [PATCH 271/332] fixed typo --- inst/starter_models/ww/tmb.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/starter_models/ww/tmb.R b/inst/starter_models/ww/tmb.R index 092bd2d0..c0962ae8 100644 --- a/inst/starter_models/ww/tmb.R +++ b/inst/starter_models/ww/tmb.R @@ -37,7 +37,7 @@ flow_rates = list( state_updates = list( S ~ S - S.E - , E ~ E + S.E - EI.a - EI.p + , E ~ E + S.E - E.Ia - E.Ip , Ia ~ Ia + E.Ia - Ia.R , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is , Im ~ Im + Ip.Im - Im.R @@ -88,7 +88,7 @@ default = list( ## model spec spec = mp_tmb_model_spec( - before = computations + before = c(initialize_state, computations) , during = c(flow_rates, state_updates) , default = default ) From 5be08f5936feb934beefc376abac426ccb288cbb Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Tue, 9 Jan 2024 14:22:12 -0500 Subject: [PATCH 272/332] simple calibration examples for macpan_base and ww --- .../macpan_base/calibration_example.R | 151 ++++++++++++++++ inst/starter_models/ww/calibration_example.R | 167 ++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 inst/starter_models/macpan_base/calibration_example.R create mode 100644 inst/starter_models/ww/calibration_example.R diff --git a/inst/starter_models/macpan_base/calibration_example.R b/inst/starter_models/macpan_base/calibration_example.R new file mode 100644 index 00000000..e4b04aaf --- /dev/null +++ b/inst/starter_models/macpan_base/calibration_example.R @@ -0,0 +1,151 @@ +#source("inst/starter_models/macpan_base/tmb.R") +library(macpan2) +library(ggplot2) +library(dplyr) + +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","macpan_base",package="macpan2") +spec + +## ------------------------- +## define simulator +## ------------------------- + +# set number of time steps in simulation +time_steps = 100L + +# simulator object +macpan_base = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("Ia", "Ip", "Im", "Is") +) + +## ------------------------- +## parameterize model +## ------------------------- + +# interested in estimating asymptomatic relative transmission rate +macpan_base$replace$params(spec$default$Ca,"Ca") + +macpan_base + +## ------------------------- +## specify objective function +## ------------------------- + + +# negative log likelihood +obj_fn = ~ -sum(dpois(Ia_obs, rbind_time(Ia, Ia_obs_times))) + +# update simulator to create new variables +macpan_base$update$matrices( + Ia_obs = empty_matrix + , Ia_obs_times = empty_matrix +) + +# update simulator to include this function +macpan_base$replace$obj_fn(obj_fn) + + +## ------------------------- +## simulate fake data +## ------------------------- + +# Ca value to simulate data with +true_Ca = 0.8 + +## simulate observed data using true parameters +observed_data = macpan_base$report(true_Ca) + +## compute incidence for observed data +Ia_obs = rpois(time_steps, subset(observed_data, matrix == "Ia", select = c(value)) %>% pull()) +Ia_obs_times = subset(observed_data, matrix == "Ia", select = c(time)) %>% pull() + +if (interactive()) { + plot(Ia_obs, type = "l", las = 1) +} + + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +macpan_base$update$matrices( + Ia_obs = Ia_obs + , Ia_obs_times = Ia_obs_times +) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- + +# plot surface as contours +if (interactive()) { + + Ca_seq = seq(from = 0.1, to = 1, length = 100) + + ll = vapply( + Ca_seq + , macpan_base$objective + , numeric(1L) + ) + dat_for_plot <- (cbind(Ca_seq, ll) + %>% data.frame() + + ) + + ggplot(dat_for_plot, aes(Ca_seq, ll)) + + geom_line()+ + ## add true parameter values to compare + geom_vline(xintercept = true_Ca, col='red')+ + xlab("Ca") + +} + +## ------------------------- +## fit parameters +## ------------------------- + +## optimize and check convergence +macpan_base$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + + ## estimate is close to true + print(macpan_base$current$params_frame()) + print(paste0("default Ca ",macpan_base$current$params_frame()$default)) + print(paste0("current Ca ",macpan_base$current$params_frame()$current)) + + data_to_plot <- (cbind(as.numeric(Ia_obs),1:time_steps) + %>% data.frame() + %>% setNames(c("value","time")) + %>% mutate(type="observed") + ) %>% union(macpan_base$report() + %>% filter(matrix=="Ia") + %>% select(time,value) + %>% mutate(type="predicted") + ) + + ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + geom_line()+ + theme_bw()+ + ylab("Ia") + +} + +## ------------------------- +## exploring +## ------------------------- + +## all infectious compartments +if (interactive()) { + ggplot(macpan_base$report() %>% select(time,value,matrix), aes(time,value,col=matrix))+ + geom_line()+ + theme_bw()+ + ylab("individuals") +} diff --git a/inst/starter_models/ww/calibration_example.R b/inst/starter_models/ww/calibration_example.R new file mode 100644 index 00000000..a41a02b3 --- /dev/null +++ b/inst/starter_models/ww/calibration_example.R @@ -0,0 +1,167 @@ +#source("inst/starter_models/ww/tmb.R") +library(macpan2) +library(ggplot2) +library(dplyr) + +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","ww",package="macpan2") +spec + +## ------------------------- +## define simulator +## ------------------------- + +# set number of time steps in simulation +time_steps = 100L + +# simulator object +ww = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("Ia", "Ip", "Im", "Is", "W", "A") +) + +## ------------------------- +## parameterize model +## ------------------------- + +# interested in estimating asymptomatic relative transmission rate +ww$replace$params(spec$default$Ca,"Ca") +ww + +## ------------------------- +## specify objective function +## ------------------------- + + +# negative log likelihood +obj_fn = ~ -sum(dpois(Ia_obs, rbind_time(Ia, Ia_obs_times))) + +# update simulator to create new variables +ww$update$matrices( + Ia_obs = empty_matrix + , Ia_obs_times = empty_matrix +) + +# update simulator to include this function +ww$replace$obj_fn(obj_fn) + + +## ------------------------- +## simulate fake data +## ------------------------- + +# Ca value to simulate data with +true_Ca = 0.8 + +## simulate observed data using true parameters +observed_data = ww$report(true_Ca) + +## compute incidence for observed data +Ia_obs = rpois(time_steps, subset(observed_data, matrix == "Ia", select = c(value)) %>% pull()) +Ia_obs_times = subset(observed_data, matrix == "Ia", select = c(time)) %>% pull() + +if (interactive()) { + plot(Ia_obs, type = "l", las = 1) +} + + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +ww$update$matrices( + Ia_obs = Ia_obs + , Ia_obs_times = Ia_obs_times +) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- + +# plot surface as contours +if (interactive()) { + + Ca_seq = seq(from = 0.1, to = 1, length = 100) + + ll = vapply( + Ca_seq + , ww$objective + , numeric(1L) + ) + dat_for_plot <- (cbind(Ca_seq, ll) + %>% data.frame() + + ) + + ggplot(dat_for_plot, aes(Ca_seq, ll)) + + geom_line()+ + ## add true parameter values to compare + geom_vline(xintercept = true_Ca, col='red')+ + xlab("Ca") + +} + +## ------------------------- +## fit parameters +## ------------------------- + +## optimize and check convergence +ww$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + + ## estimate is close to true + print(ww$current$params_frame()) + print(paste0("default Ca ",ww$current$params_frame()$default)) + print(paste0("current Ca ",ww$current$params_frame()$current)) + + data_to_plot <- (cbind(as.numeric(Ia_obs),1:time_steps) + %>% data.frame() + %>% setNames(c("value","time")) + %>% mutate(type="observed") + ) %>% union(ww$report() + %>% filter(matrix=="Ia") + %>% select(time,value) + %>% mutate(type="predicted") + ) + + ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + geom_line()+ + theme_bw()+ + ylab("Ia") + +} + +## ------------------------- +## exploring +## ------------------------- + +## all infectious compartments +if (interactive()) { + ggplot(ww$report() %>% filter(grepl("I",matrix))%>% select(time,value,matrix), aes(time,value,col=matrix))+ + geom_line()+ + theme_bw()+ + ylab("individuals") +} + +## W +if (interactive()) { + ggplot(ww$report() %>% filter(matrix=="W")%>% select(time,value), aes(time,value))+ + geom_line()+ + theme_bw()+ + ylab("W") +} + +## A +if (interactive()) { + ggplot(ww$report() %>% filter(matrix=="A")%>% select(time,value), aes(time,value))+ + geom_line()+ + theme_bw()+ + ylab("A") +} + From ccf7c72b730c80b792e38b109c020d1503067758 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 9 Jan 2024 20:24:26 -0500 Subject: [PATCH 273/332] time varying parameters vignette #154 --- R/name_utils.R | 15 ++ R/tmb_model.R | 111 ++++++++++++--- R/tmb_model_editors.R | 2 + README.Rmd | 9 ++ inst/starter_models/si/tmb.R | 3 +- vignettes/time_varying_parameters.Rmd | 192 ++++++-------------------- 6 files changed, 155 insertions(+), 177 deletions(-) diff --git a/R/name_utils.R b/R/name_utils.R index a911680c..1abe29a0 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -120,6 +120,21 @@ to_positions.character = function(x) setNames(seq_along(x) - 1L, x) #' @export to_positions.numeric = as.integer +# take a list of numeric objects and return a list of +# length-1 integer vectors giving the position of each +# name in each object that has names +implied_position_vectors = function(numeric_list) { + (numeric_list + |> lapply(names) + |> Filter(f = is.character) + |> lapply(to_positions) + |> unname() + |> unique() + |> unlist() + |> as.list() + ) +} + list_to_labels = function(...) unlist(lapply(list(...), to_labels), use.names = FALSE) list_to_names = function(...) unlist(lapply(list(...), to_names), use.names = FALSE) diff --git a/R/tmb_model.R b/R/tmb_model.R index 78da08d5..742db1ca 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -175,6 +175,8 @@ TMBModel = function( ) } + + #' @export TMBModelSpec = function( before = list() @@ -196,6 +198,59 @@ TMBModelSpec = function( self$must_not_save = must_not_save self$sim_exprs = sim_exprs + self$expr_insert = function( + before_start = list() + , before_end = list() + , during_start = list() + , during_end = list() + , after_start = list() + , after_end = list() + , sim_exprs = character() + ) { + TMBModelSpec( + before = c(before_start, self$before, before_end) + , during = c(during_start, self$during, during_end) + , after = c(after_start, self$after, after_end) + , default = self$default + , integers = self$integers + , must_save = self$must_save + , must_not_save = self$must_not_save + , sim_exprs = unique(c(sim_exprs, self$sim_exprs)) + ) + } + + ## check for name ambiguity + self$check_names = function() { + tmb_model_names = c(names(self$integers), names(self$default)) + ambiguous = duplicated(tmb_model_names) + if (any(ambiguous)) { + msg( + msg_hline() + , msg_colon( + msg( + "The following names were used more than once" + , "(either as names in the default or integers lists," + , "or as names of the elements of vectors in the default list)" + ) + , msg_indent_break(unique(tmb_model_names[ambiguous])) + ) + ) |> stop() + } + TRUE + } + ## convert each name of each named vector in the default list into + ## an 'implied' integer vector for subsetting vectors in before, during, + ## and after expressions by position name + self$all_integers = function() { + self$check_names() + + ## TODO: make smarter so that only used integer vectors + ## are produced and maybe even check if an integer vector + ## is being used in the wrong numeric vector + implied_integers = implied_position_vectors(self$default) + c(self$implied_integers, self$integers) + } + self$empty_matrices = function() { e = ExprList(self$before, self$during, self$after) dv = setdiff(e$all_derived_vars(), names(self$default)) @@ -209,6 +264,7 @@ TMBModelSpec = function( , default = list() ) { initial_mats = self$matrices() + initial_mats[names(default)] = default matrix_outputs = intersect(outputs, names(initial_mats)) row_outputs = setdiff(outputs, matrix_outputs) mats_to_return = (initial_mats @@ -227,7 +283,7 @@ TMBModelSpec = function( init_mats = do.call( MatsList , c( - self$matrices() + initial_mats , list( .mats_to_return = mats_to_return , .mats_to_save = mats_to_save @@ -241,11 +297,10 @@ TMBModelSpec = function( , .simulate_exprs = self$sim_exprs ) , engine_methods = EngineMethods( - int_vecs = do.call(IntVecs, self$integers) + int_vecs = do.call(IntVecs, self$all_integers()) ) , time_steps = Time(as.integer(time_steps)) )$simulator(outputs = outputs, initialize_ad_fun = FALSE) - do.call(s$update$matrices, default) s$ad_fun() s } @@ -271,27 +326,28 @@ mp_tmb_model_spec = function( , after = list() , default = list() , integers = list() + , must_save = character() + , must_not_save = character() + , sim_exprs = character() ) { - implied_integers = (default - |> lapply(names) - |> Filter(f = is.character) - |> lapply(to_positions) - |> unname() - |> unique() - |> unlist() - |> as.list() - ) - integers = c( - implied_integers, - integers - ) - ambiguous = integers |> names() |> duplicated() |> any() - if (ambiguous) stop("Defaults and integers are ambiguously named.") - - TMBModelSpec(before, during, after, default, integers) + TMBModelSpec( + before, during, after + , default, integers + , must_save, must_not_save + , sim_exprs + ) } +mp_tmb_before = function(model, start = list(), end = list()) { + TMBModelSpec( + before = c(start, model$before, end) + , during, after + , default, integers + , must_save, must_not_save + , sim_exprs + ) +} #' Simulator #' @@ -307,12 +363,20 @@ mp_tmb_model_spec = function( #' can be updated or not. #' #' @export -mp_simulator = function(model, time_steps, outputs, default = list()) { +mp_simulator = function(model + , time_steps + , outputs + , default = list() + ) { UseMethod("mp_simulator") } #' @export -mp_simulator.TMBModelSpec = function(model, time_steps, outputs, default = list()) { +mp_simulator.TMBModelSpec = function(model + , time_steps + , outputs + , default = list() + ) { model$simulator_fresh(time_steps, outputs, default) } @@ -487,7 +551,8 @@ TMBSimulationUtils = function() { ) } s = self$.simulation_formatter(r, .phases) - s[order(s$time), , drop = FALSE] ## TODO: move sorting by time to the c++ side + s = s[order(s$time), , drop = FALSE] ## TODO: move sorting by time to the c++ side + reset_rownames(s) } return_object(self, "TMBSimulationFormatter") } diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index 6b74cbce..939861a4 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -118,6 +118,7 @@ TMBSimulatorAdder = function(simulator) { , .mats_to_return = .mats_to_return , .dimnames = .dimnames ) |> self$model$refresh$init_mats() + self$simulator$outputs = union(self$simulator$outputs, .mats_to_return) self$simulator$cache$invalidate() invisible(self$simulator) } @@ -230,6 +231,7 @@ TMBSimulatorUpdater = function(simulator) { , .mats_to_return = .mats_to_return , .dimnames = .dimnames ) + self$simulator$outputs = union(self$simulator$outputs, .mats_to_return) self$simulator$cache$invalidate() invisible(self$simulator) } diff --git a/README.Rmd b/README.Rmd index 848e7476..1c68523d 100644 --- a/README.Rmd +++ b/README.Rmd @@ -134,6 +134,15 @@ TODO + +## Interface Specifications + +* `mp_model_spec` +* `mp_tmb_model_spec` +* `mp_simulator` +* `mp_trajectory` + + ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks the details of bugs, tasks, and feature development. But the following narrative will provide context on product development themes, their current state, and plans for improvement and implementation. diff --git a/inst/starter_models/si/tmb.R b/inst/starter_models/si/tmb.R index 246b4bee..7fc603f1 100644 --- a/inst/starter_models/si/tmb.R +++ b/inst/starter_models/si/tmb.R @@ -10,7 +10,6 @@ flow_rates = list( euler = list( infection ~ beta * S * I / N ) - , hazard = list( ## expected value of the Euler-Binomial infection ~ S * (1 - exp(-beta * I / N)) ) @@ -53,7 +52,7 @@ specs = lapply(flow_rates, \(flow_rates) { mp_tmb_model_spec( before = initialize_state , during = c(flow_rates, update_state) - , default = list(N = 100, beta = 0.2, gamma = 0.1) + , default = list(N = 100, beta = 0.2) ) }) diff --git a/vignettes/time_varying_parameters.Rmd b/vignettes/time_varying_parameters.Rmd index 05e2c2ab..64cafb3f 100644 --- a/vignettes/time_varying_parameters.Rmd +++ b/vignettes/time_varying_parameters.Rmd @@ -112,7 +112,7 @@ plot(I_observed) Then we add a few matrices to the model for keeping tracking of information used in model fitting. ```{r add_matrices} -simulator$add$matrices( +simulator$update$matrices( ## observed data I_obs = I_observed, @@ -183,14 +183,6 @@ data.frame( ) ``` - -## Smooth Time Variation (TODO) - - -## Generalized Linear Models (TODO) - - - ## Radial Basis Functions for Flexible Time Variation (In-Progress) This section uses radial basis functions (RBFs) to generate models with a flexible functional form for smooth changes in the transmission rate. @@ -198,10 +190,10 @@ This section uses radial basis functions (RBFs) to generate models with a flexib Before we can add the fancy radial basis for the transmission rate, we need a base model. We use an SIR model that has been modified to include waning. ```{r} -sir = Compartmental(system.file("starter_models", "sir_waning", package = "macpan2")) -sir$flows() +sir = mp_tmb_library("starter_models", "sir_waning", package = "macpan2") ``` + The `macpan2::rbf` function can be used to produce a matrix giving the values of each basis function (each column) at each time step (each row). Using this matrix, $X$, and a weights vector, $b$, we can get a flexible output vector, $y$, with a shape that can be modified into a wide variety of shapes by changing the weights vector. $$ @@ -247,49 +239,43 @@ Here is a simulation model with a radial basis for exogenous transmission rate d ```{r sim_rbf} set.seed(1L) -simulator = sir$simulators$tmb( - time_steps = n - , state = c(S = 100000 - 500, I = 500, R = 0) - , flow = c(foi = NA, gamma = 0.2, wane = 0.01) - , beta = 1 - , N = 100000 +simulator = mp_simulator(sir + , time_steps = n + , outputs = c("S", "I", "R", "infection", "beta") + , default = list( + S = 100000 - 500, I = 500, R = 0 + , beta = 1, gamma = 0.2, phi = 0.01 , X = rbf(n, d) , b = rnorm(d, sd = 0.01) - , incidence = empty_matrix - , eta = empty_matrix - , .mats_to_save = c("state", "incidence", "beta") - , .mats_to_return = c("state", "incidence", "beta") -)$insert$expressions( + ) +) +simulator$insert$expressions( eta ~ gamma * exp(X %*% b) , .phase = "before" , .at = Inf -)$insert$expressions( +) +simulator$insert$expressions( beta ~ eta[time_step(1)] / clamp(S/N, 1/100) , .phase = "during" , .at = 1 -)$insert$expressions( - incidence ~ I - , .vec_by_states = "total_inflow" - , .phase = "during" - , .at = Inf -)$replace$params( +) +simulator$add$matrices( + eta = empty_matrix +) +simulator$replace$params( default = rnorm(d, sd = 0.01) , mat = rep("b", d) , row = seq_len(d) - 1L ) +print(simulator) ``` -**fixme**: explain what the first/unnamed argument of `$report()` is doing? Why are we setting it to a random value here? (As I understand it, it's the parameter vector; `rnorm()` is the same as the default setting for this parameter. Commenting out that term doesn't seem to change anything *because* we have just set the random-number seed? (The default value will be something different because it was evaluated with a different seed ...) - ```{r plot_rbf, fig.height=8, fig.width=6} -set.seed(5L) -(simulator$report(## rnorm(d, sd = 0.01), - .phases = "during" ## report only 'during' time steps - ) - %>% mutate(variable = if_else(matrix == "state", row, matrix)) - %>% ggplot() - + facet_wrap(~ variable, ncol = 1, scales = 'free') - + geom_line(aes(time, value)) +(simulator + |> mp_trajectory() + |> ggplot() + + facet_wrap(~ matrix, ncol = 1, scales = 'free') + + geom_line(aes(time, value)) ) ``` @@ -304,9 +290,9 @@ The next few steps will follow the first example in the [Calibration](./calibrat 1\. Simulate from the model and add some noise: ```{r noisy_sim, basefig = TRUE} -obs_I <- ( - simulator$report(.phases = "during") - |> filter(row == "I", matrix == "state") +obs_I <- (simulator + |> mp_trajectory() + |> filter(matrix == "I") |> mutate(across(value, ~ rnorm(n(), ., sd = 50))) |> pull(value) ) @@ -363,13 +349,15 @@ NLL of the data (`-sum(dnorm(I_obs, ...))`) and the likelihood of the RBF parame we fit both of the SD parameters on the log scale. ```{r add_pens} -simulator$add$matrices(I_sd = 1, - rbf_sd = 1) +simulator$add$matrices( + I_sd = 1 + , rbf_sd = 1 +) simulator$insert$expressions( - log_lik ~ -sum(dnorm(I_obs, rbind_time(I_sim), I_sd)) + - -1*sum(dnorm(b, 0.0, rbf_sd)), - .phase = "after" - ) + log_lik ~ -sum(dnorm(I_obs, rbind_time(I_sim), I_sd)) + + -1*sum(dnorm(b, 0.0, rbf_sd)), + .phase = "after" +) ## initially forgot this: maybe we could warn when someone is missing this???? simulator$replace$obj_fn(~ log_lik) simulator$add$transformations(Log("I_sd")) @@ -436,10 +424,10 @@ fit <- simulator$optimize$nlminb() Extract parameters, run the simulator for the best-fit parameters, compare with data ... ```{r plot_sim, basefig = TRUE} -pp <- simulator$ad_fun()$env$last.par.best -est_I <- ( - simulator$report(pp, .phases = "during") - |> filter(row == "I", matrix == "state") +pp <- simulator$ad_fun()$env$last.par.best ## FIXME: use this +est_I <- (simulator + |> mp_trajectory() + |> filter(matrix == "I") |> pull(value) ) par(las = 1, bty = "l") @@ -456,104 +444,4 @@ lines(est_I, col = 2, lwd = 2) **fixme**: discuss (somewhere) alternate bases for latent variables (random-walk, Gaussian process, ...) -## Generalized Linear Mixed Models (In-Progress) - -```{r, eval=FALSE} -#set.seed(6L) -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -simulator_rand_beta = sir$simulators$tmb(time_steps = 50 - , state = c(S = 99, I = 1, R = 0) - , flow = c(foi = NA, gamma = 0.2) - , N = empty_matrix - , beta = 0.4 -) -simulator_rand_beta$add$matrices( - beta_eps = norm_beta, - beta_mean = log(0.3) -) -simulator_rand_beta$insert$expressions( - beta ~ 1 / (1 + exp(-beta_mean - beta_eps)) - , .phase = "during" - , .at = 1 -) -(simulator_rand_beta$report(.phases = "during") - %>% filter(row == "I", matrix == "state") - %>% ggplot() - + facet_wrap(~ matrix, ncol = 1, scales = 'free') - + geom_line(aes(time, value)) -) -observed_I = filter(simulator_rand_beta$report(.phases = "during"), row == "I", matrix == "state")$value -``` - -```{r eval = FALSE} -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -simulator_step_level_re = sir$simulators$tmb(time_steps = 50 - , state = c(S = 99, I = 1, R = 0) - , flow = c(foi = NA, gamma = 0.2) - , N = empty_matrix - , beta = 0.8 -) -simulator_step_level_re$add$matrices( - beta_mean = log(0.5) - , beta_sd = 1 - , beta_time_step = rnorm(50, 0, 0.2) - , beta_log_density = empty_matrix - , I_observed = observed_I - , log_lik = empty_matrix - , total_log_lik = empty_matrix - , .mats_to_save = "log_lik" -) -simulator_step_level_re$insert$expressions( - beta_log_density ~ sum(dnorm( - beta_time_step, - 0, - beta_sd - )) - , .phase = "before" -) -simulator_step_level_re$insert$expressions( - beta ~ exp(beta_mean + beta_time_step[time_step(1)]) - , .phase = "during" -) -simulator_step_level_re$insert$expressions( - log_lik ~ dpois(I_observed[time_step(1)], clamp(I)) - , .phase = "during" - , .at = Inf -) -simulator_step_level_re$insert$expressions( - total_log_lik ~ sum(rbind_time(log_lik)) - , .phase = "after" -) -simulator_step_level_re$replace$params( - default = c(0, 1), - mat = c("beta_mean", "beta_sd") -) -simulator_step_level_re$replace$random( - default = rnorm(50, 0, 1), - mat = rep("beta_time_step", 50), - row = 0:49 -) -simulator_step_level_re$replace$obj_fn( - ~ -total_log_lik-beta_log_density-dnorm(beta_sd, 0.2, 0.01) -) -simulator_step_level_re$optimize$nlminb() -simulator_step_level_re$optimize$optim() -#simulator_step_level_re$current$random_frame() -#simulator_step_level_re$current$params_frame() -#simulator_step_level_re$ad_fun()$fn(0) -ggplot(simulator_step_level_re$current$random_frame()) + - geom_line(aes(row, current)) -``` - - -```{r eval = FALSE} -#simulator_step_level_re$cache$invalidate() -#simulator_step_level_re$ad_fun()$gr(-1.23) -plot( - filter(simulator_step_level_re$report(.phases = "during"), row == "I", matrix == "state")$value, - observed_I -) -abline(0, 1) -``` - ## References From 74c5842b46a7ff38d5193de48768a4bd7b6b77da Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 10 Jan 2024 11:17:12 -0500 Subject: [PATCH 274/332] fix #158 --- NAMESPACE | 3 ++- R/tmb_model.R | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 8c85f055..3a6789aa 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -181,7 +181,8 @@ export(make_expr_parser) export(mk_calibrate) export(model_starter) export(mp_aggregate) -export(mp_calibrate) +export(mp_calibrator) +export(mp_calibrator.TMBModelSpec) export(mp_cartesian) export(mp_catalogue) export(mp_choose) diff --git a/R/tmb_model.R b/R/tmb_model.R index 742db1ca..eb4e9b50 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -248,7 +248,7 @@ TMBModelSpec = function( ## are produced and maybe even check if an integer vector ## is being used in the wrong numeric vector implied_integers = implied_position_vectors(self$default) - c(self$implied_integers, self$integers) + c(implied_integers, self$integers) } self$empty_matrices = function() { @@ -380,8 +380,16 @@ mp_simulator.TMBModelSpec = function(model model$simulator_fresh(time_steps, outputs, default) } +#' #' @export -mp_calibrate = function(model, data) {} +mp_calibrator = function(model, data) { + +} + +#' @export +mp_calibrator.TMBModelSpec = function(model, data) { + +} #' @export mp_default = function(model_simulator, ...) { From a97319df7447b27eb7f36f7dcbbb8bc52ab84dd9 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 10 Jan 2024 16:03:17 -0500 Subject: [PATCH 275/332] calibration vignette rendering (except measles example) #154 --- R/formula_utils.R | 4 +- R/tmb_model.R | 20 ++- inst/starter_models/sir/tmb.R | 4 +- misc/dev/dev.cpp | 4 +- src/macpan2.cpp | 4 +- vignettes/calibration.Rmd | 305 +++++++++++++++++++++------------- 6 files changed, 211 insertions(+), 130 deletions(-) diff --git a/R/formula_utils.R b/R/formula_utils.R index 6fd44a87..1554308e 100644 --- a/R/formula_utils.R +++ b/R/formula_utils.R @@ -65,7 +65,7 @@ to_assign = function(formula, dummy = "dummy") { } ## update formula symbolically with additional formulas -## in ..., each of which has a lhs matching a symbol in +## in replacers, each of which has a lhs matching a symbol in ## the focal formula and a rhs to replace that symbol with update_formula = function(formula, replacers) { nms = lapply(replacers, lhs_char) @@ -73,6 +73,8 @@ update_formula = function(formula, replacers) { do.call('substitute', list(formula, l)) } + + ## for character vectors lhs and rhs, return a formula one_sided = function(rhs) { as.formula(sprintf("~ %s", as.character(rhs))) diff --git a/R/tmb_model.R b/R/tmb_model.R index eb4e9b50..874d977c 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -492,10 +492,11 @@ labels.LabelsScripts = function(object, ...) { TMBSimulationUtils = function() { self = Base() self$.simulation_formatter = function(r, .phases) { - r = setNames( - as.data.frame(r$values), - c("matrix", "time", "row", "col", "value") - ) ## get raw simulation output from TMB and supply column names (which don't exist on the TMB side) + ## get raw simulation output from TMB and supply + ## column names (which don't exist on the TMB side) + col_names = c("matrix", "time", "row", "col", "value") + if (ncol(r$values) == 6L) col_names = append(col_names, "sd") + r = setNames(as.data.frame(r$values), col_names) r$matrix = self$matrix_names()[r$matrix + 1L] ## replace matrix indices with matrix names dn = self$tmb_model$init_mats$dimnames() ## get the row and column names of matrices with such names for (mat in names(dn)) { @@ -541,9 +542,14 @@ TMBSimulationUtils = function() { } self$.runner = function(... , .phases = "during" - , .method = c("report", "simulate") + , .method = c("report", "simulate", "sdreport") ) { .method = match.arg(.method) + compute_sd = FALSE + if (.method == "sdreport") { + .method = "report" + compute_sd = TRUE + } fixed_params = as.numeric(unlist(list(...))) if (length(fixed_params) == 0L) { r = self$ad_fun()[[.method]]() @@ -558,6 +564,7 @@ TMBSimulationUtils = function() { self$.find_problematic_expression(r$expr_row) ) } + if (compute_sd) r$values = cbind(r$values, self$sdreport()$sd) s = self$.simulation_formatter(r, .phases) s = s[order(s$time), , drop = FALSE] ## TODO: move sorting by time to the c++ side reset_rownames(s) @@ -643,6 +650,9 @@ TMBSimulator = function(tmb_model self$report = function(..., .phases = "during") { self$.runner(..., .phases = .phases, .method = "report") } + self$report_with_sd = function(..., .phases = "during") { + self$.runner(..., .phases = .phases, .method = "sdreport") + } self$report_values = function(..., .phases = "during") { self$report(..., .phases = .phases)$value } diff --git a/inst/starter_models/sir/tmb.R b/inst/starter_models/sir/tmb.R index 1db92b8a..33d47cb2 100644 --- a/inst/starter_models/sir/tmb.R +++ b/inst/starter_models/sir/tmb.R @@ -1,8 +1,7 @@ library(macpan2) initialize_state = list( - I ~ 1 - , R ~ 0 + R ~ 0 , S ~ N - I ) @@ -22,6 +21,7 @@ default = list( beta = 0.2 , gamma = 0.2 , N = 100 + , I = 1 ) ## model specification diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index ce2fad8c..57231cfc 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -2936,10 +2936,10 @@ Type objective_function::operator() () } } - REPORT(values) if (values_adreport == 1) { - ADREPORT(values) + matrix value_column = values.block(0, 4, values.rows(), 1); + ADREPORT(value_column) } diff --git a/src/macpan2.cpp b/src/macpan2.cpp index 7e39b80a..a34c2ea3 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -2937,10 +2937,10 @@ Type objective_function::operator() () } } - REPORT(values) if (values_adreport == 1) { - ADREPORT(values) + matrix value_column = values.block(0, 4, values.rows(), 1); + ADREPORT(value_column) } diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index fc9c0ee7..0b126afa 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -15,10 +15,10 @@ editor_options: ```{r opts, include = FALSE} knitr::opts_chunk$set( - collapse = TRUE, - fig.width = 6, - fig.height = 4, - comment = "#>" + collapse = TRUE, + fig.width = 6, + fig.height = 4, + comment = "#>" ) ``` @@ -47,6 +47,19 @@ We'll do the first thing you should always do when trying out a new fitting proc First set up the model from the quickstart guide. For convenience (since we will be using several different versions of this model along the way, and modifying existing models is not quite as simple as it could be), we'll encapsulate this in a function so we can easily re-run it later to generate a new model. (To allow for some flexibility later on, I'm also making the initial state an argument to the function.) ```{r sir_setup} +sir_spec = mp_tmb_library("starter_models" + , "sir" + , package = "macpan2" +) +sir_simulator = mp_simulator(sir_spec + , time_steps = 100 + , outputs = c("S", "I", "R") + , default = list(N = 100, beta = 0.2, gamma = 0.1) +) +sir_results = mp_trajectory(sir_simulator) +``` + +```{r sir_setup_old, eval = FALSE} mk_sim <- function(init_state = c(S = 99, I = 1, R = 0)) { sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) #sir = Compartmental2(system.file("model_library", "sir", package = "macpan2")) @@ -70,8 +83,8 @@ Add some noise to the prevalence (`I`) value: ```{r sir_noise} set.seed(101) sir_prevalence = (sir_results - |> dplyr::select(-c(matrix, col)) - |> filter(row == "I") + |> dplyr::select(-c(row, col)) + |> filter(matrix == "I") |> mutate(obs_val = value + rnorm(n(), sd = 1)) ) gg0 <- ggplot(sir_prevalence, aes(time)) + @@ -84,12 +97,15 @@ print(gg0) Now we'll use the *experimental* `mk_calibrate()` function from `macpan2helpers` package + + + ```{r mk_calibrate} macpan2helpers::mk_calibrate(sir_simulator, data = data.frame(I_obs = sir_prevalence$obs_val), - params = list(trans_rates = c(beta = 1), I_sd = 1), - transforms = list(trans_rates = c(beta = "log"), I_sd = "log"), - exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)) + params = list(beta = 1, I_sd = 1), + transforms = list(beta = "log", I_sd = "log"), + exprs = list(log_lik ~ dnorm(I_obs, rbind_time(I), I_sd)) ) ``` @@ -98,9 +114,11 @@ Unlike typical R functions, this function modifies the `sim` object *in place* ( A sanity check: make sure that the starting values give a reasonable-looking trajectory. ```{r plot1} -(sir_simulator$report() |> - filter(matrix == "state", row == "I") |> - ggplot(aes(time, value)) + geom_line() +(sir_simulator + |> mp_trajectory() + |> filter(matrix == "I") + |> ggplot(aes(time, value)) + + geom_line() ) ``` @@ -110,7 +128,7 @@ Setting $\log(\beta)=0$ instead gives us a trajectory that is still very unreali ```{r plot2} (sir_simulator$report(c(0,0)) |> - filter(matrix == "state", row == "I") |> + filter(matrix == "I") |> ggplot(aes(time, value)) + geom_line() ) ``` @@ -118,7 +136,7 @@ Setting $\log(\beta)=0$ instead gives us a trajectory that is still very unreali Let's replace the starting value for `log_beta` with 0: ```{r repl_param} -sir_simulator$replace$params(c(0, 1), c("log_trans_rates", "log_I_sd")) +sir_simulator$replace$params(c(0, 1), c("log_beta", "log_I_sd")) ``` (In a proper workflow we might prefer to go back upstream to wherever we defined the default values, rather than resetting the value on the fly ...) @@ -181,6 +199,24 @@ Here we'll show off a few alternate choices you could have made. We'll (1) use r ```{r next_example, eval=require("outbreaks")} library(outbreaks) +sir_simulator = mp_simulator(sir_spec + , time_steps = 100 + , outputs = c("S", "I", "R") + , default = list(N = 763, I = 3, beta = 1, gamma = 0.5) +) +macpan2helpers::mk_calibrate(sir_simulator + , data = data.frame(I_obs = influenza_england_1978_school$in_bed) + , params = list(beta = 1, gamma = 0.5, I_disp = 1) + , transforms = list(beta = "log", gamma = "log", I_disp = "log") + , exprs = list(log_lik ~ dnbinom(I_obs, rbind_time(I), I_disp)) +) +sir_simulator$objective(log(c(1, 0.5, 1))) +``` + + + +```{r next_example_old, eval=FALSE} +library(outbreaks) sir_simulator <- mk_sim(init_state = c(S = 760, I = 3, R = 0)) mk_calibrate(sir_simulator, data = data.frame(I_obs = influenza_england_1978_school$in_bed), @@ -198,8 +234,8 @@ How does this look with our default parameters? (To save typing I'm writing a ge ```{r sim_test, eval=require("outbreaks")} sim_plot <- function(pars = log(c(1, 0.5, 1))) { - predvals <- (sir_simulator$report(pars, .phases = "during") - |> filter(row == "I") + predvals <- (sir_simulator$report(pars) + |> filter(matrix == "I") |> select(time, I_obs = value) |> bind_cols(influenza_england_1978_school) ) @@ -250,14 +286,14 @@ The fit isn't perfect, but we can think of a number of reasons for that (people While this code isn't horribly complex, hopefully it will all be somewhat more streamlined/automated in the future. Computing confidence or prediction intervals is a fairly big subject; see Bolker (2008) chapter 7 for an introduction to some of the issues. -```{r sdreport} +```{r sdreport_old, eval = FALSE} ## sd vector corresponds to the same information as the ## internal 'values' object, which consists of 4 columns ## of indices {matrix index, time, row index, column index} ## and a 5th column that is the actual values ... -sdr <- TMB::sdreport(sir_simulator$ad_fun()) -ss <- sdr$sd |> matrix(ncol = 5) -timevec <- (sdr$value |> matrix(ncol = 5))[, 2] +#sdr <- sir_simulator$report_with_sd() +#ss <- sdr$sd |> matrix(ncol = 5) +#timevec <- (sdr$value |> matrix(ncol = 5))[, 2] ## observed values obs <- data.frame(time = seq(nrow(influenza_england_1978_school)), @@ -266,24 +302,41 @@ obs <- data.frame(time = seq(nrow(influenza_england_1978_school)), ## a little tricky: we don't yet know how to specify .phases = "during" so that ## we only get values relating to time points, not also to "before sim" and "after sim" ## so we need to drop points 0 and (n+1) -ss <- ss[timevec >= 1 & timevec <= nrow(obs),] -res <- (cbind(sir_simulator$report(.phases = "during"), sd = ss[,5]) - |> mutate(var = factor(row, levels = c("S", "I", "R")), - lwr_delta = value - 1.96*sd, - upr_delta = value + 1.96*sd) - |> select(-c(matrix, row, col)) - ## don't necessarily need to keep sd column but ... -) +#ss <- ss[timevec >= 1 & timevec <= nrow(obs),] +#res <- (cbind(sir_simulator$report(), sd = ss[,5]) +# |> mutate(var = factor(matrix, levels = c("S", "I", "R")), +# lwr_delta = value - 1.96*sd, +# upr_delta = value + 1.96*sd) +# |> select(-c(matrix, row, col)) +# ## don't necessarily need to keep sd column but ... +#) ``` Now plot the results: ```{r sdreport_plot} -gg_base <- ggplot(res, aes(x = time, y = value)) + geom_line(aes(colour = var)) + - geom_point(data = obs) -gg_ci1 <- gg_base + - geom_ribbon(aes(ymin = lwr_delta, ymax = upr_delta, fill = var), - alpha = 0.3, colour = NA) +obs <- data.frame( + time = seq(nrow(influenza_england_1978_school)) + , value = influenza_england_1978_school$in_bed +) +res = (sir_simulator$report_with_sd() + |> mutate( + var = factor(matrix, levels = c("S", "I", "R")) + , lwr_delta = value - 1.96 * sd + , upr_delta = value + 1.96 * sd + ) +) +gg_base <- (res + |> ggplot(aes(x = time, y = value)) + + geom_line(aes(colour = var)) + + geom_point(data = obs) +) +gg_ci1 <- (gg_base + + geom_ribbon( + aes(ymin = lwr_delta, ymax = upr_delta, fill = var) + , alpha = 0.3, colour = NA + ) +) print(gg_ci1) ``` @@ -294,6 +347,7 @@ prediction intervals for non-Normal errors are a little bit tricky (maybe someth We can also generate intervals based on multivariate normal sampling, which relaxes the second assumption but not the first. Sometimes `sdreport()` is memory-hungry, so below we show how to get the covariance matrix of the parameters directly from the fit (it does take some extra computation). ```{r ci_ensemble, eval = require("numDeriv", quietly = TRUE)} +sdr = sir_simulator$sdreport() set.seed(101) nsim <- 1000 par <- fit$par @@ -348,7 +402,7 @@ sim_ensemble2 <- apply(parvals, 1, function(p) { ## FIXME: misspelling '.phases' gives a 'wrong parameter length arg s <- sir_simulator$report(p, .phases = "during") - v <- s$value[s$row == "I"] + v <- s$value[s$matrix == "I"] ## force mean to be non-negative ## we could also accept the warnings/NA values from negative predictions rnbinom(length(v), mu = pmax(0, v), size = nb_disp) @@ -429,15 +483,19 @@ For now we're going to revert to the previous fake/simulated data. We'll fit to both infection prevalence and number recovered, adding negative binomial observation noise and dropping 50% (approximately) of the observations at random ```{r irreg1} -sir_simulator <- mk_sim() +sir_simulator = mp_simulator(sir_spec + , time_steps = 100 + , outputs = c("S", "I", "R") + , default = list(N = 100, beta = 0.2, gamma = 0.1) +) ## `.phases = "during"` is important so that the number of observations matches the number of time steps sir_results = sir_simulator$report(.phases = "during") set.seed(101) subsamp <- function(x, missprob = 0.5) { x[runif(length(x)) select(time, row, value) - |> filter(row %in% c("I", "R")) - |> pivot_wider(names_from = "row", values_from = "value") + |> select(time, matrix, value) + |> filter(matrix %in% c("I", "R")) + |> pivot_wider(names_from = "matrix", values_from = "value") |> mutate(I_obs = subsamp(rnbinom(n(), mu = I, size = 4)), R_obs = subsamp(rnbinom(n(), mu = R, size = 8))) |> select(time, I_obs, R_obs) @@ -491,7 +549,11 @@ This section explains what is going on under the hood in `macpan2helpers::mk_cal Since trying to add the same matrix to a simulator twice causes an error, we'll re-run the `mk_sim()` function to create a new instance of the simulator: ```{r rebuild1} -sir_simulator <- mk_sim() +sir_simulator = mp_simulator(sir_spec + , time_steps = 100 + , outputs = c("S", "I", "R") + , default = list(N = 100, beta = 0.2, gamma = 0.1) +) ``` ### Step 1: add observed data and slots for history etc. @@ -634,75 +696,76 @@ plot(measles$date, measles$cases, type = "l") We need to slightly extend the standard SIR model to include waning immunity. ```{r sir_waning} -sir = Compartmental(system.file("starter_models", "sir_waning", package = "macpan2")) -sir$flows() +computations = list( + S ~ N - I + , R ~ 0 +) +flow_rates = list( + infection ~ S * I * beta / N + , recovery ~ gamma * I + , waning_immunity ~ phi * R +) +state_updates = list( + S ~ S - infection + waning_immunity + , I ~ I + infection - recovery + , R ~ R + recovery - waning_immunity +) +sir = mp_tmb_model_spec( + before = computations + , during = c( + flow_rates + , state_updates + ) + # defaults + , default = list( + N = 100000, I = 1 + , beta = 0.2, gamma = 0.2, phi = 0.01 + ) +) ``` - We use [radial basis functions](https://canmod.github.io/macpan2/articles/time_varying_parameters.html#radial-basis-functions-for-flexible-time-variation-in-progress) to model time-variation in the transmission rate. We also make a variety of questionable assumptions (TODO: fix these), but the point at the moment is just to illustrate usage and provide a proof of concept. ```{r rbf} +#set.seed(1L) d = 100 n = nrow(measles) -simulator = sir$simulators$tmb( +simulator = (sir + |> mp_simulator( time_steps = n - , state = c(S = 100000 - 500, I = 500, R = 0) - , flow = c(foi = NA_real_, gamma = 0.2, wane = 0.01) - - ## this beta does not matter because we will overwrite - ## it with the output of the radial basis functions - , beta = NA_real_ - - ## FIXME: this is surely not the population of London at - ## all in the series - , N = 100000 - - ## matrices involved in radial basis functions - , X = rbf(n, d) - , b = rnorm(d, sd = 0.01) - , incidence = empty_matrix - , eta = empty_matrix - - , .mats_to_save = c("state", "incidence", "beta") - , .mats_to_return = c("state", "incidence", "beta") + , outputs = c("S", "I", "R", "infection") + , default = list( + N = 100000, I = 500 + , gamma = 0.2, phi = 0.01 + , X = rbf(n, d) + , b = rnorm(d, sd = 0.01) + , eta = empty_matrix + ) + ) +) -## initial S is a function of initial I, which we -## fit to data below -)$insert$expressions( - S ~ N - I - , .phase = "before" - , .at = 1 - ## radial basis function evaluations -)$insert$expressions( +simulator$insert$expressions( eta ~ gamma * exp(X %*% b) , .phase = "before" , .at = Inf -)$insert$expressions( +) +simulator$insert$expressions( beta ~ eta[time_step(1)] / clamp(S/N, 1/100) , .phase = "during" , .at = 1 - -## save the simulated incidence trajectory to -## compare with data -)$insert$expressions( - incidence ~ I - , .vec_by_states = "total_inflow" - , .phase = "during" - , .at = Inf ) ``` Here is an example simulation from this model, before fitting to data. -```{r rbf_ex, fig.width=6} -set.seed(1L) -simulated_incidence = filter(simulator$report(.phases = "during"), matrix == "incidence")$value +```{r rbf_ex, fig.width=6, eval = TRUE} +simulated_incidence = filter(simulator$report(.phases = "during"), matrix == "infection")$value plot(measles$date, simulated_incidence, type = "l", xlab = "time") ``` It looks nothing like the observed measles series, but illustrates the ability to generate complex incidence patterns not present in the simple SIR model without radial basis functions and waning immunity. -```{r rbf_model, eval=FALSE, echo=TRUE} +```{r rbf_model, eval=FALSE, echo=FALSE} simulator = ("https://github.com" |> file.path("canmod/macpan2") |> file.path("raw/main") @@ -714,14 +777,14 @@ simulator = ("https://github.com" We modify the simulation object to be able to fit to the measles data. -```{r rbf_modify, eval=TRUE} +```{r rbf_modify, eval=FALSE} simulator$add$matrices( reports = measles$cases , log_lik = empty_matrix , sim_reports = empty_matrix ) simulator$insert$expressions( - sim_reports ~ rbind_time(incidence) + sim_reports ~ rbind_time(infection) , .phase = "after" , .at = Inf ) @@ -732,14 +795,14 @@ simulator$replace$params( , 500 ) , mat = c( - rep("flow", 2L) - , rep("b", d) - , "state" + c("gamma", "phi") + , rep("b", d) + , "I" ) , row = c( - (1:2) + rep(0, 2L) , seq_len(d) - 1L - , 1 + , 0L ) ) simulator$replace$obj_fn(~ - sum(dpois(reports, sim_reports))) @@ -770,8 +833,16 @@ Not a perfect fit, but not bad for now (TODO: work on this, without papering ove Here we consider the problem of fitting an SIR model to a simulated dataset from this model, such that the simulations pose challenges to the fitting machinery. ```{r logistic_ex} -sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -sir$flows_expanded() +#sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) +#sir$flows_expanded() +sir_simulator = mp_simulator(sir_spec + , time_steps = 100 + , outputs = c("S", "I", "R") + , default = list( + N = 100000, I = 500 + , beta = 0.2, gamma = 0.2 + ) +) ``` Our simulation model includes a logistically time-varying transmission rate. @@ -779,32 +850,30 @@ Our simulation model includes a logistically time-varying transmission rate. ```{r logistic_sim} n = 2500 set.seed(1L) -simulator = sir$simulators$tmb( +simulator = (sir_spec + |> mp_simulator( time_steps = n - , state = c(S = 100000 - 500, I = 500, R = 0) - , flow = c(foi = NA, gamma = 0.2)#, wane = 0.01) - , beta = 1 - , N = 100000 - , X = cbind(1, scale(seq_len(n))) - , b = c(0, 1) - , incidence = empty_matrix - , beta_values = empty_matrix - , .mats_to_save = c("state", "incidence", "beta") - , .mats_to_return = c("state", "incidence", "beta") -)$insert$expressions( + , outputs = c("S", "I", "R", "beta", "infection") + , default = list( + N = 100000, I = 500 + , beta = 0.2, gamma = 0.2 + , X = cbind(1, scale(seq_len(n))) + , b = c(0, 1) + , beta_values = empty_matrix + ) + ) +) +simulator$insert$expressions( beta_values ~ 1 / (1 + exp(-X %*% b)) , .phase = "before" , .at = Inf -)$insert$expressions( +) +simulator$insert$expressions( beta ~ beta_values[time_step(1)] , .phase = "during" , .at = 1 -)$insert$expressions( - incidence ~ I - , .vec_by_states = "total_inflow" - , .phase = "during" - , .at = Inf -)$replace$params( +) +simulator$replace$params( default = c(0, 1) , mat = rep("b", 2) , row = 0:1 @@ -813,9 +882,9 @@ simulator = sir$simulators$tmb( ```{r logistic_plot2, fig.height=8, fig.width=6} set.seed(5L) -sims = simulator$report(.phases = "during") +sims = mp_trajectory(simulator) (sims - |> mutate(variable = if_else(matrix == "state", row, matrix)) + |> rename(variable = matrix) |> ggplot() + facet_wrap(~ variable, ncol = 1, scales = 'free') + geom_line(aes(time, value)) @@ -825,10 +894,10 @@ sims = simulator$report(.phases = "during") Fitting to the simulation data, manages to converge, but to the wrong value. ```{r logistic_bad_converge, fig.width=6} set.seed(3L) ## different seeds do result in convergence on the correct value -reports = filter(sims, matrix == "incidence")$value +reports = filter(sims, matrix == "infection")$value simulator$add$matrices(reports = reports, report_sim = empty_matrix) simulator$insert$expressions( - report_sim ~ rbind_time(incidence) + report_sim ~ rbind_time(infection) , .phase = "after" , .at = Inf ) @@ -842,14 +911,14 @@ simulator$optimize$nlminb() simulator$current$params_frame() fitted_incidence = (simulator$current$params_vector() |> simulator$report() - |> filter(matrix == "incidence") + |> filter(matrix == "infection") |> pull(value) ) plot(reports, type = "l") lines(fitted_incidence, col = 2) ``` -The fit is not good! Why? To find out we plot the likelihood surface with arrows representing the magnitude and direction of the down-hill gradient towards the optimum. Notice the very flat gradient in the direction along the valley containing the optimum at $(0, 1)$. The gradient is pointing towards the valley but not along it. I do not understand why. +The fit is not good! Even though we are fitting the extact noiseless data generating model, so why is the fit not good? To find out we plot the likelihood surface with arrows representing the magnitude and direction of the down-hill gradient towards the optimum. Notice the very flat gradient in the direction along the valley containing the optimum at $(0, 1)$. The gradient is pointing towards the valley but not along it. I do not understand why. ```{r logistic_plot_surf, results='hide', message=FALSE} make_liksurf <- function(lwr = c(-1, 0), upr = c(1, 2), @@ -939,7 +1008,7 @@ set.seed(101) fit <- DEoptim(simulator$objective, lower = rep(-10, 2), upper = rep(10, 2)) fitted_incidence = (simulator$ad_fun()$env$last.par.best |> simulator$report() - |> filter(matrix == "incidence") + |> filter(matrix == "infection") |> pull(value) ) plot(reports, type = "l") From 10565d0276f664b5fe56f654b0f04d133371f2ac Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Thu, 11 Jan 2024 14:42:31 -0500 Subject: [PATCH 276/332] working on LV calibration examples #150 --- .../competition/calibration_example.R | 159 ++++++++++++++++++ .../lotka_volterra/competition/tmb.R | 35 +--- .../lotka_volterra/predator_prey/README.md | 4 +- .../predator_prey/calibration_example.R | 140 +++++++++++++++ .../lotka_volterra/predator_prey/tmb.R | 94 +++++------ 5 files changed, 352 insertions(+), 80 deletions(-) create mode 100644 inst/starter_models/lotka_volterra/competition/calibration_example.R create mode 100644 inst/starter_models/lotka_volterra/predator_prey/calibration_example.R diff --git a/inst/starter_models/lotka_volterra/competition/calibration_example.R b/inst/starter_models/lotka_volterra/competition/calibration_example.R new file mode 100644 index 00000000..817f18f5 --- /dev/null +++ b/inst/starter_models/lotka_volterra/competition/calibration_example.R @@ -0,0 +1,159 @@ +#source("inst/starter_models/lotka_volterra/competition/tmb.R") +library(macpan2) +library(ggplot2) +library(dplyr) + +## ------------------------- +## get model spec from library +## ------------------------- + +spec = mp_tmb_library("starter_models","lotka_volterra","competition",package="macpan2") +spec + +## ------------------------- +## define simulator +## ------------------------- + +# set number of time steps in simulation +time_steps = 100L + +# simulator object +lv_comp = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("X","Y") +) + +## ------------------------- +## parameterize model +## ------------------------- + +# interested in estimating ayx - effect of species X on Y +lv_comp$replace$params(spec$default$ayx,"ayx") + +## ------------------------- +## specify objective function +## ------------------------- + +# negative log likelihood +obj_fn = ~ -sum(dpois(Y_obs, rbind_time(Y, Y_obs_times))) + +# update simulator to create new variables +lv_comp$update$matrices( + Y_obs = empty_matrix + , Y_obs_times = empty_matrix +) + +# update simulator to include this function +lv_comp$replace$obj_fn(obj_fn) + + +## ------------------------- +## simulate fake data +## ------------------------- + +# ayx value to simulate data with (species X has a carrying capacity of 200) +true_ayx = 0.8/200 + +## simulate observed data using true parameters +observed_data = lv_comp$report(true_ayx) + +## compute observed data - convert density to number of individuals +Y_obs = rpois(time_steps, subset(observed_data, matrix == "Y", select = c(value)) %>% pull()) +Y_obs_times = subset(observed_data, matrix == "Y", select = c(time)) %>% pull() + +if (interactive()) { + plot(Y_obs_times, Y_obs, type = "l", las = 1) +} + + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +lv_comp$update$matrices( + Y_obs = Y_obs + , Y_obs_times = Y_obs_times +) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- + + +if (interactive()) { + + ayx = seq(from = 0.1, to = 1.5, length = 100)/200 + + ll = vapply( + ayx + , lv_comp$objective + , numeric(1L) + ) + dat_for_plot <- (cbind(ayx, ll) + %>% data.frame() + + ) + + ggplot(dat_for_plot, aes(ayx, ll)) + + geom_line()+ + ## add true parameter values to compare + geom_vline(xintercept = true_ayx, col='red')+ + theme_bw()+ + xlab("ayx") + +} + +## ------------------------- +## fit parameters +## ------------------------- + +## optimize and check convergence +lv_comp$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + + ## estimate is close to true + print(lv_comp$current$params_frame()) + print(paste0("default ayx ",lv_comp$current$params_frame()$default)) + print(paste0("current ayx ",lv_comp$current$params_frame()$current)) + print(paste0("true ayx ",true_ayx)) + + data_to_plot <- (cbind(as.numeric(Y_obs), Y_obs_times) + %>% data.frame() + %>% setNames(c("value","time")) + %>% mutate(type="observed") + ) %>% union(lv_comp$report() + %>% filter(matrix=="Y") + %>% select(time,value) + %>% mutate(type="predicted") + ) + + ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + geom_line()+ + theme_bw()+ + ylab("Y") + +} + +## ------------------------- +## exploring +## ------------------------- + +## both species +if (interactive()) { + ggplot(lv_comp$report() %>% select(time,value,matrix), aes(time,value,col=matrix))+ + geom_line()+ + theme_bw()+ + # plot carrying capacity of each species + geom_hline(aes(yintercept = lv_comp$get$initial("axx")^(-1),col='X'), linetype="dashed")+ + annotate("text",label="Kx",x=time_steps,y=lv_comp$get$initial("axx")^(-1),vjust=-1)+ + geom_hline(aes(yintercept = lv_comp$get$initial("ayy")^(-1),col='Y'), linetype="dashed")+ + annotate("text",label="Ky",x=time_steps,y=lv_comp$get$initial("ayy")^(-1),vjust=-1)+ + guides(label=FALSE,vjust=FALSE)+ + labs(col = "species")+ + ylab("individuals") + +} + diff --git a/inst/starter_models/lotka_volterra/competition/tmb.R b/inst/starter_models/lotka_volterra/competition/tmb.R index 33c095ce..f9d5c507 100644 --- a/inst/starter_models/lotka_volterra/competition/tmb.R +++ b/inst/starter_models/lotka_volterra/competition/tmb.R @@ -28,38 +28,17 @@ state_updates = list( default = list( rx = 0.5 - , ry = 1.2 - , axx = 1 - , ayy = 1.1 - , axy = 0.4 - , ayx = 1.3 + , ry = 0.5 + , axx = 1/200 # axx = 1/Kx, Kx = carrying capacity of X + , ayy = 1/50 # ayy = 1/Ky, Ky = carrying capacity of Y + , axy = 0.8/50 # axy = alpha_xy/Ky, alpha_xy=competition coeff of Y on X, Ky = carrying capacity of Y + , ayx = 1.5/200 # axy = alpha_yx/Kx, alpha_yx=competition coeff of X on Y, Kx = carrying capacity of X ) ## model spec spec = mp_tmb_model_spec( - during = c(flow_rates, state_updates) + before = initialize_state + , during = c(flow_rates, state_updates) , default = default ) - - - -# ## alternate parameterization of model by carrying -# ## capacity and relative interspecific effects -# ## alternate parameterization of model by carrying -# ## capacity and relative interspecific effects -# flow_rates = list( -# ## growth rate of species X and Y -# growth_x ~ rx * X -# , growth_y ~ ry * Y -# ## intraspecific effect -# ## species X on X -# , intraspecific_x ~ growth_x * X / Kx -# ## species Y on Y -# , intraspecific_y ~ growth_y * Y / Ky -# ## interspecific effect -# ## species Y on X -# , interspecific_xy ~ growth_x * alpha_xy * Y / Kx -# ## species X on Y -# , interspecific_yx ~ growth_y * alpha_yx * X / Ky -# ) diff --git a/inst/starter_models/lotka_volterra/predator_prey/README.md b/inst/starter_models/lotka_volterra/predator_prey/README.md index c7b1c9f1..17d8940c 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/README.md +++ b/inst/starter_models/lotka_volterra/predator_prey/README.md @@ -60,12 +60,12 @@ The general predator-prey dynamics incorporating the functional response $f(X)$ $$ \begin{align*} -\frac{dX}{dt} &= \alpha X - \beta f(X) Y \\ +\frac{dX}{dt} &= \alpha X\left(1-\frac{X}{K}\right) - f(X) Y \\ \frac{dY}{dt} &= \delta f(X)Y - \gamma Y \end{align*} $$ - +Note if we parameterize the general dynamics by the inverse of the carrying capacity $K$, we can recover the exponential prey growth model by setting $K^{-1}=0$ and using the Holling type I response with $\beta$ as the attack rate. ### Holling type I diff --git a/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R b/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R new file mode 100644 index 00000000..fcc7b5f8 --- /dev/null +++ b/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R @@ -0,0 +1,140 @@ +source("inst/starter_models/lotka_volterra/predator_prey/tmb.R") +library(macpan2) +library(ggplot2) +library(dplyr) + +## ------------------------- +## get model spec from library +## ------------------------- + +#spec = mp_tmb_library("starter_models","lotka_volterra","predator_prey",package="macpan2") +spec + +## ------------------------- +## define simulator +## ------------------------- + +# set number of time steps in simulation +time_steps = 100L + +# simulator object +pred_prey = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("X","Y") +) + +## ------------------------- +## parameterize model +## ------------------------- + +# interested in estimating delta - predator growth from predation +pred_prey$replace$params(spec$default$delta,"delta") + +pred_prey + +## ------------------------- +## specify objective function +## ------------------------- + +# negative log likelihood +obj_fn = ~ -sum(dpois(X_obs, rbind_time(X, X_obs_times))) + +# update simulator to create new variables +pred_prey$update$matrices( + X_obs = empty_matrix + , X_obs_times = empty_matrix +) + +# update simulator to include this function +pred_prey$replace$obj_fn(obj_fn) + + +## ------------------------- +## simulate fake data +## ------------------------- + +# delta value to simulate data with +true_delta = 2/200 + +## simulate observed data using true parameters +observed_data = pred_prey$report(true_delta) + +## compute incidence for observed data +X_obs = rpois(time_steps, subset(observed_data, matrix == "X", select = c(value)) %>% pull()) +X_obs_times = subset(observed_data, matrix == "X", select = c(time)) %>% pull() + +if (interactive()) { + plot(X_obs, type = "l", las = 1) +} + + +## ------------------------- +## update simulator with fake data to fit to +## ------------------------- + +# pred_prey$update$matrices( +# X_obs = X_obs +# , X_obs_times = X_obs_times +# ) + +## ------------------------- +## plot likelihood surface (curve) +## ------------------------- +# +# if (interactive()) { +# +# Ca_seq = seq(from = 0.1, to = 1, length = 100) +# +# ll = vapply( +# Ca_seq +# , pred_prey$objective +# , numeric(1L) +# ) +# dat_for_plot <- (cbind(Ca_seq, ll) +# %>% data.frame() +# +# ) +# +# ggplot(dat_for_plot, aes(Ca_seq, ll)) + +# geom_line()+ +# ## add true parameter values to compare +# geom_vline(xintercept = true_Ca, col='red')+ +# xlab("Ca") +# +# } + +## ------------------------- +## fit parameters +## ------------------------- +# +# ## optimize and check convergence +# pred_prey$optimize$nlminb() +# +# ## plot observed vs predicted +# if (interactive()) { +# +# ## estimate is close to true +# print(pred_prey$current$params_frame()) +# print(paste0("default Ca ",pred_prey$current$params_frame()$default)) +# print(paste0("current Ca ",pred_prey$current$params_frame()$current)) +# +# data_to_plot <- (cbind(as.numeric(X_obs),1:time_steps) +# %>% data.frame() +# %>% setNames(c("value","time")) +# %>% mutate(type="observed") +# ) %>% union(pred_prey$report() +# %>% filter(matrix=="Ia") +# %>% select(time,value) +# %>% mutate(type="predicted") +# ) +# +# ggplot(data_to_plot, aes(x=time, y=value, col=type))+ +# geom_line()+ +# theme_bw()+ +# ylab("Ia") +# +# } +# + + diff --git a/inst/starter_models/lotka_volterra/predator_prey/tmb.R b/inst/starter_models/lotka_volterra/predator_prey/tmb.R index 8894c727..92ddeb63 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/tmb.R +++ b/inst/starter_models/lotka_volterra/predator_prey/tmb.R @@ -5,17 +5,23 @@ initialize_state = list( , Y ~ 100 # predator ) -## exponential prey growth + flow_rates = list( ## growth rate of prey - growth_x ~ alpha * X + growth_x ~ alpha * X * (1 - K_inverse*(X)) ## mortality rate of predator , mortality_y ~ gamma * Y ## effects from predation + ## compute functional response (f(X) = X, default) + , functional_response ~ holling_i(a = a, X = X) + + # , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) + # , functional_response ~ holling_ii(a = 5, h = 1, X = X) + ## mortality rate of prey (due to predation) - , mortality_x ~ beta * X * Y + , mortality_x ~ functional_response * Y ## growth rate of predator (due to predation) - , growth_y ~ delta * X * Y + , growth_y ~ delta * functional_response * Y ) state_updates = list( @@ -25,10 +31,14 @@ state_updates = list( ## set defaults default = list( - alpha = 0.1 # prey growth - , beta = 0.08 # prey loss from predation - , gamma = 0.05 # predator mortality - , delta = 0.1 # predator gain from predation + alpha = 1e-3 # prey growth + #, beta = 1/100 # prey loss from predation + , gamma = 0.2 # predator mortality + , delta = 1/200 # predator gain from predation + , K_inverse = 1/1000 # prey carrying capacity + , a = 1e-3 # predator attack rate + , h = 1 # handling time + ) ## model specification @@ -39,52 +49,36 @@ spec = mp_tmb_model_spec( ) +## functional responses +#' Holling type I +#' +#' @param a predator attack rate; prey/predator/time_unit +#' +holling_i <- function(a, X){ + a * X +} +#' Holling type II +#' +#' @param a predator attack rate; prey/predator/time_unit +#' @param h handling time (ex. 1 day) +#' +holling_ii <- function(a, h, X){ + a * X / (1 + a * h * X) +} + +#' Holling type III +#' +#' @param a predator attack rate; prey/predator/time_unit +#' @param h handling time (ex. 1 day) +#' @param k exponent of prey density, k > 1 +#' +holling_iii <- function(a, h, X, k){ + a * (X ^ k) / (1 + a * h * (X ^ k)) +} -## alternate parameterization of model -## logistic prey growth, and non-linear functional responses -# flow_rates = list( -# ## growth rate of prey -# growth_x ~ alpha * X * (1 - (X/K)) -# ## mortality rate of predator -# , mortality_y ~ gamma * Y -# ## effects from predation -# ## compute functional response -# , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) -# #, functional_response ~ holling_ii(a = 5, h = 1, X = X) -# ## mortality rate of prey (due to predation) -# , mortality_x ~ beta * functional_response * Y -# ## growth rate of predator (due to predation) -# , growth_y ~ delta * functional_response * Y -# ) -########################################## -## old way to specify model and defaults -expr_list = ExprList( - during = c( - flow_rates - , state_updates - ) - , after = model_evaluation -) -# set defaults -init_mats = MatsList( - X = 100 - , Y = 100 - , alpha = 0.1 - , beta = 0.08 - , gamma = 0.05 - , delta = 0.1 - , growth_x = empty_matrix - , growth_y = empty_matrix - , mortality_x = empty_matrix - , mortality_y = empty_matrix - , I_obs = empty_matrix - , I_obs_times = empty_matrix - , .mats_to_save = c("X","Y") - , .mats_to_return = c("X","Y") -) From 46158e5856a8bb9ae5e2182a74510fddadcf185b Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 11 Jan 2024 16:10:10 -0500 Subject: [PATCH 277/332] vignette indexing consistency --- vignettes/model_inputs.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/model_inputs.Rmd b/vignettes/model_inputs.Rmd index 7847e4a5..89a6130a 100644 --- a/vignettes/model_inputs.Rmd +++ b/vignettes/model_inputs.Rmd @@ -4,7 +4,7 @@ output: rmarkdown::html_vignette: toc: true vignette: > - %\VignetteIndexEntry{Variable Types} + %\VignetteIndexEntry{Variable Types and Dimensions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- From 97d6c35611a2b07427b3c840e7b00fd1679f952c Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Thu, 11 Jan 2024 16:11:13 -0500 Subject: [PATCH 278/332] groupSums to group_sums in vignette --- vignettes/tmb_model.Rmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vignettes/tmb_model.Rmd b/vignettes/tmb_model.Rmd index 13da37da..c12e98f1 100644 --- a/vignettes/tmb_model.Rmd +++ b/vignettes/tmb_model.Rmd @@ -59,8 +59,8 @@ sir = TMBModel( during = list( dummy ~ assign(rates, foi, 0, params[beta] * state[I] / params[N]) , flow ~ state[from] * rates - , incidence ~ groupSums(flow, to, n_states) - , outflow ~ groupSums(flow, from, n_states) + , incidence ~ group_sums(flow, to, n_states) + , outflow ~ group_sums(flow, from, n_states) , state ~ state + incidence - outflow , noisy_state ~ rnbinom(state, 10) ) From 80160eed6f40bc0f40b3d932891e761fa736e973 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 08:42:18 -0500 Subject: [PATCH 279/332] prepare for git bisect to find index/legder regression --- R/index.R | 19 +++++++++++++---- R/mp.R | 20 ++++++++++++------ README.Rmd | 2 +- inst/starter_models/sir/calibration_example.R | 8 +++++-- misc/diagrams/model_matrices.png | Bin 45119 -> 18220 bytes misc/diagrams/model_matrices.svg | 4 +++- misc/diagrams/object_dependencies.drawio | 2 +- misc/diagrams/object_dependencies.png | Bin 144759 -> 166834 bytes vignettes/engine_agnostic_grammar.Rmd | 4 ++-- 9 files changed, 41 insertions(+), 18 deletions(-) diff --git a/R/index.R b/R/index.R index f4b0138f..0d1f2565 100644 --- a/R/index.R +++ b/R/index.R @@ -178,7 +178,13 @@ Index.data.frame = function(partition , labelling_column_names = NULL , reference_index = NULL ) { - partition |> Partition() |> Index(labelling_column_names, reference_index) + (partition + |> Partition() + |> Index( + labelling_column_names = labelling_column_names + , reference_index = reference_index + ) + ) } #' @export @@ -187,7 +193,12 @@ Index.Index = function(partition , labelling_column_names = NULL , reference_index = NULL ) { - partition$partition |> Index(labelling_column_names, reference_index) + (partition$partition + |> Index( + labelling_column_names = labelling_column_names + , reference_index = reference_index + ) + ) } #' @describeIn mp_index Print an index. @@ -229,14 +240,14 @@ labels.Index = function(x, ...) x$labels() mp_index.character = function(..., labelling_column_names) { f = data.frame(...) if (missing(labelling_column_names)) labelling_column_names = names(f) - Index(f, to_names(labelling_column_names)) + Index(f, labelling_column_names = to_names(labelling_column_names)) } #' @export mp_index.data.frame = function(..., labelling_column_names) { f = list(...) |> bind_rows() if (missing(labelling_column_names)) labelling_column_names = names(f) - Index(f, to_names(labelling_column_names)) + Index(f, labelling_column_names = to_names(labelling_column_names)) } diff --git a/R/mp.R b/R/mp.R index 43163764..4fa90dcd 100644 --- a/R/mp.R +++ b/R/mp.R @@ -125,7 +125,7 @@ mp_triangle = function(x, y_labelling_column_names, exclude_diag = TRUE, lower_t f[i, , drop = FALSE], g[j, , drop = FALSE] ) - Index(f, names(f)) + Index(f, labelling_column_names = names(f)) } #' Symmetric Self Cartesian Product @@ -151,7 +151,7 @@ mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { f[i, , drop = FALSE], g[j, , drop = FALSE] ) - Index(f, names(f)) + Index(f, labelling_column_names = names(f)) } #' Linear Chain Product @@ -174,7 +174,7 @@ mp_linear = function(x, y_labelling_column_names) { f[i, , drop = FALSE], g[j, , drop = FALSE] ) - Index(f, names(f)) + Index(f, labelling_column_names = names(f)) } #' Subset of Indexes @@ -193,14 +193,20 @@ mp_linear = function(x, y_labelling_column_names) { #' @export mp_subset = function(x, ...) { partition = mp_choose(x, "pick", ...)$partition - Index(partition, x$labelling_column_names, x) + Index(partition + , labelling_column_names = x$labelling_column_names + , reference_index = x + ) } #' @rdname mp_subset #' @export mp_setdiff = function(x, ...) { partition = mp_choose_out(x, "pick", ...)$partition - Index(partition, x$labelling_column_names, x) + Index(partition + , labelling_column_names = x$labelling_column_names + , reference_index = x + ) } #' Aggregate an Index @@ -244,7 +250,7 @@ mp_union.Index = function(...) { |> unlist(recursive = FALSE, use.names = FALSE) |> unique() ) - Index(do.call(union_vars, partitions)$frame(), labelling_column_names) + Index(do.call(union_vars, partitions)$frame(), labelling_column_names = labelling_column_names) } ## not used anymore? @@ -652,7 +658,7 @@ mp_rename = function(x, ...) { j = match(old_nms, labs) names(f)[i] = new_nms labs[j[!is.na(j)]] = new_nms[!is.na(j)] - Index(f, labs) + Index(f, labelling_column_names = labs) } #' @export diff --git a/README.Rmd b/README.Rmd index 1c68523d..5f31b62c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -187,7 +187,7 @@ One option is the same format as the output of `mp_report`. This would have seve * Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. * Naturally handles missing values -The main disadvantage of this is that format could differ from the indexed vectors discussed below. +The main disadvantage of this is that format could differ from the indexed vectors discussed below. But this disadvantage should be fixable by having a way to convert indexed vector lists to the 'long-matrices' format. Actually yes ... this should be totally fine. So the general design is to have a generic S3 method for producing a 'long-matrices' data frame. #### Specifying Distributional Assumptions diff --git a/inst/starter_models/sir/calibration_example.R b/inst/starter_models/sir/calibration_example.R index 949de0bb..1b232ab9 100644 --- a/inst/starter_models/sir/calibration_example.R +++ b/inst/starter_models/sir/calibration_example.R @@ -6,7 +6,7 @@ library(macpan2) ## get model spec from library ## ------------------------- -spec = mp_tmb_library("starter_models","sir",package="macpan2") +spec = mp_tmb_library("starter_models", "sir", package = "macpan2") spec ## ------------------------- @@ -72,6 +72,11 @@ if (interactive()) { plot(I_obs, type = "l", las = 1) } +## ------------------------- +## experimentation +## ------------------------- + + ## ------------------------- ## update simulator with fake data to fit to ## ------------------------- @@ -111,4 +116,3 @@ if (interactive()) { plot(I_obs, type = "l", las = 1) lines(sir$report_values(), col = "red") } - diff --git a/misc/diagrams/model_matrices.png b/misc/diagrams/model_matrices.png index eae69a630ad7345dda61c2cf89d2d660eeaa5aca..0fb46dabbfa9844cbaf52f0ee25d028768834344 100644 GIT binary patch literal 18220 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 literal 45119 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
    λm
    λm
    λs
    λs
    v
    v
    S
    S
    γm
    γm
    Im
    Im
    R
    R
    γs
    γs
    Is
    Is
    w
    w
    $\lambda_v$
    $\lambda_v$
    V
    V
    $\lambda_v$
    $\la...
    Text is not SVG - cannot display \ No newline at end of file + + +
    $$\lambda_m$$
    $$\lambda_s$$
    $$v$$
    $$b$$
    $$\mu$$
    $$S$$
    $$\gamma_m$$
    $$\mu$$
    $$I_m...
    $$\mu$$
    $$R$$
    $$\gamma_s$$
    $$\mu$$
    $$I_s...
    $$w$$
    $$\mu$$
    $$V$$
    $$\lambda_v$$
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/misc/diagrams/object_dependencies.drawio b/misc/diagrams/object_dependencies.drawio index 40319e35..92cda75d 100644 --- a/misc/diagrams/object_dependencies.drawio +++ b/misc/diagrams/object_dependencies.drawio @@ -1,6 +1,6 @@ - + diff --git a/misc/diagrams/object_dependencies.png b/misc/diagrams/object_dependencies.png index e39c002e11e33f3372078ca620c5888bb15030ac..cb935def77d4130958c552baef16f477d527f53b 100644 GIT binary patch literal 166834 zcmdqJg6Y%45D<{=66x-S zJDWFt;P<`v54h)Jz*&2*oO$M%S!>oONpYcz=ZMdtP^gQ-PaaF7Q0LE}P?)x7PQy18 zaSG+|ADW4@&_h&u^Yv*I>LyC~@q=fUVRM64Uz*nsww5P(C>vSSXyY04ZoRs#R!@?Y znc(-ZQR>F0s{y#<-4DZ^W1bC*-|!XjA)_O9x|*ZaDV6l3>y@sWswQLOB97+5BbU41 zKQG%Vbr*VW-(B3CElZd+J+R!M_YMvY{zNY9d;CwV?JbskEbn7qabB@rp9$zEdjii< zbtX#1pfFw?d&l%N5r^>`eXpDFT{yno?#9If4swFY<24`+_s^ng#Lf>80We6_@^-f?pMV^@6+WA`5c>2-&nGHmjcPBCCJIie?>pSQq0kM;EH|Y z^U3)c*N!gaaU>VEQh4|f{RWpgIWKR-#(cj_xxF2y=~P8Wl8knDy0X3()rm=UT{;De zgFWVD#K^z9G5^WSE8&qVHZ=5S#reckn^GgInB}89KK62N=;>7Z? zOJ5JGd2(Wzq*c^!-W6^`^Q6RXeocP&c>6RAMj@RJ3f)1l;Bci4H zXOVH}(YvPie*fbwW51&!lY9T;E%&1$<2(Q3EvKU*qdWh6OPuwn8@Jg0`Ia~Z6iJr_ zh5mDN32sm!jcVh0Bg4SdCk3!xe~nI@_~_k$AgFe}KVNgEBk6xX;ZN{C`b2s3vR}~u zD7t!7WXxGzsqmNdgh{m&(mpC0HU0e0B4MPceQ5eRmL?N!;Bk5Kmq8Xe zA$RZ6Ka22=iqiJ*|50T6^{6OrkMN&Gw~va#_x{~vUnQcc=g8KJ&$h1yEjdne~& zO%v&#m9Np#DA7q(;KW5%Btq@_-sI<#aB#yD5fTX|Y+mcX);>!O=>YR*C$I3odd-In zX#8w#WnIf(U3-Ugps<;JtNO1FG`vPCldY}f_-iz{$h;Z*7#TJ*pB&5^l~i6Q zTQ}8UMyAskN+SO%nK+tg>DtN!&%a7Ej!N#;Rtiy(kvZk$5)uRRcBy$-H zhnVGKs6pBG$CkO?Y}ZV+x-6T`g+~h%l$0wQ{RWZ#y`D8wtufPWagCQFgPAq4eSCa` zawGn35kaMJ(}bBnBcb|5pRh=6m!Q7BTz3*4>u+YWU5^%;BKZy_hFfUwF*6Gb3vH&s|)%X?U2~$%Rwc5{UQ6`hks2siiz>C-JpXydBv*p+v zvSDZpq#Fy-N$2>@i?y>*yjZZ(Kug4J$tWcyrDV6>X?w6e$)eZi|KrD#2-}r z!Ad7A8R*IH-@mtDN}Lu`U*vnHD;MJu5eX|PUFX|d`YvVX+WVKPHXrG}ALXrE?>ujC zk*wBN!n}^VGnBooaz6W+ga`Ui=rWs89s$Na^JOJEr`gP7sEyQNk4alw+jO?eOHYk7HjZ*R4Qd-W%??e=((lJ#_)lFfYXh3o9zuB7%~Vq;^; z(%H1z$Rq4`w5>N6o~Fp9SI07xW;;j7*Y@Mn_${ugm^tKQWOSb^G>hR8O{Mi`1gs z-Ccir<&baR7+*SQDFNF+J$(4^FDM^&Xyh?>|zZ<_nA;IaEvJFIFVE6Xz;`n zaq(tz7qxNW6NXv(;0PV1IIdt}%=KhqID%wM{0x5d374W^>Z|+nLSyx%U<%Pd)FnJT zlOMa7=kVDVO1;!0F(hug>_1q|OqEZOqm#=}xlyvYyI77Pnfjaw)s^<54rI6|SBC)9 zBo6-8?Pg;CMMTCW5KDC3&(Dal0zg1mR8(zmYZnyE8=wJ;A7OK8rWi zlS%gB+!fAb)<+LF4$}-^WW7WEg(g%W&Qv8Wq&Kuno5Q)7^#|Ak?ko^P)@ zQWgAQPWtiad3E&pBq}ojFCVQo{Z;gyj|`rAuduyiB}U@W?(W z97-}<1u$qeO*l4Mvvcvsi*vG6~{MnZuFC?R4zG)Ea}-TpieSZ}G=Cq7OQP{~Cu^SPUxoFTt{sU1!#=jrx7gk{Apj0g-A6cIUh`SN9s zESNj}(wrQYKze17;Ct%tZ8Upf*&2fw5P8>-|N8aI34IW6aMBAq zUqwrcx-(g}abu`gGxDjJn3!j?&kc!_c5B7;`=Lz_h42m@6)iTsTq+^(t~kTdDvrE( z&*pbNh9J}WUS!9~?KHdP`)~eOyy!U71T0b3j*=GqBjla(GMv{C+#4T-JUvKYbO&%>={G39Ohi;a8R=kO zJ&ARZxVnjL_(FxvVrgAloG4CV1YTCCPIDN?V?n`*9ulf5_7iSI11XS@X>vx~W*+cV z)Yhh25rC?$u4Ytkzz2LatG3WDnx-VNU3&1+0iWAosN_+4WNP8R%y7LAkGQkZiMjN5 z0A|o8jH_}xB#~IKit6@1?cXL~hY)y8pFV#kr>FNMwckQ>1SmoOl^{Wis5W?G!eU~7 ziWkdbbh^{I54Of03kXPo{V3a>xP}7csNq^H`A+7=UA4EpTDVxYyYGEZ1RkVX(K% z&c@TW?&b9pKGZlHK$j`*haeVQ-phWJPk&D*M8;bz4&}VtS~eEQ7HOx?kv0Q>zNv|r^84ggQzyHy>U^7Q(9z>Few)R8*;>6BNE19Vwi942)P zpq0vCX6YI-u&i0-{PbB#^^&rC|jgeTX+$e_jWctWK8YXBmuz5Vxp zEfVR6i6c3^L3mcE^BTWpQqKShkS9i5PcJXN=qXcZb#1QKugq>2{glL|2sQ%}0C;sU zn$J?7`^cp!GMUdk*JutC28S7ys9MhZ>eVaPR1SdVs_)+cEcP(;rU!0E+KP$9}t`q+zvXR18` z2jzr?qX7UtV7uCcN>we7fO)_rC6y2nX=ruuaeejInn^@}q9iy{oh6Oj9CnCAOss?c z|Fn`C-(4;WDP@5523mk2nWH6TKG$RP%!HhhQaVQPBJ}TlH%NcY&ibr`)*v4Q2adP} z;2r->e(2ouVspCj#vsk=kC)e`+g$->77E6&A3hYHGZ?L{JkA6DWqcEU1QVaNnEEwZ zKsE{_AQOT)BQ52J2VAHcw<{PgVdMzgRW3GXvRF{!v{2HaKfMZ-9Kicdyb8W%`|2823HXF5K0bUZ_J1fmo~ELy*e z7S;ZmBm+dS&xL>TSpr(@#f1yRka?`uL#^JIVh?zhSnxee$DW?&s3FU-iw_??3g)r7 zzdqYd#lf-dtciW`qWbDY6P9u#XN@I@R%~1x!k!#o-VBqWFW)mBvRUNmN>^?KLtO~G znuzK3sZXCiRoB*LS+BoU%+nP{j5@5L(fr>_=YaIeOf4Se2)rjr63Sg7&>?o zF0c%ExVTk?lM#(T2dzflVNpLzymW)p^yLrlTZr}MzlplEw-|E;H|x;gQ`(DsL{&@n zH~K4QHN(xWO7N`Xm)dN`zp3}l(dqWojI;{`LlvWPXlQa{YjuBfSfbE)JW)FC95^Sb z3fV5NzPnt$c1<+&zTQVqPs)4ud_Un)-7_4iP}{Nn3ns#8&>?&mT*%{W8vla5l(sW; zqh+4SALD8>k$p#bV|ZwgZ_bi(L+88%=$Ie3!UES2J1ZTXN1SMobi5c5$QgD~zD_rN zAcLxSZd1#Vi%Q}&C2=u`X(wB-UBV;$Qyo#V6T|i8$b&s z!%9pn%xak8xxwNf@N`I4i;hX(um5ewQ9=%D(d0BjAA&x|E3Ky}tVfPM4oCA@dIx$V z8cR$}ERy@SGpp2MqgN9T56{m}4P7$pO}%ag0fS1|?;7{XEqknUr_ccjnEGw5+dpT%XkEav082OoV|+W0(@+i<0EWhb$cMbC8k^6+4`Cc_w2 zQ&c~zk@FRruzBWU8EFo#EE;zQTdz9-G1x_$jql@>I=;~c_m}f6SAmMMz4BehqD%+c&8nWCrdaG)R1_<@L=C-22Vw{9;W@y!x&RK3#j* zLOxH!#*N~hdmmOvxRWKsFWH}zSv#N_AMHYQrn?#`OC9!LBl@TH^3Mf#BL?M1RIubAQpV!gh7B zFDw}2sTgFVz_|uUs8pG-PsGBSbx&8dX-~OsmrE7CR(PxQd^>6uX3yN{>6gPs+_tv1 zU@ikRn~epGGY2IGs>C>h4(Cx$Bt;%}yKMzKE?5O)T{>14duQx-)S1*&93s~~Untv} z!IGAi5mw3#)G5Y^(vER2Op%vYv18^fm|XKJ{~g$mWitCzo@u7=u&#$Kp7Bj}&m#?$ zY<-ND62=G-5bY_)2=3J?%Y_mLLMN<^c%*{ecMhmsvJp*HTlZUDgY}Jvn~WgyBTm|p zJ|RUxjTt3QZ)It{8Misi=S9?~m@_gh42V^2*D>%Yn_jN1;+Qna2AS*+ACT^D#ARA6 z^6D4w3Kyx^VV?DiRJU896y!eCFpQvb@rZtFihpQpjXK<3W>X<%CUTTN*gliSU~sBp zsC0L0V^>v6GG7Dx@G2%yI;WFlBKFsjAodq%(@y%|4V*7U$2|AVuCAG^Of^INB8onV z;F03aLA!l7ENp*4ZM6ikut-CMQ$*x?uhQF^XXAyV`>38WH3GYxi&!IZOn%d?rFeGM zW|%LRDuymx+a~tn?OFA<3uQ@d8hKZWvfTOpJa76oEvt6YVKNaewKHq92syYJFoW+o$uUAh>hsbROl({!!-?12(rp5Ri8 z`_+t__B)d$_w+@U6dzkY&$GF~F2DSGw8Jgw9DikYGx|*)9;c=3@0K&u^4i8V!|!Kv z3Ar;fk^d7Aj?sJEt8FAr%NI+Nu_8V&4!`&u*YF*;mk+a#&> z3@@gU&ttjrwHc@Rl5&#LQcI1Si=XdKIpa?5^74T3?o0i-K2PmMZlO=V1+w?W8i+t5 z7T#1>ep1DGv)L8LkrlWSlzlGfPe}+kWH-PeLwv~p{Gqvq=p1$4#s`$`!8kW(j`r1( z2;VGM98LQsJe*;YlE8yuy!Y}HX)WeyPeb!tllJ2V%w7shYyQ<4o>b2$+d}@i;Sc5M3trI@XyqnHwUTOcp-S#$JNrl;a1zr z8xy8UZo4PmRbN@0PakL9e!!6zvEuc?#3g%%@B7DOu8I@ER2rOyGEYDM1XjRn4Ubtv zo8g45nf`5vrUq5Z>J*N4xqdo>Gq&*}BU-Dv_p!4p6X5Y&Uw9RtY6QM5ANd=HFU9J! z*tqg(6nWDZQ&o!J1vbsLW?Av%=?z=`vY+o~QSZ_;3Etn{)E&qP=&qBvI8h}^W~Ouqk%}h<+gy16q)dIQj^S!3BzBOm*rzM-~5VwrFXmPyXAK7 zW>2_p1K#(#rAX4Nr-!fY&)5I~WVUZJHJ=-J3YzjfqQHfiKrY_=C#G&+Nq~Iq(_1_| zV-lK{wmpplt*eT6 z;@u2xmMP4t7M)tauJ?;q8LmF?bIJWw7<+y)fYL|X4P6>el;6in}6b2I5)I8Q(~Iep8naI7pk>_)Hu;odjzgH3asvfWWC zRTH^-(Sn#oNf#b_Hr5pR_!pDx(lVQpS+=6d9c69>au>HX7Fs^ruMg@@o;`c^oBlqf zuN)P#?w?tu=lLc}e4D&mYf^TU;p}9iF`Cy?2k=#H_nj0auXArSG&CkQF0}>gkFKRZ zGw3@k7#hqpVG|8Z|N3xyl}6#~)UC~IaSVDnmOXEq7kT;#kC*os$L zg18v^U8qw%{&N;!R`!k%rUP%=W^~DTg`cyBGmZwg-HwQ;C>2Xig%n?8LfVNebCR?p%16nEv5EgQ1qo58UI8pwvz>St$(VfHPr zJl(-&o_(Q9L^p(>vmYZ5td@p!#>SEcw>ZD*nElSY6$23$Dlkx|>55ioLP;gF9}rw+ zP`3IJGTUYFb+!rTGQ6pjz_mS^Ab?tP_+yp6+s}XR-aVj*QH@V=?(Ov&{oGsQ(l4F; zG;*D;fAe+g@5iXNcIK_!CHarQHh)a9FW5{GNlR~_mQ?n174sGaKr5&1`(t#6GiN{U zVu;y0P@07fK5@D_=w;crh}F)Upy5Q15Z;vZ;>g%e*jy<;$oo8@XDyv0W}-i}%m}K1 zW3O6%SLs}Z#o=}Ul;A=iH~UG{+b8r;2<%NIwN@_ferWYWP|pU7yD95RO5sGtzSn+` zI3*dClG^Sw4bY2kTq+Ep#3zdLk(R%&O}#e`s)sp zL$6E&u~$DnW`af|1Y%*D?|r(w>Vs)%3LHfSW@bOy;tlo2!1>Rls}?g|N#eTM*LsTd zuy~3rUjP2%MYt)4j<8Z9s8h8#SM7Uk8c>IGG)Xp=lTD+Kog?L^^>FeGYpUB@Y9mhZE^nh>k z;oQ|XFUwyPiN^DEr(-h`eo5%u;!CMhtQ** zw;FED4K@x~oqvANi1qBQs*4=m3^P#8FW_x)S`>XwI6YW1h_i>`=)k*rb28FG-!SlS zw+;jU&NFYYoib&DHs%SjD=RvSMlCnGjVAS!QI>O+{e;}6EIbAW=TWn4y778SYQL#G z$iaVHwcnZlwren)=?7kCGb@HPpN;p_ox-s11dx&yBuL0){@!?b#4YM^f|7Z-(YUb>mJ#sQXt8IjT-hP|m(1q4B%Mj6PWoTQ20y5`;K5#N zLICqL`Di;GHZ~vE1xMv#Y3!m1U*85RpGnlxFS~rn*eAE>-}VH6vHV>Z>45OGXVdn~a8|=RivUztSCOl?ObtjYVmYd<^m|Ed_5G&#D6#^kd zRkj#fHJ+}t$z>>VyuoGR7ZI^sz8OT}1NC{oUWY!_eK6y~_-52DXTNKQ*_k4Hj<>&9 zNHc_ugEcdq>ZB}^z|ny=?MbnZ&Jy|Tgg04oP|)XGbF zWWTjxKb!1@T>Z|E|K`-O`de-aGKV$b=@YWNS-ZTMAzi4ng#Xf!qESwLw^aFA%sE=W zt{W>#V(VHj9i~FnffusqD7m6z9&9>{x)FFq+3R|Y9K7)3Rc{u`Fa!@nNhyU`r%Ue) z@9qLt_SS(bW^c36y^}j| z_Qx_1R_o4C{uH2wOny}KB${)$aS}YFM@2*BOY?<#iQg9vO-fcOHTALdB#F?8SmGqhjakXUMs$%l* z_YjNOn^+opq~bXrP7Rp2F)w%CGA&lWiNn2j9rcK$*e|4$7O#6+T#+S3G{4y>Hhy8e z=~MgE;SUp~q2IW`y$T99Z-Voe@NWy@%-xty4j1qXfi5efY_ zAr}d{BMkm*$xd4o*+Qg`&jY0t7p1OfHdJ_@YRJ51qD%@g_%d+{voF9Mih1Hw1%U*j z5{FUDD@oSNs;I;(G6A9Ki-x~;je=PJlojibwB_Za?Mggs-r;65)&3remmozmA%gfZ z1r9jLfYx#eDYnrn@!IrSRf8vA&-l3 zZxzS)q@j)BLY`XfX;ZJa*?)3djBX$H=crBU%Tc7oh>TO8yUINqWz%ml_gdgL@mJcQ z^DfCK@GB9z9l^6(5GAtxU9hsZH<#( zEc(K&TZg+rcVZJXzi}I}VxEr>c-H&FY2pb)njo$%#agr{wOIaoocJ8UooYbj>qhC| zt$c%ye5DN_19tjg6a0)UdMmOczrGz}fQa|PX0C^Y5cua{i6LN0tyD1bV38_KJl-@3 z7i*s$;uFHcgO46t_FSH95dYm`kZIGOtY(ihl3GQP)V2~gW-b43b{UHYyW+@}@%AJR z!Z;J6Vdv=H+Yagq#Byx&+p`O^XVg?u-MP*E8T`ZM4HXE5BwdkN_eLUaZsU#DyX`ey zuiT0kTw^MgiQ+%g8qlC_Qz|l^s5_i=$?Sk>(IF=?>F0JFwoQiN=zFX*#HB~VbK{NS z1SN7a1GxS`-}4rOXkYAj>qJLmdeUN_PBd&at6L|TJ9LFj^#7y5rp6!kw#j_S$L;<@ zyPKKn4U7y`nk^pPIX*M7!$67qf;s# z=V?hdOvl;}$8y}Y)P%Y1>7j*bUvr-Nnmj)?G~wb|u5 z{z5}J_k-sR7*%dU*@n$aunk>!H`UTq-^<$|44dlgWH)<3H_s!&{h8vuy{7U{Nj|#{ zPx-*n1s+UzP(6sHOuE*gB}bkYx7s**QNY3H}%^oID^NRot9m*A33)sFi(oLfaYKmq+ak_lKJ_(})kqT1dwl-ij|><%THn zpMC@>?@e%JP2ZcM+m|q6!3+Mj%vfy22 zAl@Dj&A7MIX^}*%%v&(*q1=z`1y~QQ5zwcbp`p%_Hb(=DA8fCo>{Y%w2i1!4zVTw6 zy}tzTlk_a}t{3)HJuLQQ^R6qCU+f8LK;TyKw;YL+kf#>T4`Z&IoO_;DpjOt=1E{jJtgj%s25s9jqH+M{bty@ zqN}Of_U%6Gl%S7{QmOp*gb*q01wKo|t?6OoeK}mlf@u3f;TY#aN!odo7mwvNCs)t- zU9)+Gyw|M5&Gv9Cfkr%_L8|(GF&ZdoOB65_Wb9y*ezZ**z8Y+X37Y;`e&RIFxH59c zfRpUcgm9JS!|QuOpJ=6X;6^Jl+R8;s^HSeL@AxWXtAtFqmejGqU; zZBAb-fvC~s(mu|4B*+fo_m|TvhZPl+uf_l5$Ke5k_}*HJ@X)v5 zE41At=0l0bK9z+i2*fqkWtT^5tT$9@5_hBp&ll5$bdGOQ&?4%EvRI#~B2eA02+MGY zRqNL{h5nArR_6K|555O`sDB5K!;aG5TL#1Sst-yzS>LzFCQG_NAOynlI7*Unwu~ap z)&wu7v&cc5@T60D-+f&#_@=Lky0717U0ry5yVu_Wj6?&TMoUDKvc+IPzZ!L9qsNmi zF@TI@`IH~Yv1VZJP>|(P4`|mOrT;UstUpuFq*fxNIZuyEd$@=uPBh?8VbIW5wf+f| z(!YEU%t%$hc@$G;Otx}{%&utg;rip>2@c4VWZT68O+&uV{lc6oNQ@nY7SY9XxYk2V6JC+I@pw z`)AD8Q&Ko{M%K^DAi@tviO16FN!upe1Ys|o>GJ5SJ)JqOIdS1}*no3dM0~x%)vHVU zC77qqEDg9D7_DE#xo&zc`-H(@2qXoAaqh|;Fc{|`T4xNgOf+!;a$CyUN(HLE)97f; zoV5NBDiK(;aYcgMeP(5$ny%|WNcr=qhDj1(e-o`BNBAy3V^(R7wlZp_BUy0w{!{I) z9XYo^?PgHokAqp)c#)1G64F=+n%ygzmDQld#^6*SzDiO#FLl#Hlpx2z=NBbd^7>+Gy)#evXrklkNN^E=8tKBcR93nRbUbxv#b? zFWmP|Uy><)XHB5M>rYQoeFOrBN$nR3nr=TaCR1stBl9Yp3|m~`n2ca_D6zXk%CM<~ zHdcL}Ai$7Up^TR#E5D`qnu*kw-QsngzV+4DFGwNd4*58~|4hs(a{lVwZ>B`>D}N?9 zzpcI$qU6H&ucl+7#NZ6*U1fBVd-)c)^vT|a3TLqht|T;g8Yj;^zpK-28LOC?YU7@! z$HCKU=O4eP=d6^>sXb9lySC7uakaj$S@zDAdQPj%ygc6wFQJHTD6Nl6mWk+2E_^1% z8g!>dj^L;l|Nch=`>;t1Rq<$E?#WhCLPQ%#+=32WRi(OnzUIaGL-V+X-kzzs!t;u` zYjhWhel@kJqaAn)DuLp$uNu$%#NngGYiUz%N?U=tcT0BoK#!h(rIezVj{K#|T;{yDISIro1y{f9j zZ2bB2PDcT7KxK)iv=eWSKkw*@fd+kXNk~YDuvy?hDjpbgc81xT`(Z#O+gw<4qmJ%H zp>Ssj*Nk6Fp3c;Hne~l z(x5vu!|T`imW{61vn?|j7)4jv%cj-VbEdPcd}F^0dFs5L;xmB;H_Y?;O2-~`TE_M% zw_H@^cyX4hT&}77yJGFSV7NlsSC2`#~K}ZCB#pNa^%Lx$ECa;R-Sv!B3V1M z{La#+=$${S^BUc}Dy0G*q@;;`9@aFb6S(Rc8roeYS9Z)>9M(tO))mXGg1Yqj;s$MS zsOjh)y_2}mnH3QFhqOmE_u6`!(5q_{!c;M1teT@D0(B9sDF#1fp8eEbzp=vBZZ)mZ zJw#*@SU=XPZ=5nH497F%-@dheH!IRMhxuyd{1ly+P_cGxn0iBfJyWskHMV=K>N~q4 z;$>g=5BFs&zf(=8Sp+t~%WwN8GIk{XFl_UN-VygSPMf&f{kGY6s=z7lKza;sQ)coKI4zd#y_9CS1cz+TVjT+W4k)Z&+i`iX$lBT% z^FH^tA6>i-*TH^igk5Xmq$^Jvh5Jy)ifky$>frdyt|?Y$#$*hd9aoC_stc$2ZMlTs z%`{w8FMP1<+rvJ*Qe0}bCSH- zXvW3IO*aJK+hraMb8a;b)F}!>V1Cy`64zXCbVaigI>mJ?9XI-X{T}FVao=yT?hXk#yz%eip9Wa7OdLqoShX=HUT)<{13q)~Lrd zHe77_iu6Igr1>$gV9!vj4mLYpYi_|tIep(~X=b83tnD9}45k6aER@<~;zP;nGru~K z)9~=bNapSt-4$#i*&RL@ieSZe*s*;#yRp!#0um_cnxNuul17!QJ5DK^u9R6}rDAqTL59$BC0N3LhE2r9Z0;(v=%eesldTocU8C7+ba_Rxif;#Gz;?#7)1uX(qg zvOk_WiZ5Oa%g{X~wRyIj-)THoO=Gt3E4jjKtc85)#@KU;$i6uB#iyN`vt?Z~Ef)9% zUTJ!xhiVsO!>-FV#knE9?p0e~Z~I}tGv1B&Ap+-k=kTv0nAwos!Yhixv*b5h#En%h zCcCmcC{g5XD*P%XKP27E_MYUmyvK+UC^DZfsula9Og?CmmYQlxM=61zpnLbc)?obXA*N(ezuHRezL}-iLo*MH4C}vX|`wG z`CU98bXR?YgX21KOgZFC8Mz_@0@9{>+Ou-$sBkRuAI=9wL`1{fCI5a#Bfn2gT)r3{ zBUEg!KYi~%D}|FJ6(gu!`5qHc55A+m3f`z<)|*e{^|e*GC`Te%TG|`jmXdHFe`RAM z&y)kT@J>FB*eZgJ@WQrAG3ovLU4>PZzHjvOC{#(kiWM81o8`QAmNidE0*P+URx{I_pji0ZJKjthDQyj}DJ9-&Yb81=Pnn5c@#^~K@g zHj3kK!n+k;ofuDkg_3kRA_RDsFGuGOS&JTlJEkSIz~jf%ckhbbs-)im3MCrk1PPi+ zd2yo0-=lzwFxGWZ) ziHUu-wY9ygj4eqC2W^vJZ`wB&4pP(5&}($a~R$d*OoKo1(o|p|N&4}qZ{_9X$m>**wry3i$E^m6`YZxUA^@Zs;o@im3dY7+ zRaI5^SFchEb6PG%ktactR^Z+9$6Mk*n~ltyHvt0!Lrrb1V$p?>cfaT6Qi1VkAM9<% zpPVd5R~!VYMPQO4nZPHm(UO|z!fDYU$z+~0Et7IsqrQQlTRwItdi5p7iBYmD{%I(s zQ{_#k$|&vjcq87()m)wKE;tb{`jiK!%IbKRP$=?x??=UaD9aI|3of^3hjwn7{t%e1 zK8*YCQ*ncEcSH`PVEju=@!i1{rBldy?E6M-R1t-WBYY=eB0yA4PkY86QLX=e;=BUD ztdiV6n$fe=PLLS@3-{k*R1`Gsd)PP2)uhDgOtjT24$fD*w`@6YvY~He5p5KY#IJt|^Rn>ZTGmWECfXyQ>8m#2S+Xbc*Cy zSXfP=Y%LE1PE^K!aDf9pf%NDoLx&5eo!n-;Oq?oT?(BU#ykfLIBP*MxD0_ug&Oa?J z4K}C$%*-T1;?7iGu8xR^h;K|xOk86Q_0WHvlBJuFd*=^g`|=3?9A96?#r+D&noo}A zYunh=v;&eSO`%>2a`GRotx}UMk=?Kz!}_Yq*_HSD30EV^6p+gQ8;FqtG6~Vq(Sh{Q zKSQ{!vQpF1Cj#Cpx(z$WKW9 z3WS<=g}&<4RzrH#X~Rd49=R9#?kyh#DC4N1t{;D%`h4nLWz^C)CSs33dNg%ODc9x` zOOL{#M+SZ3!%qZ~PyOm>veDEp0ScTbMkG&!@PRl!){)|zC_}}aLRi3wPt=85h?a4l ziD7qkaRFgIQOZyA0BKAxrWH}C+m_fTN((+Xo zOue|Y`*M=*>-9bRYI8#DQIZ}1C!k*xip|qP?&~F2RPfua)Kh76Lz)$ZgM-8N_O>>p zH8(Ugv<)8~Slr;XeLhy_)y+0+>$h0YAI@bV{_fp7;O7D4ProoeU?^F7k8o|}G{xLD zNQ`HNJWC2HsuYPxUf)7S*Svn6=z9G{+$oap+ z$S4^^uiW+y1FIw7K((;2FiFUZq%T|3i+mdFy&NR$vO*pitNy@qDv5}WFb)%PVFd++ z5GFMjZf@>B6BF^!FfA*qEEs1X;T>=<1GzAmNST)}qhR-qLKxD?0wW_M;bFK5;llWz zKLd67GM1))VI%_S)UV=beh^)|mI7&yG}P2j7|Qpul>06E0wc9P;HBGbW>n}!@?S!?T}UJ|$@pyNH}c~g)T z;WjXn>IMc3R8&+Kud_=659x$}4&>DFc#!ZY0!Rac4*}OO!Hak2iCUd!Cp4U6Fw865 z2GYi=*?j$J7`qhYFrLG~>HZna91t9w1UEahALM%)3=~AeT?kSG8A`Im|2@DvV{ z3F>?IvMkF=_y8X$vQE%RRMZ;T%?sSskT)F%qtON%Ld;9-7|U)1w6mi8HM+ zuG~C4;}8k@?uygc(joz7q9ewO_YMI;kZs+*f)@$T8f2@@f=1j?Eh|w{RyJE2c`!d% zEHA;gp92{FR4tTMhf?@$>(>vuz;1;XOE)9nPKGohW{oGlko?-zwK!0iO|A`TufDM@ zyt`u1s-Gz{H}E$l5aT3aKgyl?{NbPIwf%W|?W-+(26;n}L=1w&?m@&XsMnt-tEHtC z5$PYrzH^Wjy$~=?M5I3Pb1-y zhD03RE&luW@4xN&77aPRUfkW(a|Pvl;|vI`v)@}5GFuo>fQ-ma9KO9MgYtd3a89!# zwKSk;@%!ZCGdJf(MxsW)yCOoxX}?zhY4~hBa7Tr2?4LhxI+J9C0`AC41FZFOgQ?oy z*_r5D^x`{U#0;RQ@9%#B+1Q1(5F!ZSg{gP}vmgVhtbc&L0c}kJduCCR|8;3{_aqu{ zm;4`vz*ALKt*NOA_LK-?f3?HyaA13b&t3_xnJ6+IcZ2+F-$~*RH$bJh5I6{BGmwL1 z!VdU#exOhWGWLJLR-RRzqQN3#Smema%QV$;6$JN~Te!(IH8sPzt)$@wlGcGjlYo$r zWRU0qqZ`6+0i+aeZozhJJbjI?e?S0*utp05FR#jC$*Lz@dXTHzd(RG%_7qF4&7wDo zRpV&>!ozQO(W@#Rr9$u@$VL$49-59;VSf1VAvhiBds7IjAH)i=ud_#i61U;ITx92#zJrZ-H3)MQ0SXLNIOlNJ@8}>9A`3;@5Ux=xuClWZ|G|;kwX>K{Kpp`-~z{B zcc0bs(wxIvb8~lfbaW6q1Hzq^mq%|nuwP)g{5APm zlCdiEj(~s)BgX1c2_uAsW7WI48|fL zAdmzzS{N}|NGCVx|JnMozzARi=0ES2`a(;duu-u`J132o+`g+r9U!+rIeot@jbgK|) zY5wh#yowrKE|4B&*#kGfH6dK++!2vKK|ma(-y?GKSuo~^`z)QHdMLy5z`d@Tfc)VO zGSJaIh4k0EtvpiPaKL5J=Hk$a&6Aq>84)fT-BDj(53aj;`fB;!iaQ8@0&FqCs%e2h!g@Db zaG$7;TMMAmI^smMr+0FdD=GK@4Ix{jh<7fj^6j8FVl5{#Y@9J?(}|j;LvAQ_#B)vmx_5 z1^64%AYAqUeYJJKS-+L@rXCmJQc!V#tE(%e*q10^N)2Ez?d|RR^7KNBVz`hNbtM=1ez_~Jo3@7z+?gmT!Vg1!DSBT z$MysXoeD=Z$lDM0B;iShOYd}kMLn45%}!`*Z#Nkfg*M?9CMsdfbcf>e1oVl3(8P={ z9g*dQt9W$a#uR~_&+Z~ezQvDjAek&+Car92bl~c|5I%>p+RsE}F|CPFQ8!EB7A@ac zxKIJPs3e5l=qupy73dJWBjO85K5>9r+VFT^p-F=Fe3gArK06oJ><0oBZ$RsK@$h7D z6+%B)$AyNw+HA}vxf3wF1@{nFKcjZW;s^sDP>Ptar#D zbVe~>pH3Lz&Yb3Ri3t(bBH+Qc!?g-#;OKXzD#*BBrSq+P=^Yvx3QLdxR!rCz=wPnh zt~p4U_IzKih?tl^Kq#d|L@IJu&&+8Xc(bOiuCcLE)HerQ7u)U(mC_>ZA@EaT76yxF z*Sl4N$p0V4z5=Sst?d@|*nx#8iWp#kU?Ktn#!&<%1-7(BBcX&e9vcO8Qvy=bjnZv` zf`G(k)2K+7bhGbVocQp3|9|gw42KTb@xJR_>sik;=X~bE4pFFks9|Ja&x+6~KmGcQ zNQRXEcO-e|k=?_DDi(E`E-a*|MizCV@XE<)U7F$A*4B2kv2U^%jv5ne z`!C{uT7 z8Id;d7quY_neu3+r9$Ra7ckJls zO}mkL%P7^v+|Mqd}? zp}QoOtXNSzFo~pcAlr4a0b|G^Vy&Q>Vp#f0tMFWB-T0JS2+Yyyh7P{u^iqjcj8!rK zlYIHfP5pp4jU#af7qUj4b~E+lo1OOuo_m=wu#)K%Kv*Dqbsnm7ieZB^n9UNE^`WLF zL$*^h>t^ZT7<_&Z3f*yUuz8DfcV8PC64HihqbAcbzN0eM*I*je*dY9Jb{{^h6rVF5 zmFL19-RQumH+<||ROGPKbb8WCf_rcryIz>dOHROFYdie+1Oa=<8Dg)yyl?$=XHwVW zVMyg87R|&&j26ng-3Y|g&v|u4nerD9WMMrhY+0OSboJ`hxSltpVt%&&$|00Qp&Vj~*Rl7e9TP{9w#~Z<}bpP5yvzFG>c1hMVt_lx%IYkWrtrX&LI8bJo-> za=Vs#srT(id(?^J@ZW!qdH-25i+ohRfF!5Pve7QQtxcExh{-Y32N~h!(_?&4#v(@iF=sh`syL&rFs%NHz zFz!BEsv?GQLtIushK)=(6j{AIhL5PtCWVZY7Ate85EkJ5E5`x^k;N!<9uCA_Ri$GjhYu(e$er}|< zaWC?3*!kTcm(kVTjdE}&z!MFGMYr3xZs*}+S}Ws@HD!*y4*cI!{FQVsj^ZDrEChB$plG83WtKn{msR+ zXv^l|?Ea3&b_Q9G9z7DB=sK7^J>K~$G4VW#{zErf&W%k>_&URWD?P)qm(SADvbwf5 z`o!HO*Uimo=zwKexiV1HZhRLI7(PC}>z0;u$2s%*MKr(I{TOj+wX_FY5~Z9NW@~5H z4D<&Hk1mviW1=>Jha%)seV0{J7B8Nig{)n55^Ri1=p&}hI|w*h#`hL%isv{#s**}P zDn)eJPAp;1`|yEO03=XWkG+5YJ3*l7bjtAX@K|v!>fpOrhM7-TX zLMq6q`+xr27u}4oidpmubkp^0>b12WdDnd?E9u~@_EXN z(X@;+bYABJ2NSPexpJ-jz~{A=QV+w2C8*USHZ4^a%@-7@6IYs?u4(=?KVQGQRuLhm zl5p`>Xd(HL)2TCO9>CRX+e4HZ-<^bH^*%ZNyv>P~0hgjm6!_4?@W?KBfCuiPtW=s#CDPR)XJfL(Fp?vHJ2!#_!WFVqy%FjjE3{ z?zftMCzQdwSd~JxS##oV(Ax0@5z%0URd>(1v~ekYE)&;V@O!ZGl7|c{sP!hYH^%f- z4YHzQf01YaYJ?g4aDmHl!XwysYxDAe9A6&8P8D8;^cUtoo%`SazV zL%!60i)1CQ3_9r5){)#9HZ;N*p(J-7kT*&e)`MQk!`(LhwROwXz#1vvp&YR;q7t?#MS*pZz-C?Kk`GYw8aFu}`Ax!&%CqpXr~X6oO-gjt;%ZX&Bf;;%hqbNm^fM#c+M2OA2$|C3lkZc5i^-0O*_M)&S4SEiVae|;hBGCRjPX7nqp zAE;2>HzXRXJmJyanwYHFDrofOfq-JPwt3O$#G{(5DdD@uqo;S%&o5P*_?-TGk!F@k zgGP_fc-|TE-zz&ldz$v^&QI2VLD2}{kxY3iYInzGOamiTK1tdXY@7g+bv zl=T}bG}VQZl_o5TXyFtaB90Sx&DWpEIK@^G|HyiQE5+LS^sN!jRjU`w{Y`Sc1$K2ts=rTK@(J4IgqO-d0%s;P#1ntsSpLw1)p($AKR&>VC86!PgO zA+xG<-sr6|-nYQnMlUcg>x(9Jtx(_m4UJ>6Cl^{!pirAyziNk9lyv`4pX@Q;H`EYl zQ52*sDZD-dJ_x&YYSB>|<)T7fPZTbS_z5Q+=pGoZ*st>HvR~fgEUQV^^mv2Oui=Bu z0*yS=gH}`P(v@6T*wJzO_s;B3+3qM)LZ0il2JsT)T3p2n+5l~&PP)~2<3MDHHI zJe8Yl(78~5VDySUOVt_dqzBIQn^>OLSIMxoJGz$L=JZ2WzJlUPB{xkQrS3oGx_kJk zN3^O18wvOcrl^NA^HFWwOz-;4-jtYxK;XKpcx$;1bi>b|KUX(e!aJ7jz|}JkdsWxY zpnq>gRFm~R8h`~~Zqz~9hwVK*gEu@Z45$i07cH~H5)%_qbt!y(R@)ej$`d@fniv6O zA=x}+J8A+<7_V)ETdk$KE2otIQDuO)IpOJApL94%${a>)FOOFB~HPBJnt=AxkB^mc-;U5=<@$O1uVd zik-1TJV9rWRwf3Z3?(WQ?UHXw*_H;d#{@J=7%z*)WH0lSW3N6jy5v3tNbh6k5H^*>9Hl~s%Z~p-X+;egJ;>PD^y5cF%bXz(Y64~VVjJ__ z)2iK5*4`cCmD~ybg_P+LJC3pL5U9QiYibH&QpBgS*0MSjo8^N15wiV}HrMy?lXw}a z5A{l&2s?>G#zjv9Gn^W0ZT@+S zv)wfpdoeckuTzuW!dw4q=x%=OLN!|=#>hgswNpSqKxFKPo3@{%&cAs>c_k&!x%1{F zLr0G^uM>WvLMC4w$A`Ol@TQ@*(vtu+o^p0K=4zr{$-Pw99k3Rmb&x4bA3uH^_|(D9 zwV4hK`{Zl2N=8OTgjs@TAh6q5il&_+3c(I!Ryj@oM&P{bbqJ;OX=lJQ7qPJ)Yi1C8 zEQB59)+g^RD{U_iFKP)33E2g@k-*8Ecve!VO93tHN$>~A;^Q9;NewJeuQN0+Y#2d> z5{2pt*{2t&sQwL|3HAarR3tnLkS`PIUE*k2wOaq{<9rp1FSYtI+l% z|Apb99+RZHY!ZBd75ousA4p~Gpzo6nkHi4=M`dMa7uiFt1dB&LBv*X&(M-EWeR^E% zg5_*&Q1oRs4eTm(+3Z>hd0UY^AE&y!wzlWG>CW?~K0H?{&)-a=8PK6_JguH{U8=m* z_NtG=M4HWznd{EA!|h7sHK+5IlRYwHGSmZl+NW1eEwoFp^@|G+YoBth^l3@5n%sYC z^Qe><`>51{4M}a91{F3xJ72xx9%Z~Dl2#{Y;pl@pYliwdcsuHXwBq*1&fO?M6YRK!b>5c}jGnp3R+}|?;%(k{u-zpjN7ey;3X5`db9xHvRWA^n6M@`bT zRL;oD^B0QHl4Bbtel8_f%tJRUheow9G|CO@T}s_G{2aqed+(z1?M8eM_eFC;{nr#1 zu=Cex`WB?5djjM#k9q>0J&OQ@u^Y57k@W#sBP5PQ8Op{NcbBs2Bi5>9m_-r@21EzX zzX7l);G$9Bi3t2kAgR(|aq=2UoLVUq@PmX6hAIjc+X0|=!oe2n0h=L6E9!HPCr<(a zGDLuW1k*|ZSV0Oa(Aw=|tpQ%q(a}J$p7$P+5W3Owm=HIA0h8Jz%Hh)NiPA4YM!A8m z20}CVcIjSHd4aw>kNvm7ZbUsX9WB>Rj~^=#cDN7X*d;MKu-X)|9*jsaZj3`!7KvRs zjNhsDcFploLZ73;NA{-;##u%^>!w^ZC0+?d5Gk?XYuoH2p!;PDXfT2=e*tPoa7wUT z!aWp-|CVn!@H^Bz?xYmg!$c0-m%+)anWJ{2<#Q=mTk_hUKTV3B3!bM|pk*51gET&7 zpZR7_g|9YG=^|Jcd4mI>U?oYrh}OEw-13b1*~aiQ{sK(Dcl2n%w%bM)h3p5eN3n^C ze(7o*FxTwNb3S+X-1GS0{433!O+oyHIvv@e->yfE-Jw$Vo~0IeB<|lxiLhf-9Iv|g zJleZpK(Uq7Q^ze|y#YWnrVKK$KU z&i3YE&kaN(jqEf^-&V(efsl^vHxwbzVVpJl<(5-tkkKb-sJgTEwLt|Rx z&}H4dB9&F~vL!<$MtAR&tXw@?%C+kW(SvV39Ln_(*vj;Iphf+IgD_`LCJKSSn0G>$ zV;=s+ymzAUgUUJ8#KHLT^V>TviPOPyc$98$E)tylka5G&#C=`Rsx*UuCYTCf2wIwH z6VTRc9V=|VzMk{n_$&`4E&>yc6v}`^lVEUuG@nFBDp7v{S8rWX1g1je90!u}WrFJn!#`72$%{9Z zOenHAmFn){@2#vqVmq1u6o*q{;fhVN!0Sno+}s?B_(@<~0;dD_xZ<;hgCia|)Tz^_ z3xH7c^z>jrP6Z7Y78J+>q8Q5CXsiqP(CNVgS)f)GX{J}op8=cGhq#CBp!VYs-ynP7 zt&nhYb8}0IO^*fxhWydl8SUuEBr!ji)nT~VOS1!TUsw|+ap%LG{vQ(?Z0s5V@A=MjeNy;;vwf9wfnjWAeRSK#jh(ji{nCefFW3*h zwk~hIH=Pq^MXQhMcyA6+Uw%CWpY{IdO#i~Iz=bS)g}g%`H>x7WW)m963Y9Zdj3Jbwfg6o-W|( zg3q5TpsZry;0WNUvH|y>*d*-S3Fz0KogavAqz}IV_>UZUaCw-eKU!PE!SYVwC=Nov z$7y6o;ROZd<%zFbA%szmu!amb1P;^@Q27G9kOR=Md(tTjlZfbbs3F+UC8(*XsnyQ_ z4J9@~PfTPPDJcRc-BM3v+#T}DojiqOMIMt49e1Sn^XnV!TVO>-PvSV4u=c7yWg6o+ zWJLky@9Al2XEMfY%+6kF)RFKxq({ZKD@3UD4%tOxNa9-W)otQY`f|1D^0m70Sd~=6 znC9ri?de-Zw60uHziM+qRrM1+e|w1mpQ7IndD`w01DaY?1ez@}YocwfgcD6n?4NTB z7ev0g;!n@gr+pmA^6>CL!rWq2l$R$|V#gz>v~Qv=;y8O^Crv+DDOn}9_kyv;YME}g z0>!NG^uNHLnSYpXXEB6nOiogIZT&vapBs+bc2(_{5IC7ls7>guSk|lwCh-7!iS~fe z-CZ*NMQhb`%ZR)DVQWvsC2pI@USBowe(4hx+!C3zupvsb6pj zZMT7-jXObx(pfcagK*S}x4QBFjI(>sZAx`br;iLu_YB#cg}OC2mtohf&IWyE^K^M| z8*Iiqqn#hIRDIUW<#4V%yM03M(+h##qc7dtt7H9yl8yELqxmOeE9>+h#h{a`D;>-V z3F1@Glu?l-Ao~afqe|ujLVN`hl0gI>pA*kg$#sy;f?&)c4D-1fs9y~Y4c9{akKCdYq&Hc$06bY(SVEx$??7%v2w6VOoEYTi zzLlAFYR6G2L_^{g0hTmfXrDe-OC7Pn_xTA<_Cv8it#B;>z3niS7zi0}02d^=EAV&@RhI@Z6> z!M50s;O&Kisb%3GcAw3appQ?Ev%B!{Bv$XaF+UzorB|F?AAgywK7UBNVqSBGO+sv) zWr<%0YvqRqt=a49^({R7sI4+GGK3_+9;K3gJrr~-9~2dTv$KDr_t2AB;}{DZ`5`Ki z+xuHxvH^ua_8{TpzyT0}4&*(AYE_Cg-nX7m-$*@)!U&~QIZl_j5*Z9wlNu5j7||HD}HoA`rxE#Lj-A&=ycRj)*D6!2r$Q zzRB&XQdJXKv*X+sq`)!YnxpZw*@P`(h)9W0px83Ka2MqRQ3cqGgF)m-QrQma215Hs zHeI;7d!m2|!K5boO61dWc(B;OL$JG&)RL&bu#*}rj-f+;@g+?RphuKklgj7~aQD;@ zkVEgw=x95jh9D|H%K4G#O7&?(S+v>e$123tHV=wMDT+=4pF`sJ2_gQyJzu0uyya9H zwS_mTk+Yp}bt_iz6>g6_z^9fKl9jbF_v`G3!=$iqL1ML9sqes?G-(|5lHN0{zi+*Q zk7g6O4TLrl#?1!XQ#I=j$KPnXFGZ`A!qda(zEMnDwU#*{aw+W27C0n z%Z3{{KvlKls4fa}E20IHfVNj2|HroT0#sAIi8LrR@J%5k(o}9GBxdjCuKAZxT#$zS}Swk>Lda`p194+bZI&aXDt(*(0jYw)phY8nuO{L1C zjb(#p^+MTQ+^)+MhFg`$`>_gM@?6I^?P_1sw|9Q($X>cn!O60|@|yHcR_D?3`kDl* z@Y5n=FXUI}Bv~qZNY=dUrk>qCmbBqgFAg~U^N+_lGvh%jXT?9qd9y={2o3(Z}n>iWLK2%zI0a1YTxl=2PP0s{EpR z_sSrcKtiL4BM6egN7e&xgT6UCR4B@c;0=IQnpdw+wLCWZ**HK_FuWpg?VN8sl>Q&` zxtS>r$!9DrI%VUA+`W=D4NKqW7R$ZOp%r{Q8h`S>TSazf_b^|yJSh0Tu6`HKjG#WE zAdY#F>PX|!g|b>^(C{GkCR%QwNkVs5xOPPxp9WM`AWHK(`at`PgG3KUO-pE1^1?~E z+ZB*4X8(MqbM_T_tJaxx^SIBEuES{Em7=y$?C>GfEc} zx;_}P*|uvw7pMQ1u2fG+>bgDSuG(4sIh$?t?@dm=GiUs4^|07+k~&y;YkLRdwdUHj z<4-jF5-%`|h)U(lgshDc6c(o7nt3eR_2=IeWq zm7r=oJ3{tL#nCT1%_^Cml@7xx3+n1mjx668Uy+oU@yuKzAhscd*(;`O^y7mQH?Hcm zcJ#byT3Z!%x6)2X6EuqTwI1;UzC})qOSwC;euizkSHC@A!O)>2+axz^Xie^V=iO1) ze)#U-=FrbKSlxYXPRWQdobaMx^MIUC_X@M@vEiNl14ET=uFM|suBk7=CzZXuS$Tt+ z&wtXh9vfn&<>h)ecn?sz8*F5I?M5Rx+n>H@TSgR*ZT>f2Andp={nclGc-!9sAugCoFM8>rl9OSVWMfou9N| zaBmJnLOW5J!!}8t{7p}%B-!*HpSr%~c#-enq&IG}VeVsA+vMg~J^OU^mCcz^r60}B ziWB=B{9(ny5<*BJ6)*me@s#3nAdf&b?F1X}QW80P6~<=3yydO1GQ9d(K$zBDhsiMBW9QG>65)Vl?Zp-BffEHNxU(I+;!O@Lmr z#He>KvnA!OnPK>HU(0nWnInFFhpyC|r-pFYAC16zHv4`xYPGAs!X*7{^LP34Qfw1Z zs=4`(vKIE{vczKaDz+!xzU&oOS$ZXWuyJ`~l-?T6q5zRo*g)-bhG=mECS2hm24AwX zhq_0^qHcs+TsM#9rO<0?Cd%V$hU+z!vsX~~^?%y^(-=wuT*1Bf3mxVAhKDO5lDW`@ z0}26!fAA=@Mpz=PcMuvY@C|s}N4*OXKpv6|MSm!%zO5^k5%P=~?p7%*ZyI6u5dJMy zPEtRE^Hce|?t3N)ob1B>F)4G3nlX0Nil|dpve^cnH{VR<1F5q8;m&w=ZL+F^JY5Hl|hd-(cU%tX^DP+7g>tk(ZmUrlv1R zJt36BW8tDLHMNJnQ$kQMUGm8F6Fa5D3L>Q`8?rJY)LXw%Lk_lkXM}4C*dD#DK2m=pJf}8YM$@=H^yhSaY}CfT!sg6BE@=0;cm=&c4O|Lo&@n&_qt|?Z0$@VTfF9zzrR9^Q)#Ik8Mt)U zVg=uD76xIc(>UMV%WJQMkhzeQgXYh@g{HT^sTih9volM`_Zw?+^Jw|Ll26$^?HkG_ z`hzc)aXnMJ*oD?IwA9_}<@xfRB9|141ZrdHp<#(Dc5X^k6R+`pyt*aA^qr1*r*)LU z8Jn|y-aB(DQ}$k=9S%$%AMA*+6XU4#8B(p7D3nR%kQ$ZHJf+p|M{_wLE(|9Re%&)II;HlnGqmv|Ge!%^inPhiuvQqxn6iG4P z$g0q2-Z`3(M5uqxFkHOFE96Fat=YHlTp#$;vWFPTELZaNO2zmelLFt?yXK#&5RN?PgW2$Wb@o*XvU4NS^&bE;^tI zYpns!X(9p?_yjYMDJr6s%ZSBd@Q~q2BZIOYC=zG}=78>8`J9{_(%^Nyj>C>I zHR2;|83zf)3ka=FqEU!Qt$+gawW}o5K+K3%4xfFO{($)E2ZVJvYn2C`Lr5%%-irlt z9nAB~bP@9ochaK+p$XBEl20V6PVaL&pc6eyC>lm)rg|c<)?IW~nHI#Op z5(%n)rCG|j|D8sfx|{#TP|*<)UV&VH@(NzfL8s?_@!f}Q8IBtLgC^%EEMnJw$?&$d z%i?cq&`>J+r9UbE%0Azs)PBw01+tRpVrkBJlmNW*)D#;B`d3KbkBW*9sY*aV?c(J8 z9X%w9I4W}xqEXmvUqWG9j$_+G@LOFJNh;`++adit0uppTh{r#g(xGW8Kglc-f@jZ- zCU(fzy&&5n1v`ej6Y56@`W2BXC*SP5Ov<}V%U&hm9q!;Hz#bGB9*zdcWDSucEnYKs z8=arcxhBmtICKG|=J-fy#JB@GMn-YuOL0oOF3vI|3lwN0X23R(Xhb|h9y%IsxqmdU zCJc{x>S(HSJV(Co)FoCGz+aMjXi8p1$9_VgKrnPckxfI{QSVm1w)dYD;G6urQCxDY zXm1(pErc!QoY@ZVvQVoGEtEWJEr~7dh*nJW-&%kO4fVZ+t=Q@pKipY?(pFHI{v~~Y zwOKwfOikTj_ujplUWqq;w0~2&%)Y_9HM!kaolPhr9$~Q;Fc4;eq{h?rhbBWrc9c#im`!@i1}pEhNfA7!a)$W{1uQR4cI zSoO>3_Nc2@BR`Toav@`#{|epu&#S-3HZ^ah&{e3MI<0IKaKFl6 zAKP*Vv#9CGs;~0*@vHFTA}E#OE52Qv9f#Z>!;)wl%1_DM>G5bNjyWZENt51_B>*E5EFpqxzm_8dB7i86GU z?g5B$ts#r_+kwe}!Th>bbRJZo_c9JxE{;})`UKT@;gDu-@T=^Xuq8-NHf*@iw0-*_ zhvBuzACGxkFwdr4jt!v^IpJl&+8<`7rlshNcmV{lu)ba$;eoW7L45+h;7EKX8UlE> zY&nK5OQI?x*cj%W1Qo0WxqM8#4v6!g0u&&00k}AQTnv#q<9Oysw9WySku2-YoH~ro zETWo4KLl=>7mV2%1SoD=#{B8D+lXaf&?P_EU$~bvsuFq`DUd;W3KqofBS+HIdPidS zd7m5R`?ZJY>y~BOiPeU^+nC(_NHk=y1qj|ajeX@jru@?RdPUla2q+a0>7w@OgqV-W z-T_2n=*`CZwXCeI*9rHKypyyU5K}K78AXq8oMYs5#(F;F5B%}VVd0;AZ|J?_LCK9%eC81vzJcP zj=66goo6DTqw1{M|4G66qYDkY`@|RM)r9OvE?gOE=xjI4rKPR0i)Q5~oLt_)a-$&h z9>`*;m;Y5_AiJo>InQcIhK=teW;R z;KTPS^m-rkriO$vCe@AyF@MT^x6&@juzUH){y*^sgK3*WT3_*f8jo#EPF>PTW`>}O z{q#5M(Gm!}1;wdOS<~PB#ZBq6lMF}IlZXlX;d9WtT6R8yh=~-iEVljQ@g20L6(Xz~ zf<6RHZnhP%*rt4c1&2qqq0%Jt1pl|Y0xbKxHjf>O2!FFmuHfU5crA`%g;wd*-612p zwEQyz14ef0zHiIzNdLHqS9xVHQd6S?m+aE6Dh72Lwox?0 z&lk64_Bv3j4J#LN!~$iHRWRydA&5fmrZcilr$uYVyEmig<5P(D^2+3F4=`oW3ipZz zPrS3~c$oVPP>ias^Izz~IzEj^)^Hc3L&a?S1_iCG(+s}VGQwE`>?8R*bLFI^jh@mq z>jrjpjL(i%CY{f_)tO}LSN{0@6hhhHx-z*my1&<|$pP?3Oek>c^ooFmH+$|8M z|2bkI@P|1c7zvsW#p2}(TbyaR#uVJB^I?1Tcy*iMXi~_F9ymcAEPk#BBRFMsp=2*k z^Ul>V-NM5iNQ$~$84CWJjmUE&b-`i#HSuB2c3cvjqJHs?l|@OH2BZte37C@SviY)i z?b@lw8UZ6u^Nx-?a_;8pj_FyNd)|N{Qu6A2@6dw!TGd8-MzD~j{!Y~PDDh*n#ipXJ zHFnd!9Jf;p&g*TQmzkr%>gD5Uke;a-F+Q@KCbn)`^`S&~MclQ*jlJbvyE_^M6siT& z$E`b!io994WS9j+Of;XNwxpxT##HR;dcG|{T#haVaDq!rch*{FT8rLcCZ>o3hwT#2 zUb3`!HJNE}W@1~uMdxa`{8DeqQ!BV{nglhGmH4Ag!hD(0m9efK`ygrvARgWTF7~sK zi^c+a(H@IAX|gdiBrIC*iYA`f?m+Xzjg?|%qY;(%FHgNZOHmaaKoUL>wtT3C2Wdfp zp>x0iRZvuAS@a$CCetEQ8J`Ag7Lt@;&QvD!qY{nXb>Z=;J!RXO>s5c4v+7pAc*^yGsy(g zrW40H!-VV)eCY3OblJ4`dJK{}PFiHsRtd@J?z38(kUjS1$&f$oG40&oEK|JhQxmPILN@c&PuOu9Tf=4yT@HpYr_rN)-q$X+O74 zk$}wEerw|Gq3feew7BjU{`JD#X|*SXtlE|m%*x;U$l3Fnwr5%SG~~O45B}&AdG^v%BQ>YC z4eK0*oiin8ZS2(k4|)q%W6m%lshQ;{#Bgm-ERu1EAQ89NUUhxEMg&< zY(SGgnLDl&;AY;n-^QftPI$R8Cf&uq{kvUbHCqnAVC# z!TOlBf8F|QMnl+R-!CaYUUG~JqT7U*u!6Pma^(IvlQ7L5G$zN}CMlM4j+tLM;9VfX zX}E8Ffw`!H=#?2m%=&1BA(hd#rSHx%?Ry~L! zR9bq0pkQ^2u(r;@;=*_JihO+h*IvzjHpdh((~fcJ9QKW-F`_#8g}H4SH;%_!j|A}B zHv|OwG}KFOV$C#5wpFQ*l~IbJw}tDpc?hgy5w?HV;s@>nouuqyv1uL~jYhLxU>#;_ zSZz=F+^VUWnmpdXNSfX-(s&?VS;e5zA-H`%Bca-Q_AM&T|EKUd#&WfU{m!kZuSlxs z3@P^L@PPGU*xA1XZ|~)?ux*v@PO|>6Nun$#fZ4#OuUEiU1yw4ru%*EQO+H86LKWgl9^7<`SQ}$ zGgdKQn8Oo`;6SlTAOb_Skj~4Th78rEtPF96a^ehoVVp7**wv**-Ue`u1tbkpu+sZy zl_aQtC1)Oh;MOpN#Km3wD{i}HUmR1TA~)1;>AJw5*=;{IcHf|EFdeB=c$F1b`;gVy zwd)U@D>BN|JgAf3V79Z>VR|ArBU4j=0BqlF4JwW0WZO>m>u(ARVwS0V=oTqZF(ocG zJs7<$@0=G>#i5Psk$=1G$|d>YxZLu#F^Q z(^q4gs|ZB9bNX@_m>1d~V~lVR3Y9SetnF`trf2{Qoo!7VGc2>!bEiE+uH?I!(`*&{ z#JcF$1! z+VrgE`MRj(KKwdzF9+-akQYtmAK+EKShjJmOa(r3*Yy1<0ku|tyZdG~vW}cA!j03n z#SdNm!Fd^D7NXk`AWXL810~jB1e*PQ|vZGYfDWUMd&q_+^0pIYFgbn`9o7P!iHYU z-t6{qOenpDiv?$<{FCe5x5pH(&$wASWiaTrRdqPyTASAXL%z0 zC$_TJw$mG3SkjC8+;(FlF}9nSawVi}dPb!u__|@t>J+E4LlLh`Ps z>*60aZI+`y87424pgKf{Wtj9)rFRCTS8x9%VUU7s#PIXg1J#+oDZuydmci^y&!(^h zrG{cx8STJL^QUj3ALWToP!)s9wWZ?cn~8|jaS}2{iFW-bJ-DN+ePl>ASvO|Q&hwA_ zEUUg|4vucWUBS4O`!Ul>3^i!1koi%gXToHf-SIX)ta;-5;-N*^^#{UEC7z~+fI0h6 zne(%oQ9iymHQKb+#=PXK>D@d{j&pIU^u+h(J?(UwEC zyeSHE9c!3MvKpD%<8>qMIP_hWtjkp=dn=m@t23xxlrZxS8#AWFsNn{DC-X5c5}7e?2#Zn$1|F~Mm0JB|ew9V^k*zl2?` zZQ#MBZv~&Sw?Px25Ph5($U@uGRU_+!dp~}gH(2=QH!FeMv*y0T#0iDJBWS{CeQh{J zhkbHL_n|pHB;wY{uzc?vnSyB3XY1^;PVddxmr|8sK)YUg7KQ8a?K=ue#MIuMmMZP? z+WE04d3d>jngg?wia520cCru}-;e`kSA zl~68yPc{$k&)Q4B&yh~QTk0SX_0e8Jc+<{ie@~eKf6yCkFV9D*M|-l3)AkOg10eu_ zQ1B`J1xQKbMH8NiAj$Z2T0fQt|Kv*@8>3*;l}Bzvi+LEJ*a_`Z$H!7Pq==pOa4~oY zwV9`O^|dBP^{R)z7nJdnF2DQ8>qlc$MQ$B- zl=1A|iVlOZJ>69npF*G7);!+Ta_;Y^Xase3B)Z|NGfBD^c;fd{r-)B6wd(l#1VM|H zr%%DH2PmV-E?!@O9KAI3iAQJ+yRC4uTo zCjIm5SSYLhuUOatls7NR+2IC+1wmjsMXklc)ZW$oeK+J^+O5QF;(L3nJw3g$@_|_W zwcZ2V3fShWGG|*)I!Zo-v*pHvGqQh3qq-HeH8Ny2SV(Z31Gtva`3P4->xk1+@H zQ#wDtgxcq~6%of@o7ntf*ZMU9_C!of_kWbPY#mFo`q-d#WZCUETL>0Ao9kziU3{cE zlPNIck{9LgjdJ$2Gp&HZ)5R+@iz#pk+%OgFY*+pAf=Z~MpetSc^VBatz5r0*;*~C@ zhfc>#R{ruESx54lKW~H(w6bqTCuYZBCIiGQBt*ZBZu{kZF*vj#VZVhVz?qpL`sJ5j zx8QaU=dE}Wu?QPfl4)y)_h>_yuHR~^{pu_F0|1fzPPvC&ao5X--oBzP!fm}l7k z2s2y_(fvYqFA1VICK5%ag>m+ZHv>*58E+bhaV5G+^hg`VA>Ea~bapYhKLFn)xfNPv zX=%EKGDxAWh9$l)7#H`>|MnGxH}NV2Ps{t?@A_LZA~`=ZO-NWc(WX->;kL;Kyp@@L z|27&)5L%9y1afg^;U=tY?lb9MF7w&^zlc?z>>$ zBJ;ktE!Q_#lRK~I?Hh-q)34_->q3_+Ao7o0tq~ubfzbF5ksn6+*xd(CPGvClKvA=jn8Bq|63f(}ftnhf*k?6b#NDDp=xjN9$}ws7-JN55a(c1`Ay*$CgO zs?0l*kz4lB;erLC=)ti%j`cIzwosNqO=6yk4lm+h*1#3y^lHZ@HOF793YeTog}#5L zV&wMgoOc5wIgT!g#fuqp@MID(Dk48$&m$x_3@~6PrE=nJ#L7?{yt0M!ZxDb?9Y@NoHht1+H%^RU`t{*VuKK4gjyUAB^z*9Ke2>}h z6gFZI=P$bY;%L1j-Xb#F+}*KrCPr)tH;-A2mrwi6!~;c1({Eqr{&qlabHR4pod4_~ zaBL28$W79Eb8p#L4abJy9qM>;m+Naa zbLPdtT}#H}_sm@OrRA!3ZZG}fHv7YAtp5y+$&uy_&!XeDUb0xpm)YMVHrIQX6Ysl! zU5#T0{5VJ)UmE=YpTBIeFJa4NcPlC|R(*WWQT<=z5@C05f$wtW=86L0^NVsWQ*;G#_Kn3&x9!#`$4@9(p2 z#IeKWpB<~pk9Xb;8W-oepLNzLJz^)`(!93cy}eYy%X0SCa?Bi#9gb4f+xLlYSuwwh z=5XguG>7=Ig{)uwX0J@yq5tI4nJ;xN#zigsUUfX}>hR15-`{>8A3XHmADr;AgYAde zo!4rLE8fr0XOtEwMVo(`X@ZT5jxTN*WEy1~_G}qvvMzngi=W-W3wJuW0 zS~GiN_>-VhOl7~rzJDKuWuEEG&>Z0$XHNP4AX}T)dUkXT>>w}=QAAoUKHT*2R(jOB z2hm1vZWw%VNMI4~;WsG#WiBWZ<3^Sj2Yzc=^j=YYki#(az@Sg^FO$(R>NXkXKc7ch z!P;lh+_#Qctej?_TBsU%?!UfYAIA1o7ZjPVE9GG%7X396E8E1XB(=K!{q-Ik`19pMP^&XHs<_m;X!EbbSTLh7ROI@o z%#O?2y;A3ZcbEDfQXa_+`|PBbAX!Q+~2oZ#NGhieRot{9y}5C@>;W-E-TNe}#n4I4<0jgVze z3c^n@&!l~e0$QGRzPst-c_AA zULG*5&|whD=!zD~?)>+2T0tP5=q}+!l>FCfT8rM#gwTs?Qp+5OUog@RVh$aRmL%ep zNRHOXhfm-&*m9mkn-;lfbOo715@i7UT*#+rFc~n1h%+0$xz?}&roA||lt>O?Mhe9? zx;H%G8UPa?;gpjjz-pu3xj6Ln#U4u+ zwg=G#!i&5pjG?aT^T4bSE8>{VJdhwlN=oG<(oA%oW0*+C89Fn-Ve`QXu_!=H6W!@&TyZ%I{Xg0TAJ#i9Z->5~9_B#IADb-mHs) z8p!T$FIb)g_X%=579>(-@FqTFP`QT~J^%Rl6#8~g=x@*yN;;*8z7D?bXiakCBppc* z?vQp&^e%1xOK7K(W1EiKG;U|(%>_#XI#3!;nZ zBaNnr0VBp>NSnDNh>@{`gdFUk;Q^s;MBgGP%@CB#ty?0V?&ZNg@^>95yW#i$wHz>v zd)2k&+?VL9E=dMqc-)=i&9U1iAYF}p4`EAsW+o4`Lo+h92g?^NO6Xdk20sG0MoUt0 zS-jB2khWM86O(kv((*fU3m#jlj|J_Vy$^0$_)oUKYt=2mPC-d5tlvsvk291@l zFD<-eGoop4p97KDwHza>1WShE;o+X-j$aeNguNt37Sw;@_PQ=MQD;jr_8qhA{+2~k z@Y5^N!vqqrwa3)PY=m~nQ?KDqeSN*Y71y>WdhFtD$sK?El2ImbAfv=ZY@cfy+S)dS zMbCS!D&aWvhI`7#F!#%0I8u-vMWD^*8K& zJpBFnBnk=(&*R)8(pA59ZHU8U69=YQ1wP6gUcn-xUvD2y7oF=ZK&fspN6W4P93?^w z90VXBzS8Pc;PUHj;*65NBu5wJs<9hBjGbrdvhHZ=J&|!^#`F~F)rG6=5W*v5pfcOV zUEHe;JxOaqJC0{8&FsyI#*pm7K7t59N*Q?n5f^lFwtLs%TU1GSm z`d8Y&|9(fqx}A&Ppz<6vzLn_Zt%HVKd-m+^6+mdH@9|Y<~Z15MT zNM5!k>H6oZ2E3Lu+M$Epb^1*)A$pr%y*ePH=RF-9c%5H3g55;;$yNR44RY`2IfWnF z;?7vk@mc1#`DV`^ZO|xDEjG7{?;Y}f#Ozgnb3~3bs!64y{a4YuapZb=G9Z$8v@mGE zh(``73o1!C{wqf6?EZCqxcq)>y~(~uA1<3MzH^)Xqui|{IzC$ruQIx;6UAX#k!ZI9 z8$YLN)l1|NDL3HtoA_5NqDPu2mIpEeEDz|gJviH81W<$iaX1fOQx(>)*ahREV98t= zD}C!N11dJxh!Z#t5FySiXtyNJ(P)vykl)Ig8djZ2Iyz%h&2ZvG$Mi%m9d3_SA9k0+ zdWCqrtk`@s0;bj1>N6DKnDL4@HlqcRSWt}raFZDv$z`ZRe@lO`YI_Cx&4|$vEV*P6 ztKe6mh?q*;I^hZ|huYj6O{9LJb}GHOj2zfn$KWU=A{ZqtES&1kjIv>JOnl$7L$4DRvZn-J3Bw^nL}C1z9QZ*xCx9)Hm_pi9&?L*Lhoues zI)8MMn>X5xNKlE9CeEC|X=ftX3E%>*^YCk?!u5<4PLfOEQ&XhqJq_8J{&?~d#6!ZO ztIAZdL?J5u*A48C0AXN{2kMw@D^CCOUd67BmUB$cH-l@?Ui30H|qA!*Z%(!MGtLX8$lS}g4)(xz0u z=UmLV&3u1eumAV|Gg|JwpZEKm=RD^*&j~d_*;4{A>5-=uD(_ON7Noh^Dg787_ zmch7>%v@>PEydm+Z8BZD7)*@2khKwmv&061W?Gbjy&M)Gz_7qj{H~L8Ng}pV9cUn* zEKI-{e|h5{cB|`WKE^W)MWVpdNa47$g~57yz>6=)p&zkN2(#9}j3pp21&t2-0(APk zNH%*igPh*^j7|KHN+Eib546Unb=E9PetiQmo1#2k6IE)MsMse{gXvie`fkGUDgiX_ z{A=?hgb$=DjJ2y`2)GWm%4m9!zdsCG|N3>(x&=*}PTn&}K1g2fq+&KWn7WmP3hPaZ zguYMU)JONs<6y;qeanKwP*XRTM4W50v*AgOj8w!LgEZXR{TjEhGZM1MFdEX5me#`< zgIMAaYAQm)aLm~OgZdX&ovNK5x}09xH)F@pG7*ttobu9CJ98#0BEtMiIc-rP^ZVkG zlCGD#&slu8yimSvQJECm-ClF3|45U4+nJ>nmC7njTvXL!a8&|NEfd#%4G9?k6n&#e z%x-fq#K5Qg`OO%ZKCtUO5--^~!9x%u05N#!tWMbd)4~wSLB}VpkHl=OGK&V6D2~%3 zKs}^fYK_8|R@>l2ux>(nZ#dLfp3Pt)0aCW5{ZtrRx55Mq>~{X5n+)4w5qQWTiDE-R zTcr+7X^`v3*l9}|;VIz9$i%2c+L!tuzS*-y&OE%4o4W}uPugrkmKdmc@ujhVpAF0FU-?_rxy6)skQkyBLkxCgWN z*2SG{p2~|O?qY;N2#V&g>&7?yHG_TR-5+b{jW^@j&=?Q`qr*T~IXPDdc3)m)b%|C7 zP=N`i9&+)Zo8wCi)?joVZ>JF%hq42M_72eyJdp8T5=X;%gA2Gtmp$pBPLYzjKVX|EEJwg|kxaKeduUG}x*&7146Mw;4kX6aW@ z_^R}6_ygx8iV*+6pCsYS$5g3jPBbacSyLD^Zl^e826Lu^Q zGO;*-7DYAz_Gzf8U7)R$i2hiGKtABbaoK~>I9CW;oO}SE0<5fwBugmr4~3PTXw#<2 z-SdZfPmpu^l*RIqU~2k0OLlaf^Z2?MR*f{0O`}v)&jCp2z>?SVyG3Y>n@ zAk!rgvqC1za_`TyD!zFNk&xUjnhinZ!vJdq*unw0w096^C}_j7W}TJ|dsttngPvEhgG1Z|D3N@12&hdHWdrT(!hDX- zmMstXBDMu>yEPRKWb@rL;iTh_mJqpQoS5gLNZuP{S_ePueGoY)DJq_4!0sUm|!yQIE;he;3thXYV3XrIYnGxFPQ@N*QSDi|EmZsj(hM2l&t_-%5p`rFk zQI&`HcQlGlsJJeZkfd!`v>J%U;$VZc9hN>V?DXxUC(SD*RtEW#%;0Fh7pxN2kjLZD zK=u&1uv?%s00N`Y7xHMwnV{8RE=c;YIao**c*uayK^!b>%9S%2!ZW5BM)D82apMMq ziN6#B`Gy0MSOB2W)1-hyz7(LW;5Jc)U{ZF+bn~kHFIDk!sD!r1pe1>EqGxoTAQFJX z%Ui!n7-#6>!z~9^xt-?FVu{WaCRPG)#}>mYWdIL?P-2@jZE8g@*2EMsbh$Or7!>28*D?Rt_JSs+|Svp1lTs4q5sIs@PV*%&NE z6DA1Zr{afG2Do^2YB}vlK(qyT5nZ8ZECzT%pi9*SuNuYPdl;RNw+Oa4pd~5DmUiyA z39B6PTEaAvPNQou047RP4XiFfPZB?PY;>)XTgO%3~vF?`7!G3V@cRk zfaTC1sw!9Ob%YLmjle^mjs_s&XETbxzbK`{KYinOEZhaNjwW*KvbVq2R*ik^QKe2* zG(?5nq!%C4$AhU!@@`4`jM9j;C#Y6`-S4R{Y2yb{C0dlwB_VKD6vAaiM{ekA0#%Qg z*~2!x$fvVK(mw3!4i2G_QXeI1rS4a8g<} z1sg%U!VtdnMFhl*oC`W1V#1JWyvoX&nwlA7#CgY=M5A2^ysYA#2eAW&5An;5ey6WS z7_FdVL=w=NlC6Wo{fZ^^x14w ztN09nKGcM-Mic`rC)t5+Rl@}G>isXTA=AqCwR2*-VSG#PQm*}+1pS!yJF&MkPxAs@S zj(sS>lkDcZbzZ<~6%kI?6=BvtARq#1PZWkYuzI=(6pG1a*eUG~ntkLhIxb{MR9#(- zLO=uWfp#ANJ4XVHVcS|UB7}X3%9EWyXq13??!zR}IES?!0V!Dk%c$_wkZa!qqJc($<&*}>Wi>=fkX)iPdJ_ z=A!839NT|FALO${L=9mV%H~)V9tYS$4R@C}-ZN=NmKd`)C+5F^B%lkM;iBNMl;nnA z908+;eaL&Po8`u?DwxW2!O|0QaMLk%-b%0wKR=BA2|cT{cfK;6@GYZFF?gBkpoa*f z0Rn-i*nuh8GK8g-w3PtInbssB`!KMTgNiCM_W%^>IzXE+MJl7Qc!X9BS(nbgkdaoo zcJC)(gLS249CJ}J`J@fs_+|`K9td3T=Hj2R`DRQ@c1+Vu*J$4C6*}*5VApyKW`Hw0 zD;}FDt$9LfI z*39VZ`uGt$S9YYfh%{R+O`R>hF-1ziQ0r(G3I?4uRrVL(zI{uC1#};}WRZ)VmSHdg zKnx-~GddbTqln89WYw@>!Lfs#x497Z^iOBZZci++(O+12IqRheVd)K1n>qI zoj3Z?VXxyqho#mN=l5*y{GkQ#l&sy^&1*X~(pFYy+TnXxq-3a7AB4CS=$m&nob%>1sdBX$cV=XNBaYneRNI%&yHNV-V={Kd*7_E0jP~_#&zJHm8Lr# zqJ6b!YhAKmxL_U7DNR8G@ujsU0&CN9D$t(OvG5Ob(+4WzA36@Ai>XQAVMweTD1Y@ojCo;@$uJE6ea)DQMukwp|T zzJ;tQX%`CE$GUp$zPNl`I*Q~Rz<|`C$eKEhjFb-=l;bcjZ%08Rn~CZd%!4Y1%drBj zduqdRc-}rk5N;!Nu1wVE=oON&9ScF%)o$5091k)ZxKn@E)y(BB#{9CX<@jh|F3U)API0vIX zjt#qUgU}Hz`=jomR=LBfF-A2%?XcCjIW(C$Dg(;7ltw<8+UW-2O(D45fSM}DHf zRLF=>x+xOF3zuW+pwL7(A%Gmu{b*0<*-&u~Z>*fv7(S!#BU`#j&NtxS2 zA5t6UErZ(>RfXWm$=OF_zriJP@dQ7p1_T8kjV>@s$}DhHd{7S4us_Z-6<)}eT5nti z!3{Nopw-3~li?t`qL)$+qMu1Cb+LJn)&nE?Um7D8Ac*EM&U!fli9N2+lMe@W6wZ5j zyTIu!Vgu&+7l7T|0rn(@V(;DE`ATV%r98+{1+f}JOa+t=&yg}gXUdad1z_%T@bR=d zlOPx^c4Uw@JU&w(pJYz3!Ak@xLc^fStmS34M>bJCuU{Jyb)>X?dy>kvOPsb01eV|_ zLe>xV?gsH%039b5g#k;2;tlX28ljvdv`voe#%^6AM^!>Vyet4vF;u-fY9kG^ac zj{d00Qyu3P@?W;oR7^R%98NvQ{ee`_%t5=4z!cbjk0^%#`W;}7sIH?U)(m!T|Y?ofd`U;6{Zt;)|m-QC)#NEu9YTBsWgghMqSFiHh` zEVX8+Ql%B&HXkHBzMQ!^6trh9ENf7`p_z_7iJEAA2gE+aT+#It^NMj2#8ISRWv3!> z)eOAI?LaH6n?Um$S@*!|QlupNWMBnik*RT4?0v@urO$zyWiakR@s%E(CX%P+^7Ev{ zwX_b&G@fg>?jlUbw?8~8HA1JbNq>YL$k)~TPv6m8&VVVhNP15VO2^&(v=f&O-o1M( zVR0cMB=l&;ThOm*t@BU^(h_!ZCk6BDH)ar6wx7I7aQut$> z$>(CMlUFt(sM~$1@$5cvQK@Qr>(v<*-Ogypux{e!L;|xB#n|>5goxnd8LhH#Mn_S@ zU}g_=2jL(dSXix$j6{oIshTI};N}Yo^V9$gsXGE5QN3r(m@ypA?>hwzdLj)VfWgE8 zF2#@4;xvpVK`+#dV5E8YaIaZQnl}3d_xYVN&Fh!ZF$I$&!j_IL3S6a(&-eu)PxNvo zVlfTz+Q62?V-vIi{KuONa330QA~_XUd7_`eSnaE90LQF~EQ8GXh#d8`z1Yd?CFgygh{X}UI)9QLCDB;XCVx) zH){F6i)7}qh&j1)M-uv>d#}f4&$L6cY2upKs`abzSqP$M<^cQqD(8l)E^UeBD=mr{ zWN&h)Cd-W7)zJ~Jvd5orCv+15uICWhr|@Qt^L1bPCzv=8MYQ&T(S$~#1}HHkqi`e? z>Mv7?NDUfds!^iexpWiPR{?nD)g9C0$mutM6OFsq53iCBv_Au};s%J16~tv^WP|%i zx+iMWA@@??K_4yFC|#S7Wo&Y_2+mixERaPBqG#IP8SM=eaa4BziV` zXfU1d0NLsxe&SbO3_;sh^o2vBVP^SH&xj{_n#ObgL0Ly0$2HC!??tXIGJHB^<88n- zT{Jj2EdW}_OR_rh|0N?EJh|uK$r2&frWOkhf6{db~Mb@p$Pznj%lO1|dw!itU>f5qGzPn)NyV`3^K&mbpqx(li*ih?p z3E$dB)|O~8P)mf;;NinBeP_;?VTtA(RTOPTx?*Sp;@zjc@tI>^?e7}^XW&OhvM$js&TiEHUgu8c<)2QYcWCa~ei`udP7}Jp) zlK@X+@R?UQW}wmD*C00(MFjG{RSTqcB$WVlTOsfvE*iUlBwUYx!X=#4HgDm=h48z& z<0j5dfDIW5Oast8_cf0$(#lx7%*nE zG7F~?%o`QaL@#hC%*`ptyj1!x6FBlaZX1jtFS&RS0AVm5%xLcJ=?TC`#dus>oU;O_LBopT!A za=i*sPdP~BSQu(!;K7)Jethi#l`&8GPI%3o^5}LX;CSAL0W`d3JGLvAc_B0`-kdAH zubh2V{@lC+_Ok>_OO1b?+>xA3l?h7L&Fwm6M^gaAOi)S_4@P~*MTZS;gNS6^L_0G2 z&K*@?EZ9w;8A`(_Fk(Yqqpm52?Phi7u) z2+T37&Cb=>70D9(r*i{NC|39GCaO`Q)|L!M*E*+o?qzCj-N{ZIP{9r|_N89yV zJwGQ3&kIEB?%i@eLF88(dZCau-6EJqEj&~e`($+1T}l4Wo)ign$z1|dAzQa@jY9oH zw|4r>nY1gI_TJ+f_(70KoEGfW9|2G!$dq7tXeo7ia9e?SU*sT?s*hq1vJ8zh=wkxR$BWqqD3Ul6ZPAY0y4GoH`J45qj`w4e6e@U zVn_53)k?c04(VUTy~D>FIQ1$Ue`Iew zOmq=M5twL#S?cSggWxwv_zRWukT6d0(ANezLBh$jXC1xVTPX7y5jNG(Q50l?mMTW% zPjqO}MoHR|RC(|sqjFCTG$1H%6=Cn1@%9rc8&Q-fl;Lsq-Fx4RMsLecuK|mT<3I}t zgR7xcee8d#pXAZ@Q{O9DVFwf)I-@<2X_-?&L~ zUugjlsSH+{=8mL9kn|4so$e;+Z?e}%40zQC%ZF*x%C2u7jfZCeun(Vk>!`HfXl{y= z{O8InVUoQ3KsM;C%A{^^?V2Dch!-H>g)5odvVvvlIOOkr-n#5MS7tklVGnD`#Y@J_ zjEUZWNo$8vs3&d9srLv-2A4diC_8FnA~q<^?SZ2#;OqYDZ+0P*A;(`uGt1!mJCy81 zHKPgVwcEoWX}nRXh|! zwEUPJS6kXZe~%SlRUc?+HV&rMgOgJ%YqB!idZ7v=9V7Nr>_c{<(pLT~|8<;1qWPc# z&;&N16Gh#4D$TVjISuaI_6+oNIe?H(_H=AbA$?{+QBgSv=+bcInIxqGK6wU_}b$(m|@Zh(1Ji zpxz#(P*SI%V?j&*IOMoq>j;-0vuD>&#{aEol!z0$H!QYTP0A_>hR z9Tqxq+*KvTTbIs8Q-eyZ@KtJ`kA5oYl`k;RCTT%{QU&LIKn3|(G{Xd+34@9HBcUxB z>V1?+j{x`p3CIx?&e)E(i@S_Z5618>8EYcLU0AwcbVus)!(G3pjbx5|D{%k9QEBrl z3k;tc59nmOyD>Q)%24@W4KwkuwK+#|GC5R|6>`2&(?C^KmGWH3Gt@zVw0*{tXIo_B z>_wt!kkm)3a}WR$_gBQ@1)R;o^F^C5}wXeUfW} z!Vtzll~~oz4Re8(h<~6SA|Z1;Gs=Z?u$3G%3BY;r@*b=kRmJ8o@AeAVq0%E7!a1iA z7F4{kPaO^(oH=+}*5>U4zMdA43@8AQvgFXxB`OsoCP^)Eza4!RlP7bxW7uP(wa|G} zPA15Wu;X)z9Vmy%d#quqI!SraJ}r$iks}QU$~Is~Vrmbjx2=YL06o6+2Z;}VdHs*b zLC7XU43C7P&V4X#$3ATTNW%*8IxJhHT&iFBQ_~7}*}h@`;>uMBWYh~qzmO;zP*e*i zO**F}H|f-1x^~7iQD3g)9+|ZbF>R~-%wI|wNC0eY4X-m(I^!pWg@noW7odSX*czF= zh~1S+m}J>9l{K%ekF(Q1q)l!3saOxlTn{x9(HH6IuBknwd?3C6!30%4`Sfy_eE|LC z9PoLvPR}(sK zE`H9%2oxSAGI7S4hmBL&CpQ?@X`RS<4L!re)VNG(h3+4HqDt=jbatG|K7J zi^(>P;X3G^pE2(wb$P`|>9G&A3?0Tk)r-~6`*7e;;R_FRqI{~-(>g)LLeP^t|Tu7UBBmX>AaC|cCC`0 ztP|2a<8p$on0&xaCw8d^P6-uxRoyWM2Xr(ZV8il2CCQno4|qbzRM=Vb2V1V&8(Q zSIj2G6qnKUh7QN9e|PP!AsPb>3Tk4(x0O0*$gV)@vDC%IF)GCnHE|Qon z1D7M9Ia&}A2dqQ0&zXJAu-wtPt$TgWp$s*A&RW1HG!W#54|eU5-j9h0-DT-RhDL-0 zcm*OMQKX~7GVH&q$bCrUIETdUe2M?BHg!=Sj!JVLwpO9>0Ls(7{dQEu(8w1gn-PpA zsB=kzPFh=oncuXwZcr=Hpbx}G3}%uQ2bQCjI}e>Mic)-{f=qn*ORaMK+i5+aqB7`a zphzG<0kjw()o{9f3s*+USp{*YsxX`Xr066}A?M8-Xn<_(ZNlR{B8j(v{W*2w_g+aD zE-_7w-~zEUcd7iZg3DRUVMf@{SvdK_q2e~UW7;fb#V?^Fpt$NDH(N zIUIn7&@pTnH~j4dFff4V%9Wa^aKYu2 zU{Ra|_HNMv9a5eFMaCfQ!oL7Y3EGDCpEBU{kM~5|tG>L_SHwpR9T1Rcv2OQ%A@f=0 z0#{bCmc51BvncEgkL6VG-a37Wc4a%S#JIr|S#uQzx?ogETm_{MQ4M_uJgu*LV z-Yd1?EN%eIw%j81lJ5`n-w`8p&|(j68Fm*^#OW@i?mAy`WrR(g&5NAiGRswIBl9rD z8~_0dG@r96m6) zk2%eI3q0jpib|pIXiA3^c-&og`HhafX_PaJMLlYSl1UTob;#jblMbPVTHF=P~AK^ZF+7k=_CqQxtnQ0BW=xL!1A)`0!0FI^Wz$aTVEM5RdR&P-=?<-g&-% z?v-tm#ZSQ+fk-hIxtsl#oJU)Bb8POzXbr^@t-LFobRHes{K5l;kyEPd-d(~!v;c#4Gf)r%6%v{1 zGhx;)-MPO1dJVLJ38JQ$=Ysq<)_ScpS|uP>_7t~NGhlG>9gAR)Bv1CQCgb_pvM%?Y z*J)bpDCPncczijgXD3*s*-G*A?Y1A9@^U3sg&##|#8gCQsm6|3M!qUq!wwn`&}gkS z=cP|D+oG7T#OpY)`tR~x^N*_R{wJ)+2dJNxb!Bl~;tjvk;hyr|en02i1Rq-_BEpnd zbAB@=A$Inl3;De?s*69FC(Go@61BT93eBH8W#O~Hsl4)a($Pa>_Rpp_$GQnsG`}J9 z3p;#i)6ew|k0E>qYjNnD zBGkR!*XG|OR%E$hFvNe^v$BZq1Z!I~^ zD^BHG>)1hMq3MXAKV8ABCSsW&^-rMTln<~|VRvr7$yL~@&inR}XJlB7rJr(#0mv7o zZ0%A$b?TBk78zKGM`!~v1rtE4(*)9mTCVE8wp!I?^s^_F=V=0f`l*5EtyLzC&U0rk zVQiCnc^KbC$cCgAQ>K{OOFKGzlCeRfJ;~Y{vbg8rKhr(eW)mLapXT*M=gN7>H?`wA z*EZ#BftT0V(R12SOP+^Q7lK8aW1FSDwAPK@tG$rpP5YZRri;75`!e(;r_YLyh-4zM z_a??%?#RvdIv(Q#--m(JB|VnTZ%m*y=dX~c@fgaaDSNctw$X)b%>40cJnhJo9W$!C z?K!?N{`_+88IXucoEn?U6Ma`yW$F@vv(b*>%v`VIeV$B9P>%oJ#I?k(4ots=EKd{o`TMTD!jw8al{jM@Efio1QYjNS zciYeykC0?lx%YpgzCO6dE{KhBHGlqoPfM6oa6}G`Q)}V0oL5F^9EpmQQqRMG{!*T@ zc~lvx#!j5S-jH<w z6z6_=R?#r%w^8|(Y14oHwK-9T`Hk=&a>Bo-Luw-iaPHmDU&gcDdn{rTey!GhdZr_G zuEBr>9G?v%v6vXb_`kS6aGnUxv*Y}BYdA)}5dq_OtGD+s8EHsK4SPI15${dy>CLjR z1fI5SZh<5|6`oW0Wv{3g22PSNPu2~ zp4f~qWzX@uj!6bT#t$UFJR*MO1Ag`};Ynd|zkN+bq!94OH!#A&Z#3@b_g@2*&Hc`;w~|61tZ$-iol=-G*Eu!BIsTq@b4E>Br{J&}J#5;5jew5B z7eP}o)P{#R=6CM1C`GZ)Qd52DVeqa#E?*OFKAL9AKd?$_*-DHU{f_jHn28S)S=o$x4HRMV$Ab8sc=zz^OUJS*V8%v ze^(zP3@6{W(;Ph%nkjQGCc1D)YH^|#NW%Oe0}isiti=;`b#e@!){Fr zv``VZC=c3ue2(<-S^frWGfRuW-M3aXA6g>3!a(hU>TABoqaJK*Z=W>(^)F^Uwv!Bv zi}H`>Z!XMiI8xK_y4|7vTyiK?_%SBg?nHc|vvklgczKj$U9FfOmq*z7nd9gvFNFR% zf@aOo$6g_$bVbw8)_R5#{^g%Pk0y>X&3TNsUooZj*yROhi+6%Sbb;jVnXlRsz6gO} zZ`RMCl;_}9OoS{y9V|2!!m-vdo*4U=PFU>6bW-On+{EPibSu=zMDU?)+pAUh3TI!T z(dO5=y~D(&d;PL>_c2MOV<=W+Mig8J19fHY^=)>0q@~6Sa-9%Gd}#z|&%Fvl8pJAN zv(;++OnwZw? z*OVkM-2Va%v6m7LRatel2Ib__hq~(4)W5NbXZ#-2XHW&MmZ1}*x`cZLq) zGr079gL}o3`$Fpy3hqz6$aNkI7LUvIya<~4S?d_HV(@}>^^Tw8#GnpS30YV1eoHuj z@9#3llttV7+rj>Dgd*^_K?|U&Rm#lw%D)bH0S>t5MLG10U!GbZ2*BEo)z5(KeRjqh5x&SjTFf~$8)v4{rTW2#w z{vI?GmUJKpOxaUkje5UZw$_)Z(?J~`hrbzBx=gjYbd*6I@@9sZ^7CdI>ybHP!eS}^ zXM06I%A!lP!tAtUe^Hm);Fksx=vDk)5L~P~{LbWb%}UB$S;XU~?_)YWdc*1Xs5_x? zi+eV97a!VKzGo;kDfdX$)(VNbtrp^=&2_YGf8>Z-MbO5?tXKSw7vnkgcd%msR7$MZ z+Y0eDwaW|Cn^yNT-k|ymn#+~ZdAHy`$K%jmnfr_QBRY}42=x@G|Lgvh)7LEroNz=g zJ>B4$TCnTiU$c250zfUVU){g19NYVtWbGaRx7IO~A%1)MX57D?so`h}MMAbW7`pUa zdFp&c{f{T8{(T_nbGDv{&zXnk^dr6}_w|Iv{){&K+_cJG-;V;oM{ttQ`oX};x zaeC1&4MLOt^*Rc~DR}aDtA-T(>vyPpf6RLoIN4t{zF_yif5!>>9n+=NrXrbDOyLR8 z3ideAjW1Ua-cYfD`(f-><1L)S6)!!D&mBYD-mu#5 z_;`7)uzbxv4AArnC*Tr<`z8OHfZ<_3uIs8KLAOio{T3#0+g=5pf^`M=w<-+#F*jg3 zYt3A9@UP?SGCm^&5#x z@93r8fENC9lmdb6t~599>FGHlu8X*Cgql%VhkzomRJ0r+hYJ2Clw3tU1F$1Vjtd1; zg?6;sQ9!V0R$XnY>pt}7XRo!7WLN-mKu0EYVb!6!N9_+{hke50oQFv7%yayW!^Z1Y z2dMx@@}^hQFDVd>Yoxgr_^))L1NFXt5b^>yBt#ZXd(-Gm@?3?-w(`=w+GM6SkPw0a z9uy`Geh%Z`ZO@uE?30{CXQ_lFO6a`b!GR#&5z6Wo)G85BUk*>pWR=# z^%7`tB~5!N_zFDFIr?i`zF-r+nTEvaP2e>UQHW3Oi}6hFcPDMF-CafFWH|yq3<@Ao zm4Tun={5AbviT0m@0@N(4to;3oF}0xMo&VdWttb}cO#J{ngIL$qER;VX=xB0oHZql zRb~a#;e*x!A&)jpRLa5>CQ&$rE=Ln>UL^JPUq5ap`ww)&)S>4lNg{L2(i`~Cd1)|m zrBO>fOj1~(6mza;VoneyVdMt}Hjms2y35!1x2_-=;%oqMs0L~NoEmD>;s;F&8sRz` zVU5sglhFI%>y+zogh4)5hFdNnMp6X;DvN*yrQtA0p{O24f+XDA{f>2yejdIY{kl?o z@+8&6k6%&Ka$_g(QA2WaN9AUiFKA&llWb?vmRDRXf0owm+8L(bb6a})7*{0l{POx4 zFOT0Ev&=`QiosT;J*cL}BbM7mLEKHQd6?8IgY2)h!~q{jG+sNIGqF~h7YdNX23m&_ zQiAMyFa}?=oz+^R-UCH4-V<>)5IN#O2T?OfBCB^3zTxRAe1{C}3M3P^;%D*omw(^Q zV1k8(XeX~V+aOjOO*3-O(?W$&cF#fsy2O?wE4b@a(#L})M`5Oc(?{T-T#|355ZD zGd@oZ=r(PQ!tNbedRoMl*}FY-1tp6ge@^>?IPp}!j&>}DF#YTkB_ks?Jh!q zgxN>`^XISOeZ4A*!Zb63V|s|Lr(H(3+|Xw^tj1RGRb;`uKqyZVa>82}-)V$tg7e-A zbbU#H+TX6H#~_vp-aIT*{^b)klXMAW%lM3JAP^-MgnWWz#55{oF_RW|e#q=G?r$fF zi>+nw~uHx8HYXiL^Opy%BGH;IPsa< znDx-0V{@f*e)+O<0(!icI%*8Qv|Y~?QK>og_68Rs-O2kmPTL1YiwXUla;tcsI7R`msF4%B-XXT@Y}l|Nt6^`EzuQ3Wd;dXKkHT8KBH|7@*VTm|u8z@> zz==_T`QIf5`jJXB&>*72z702=MzndTF75(GYl80T0)wtQi~J%X*|tIphO`*~*d2xu z5NA;19#)*>GeTZL-Q}d#!-Hu=xFHihQm~=>Dw74Mx(4=SUy_QyEIzmH20l|7%?B44 zs2yh<3zz0v-*VAI0!BYRj8>RLLNTP?I-#&{`|GjXkh4%>BfroI1S|v)NEIegET*}+ zkoHX5y3^G5gHCNx>|IRqW_A>ak~Gv#VA8~ii21Px$81Dv)=db<-b>+7 z>?8z$tbu)wrjknlP9TJ8uy9h@8r|~kEIu|4N;&cPcIAIML22UX>?KUhG@AQ zZWB=mYnG7C;!Px*G$3tJ4xBz`P7GM8CQK`p(@(=7hz4LkIZ3TVjRZk*je8*(3(y1~ zYGxXJMNiEic@XCfoia_RST92EEnAMGWntohzmg@MY&+S_Vc>xxF3rfA4E5>2C8W~b z12^ocO$|5;n%f}nJ6xw`4D@Otkkac_{2`xea5mA3)_e|uKqLe^bsO@do@6ElaU0J!zcVj=-aVh)1Vt; zS=8`Cg{+Rz@2lD)#Gf`8_zmpxVTp`352&dwu$p8R0F4|RHlj9pk9$~Hd%OL?Rog!s zfY&*HHn`KWT&O=2cGKSlccFyDC+tsSzD)=tSsD;1Vk#`1whV*Z)JMWCp|LMK8p@d% zfG3a6n?b4A_k8Ky*N10jf~3-;1{5axVgLm#5n{;Iou;24WP~}>!hC6@dfF6yGsrs$ zl|4<`kWoJQyNan2;&QKe{o|80U-D3e-Hic_mMtH%}Row{a-3Z<~A zh$cM)B!rlnfB}&U@WbqFHG7*SjQr(G5 z4ybbNo*7@?1V4aLQF!><#)ywF_gDK|Y2e&;y%kN+50L_5((N)f5!}2$t-yy-*`qc% zTa)b^mj6OR-+e3&25Fe>WH8C*hZ0EeXCyNkT!l)@j{%Akvnx_5nH2|H41Ju`i!eAwfU>WAP-`#% zUb1^c9D|HxVA8c!e~qN%UxN*J6vDEynW_yC!jhX2U2)n}Kx!S;1~a}8K{EP9r^b)r z`lT1a#JRd$Zs^Mn%ocCKYd+8DPi6T5LqJbJCMc2 zEk4Jc*9Ff|rKne8r5uEB9q5Yl(85?b$#8+S_8~Zb37Ix~c3{}#BOhxw;?gH5wrEGH zUe|R_#xMtU>5$Gym5y3voI^;uYvKJSAJf;}AsHWN(>z5SfEz*Kd{?qWf!hIsQ6okq z&=xEM1y3c;+t75=0I#o;_;zQ)QBD=%;@ z$(06%P-gt{Sr6YrA~(zDIy$K!#5R*bpT54n?>k=>q70^f{CoFwu(Hmz4ttvu0l%{!MH}^A_qiJP zXho?DQcI7zmrzjNbL}|@3KI8#sy;CPItcLO5T4FIsUhTp=K$}1fq{YVQo^<fCv88XqVs2wQ*hXKZp>ArG0@jA`3~D8n+*WlT;wsBfJ6|6+p7yn zD2aWtzIG^64#i*_*_}|+Di$8?PzI6N0;LO7BnBA&BN;eerNh@=8*)a*G%{#eEf8zs z2yaV64sSwP>kkPH8RyVsdmm0~7rYgOmMoE9zkWT%Cw2?&mJXnp7RrH6gw$x%9h7o_ zoyfI^49Nhv|AOh)O%k+)2N;pWy_BgYF{o&7U~bb`Cf~65NKtUW2^3W zpZU1V?m+nd#i78j>w68eiOzhvK{ZnR&pYyQBXv;;-QaV$x9|C)xpMA?=w)y?%*~r2 zF<;_K-W;z2r}dZ2hb& z4M<)31C1%rH6Y(E3>hk_hxD&X2Sl*KCl4I&O@ktsvDAuGU0B0%K~}fRJVz7nFsT?f zabg*AMg;zu#8`proa0GcO8$az86Bq?b*XV@K1+kPSe}g8R;~hxmPv;99*?5Z|S53W(Ktm{5kigm#hrG=Koj)^)*}oxvBVe90R$s|0}xgZ zW9dXgP$oh#Nr>d%h~mkr`L+QjQTH&P8m)Wx2KA6ptLM6}i);9RsAwMQrj0w`f(M1V z>lP>?W?y^5^AwxiG$aZu{#fcD({sZkqIERL35=@X#tAmymwNy95+9J)qiL_IK!bJlvF?50;CGFnTj!B13%nrZr-}X7}90D#)n)TuR zk#8+o`qGd-nX|*&gGM5eh`)Sj`6|i9FXq_19`8P$M}h2d;5D$#$cv0j%$-H+Cq^Tg(u{He+D(|nqyIuDgNg{0 zA{sn`eTp%o@Sk%B&o3D!*<(}up+8X6j~7g9#Z z+X^|mDu%gztx%BaVXQEkVF8vaIny0>#7Zm-W5g->i&UJ8!$cUq^v;gOm!V;xdj^h& zuQ`on50C)ULbC4X%%;_+nIy0;qEcN(A0p-r7)*K*^ zN(?3tv!5wHYEG)RIl5+byckN0Om)sU6g+dzlN)!Han>K$YJl;vGG#lqQIf}ns~Qr~}M zs53$i17d~%BhK5GBO@fE$BNi!r=a(v9-{D8umW((bGdL#aNw%g9!x@}fdC+81_5az zzvKp%AB(k1t^s@_>)<7ULuCHNA!aUoBW9X$=x7{^s(es4*z0`;&szUzOZ{r+E^W+9 zf$T^K4Ta4C{E1^AdvW@>N8kx!PQQj<&)Pn7L}O6Qm^J-;AQht`k%)gN_%v94!NL1I zVpEDTsgQH=A>SAbl~@i9!0m~3^Zfbqn@o~H&trfyeki2eJyQ$!#4b2s)z!3=gH`3p zmmY+F4Q)rA-Y z3=6oT0MV2q`x9s-x12P+2?MPOMScij)Xv6bM#Hx2VET4ABP5#U{YEaiWVOC$59y}D zU>i-W4@G2zHSz`_>&H>-pqbxT96fO2H1GzKr%u@AJd&J1bS5;8YuqfY^`65#g+}f% z;#_n<@C|%&fW2D2`Mf8RGil%S>^@M>ZL7I-&H6*2OX=WltLP?%&grJ7(&|Eh%SILRJ0-7m5x;Del9To7bd zls9C;8>*|RL;<+df_?G&&nHJ!YUO#JrJ_Y!3vWDn^yn`<`8KnZR>V47A-8c$8Jt#K z#FIcneFPm0dCPlzd19i8yGK6{pQ$EN8G}jq2(gbaCO$^OM8ILoCoERj_z4~onh0aG zQvg)|Rj>+#p74NZSd|71q0J|e2$-_*hHE5fa*sj&%EW*4etK8-Z9L2pS{CQ}%^x~M z#_B|CGD)+9L$?_1d$JzDuU|ePET_Tva#In@_Q-l4Q?e=gfu}>(S;$pG!vQNv1JEXy zP?ZrEEv#{CdUKjyeD^boGIg0If=!rSNgvQ7*$Q^^kl`0iu_4^QYM1EmkAKp#eq7kK zhM@Tc_kTKlJifbM9vo;t{;QA%z^kbHAK*Pm20)kzaiHMSD}W$E-sA)W-gu?(b-U@^ zpB>(>e`GvTtT8a$V#p}fb}ODDIxk5Cd6~tC#+2kZ{xCJV&-XJZpVn2;KqbeZ& z;+-A_bDL1SPz(KN%M1oC9GWQT`(I)Z#K8EnHi}?)pPY7S5xgZCdPuEXnOP8H=znn4vUmgb^1W8VEE4ZU$0=9-qUJB6Es> z48(0zzuO*m=XA#r;n6?ZsKI9cTdlxew{3FP_kIy-a!@eZwC@Odt(EXyMtx535eOoH zLB`h9FG2GI`&4!h6;(~5NY;rF7(h!68sQ{+SCqMpWD5(lfFQSs*@x6hwq7`FvO`}# zQ*Vkv^Bm+(G97-N5g{&0umKLRPv~5YY(Pa~-))NJ^?VDc%gw>yYT^nfDGF6E{|eX`SGCE+Dv&vL9HXfpM zvs+elsno8>N}?1663T*yBBPay@!H7_fp*%c5B9Hm0-CN9OaBP^Q?m=S08RN+KX;q1 zZ5k?3xAW(-md|OrLWwT3=~TMSJ0FTXn2gwm`38b*0RO^B9VAJof&>%fwE1+;Nx&C2 z%HUrIa!Uhq;TS&))-GI^0-*4+?>7S)Q*Y-?55fMWp79c!{e8u-zp#sMq@H40uKfI6l z)g1PusVD#sI!=&M>t@{%y|ld@4A7#?8VrY$Lv84KHzgspPikB`UmFLv_o3ht{-$bev zy}y!uhGqRGc%5@`la)I6XXcD%@8x2}z#65Y~b_WQBLzGL; zhgigU+W88mAzClt7TUuLVZw!Sfr&6hL+Jy-264}m4j2&FBjO72hnRV=*Z-61DMyQR z6%6h`6&QgeCbV3NEV*Ld@jP83tfZa+ZYDal8&f~S!fFst^3kV$^p_aS)j_cjUfiqL zPX>FP;0an#LoK`OU}&T#GWLXzPcyn2!XMp$pXtalm?Ut3$GmRqstDMHVIHrsqwdMW zEO}Gz+h9HK?blNt~8rwxdGS6KMGjB1l;mHxVmA?87-5-t1;AtMI}dPlo$%8I*wl# zlx61Mh_=iE*ZD+A z0o=c!`FzlTB4}j|!LmY-3CG8AK&k*PO7BzmT;~cbwT5(K*>D3uSAxYLQ{nRR^1)Yd zFVu>(q9tbe&U)~SKVcp@Q^_^?2ZUCAp9G@H}@VA zX<~gO<1$ch;JcY=dWuDT{rdrB2ttwh0mujHR>oKCr+zRkU8Dt2yZ=0^hUb-8+7F_G z)+m;eQ7`pRz}mk`u?ZWd-k^m)@N;cQpF|QDGU0$4xtt}atQoGvM+^44TM-cGjpc?G>K0os=a3&M*E*JO>@5ygMzO06RSdlI|H*jh-?@>!3{GY^07;$HA+ioaZu4urHVSp&a;;gl07Rr&K=w~;{`jT6Df28Z#b zi863KKGC*m-hxr2vvBLXM;{sdl#ATgiHSWOY(Tq~x*Vu%Bqx9n5i6f+P29)h_4oOm zZrxPM1qVYF7?}nUQv?PJ z2pP{fwQIv6#x__7#iDDfE-Q1To-X*HGDO=*=%LBOQ5^+RYaBfbQV?MvKoF@#1$2%o zY-^}u1SM5b;|gnu{?LK1U&(34wHxOP-NVmGiDW>_j7FE?!t%w31huBl!^RA9?Zw9v z328h0xikQcNP21-8yRjl9iiodl#rhmg~6pbR~Fq$adxz28m5ggWOBKn8Zx z8|NOi{aLW?nfgO+=w%SWN{hxB{mxGPFIYkC?`o`z16Qg7G*XHy-{mpX<3XC4>cj6Q zQmrl_f!j)+WC-vGJ$~;J0j%J|ft&;S#}aZaU>sp$edsh$%O7Bt;P^lW{v*8~N>S8P zc=SjS1BDGqi5FQcAl143ojOwTOh2!5*!{yWo)~88Lf#yjk%0#zjyBY^X^WX@$b?zt zWO_$ZA~Gz;RWHFSZL2eZix*r--XHdG@4vGE)hXEzPWiCXW7t991eI@k-fJD|>*5Sk z<0+5!Gpcd0DT5G5#)=AZmi^8P(5w?cZ3JG|#97O!gk<4@MiBm@G78E7hH!p=#|qSg zOm%W^$p;R?I;E~Z=NzIXCiGzOpUHy106yJdL1`r49)h(eH8n&=cH1H5Y=zms0{J1p zrxGLiKzRP|UUAy1(C+20+wi`&k!4Lk705!FMNJQ;`u_r3rbB1#K9k1*J_Az5fOYP~ zw^<>!P?#t~ctUSU2?dcqZC9yFpEQ0dypBO|$z*w);yOqhEKFab{;F8$+3`Sff^>&f zLu1Qz``_PverBvMw(r1-4vL*FnDa*$ci@S8E4sB54FKU`4{uW`jqFIz9FkA4#AHQ8 zNx&d!zl|;%a2w5?7+7RUpUz=*0+T%kWJEM~@_mPE1&D-sesXQHU3S*z#EBMkjEd1? zeS7D&x7N2l(LG!Fs4l=lvbGBd2p~ETiJ_ppd`*oVq#V?updOo#DGAr;F?f&T^ZQ4u zAzk?R(ER#(0D5Yiy2IlTd|r$1hYNpy0@uX&F_Q?dm1n+0LiwEp0v1MIoeBYQ1Z3|V z0jI+w>p>NC;Cy{(kLLhkKqO37wv^h;6R(x9I;T>N{Z2qD0l806-8+ zFb!0R)7Bdba*vmWirx3~HBG1>@0>+dqNnp`3BD}dUv$^k+(tM``&?u^! zbaKay6DgL4+zRZg==Cjvi{bRqI;Kvkn`P}RoG!m*%j@S~hJE)^%v$^-iXNFG=vU6n zeYlakitdEuTIvds2$(y1XqRInG#nNv`bb8Sg{I4`#oZcjz?m80tHVkJaEV09_{pJ=?oN zW{ZV?XWU~YWfe&o`75f=^L99$IIF5Pl!Td zD^ZTt=fqG5h!HQJXA(DvS|4 zZR>Wwk(_+NBVr(E`t!+2t!>2w>o+X`4-fkZ=4piH#|L<|RLgAC8mJ7M`hLa%__7V$Jie8oc+M_lIetI>IzdE5 zkVa|<5#8Ya0lfGD!UcBBmz)`|r`V@MD|*Sp230P`k;cIVViu~Q*)TqUmjMK~=8gh> zYT?%znChy&XEBZYWZLH3K~bHs*-tzB$ZFkZxhAV|bIB zEE;8V8c2!y&wOjuY2~ZL*Z}kCMRP@C1YV=!ObE*vB^@^Gf?Yd;+js3L*ReC(9mC(C>O^c~Dg!!C>`ML0x*!3NN=70QkkOLJ^98uKy!POil27xSOK<0Zl#KAO@5+hA6R?&B zl}9I;_E9=c_xpuvTYMMav}wEXSG$M9ziY!m&Ub<2r3Z|D5PjJ%R8!-x^c)?hUsL3c zF+4nv5X^YQNqPxRqYUe}m6SLEg_HyMldyG*tLk zsHu)auZot=sdG-3XXSgHVfWwjnCj%;YU636HXIwt=|xcRVXKtTRx-uL3BiwhR8&;d zDz$-aD>~NP|2S4yY5enSzRI~e&6`(Jkz(^zhPi*r#?zW7mvyg7TA&!SQe;=nrZY*W zyPu(E`Ixk2=9Xn;h4;dhgrDCyJ;n(3cFFpi%G5_*IPX37uHc2bo)e)KCA!BRoU9IQ zu{B!J5ZvJW!LEMOV9MvSC+W{?zj3*XI#gR;C5l2T!vBZ7fx?B+C!goyE}Sslw@(Op^zyU4^dmeg_AgnxHV}B* zKw-*b{}#bFr0f(BgCPLdpVYTITX+^N>N);+&kX>=dk3p#gZ?wfpEBpv*|QJ!t&B(a z3_l@tF)E-x0rBg0LetFk=P_T~g5VvqoqO^to}DNyUDW?p>ac?^Or;D2~@7KOQyI5^$`;%9TCXK@{+qPsH5Lc4#qSYuny&msTXBxtJ zpZm`|OOY`R$20qR6P#*H#SJr__5=-@%ZB*-KlB$BeqnWNsk^mFhhbW9?)jWG(oP>n zY&|o5@8`bM6n_a{K_wcvZ{>UY-)*k!m z6XpD$(*`LD`=`@eqGi{J0&`#FDm@LT`8D^9MRr`&#YH&1U}vfM{!OCk2W ztO>JMA5FOQnMX`F!)I*!k*9biv)1ogZdV_-Cs$_v3hTy^hO$RKwR9~%WzOPxnb@!( zbF}aMvb>o?>*MY?wm3V8#1q> zZ`tRQ)9sgBJO$kq0``HqTR((>GD8PI^2#=RfVSlrbKO68Bll;wWy`oRd|sb*U0i+} zKjXxTYegEJP0x=?XdhAN@K&y1+bnCy9e+GsgMEAayZtY09lMVt`O0|rN>2I+wnQLmO)aA3eFlTkTjR!8d zvg)><`ZaFGw77V@pTwIYms`#_zgQC(AcH|KJexe;e@2&soT?mPidRYxG9_y-PVZV* zx2GK#6N?k|{N9DuyX*JQU%pJq%}oRve&~CCJ}o+|U{KxzmoQi%zAz|!r{Dc^Bc7j% zCVjv0>w{L<~fXT2f?9p6nSOaip?XoJdX&+JGUBSTeOcl0>6 zJ56=oy?1bMcF~HQ@5Ym{)M)x}*3ad9kyGt=sozWup9I-0EiMug{t8*$QsRtq^qs}D zE=%RTb1v`?Gv&*BC5sun+n3u078oMX}J>^z)Deg;#=r7$2;vLeW+cWjW$fD2c7 z(5O!xqiaYTLrX_nLsu8$*8I>1lF5C`m4|)nCZBe#iTO%$&oubM>wKG-NDcQCS{cC@ zJ6`#hUE5P4BZ3CXT&uJHVL;&vnp@qwEl)fv6xzp59bc#2?5%47a~&h;gG2u+pUkaN zoo}S@H3PZ1?&EUtu2ueeW0Nk|r%2@8#wd)wL`!cxxA%der{vBz`Jl9yjRE&? z{l^7t?3&@>2pCp?7tRZL{Fjmv9;h(D;`-_SU)JS`9O#**KjBTnNm~($c%Zp%biBEV z4btyYRgG6&YgmPAs1=e_TO**r8p; z7DjBx8}vPM+%JN6!J}I)4fLR1K3yMz%H;oHnV&s9R{Oi}E>V9F_SxH;ijA!se)7K_ zhE0{IQ&Z!9mW&~t@!>|KPo^?}uoi@z#Q?Kh+uY2xB58~VOcMS+z>WD}UmRH>b5atz z^2e$3J^h&HJi$Y?k0ujyS>Leq%i}B>bJs{=-r?9TK&0jYActUo&z?Ki&+Rev7LGD3 zfXMLri%S{`Z%FQTYjn%e%a^=XVrGp&J~x<16~nI4de{Tq$&9R>tHFL z$H7T}?lag&uq}Vz!QY=ZoOyI^oHt*i2e(_l6HgvjP1Six1r7^_LXPpJtkaG!!F9@>|QO@vU44YZXnapNKp$)jqjua4u_OiGWgI_!|@ zbnk{3_HACebR+XM;*}{@#oc|reX(0+8HwCZX7|xsW_hg3a5f{*c5oM`5`O%+-DmcB zcO%ILR5;eRb0y6(CqJfsWQwh8ooWh{0bp*;-th|jw&+NCH5_uU{0y@N%R5Y7;TIof z>Z+DZA04x1Hn=qzKES(iLlCAT(qkJK-hccEU%B@i{@$D30u!}=+#6VdT$7WR``ARD zX0`%1+AfR_k~?eEE?iy|AbeKf`f``Q!*8Y6@i|znQ|(@N>F7MA%|4gnbNdjHEq~Mf zGF{N%=E!DpPPZ*GcD5^XEs9|Ohj}BkaPqX2en_=Vh$8Pvkx`!;BeQ*w)|lL-YfEQI zmX&zz!rXLoW1p?JP*k>#$7e5UZYuA905)#RBiuZF87hrhLq-*m*9$G}<}Dm28$DtU zCbui^MrrTGB-5+qy=`Qj@rRJCP37Pwvkv!>J-;xxR}{OlNU&C)1>e2#sfPDs(FNtB zE>rk+FA$IaAPjuszdSzizb+Zvb4EtS0`gl@KY;;c2zB+(^B?DGia7p8KW(%gdidM- z@gt1;?dPfBbwZ?+sJ(&N63Ttib$+GvafD78*G*#Fhe|7TV&};&{>i~u@xRdJ?{rmk*%fxw|{4S7d=3pu(^X6LA@mZXbe9`fmvDadu+`*qKt1)a(hI?(d=NbEF zU@G?hWpvMXKqYexM_kwZTM9}?D`xTSzQRoW2SCqXvk`%2U5-__1MuBcLe0(&#syb5 zHjwY=!2g{;{~fwx3n~;Y#?@3eb6IG8NY~`OznzJ0e!F#KNP4$j>(sM|p=6YGF+{R| zJa{c-uCzUQS+A)lQ{lel^wC-sW7EN!B)ybK!AIVj3HKpf6bvL%h@|^hq(Cd{7=SF? zuQ@%$dCz(qrF|H5Sy(OX6>rYh)002-iT|ch>q4L@z1(%zl)&L{*ECwpjw%IwHb#5h z6-MRL7X`%(-)%X0>TH>;oz38~r=JPhaY!w_gNx)Lkh!@*QUcRjD6?h7r%%tq5Q11Z z=T(4yVfIhyyJ(e8PRQKc+{c(gh61?+DT5|CeIqoXAMUl-0*mpM*Ama#>RK({|EIx6 z-ZHODcW29!Eq(c%Wxq>5z+;C`!7kkPpPiu<5)@3>vg%$_z|fb=tcene5teMn80(&Y%KvRc+dE5~xcByq z$G80y&N=XpRBtUS!wzH!Pqigxz z*7ES*aLm7nQ9&Z*>mU;&cDdpCjLeiFeEG-2E-C3&_{T%;e4WXe2*VRQTwR7)JF^@WA!q?X7sXn!OpWQh6Qs#!3=)O^any?h38vXRgSKBLy;4a{mb zC-tug^MjHmv*O}awC6oc>=qj3IF!+H?c)+F@>7ps-v4u>)*Vn>l%hd28Md9$HxDdL&NOL~($?^$FA zq$|@&PL*+YLbSsz(Bqy@s`e9%Fp@iNF+VZp_KApiOXa1%(d|EC%eLqahFC3aH)t(3 z3hTS(N4e77CaoZsvQuuSkRX`wh$oM7X+buHeNF_-W$uNsramf?NEd&+&M#)5VZ+Dq@8 zd#0{_3n-B(fPN44(tJs9dJLTr{JsQw3crg~K|LICaZsN`Hkx-zt_%0c%(=k-3hdWE z-3CdyX4Gvm87$OJtV9^I4exT=H1GDdbpY+FJA-S|iFie(Yb<24bQ5FhQK>}|;k@FC z1G4(IGELS)kdg`OQZFu91R23#FBho|X2H6)#dUioHR18YJdS^miu;ZXU$hKg)zn+1*QWOOWsjx|a7Qh6E2AZ?LGSz%lAQRmge+lBN zg3{$jSIEd9L%s$4N&ia0hOj<=41O}82iMX`ezL1G!+f!ZrYVp?d|s-Jqm1$HUA!^( zlSGkY;V&aNUzaxcwr07r7sHc(*}hNL`226v)bPak|Blc$YVR^qS^!Kw)D#>KnPCwl z))^UVrl?pF>@lfdjKY11_^5!%vUmPy&2Qweq(UtUIz z>BPjle#5#qY#Lb#hNQCz=1Ms3?{fTq)M`}1_a1}+>(*eg?L3Plu3f|(ZN--*dQ8f+ zd~b!SjSUZII_+C~$HO@7=`me7o(R0&{bXZ^&oLUGsH<*jTo+wUZAdzupRq&{E@t>o zH<;!ZfH7B#&Ooqw`>$A#!;%`#$;HRQeQ(&VUAeOQb7hD=KNuXPG~YYU-C46c1s{Fw zoT-JWIXV)Bum-Fh-$s;dpSi-K%~)~=cYt&0NcKfJTNT_SckKT@Ov@htK)PddkBuz= z8eo9EGd)(zf&=~@*_0n3&i@91d{2*kB)7>vn&opmZVY47e}U&#`7qg!5{!5pn1dEJ zh8eE{0gaXN>MukB{+Gj?Pu*fXray`#MZz8DGS!*1X-DtL^5N6{WwTc>N_I$Mc78IK z@;F@eDm|EOFv~I19I=gpXbJl9{J(q=+sDDt8F28#P$12^8I|Y`cu@1tXuMB+fxja* zJF7!_%*$b}4x|FAqynD!sm|Z!{39XQ^Z#U0aO(b~TkU(X5K##6ke|z8a)$^`%GiEO+a~>U*k#?NyyFs8vuh354>*`2G0zLHpMy4P`GMDLkEG1mg?w zFr4Ll@6$s{T)V|sSrjC{GshZ`i{SRSex+oGEKV`-Vt2X*ay=EGL5`sYb7D9}eB)S%cm2`iKBJB_h{P_&Ld{t$c&r1w_56gLq;W zm=#6KW-7b$Oxu>8Z&*cON3Q6HY*du;&q#CX@9ev#)a zkKNY#gRVdP{nJYb)(CO8ZS|fp$MSUW=|XTTl%m{gVigh?*KO#VmdiexT5m)v=TX1H zUz-%)2&A2VTD{MXaZfqv3dYTwy&k1o3x|0Y=7CMqduC`8lm&)Rtj4F$;y*LAEa0ys z4d)V=rYrCncUxSilY5WJaPYG1rOh3e6TEtjXC}L)%ha$g-y{CZ11^8MSY>Ben?jz{ z6Gsi$Y3boWNnBi9mP7k(dt8$0k{p7Lh7th`xbRr~l9y}Ymcy+#MVXIq*9Y9Zlr-BD ztSD~JqCX@C37FmFl22dB%Kg~7cCG3JwZ1|-YPIx{=OQ9Rsy?_AUC&aJXL|_1vwtfv z4YbM#Gq44x&IMO1Xq{Zri^|p;Ch7Edem#^lYaG(9et*bxX`_)K zJ#%MCKm?MezQL2NNmUr6elrr6!QNZ+jl6$*I1ym8&la;xt4p+a)Z3ZUfj>_{oL=|# zNxiXM=8ot;L>j->`3(SWn>X2ROs+c9hvQX2PE+I2LtF^{57wyxlEL#tBR;!lFEI>U zuFH8O9>f&tK}i!rc1G~b>H)cs?MijLSR`QsHF;^9pZ4evonf8G`B|YnT6{w8LafNk z2G>aO>Z7ac8(W&RW;2eEj4&s`J0?Ps)g7};9CiUoKzms=&(AG2e|DJWqn3!AL85=9YZCR_3^@@d zkZ8iK+dyX=5#GOlP#wSNxX%tsykJvsFTfjG&(O9lUdfAq`J>Kd&L_?XLv}0G_y5V}+`z(eD8~ttfTZd(7rvzS zj!-`wcD~}=>i2|aScXfg8H5marAE@H?f3lr{OTr{P|P!4{iS=O1kCL!?xOM57-wenU#C7auWMQgDqv^RFp)1EGg z`6N8f9v6+ZX;c;lc#Rb8{2cY0#aV|8hipa*mJ7iFP5FsX(49j8J=C| z^!+mqt40t#nM`YT>m{x?P8E=)YiLYbEbc0$$Xp2JNN)B)XD-b+Z0U0!Y?H_9Y>9k{ zSUvU8Cvs|+d>k?W$?P!2*%?(H6JsVnZgUzI-CixDl0{Axcf=9(kX>%Ce|RW<&er3(`yQ%0Wu|v&l7BigG@ZriP?dQ()m*85~47|GSZ8~!yA7bqpN!y zKXL$?`m9!uG8`)PUlLduhpdjt`&-N2lqXk)!T(`JI33wSb-d6o`pbArwn?VKR&2id zhFoxUE8`1_n^KLe5xKMueM61|zHj)y;o0fEp?o+{J3B}Y{xq^3 zrY@k0+7xV_cU=tA?UY;klk2xsj>xK^a**$R`%ml|7Xxa8FZ>1b=6lllj2u6{NDy&= z6zlGHc_D1~#Vx8)YDJc&T{a&pBKKGWgO(_I-T6ZssH4t8G@(G4e*?-&``9GSP`5y^ zN4|;^;d-2O+qJ+(`V;#z7}nMtC%f2~XO0|fv^Ja1%rmRAO&eIo8HRMi8Dvw7dfQN; z0!b$Hi=V}sHQrenvc1w&**h&S%*n|~2vHFX@@*JO(Ck8#-H>hd>2zF|{|MMQ9W>#e z?_(X~zLvIjvV#TQKjhRCLA{ib!gu&9AH#jV&Y;+5&kH4KX*1mo8^Q>9CAa1o$DP)?zMV@N;{%qwya*9|bd+e>WPbb0w!0ZKso z1B0WNz`iR9?e#u~wE07`biN?@-;omE(>nj3blF7d-xUolI;=C-X{5dZG?&JM8_Xa* zpm^SzW+^=|+-KZ0yw>B&+xUBO_O?TygDq(_-y7-x*Iq zMKxl5=%$QQzAPbH1x-edFwxxaebY-2C>oaS61D9(Z(?I&+i)i6O%9|TYt-?jADHZ! ze;6E|9dY`2A#(TBSB`&p73A?(hbXmE8`YKh9&-ftMJuLwl`M*DZu~)1$t_MRI!ZwWjsbxQ`3Y* zYO#<{?fs>)XBaIZ6@ExB0HnUATdTF1z0DMn!l%b}1iXLM*_>}P=HGO2<`s&O&)d>@U! zZRnuMIW50fM|_-IoN?=&bn83bAj5FpTf&9l)|(^I`5h$Lq4`ekQ8vdCVuq)YpxEck zOH`4NS6Cyx1>wz`A*Fmv(nILoamclk%vg)eDcR+B?7+mT-sEuuC~%XElWNeStf!QT zaI}io<|a~Dfr$KjM;A=3w=C6D|9KVdo}PPccS|gaKLUmrUX7CTJAMObf|%-BBQcf# z1t@)MX;Hv*K`lhR*|(J8vh+^pQy~0UU?^Z@;_h#|{i#u_*hhGJbZn@|6D73-+3?84 z2%*ckHZ81m>q=9P_(Kcc{bOY!yVZh!?@r0i@s`cj(B36f_DXgj@wc@~4ONoPux%fQ zwSGT?_?@&g`r>V3VJRsV(U~&wt~#qXo!feC5fxvJmc!DcLPOn6b{=}*Ge99=BoUxR z%Y~ayp#(5BB}xzRp>M@W<2&3Z_vF&fBdCW5ev{d}`9%Yi5$0IuG+hhJ#}2?RXI#od z^;Q@tY^QSQ)z|{3Qy<*7t2mP$e9S0A?vdf&D$h_AR*$vcnC|x{fYFmL%dy zRi_(JVuoIcgGd4SKV3`_D^5PnEnIiFhEVf0%0Gq{^sh@tFuFjA!u{gx2NFM|SIO_(mf_=GL^4G30Ak}^ES`VqLcXW`{v}}c=%LnUz={nqJF~xAT z!Wvg`z{v*3?!Bb+Bv8aS*QJzeLqH9NtCr*B3#ij3P4bTu(kq~7_wzV{4wBO6VRDfz z$EGv_h9koLLLz5=yji9Q_u%o1^pzNWREYsrL36K{m~dyo!m(n_1*$TSp_22(PV>Rt z$(f2!YCFRbVKd)r5hWF1o6hd!8d-}SXQ z65bPG+Vy2No z1?JiXU1)_yolBhPZQ}|yov5m9#@JjQiG*E8_Oywe*lsxl64ybq)%Hm?1Lah%5!Pi3yoHfDyiDsUStvn z^Nyv!ONH8j3OqIu+b5XdI^LbcMX!OPu^V(u(cfrxYNa-3#lj_{U}pA(>0kg73W2U* zYY3(db5suQBZgj9cz1(_1W?MY%@tO3?eG23DwuUQsYQdbL;DJ!Phw9y>|KGf8zw&gM7QdMw2Z2 z!JpYoOH29S!*{Sv06l(@@7l+>&geMFEI6X#unT8V!+_F$sF z0g#B~l$X)H4G)5)&4fe(>ED8pvbWU3`WfFWpEhg3+72KC5ex`26VTwvjvvH$)a{Ku#_x7K!@`JRURMVwT}nX;12u_*g9CPk zRyL&uXhGBJV`E|ruS%bV8X!z&s(ot#75VJ-Rg%x#jpSG{2tfUFzxiR5%hk6Kas#F&vQdxe`}La>)_7RD zi-6eV7$jC~LlM~N}NJ_UIiFtW-cDi>2$y3;jWiGicAN%CoH)McPtFUNE6EuIV zcl-BWswya!{8wj@-{h?8P|Ja2aqa?Av_xpiuOW8)xeCL};81BbLzFj*?z)XGX5=Ck1G9ThGvNUYg#2Ycu{(UJw%rA?jK9G93L{NNlA4jjrY4j$5Lmhu*%{hc7R>nbv62L zEe4tG3C~01kp9RZaovh^3AnHS=%=;FK>570r5E&OoY~UtPSMM4HdBhjSouTnaB0Uf~{xA#?a3# z_U%5>^8yWQmp8TlPTsuF16IRx*=#m_ZMN^V;tHm8|Z#bvgben8o z(s$qI9OfU~;vex!uMgu$HgIy9TQ#fdH5rE_D82sp)kWGjK>8;Pq{rzic-OAkj(;G4 z;XW)F_vJ>EFCvvdKnvD9fKI;c#>;87F<@bg2kxoO#r}<-iyxu z`Ji4N&c3eis#ef66ppronoU*XH$YWb-WpJ*LNE4YPcDB?s_c}OVd>TYIFa;B73lO? zY^YTB7_E!nHL4*|u1~PCSf9uzCxn@L7zQCLg|X`Xrb-^0WgrINkA@MK?o|19`$OQq z%Kt=zn+#XYMI7)*=vRkP4tS}r(VTZQ&Op?;uK$a*Xh82zVD;&8#Ko!yFDf#yzzb_k zOmP8F2B)PaTx1LautT$39yF{sRy#`ZEw?rrw|JrX<@9}%aauT%yE@g97`E)`LbHhj zKg>2H5WpCGM3!lzBDxS~sdSgy5ifDF&2()-8>zhQA4JVgFA4kr{s#$YUV({DdezcC zdqknz!!-& z0OAr*d3p@|7Z5mp+S&zL_)2I@+=W5fA^>_BG(=9Vn=oRjCH})}Za8|~0O>8DFjzg% ze3aCs^Cew#$RFwvSMb6f=B406TM*k|(PT&BJ;k{5KE?cgOSW=yHvmhgV&9^L-D%p9 zT77HOmDDxg?5J{AAPrh&fM+U4vmOT+R)n5EKaT;!oQW_QQdm^f&l$~oPX?z=qWy_E z-P)@8a~|=2k#rLmGky`t_MgzlnQ)V$q-JjEMIo=Y!Df~fF#G=Xpiu`7f;Nb^4N0FM z2n2=N7ZNT%(Cfn(c(fZWpQ3ePp?9kRrjVe($RNZ;n6Y$CIo^*Ln!t{G7YQ9XfDI#r zMf+kHhslf=$H)&dwK98l7*dS`27#7HzCrg3(~rKh7)lHXE0D24FB#Gm;G5*n)wVR~ z67ZOjVcjXBB8=CG|I9~;LbMh1Lr(V-JXzLX%#XTbwzXP@&ik{?d#Z8_8Nt9k<8V-6 z9i&`p%ZyQRa?AshWvEFYDh9ObI`B%N$?Xc0NgOR8aYOHYO2=oA`qqk`oscVEJd-6M zcLUOpku4xD-t`X+wU*ZeA?iYN%IR%_KYJ@1&WQ7z_nh*FiFZ(Eu|OZdWcoKayVpob zy;4(Cw{}5jy%HwEg(W2NpNTb6(_)QAT3-tSb_)}iv5oE*fIQq}GdvA#NvDvM8s1*K z!-+h+G2~r7FE{<1D&J)XmDV2$8Am*&(7?%Nbbt+zgT`7gI=l6#-_H~o6p4M!K_wCS z1UeUKX!MYHvY(tG-q*%uU>9e+#7m&-WN!`7rcU^vT;vz38NrUuf6<=G1i`1}$_Xu0 z(zuqVe@1KOqIpTcGnSH(*FdMKLVLP$j&FidRJ7O16{wv5TIn~#KycVhIg_)upkLr9 zirgnB40oxdw8nEA{EuZj ztXt4$`Q)R|nbiq8X-*>iCp8}Zxo_m6f>lEv3kp*C!)f<9BebW##s*-EXY?Zziv`OG zr^C2Ekw6Lou0HOc6(7tj@A*J={qv-|Hjgv7s9Kf5D_aLdDRR~&lXjBfysbR)=-1JbIAe<|L}n;Zv&>K85~A45W4T38vLuq*6!a z<>LG6RTQ&*%|ncls0X0{BKi7-*1_nBCd9MQNEatcO^!p(7s4<*!PMK8ZFfdTob=q! zO&ir{iJXz}FJ1+zIeO?!NQHm6%wB|q8VSpD1eBX_!)!EuK7>9?Tces=FsIQd7Rtgg zNYqHcm}Ln0tnvXf%p-I)rQQaz%bMD#=#rDk(nBeXWqf6tWd&?kHy5(R7q^ez-23Cy zuHS!gsiw~3I%A7FJNf#XBB5pd!+!yyZ!cEihCq-G0|0t3>DLapQCdBWPBg={e+z2~ z5lODmAj+L)I!Xqo+=2Fjs45riG6ZMVlwrC&&NGW#7z;hgI(%~R=623Q`%3ww#~jAT z_~a{fs2zY*^UC=xcR7(EocWj@lqO_k>6vu$w73sD?tDmADhb_E1~QgLfDI!z6tKY; zAF`v42Ks7PU6v?Z?r<{>@?t;|dJ8&WDo)HO2Ft)Z2T#5V|57CPAVW5m1p^0XTP))G z*`MRF)bm>{gpeeQ!61wI7tnP2kB9HO);82BvIl_3A${55!;W(!Cm^GZmo|IMN%)~% z`iYtK#`yg0On+#i2PzCI@uT+|qzrZsX(NE3n}o;AL%y!<)`BfP z3~-&gWr=!>K(qj|LI=5CekN`3yTpIP586!uuYsJWrnZ(Df&=s#Od8{nk+lI2l+jh` zdxg_u&?_1Vv`HmIB_IN=tfF%28?PM;@IRF%(u1WJ0-k{`<@FUXI=_o#5K|$c)&F(_ zTyRC226~sl^#Gjsqu>NxAHC2+t20=;^SoP)=cG7*v9ZtBE#0F2U!hpq?9$SbMMTb{ z;f1I5HPZ@-&ZM~LENFZ-Wua$& z!3hUA8jaABFMZOVu}QWuA^gpHYC>V}@-+Wi_Md6ebw?PWi$21Nox5@WB9xKVk3w-F z)wwlRWQOuvh9q|t?4Khi-M7mE$5p$+IxH-#{l%IbG|74}F7l?s_#UJ02RzZdLN+a<$tkMI|L*L%m2f)jbg+Jjh|3f@D+^X;JWW~ zG7AffSP%<>(GXAQ-&F7cPHa;D#-~V<%kNmYWeX$F3>wGi>)qz}e*#tS1st66)GU}^ zQ!X;E-REXy-FR@6MUL^Wgn`dSLXRMaf!OCt?p5-1|{bT*7X~Z_A#g$T!X$Oh;gSBs*?-vLgcOpwyV`7TH!%mD+^Exs4!EpN>|8G{Ao3Qym-Wk%W!PY#}LM$j_Q7i z820Y&D++)00zhjxGSQ)6bH}j1^pwdLr?;k z)$)zkFP+C44_+K}R%jr#KNw1C-`tpteKpBxLG^jxiqDi*^$T>>;5o5)gTsN??^lU~ zL%!1v4H!71wR=$Mg%`9j+djMBpF}~w zgH)aVX^tynX<6yGgGDB}_rt|JZe%(^`osrvt<>V<6Mu4t=-^O`*?x`}i_TlmB>`QZE zbHbfLD?62y4f~3^jU)ajPe~VE+iTn{xhVDfNyh24>n_WBdFM$edM`3)8yIL7pA7sU zkWI)fDb=?MC`qi)XsPLD+H~>Fu!6W^S(-+CsD7g^F`J(9$%%^^ z3y~MCRE?DM6Lk2)jWO{&eOvT;pjKDZ<>0s^O=b@+ zQtH@PlYA$RNY(v5q2dyVbwk|-2v<{ta#0Y?O)c>P-t z1^D`a^ZnP?+fROTw$*&PiimprX0`|JVB~W|{Q{;vXn>#}2I@lj9P&Y*B?S>h0x~nd zI5=b}K<7>?75530><}6oJs?5_f*R=;FTMidydHM}lm!q%LWHpjVZ%#ENC8GOL=zH- zZ`n2LutDle9O9+UOH8HTL3RO|#0PjA1>8NBaDPhp<$+2G;eoQAGh|V=UaXHUP%PlBQ z+|yITeBxWF{!2IYt~T395~bxty2Lv4@!VURvaXHv3Kr6{kuF0{(3z92SG}2u&BLwF zz_NRIR@}1TAW-or+Zw&`*BL@SHbk92Kl`1ir*w0W{G)i(3BR?7-;{KZbf?$%6>*_h ziImT3q^}f)^d}alrf!WE_vC7B2>HdKd%LX0)C1s6rC7H%dGHaNgdX&`o5-Rj<$ehJ zX=t?TDbC$*4SArbRB)0OtywiC-70-%A9MP!`p)Po5Jwjj*yLwHj;ROX7!bJn0rztT zFm;LgA;MU|quSWo+DZ~XLw7<&0*Yr;;s^zoNO4E}4)D3-DwtIRoUUbHfJivZ!FPz5 zHQoblCA<05b09zy18Om8NNN#cYp9q5_gpFrZ-gHASNFj_!_W@HY zLW9;nFpwe^!U6=r!~&jx1NH#NDf~2uq5*qv3E*$I7I?2oyJJ0g|N7|xV4CL zvT0*xPMu-bQ+C03X%d#!agGUD`BI9u7Zvp^;m`E6VD8GRRR3BM)=X;g_l^20aFd+( z9B!&ns^Gr;pya^Tq={c#;J8wEhPd=ArO`R%!ikO6{ewQ52u>?O&!7xxr4zw^6vtX> zEZ+f+{z|zUOT%}&OLEE5nOR`2F6#FxH_Hhw>^3SK+0A|9uHcmLu3gk-BeI&-_W-;o z*LV_Vi}d;(;f0~-547^MiQTydMF-cuGFdM6y#%HxL>>d-{vc8VK4w7C5r$FV>kQ4R zDrKOqs`LY~AH8C0OG_CMktR}$&aSQ;Cu@+eOixdzXasZ7%{=gi4JK3{fiVEC4usg^ zi(o7^rW;6!+(G1Azfu5!sg$7RHu(wAGX{X{rQXU#eA+ia$n6(bQ}Yxd=mJhPX;2|^ zib{U&d&eHSB@kwPM9vdJnuWfs$tM;5F2#IE=lfpY0R~VqFXgP8tmPIpH(bSU3hZp> z2}=GHAKw-({!5p z4R$L_r}9E)ibQH_>nh3k$&@{kUmD7O&N_M|#;C2-P&p#U?nIj`U+|!4bF(4S7}sM2 zTODMTGJz^Fl!6B2pa5A!^uZ1f54%D2Fz}w)Nn6;tR}vrGC>yJ)CL(tLucQzL0_mm) z#@gC}OF5P5g_`O9{{A4SB|iKX#33>u-X7`@-nWMA2a!I4tQ~JOQ+5tVU-May_8wxd&BUgher4sWRwGx)nUto{$5`Dqt;|EVGHfQG=B+E(o z()qr_$%>Kjr7Aw3mZrsB?75kR7`1Y>46ujh7FI!lvR8#Uj@kC+tHT2QLaCsq=;&yW zo0P{{u}z{@rs^nJ+0p(w1+W$~t0*ZbAgW^sQ#5!lql;Me_4Nio_>(a9BO)TAsHv5m z>n2WlFpTFm!f{0O0f3$u__z;+Mq+`S6A}B_ULK|cED_QfNeF3rftY-yC;`at5MxkJ z?vlpuv_FTaEdWJ1BDy*+*!$Y8@;jne;ujDSV%J^<5*M_>h%^>Z@5TycoEV?~44Q2IMsZ)SqQ>~KW84e0NI^tHLJP7t^z5bA5!Q)jZR zn97~hL`6lnmIi?!Jy|-@Wlzqyh>yZCXOR6XIbycP4GQ>u%WRHz!-n`w4#jXn_I>wp zE_T$4%4^+|QzP(W;H5ayfJhg<292_3P}+(1TbkAl%P>)Cn%YN?)Qv za)SP-Q?55@#g-^G;)9$s-BaXGgcXUijYC`bVF=)0T`oCKs^o_(p zz~swpVRfQv{NC+k9zcFS3L&BTdKhxySA`>p5FHvC8j|UYo114d0;{ecWzLBh!qN#7 z_*%NUi9o9eK}=I)<9y@+6r>)32q#c*_G|XKFwN}ptCOt>RmU6XPATU57Kt{#guqRI zYWJ1LKm~vO&av}O=EXUd$H5vlwEIufU`3L(K2Mm_c8SeOI?@r$YWU=>SS+c^E)k6o z38eT?%mm3y`J}B-; zKX#mWcFvkKd3{<$PpJlLAZO(}5gR%^I@7YN>$3h_kFWGrBi3!iSLj9U`U^alqnuVu zMr-on{?AH(Uqxgtp)Rhu%mUwXrPe?MRe_10g8O_}V0`=sLEI(Tr0U9hgvRb}l*y2< zdieMt0(+n(sZJCHhYVp+11jWsxNvMjAwIRruVr9yUGQ;&DCFhkLFC z?>fYH)N>HXVa2}|6C+uJ>?F7KVQz6L+j8Uv#k~JC@#YKnhKSwd^zC zHPL8nv>lXtnp6zNGWEKI8PNkDe5QDKtm+XpNS#|o@Y$#iACxn~U)!@Gj8StDaBM$< z)~TtPg*zPb1hV(!MKZTn?g)E`K9`v#s0WcIOR|jJ_m^-FBf}#|#2jSPl^;3TV=gp| za+D|H5@dF`mOrvLnc;*c0znAh^eSUkv6e6V?e@4Toj-rZ_6`tEM}vk#2*1tJfGo@b zcR`6g7x>Y-5QIG%3}+bk%nYSvij$>a>`QR}`=ITP$Q)2xx3zvX1q$Bt7w(4bzNcVN z%~u6PWZ&HG8_D7l+i|N6rLfE4Ucn5bLOf07lg%{;|J(^rhJhdRJlfjYdhO+IM9jts z#Wwb;;BM`sN1yUdPsBB_TgcXylwx{?r$BEBj3&y$Rax+Hb1~Qs=-3}m9Nhv3-OZj9 zPvt$IC~U&?qGg^l!c$bHdd5kn#J!*@SX_GIgXqNtF2QAEM*AX3p`xPVHxQK%SwvFQ zMhgEL>>`-AG%U5Z-0q#wMqBO9T`<}=kF>WoO5Yw9qGMPZ;2W)N{FcM*xLG)F0aPJ% zhoj0)S=+?omq@8ZMK9&8@QXl--|6wpUp7-hw#TSbRG5Hi)T)9`-Y?LUU--hTf9}c8 zmkbKmGd!k6Q^SRX%Z?QVgryqLnGIRrj?Pph#(ByIz!TRl5NsYTtL062w(9pLBvZ`8 z;njbUS*&MF*`Cd9dVk@q!(WY0WgIV7qb2*@(2Nb0{Lxl&bI;(uQ!*+DO1m@)h zHrWNp^y{Azkq}01GAbiH83ypL<2gu7^NPGVc^jLczEHwimtvA>?HBj!vs-t*?A_my z^?drP6$W|50jsX#W`29o{AWhF+Gt}^e*4FFRh=FI3Vc$*WBO(L=s373z`CpS2Qx$n z<~9x@vI|zIcu{%qgVNj9&twba@LN@WtY>VrMe;C#NV-)=0@G~c8#evceIpOa!dnJ( zbmAmklI;&NQoppcd~~MvA5r~|A7z5d6i5pYx@mK?K6rbn-{wUJ)`KJn+wC(#GJ_oR z9}QYBz6ATftRR3cn-1L?SvS;74HEOE=*g58_v`*q?vs}0!TTd9Y}ntVV7{67(Q@p| z#Ox1W_?imo82Q(tqwxNAO)xC4V5;kYm_rcA&m;h&A|%csb&!34tPm5T7BrY|)Z7z# z9&47o;N$ywiZ5aM`!as8v=qsfH1fio$7h7JO>|S?M*7BEoauSfr{Y4&6=|eWz0aS3 zuDL`Yn{55nBTzqO%;ttp*eqQ3!I%er6!6b_xddHgp2s|OGzive@$ecK-Ww{v` zt>7e^-22K8P7=+aDsE_R_Ny~Dy*Q37mbN;MWO9@8^m&I>RP(qO3Ix2rE!Yy0GagGp zl8S!ZNh#c!HP_`AcaeWd8@#fhkXmgmH7);ml}fJ<*?_27efi6wER)S%CN(5?JQ`Kn zAm2Y6{J<2JQK$)NN_X$T#xM(4-=O|XNtCeP;*y7sgJff4a*9%FlV-$U%09^Ay->J% z1@o(J0Hex)j4e5{y=UrD9dOOfr}_ZvPS6YR0S1JE>$o?xVG-f4v*3&J98(?5gvXybUs=;l_`vcQ@STV>cB0hC}M@ zJhzi=GpoDMpO=S;6BPqf#9Z7XbDa*`E1BZ6D|^QMDkb2^+}t9RJ$ah*3UiPoY%O+w zu5YbNM7(VK7yHoE;?%9)i8eU#FORr;*lZulAFa)f`o)2ho`<}yYbf?5wQN^n=iR8k z$K)@*{a#f7PsEuj4zmpnjV&9#czMYJpD@f~av`dcH$cU#2x<%>Vuv;^??#j(+&~>V z7IfTrQ3HUWAd<~boDjYi-SZ@~zeBW_vQw%9o2Zs$AZD8XE*O`1TI?OpXD`#&;RNMa#fcm|CtI}tmQ$eo_I2ug`_FhiV`9~n)!Qna$3xQjs{@<;fOEz^_f zRuIrc?{)MN&rZ`${|I0r4p`$Zw69_ck$}kKOC2%I{3YRFN@2S4zBKUY`@ix6>0c@b zXAbQ?`zWy#igtTua2?*0z|!6ma(dNRLCIeGxw60C^M?-!AM#73$j|`e$+H~Yb`&U* zXc3+4%|>kjhMH>FpPd5QTY85eOM+WgPy~tcCS(o^%}fBu` zMv`ioJxIevT_+_?2S^RlVofBKi;0Q3e&dD?w*}8cafhlaMYER^w3hOFp_FW7(gih(Dh z^2Rb*QsO;Z@oNiRtS?L?)#8q@yq_2MJnDlS@m#KcT4&&3LtT%gb^fzyNbOENp%&=g zgJkD#DYO=PT5hx`32oo557fRYSmT?k@fmGNP_5WaQDoDl>z5>C*UL?g>omhBMH$$9VxX#)Ho0g5dc^~rCAh_u_1RkL=5=kgXnie%9--`SM zkboXc9{Ev-vi6?F;9Aguy#!D>9opa&ft(DLSuwINy#vxCGN8{42#zJxYCv5zgn`77 zgWP#H8k(&#bR+Eq5$zY)Rx3~x1ubzJF0NM)#J+?pIJK}q3ju?$6gWbNf8&8?2$7A1 zdjqgJ3YvgGyb*XPKszA2pg zCs8Q5louHR|BtvokE=QV-v{7>Awp)5wX7#?BwD4#UMJc&S`n>OTBOoKVa8StmG;u! zQPHBkXs9Ud3);15r+rb~*UNlnrp)(u-+$gcKJyu)bKdJ~c|Nb_^}MdmMAJ(h4t>o^ zdG*#8_4REUoBFa+2_Cv0=mbEDsrD(AJxu$nhN6cGZ0g(tE-H@RNfRvQ7S!X!|Kx2&8#`ZCqvcL7(qFxYdzN~k0H*JxyW z)VHsik42WlI-Ph`5d~ta5(!QwCMI}&D8O7s&BVmyMKbX|RE?&?%ue+=)V)UQ>gvv! zoA0IpFnJ7fi>r-dJ-xj`@QlISDNrq}Z>RvL67}{Ftjd0QX2l}jV8h^~rl^P5b!EJH zs8NxB+?A?BScs%I>ZgAjSuH!3G)T=ZB5z70|pA3wB`_ zJS$1RGDetVjU8mfNG&ApFCzsWSo|TnbPh|j`~*Ls)f+a%!2HiQnWZ(`M2&|;;_ZC1 zBP|L)@BCZ}b22>bj8OE$R#K$0wb_|eM532kVi_PFxCEOp%KWDP9kvU^?u<$dWDt_u z!w-XlhH!nfzSc;v3*!kkZY9D_a1bI!A3!XLIad-4`;bY?(0A_I<@xxrk(fDLWyGx7 z?-D1Mt%q+K0UX#j`qdo;N;#Zg6|BShIc{Z?)v{jgd=yI2t}R<$u3p&y&!SU@!bM!O zZT0!r5is<<|2`Ew2B30^Afi?=GP9c+YcXJ^h7JFg$QNOebB z)ouyfYEWH<||M zVHnorQm{t=-@^cp@%Q)d%do|~2X(r&smUVW2M`jEUUnu~L|acsLDq`URYZu&cp-8vxpuM^^my%tReVE2-b%fXMr>nNpAXonx7A)lc< z%q*piIX#Vyh0Re_MnlGWKrt{?tVq6h?;gRqQd7di+t{0%{0!Bj6kiQ}tg5QA?*07v zY`XBeWw@yC3rg&i7cEUfjm290M)K$1*kT=N+QfWZMpQzgzK%nD@6R7FCm)bxJ}mPA4>Sk%*^YZx zbu9gx3($eW&{4R@DD$aVJ403n6wL4DwwcPydmOW&o$t@pEEuKR zn@`WV%v2_F1HTT)%3Q%_VGjkoO8EC8RQr-#qgw$ym3ZE&#c6+3)q(-*4tP`Hp`wUI zuvl3d?E#ft)Z##i#Hc@s*&qaZR17;F=&HdGVy>R#zn*;hq%$!r9KCzl#fQ7-66J2ypV3(y2UFluB{S* z{MY77=|NEEiH$BcAK{7yEDtK!kkm3RUnHM{NfiXH4eW}YxBc{tR+N<2Euxg|$6$H}oG69p$?mJ0qhDL`Rcge#Ub18<2gwqvD(;=iY#%XR(#FA*C(@}rntS7{u- zmA6t8{^*ty(#|LUADEw4m)gilRr|-;3h-{N~fGi`=^cY`VNV(^saH)P?_j zk3ascz=qdWa0YA9CVX&>gQ3Eq+}R)C5(H}da@7Huu~&4wIwsAIeSnmF1Z3 z8RjMZ959gi_s8Y>Ef;=D>-td}X=hlLtiqR%%KW<{;Q$MNBX@W7 z#vbYk(RFt0ThIJ^Pzq(WAXf4%-mDQG{J3>&26L|4<5%ok0$j_sp2>YxU_Q4BhxQ-L zRs&^^!{mzeetIYe$@*RtwdJPU5~j6m03F)-n&ZGZ2r{}j_Z%LLr+hO|R_ZZZv7}>E zaaVH_@~6h7wdE+)s~_)JcY4piU!zb?%O3*(Oi679wFSQOX@z!FH`hUTLLmsflQS{2tn&75 zVZ)3RgL>gA(}5?XzS9 z;KcAp7=Edulq+OZt#!vcR%3Wkee0=2*EPFzynFrM+3bId`pTdA#fta{t-K`XdvruB zQvv=iOV3WFW7Te1Ivypf*zk6>xLO(td|#X$R*09`{xbXWTgfKAWyM=}^76)|(cCL} zYEwv{juyjI@p!E6U^1UhHV;kgQrF$|kxlV=C6(2s*XoDg4lJfAqf(mg>mRe+FZ&@? zM^3&BD#~gA6$o19n{nDR;+GX=`TcU`w|BoV`ZJ>ck&kCIC=)yv`jiQS<-K2Zjmw#b zp1$_*3p^HW+W&2X*s}~TzGY?3DN|8G-6>t#+D|qJ_lJHG%4E&H;Jb3Swz6UgoPu{} ze~JOXT@yU5Go+okXu|rao1N~K*|+4OaFUFb1D65%4%&b5!uw2UnbbqP&oi?|fmcA_ zIDPeVyGVN*(@;|J0ntEkch51d(egIN2H_ni=tIixFPQZ*)Ck?D@#>@$3Kda+T-nDb z-TsThn^X;~r`;B`K2p6SRB}O?u5%_w%oL+x9ib5NN!Xl|<#JcO>pDPrx#S0E>dbC*#v$ojT zEA~@Vp$1*;eF_7(%0p`G*wsX8XKnvTOtc<*g*K=}Cu(%WBqJSjX2Ma{pD_6|rEB!; zex#QF2h`mw?!LE=PDP=grmXo%h*bKMuk!e`LudHFj9qjVwBWeft^M!b&)&P55zjL$ z28&E&^7VP_*@KM;dSTPT0(xsra7b9 z=VMVQ*VuFmY_0V{cx_v$lqGw#cXDcXMF$yB?L$aN_7wmD>;3Tk0hJW&*hSZIn8zo? zbgIxIOJ4g_vyD%lW6At&_N4|lJ#ocCdB*PfLw+nK#*VEl1Ux^TBX9EUX}qR*H$!7$ z5rN|LE{>_si{03bM{G1%oFyw76c)}aY@pScVHkYVJzy(2j|wA!+G$itLlHv_)ftn< zerMxCkqplVK6Z^_pI(@Rk#V-!X+j5+(xmivZs>;I_8qG#3v^^kwrxDP{#q+%Z%!SZ7I`N zT)^S5S#A=|s&_%XUQ?}grmn)|H&K~5R?T#${`I4|vQi&Cy49 zT6FSt?(A>0S%J!HcX-eR6ALG4j8 z&+Mu1^39;O2K#Mo2K~txXb|Cqr5(@BIoI79eK;eRpcIDPt;T&TXP?DI`q+VWF0>aO z7E|96Y&odf9>4tZ3l~4XhlRzwze3*T2go*nTKZ(n4g)E{F9paRXm^L3nrWLJ_qt!e zcHis+W3B!Tufu`~A#9)<@?9(MIlo@bjIf}N6;a^6T~zApxey%X#&t5@JoaB;mq2{| zK|fZ)p-JCPXlBbT-Z`N;u#Nfi&IEgKX|&wP*PTp#b(WH5_K!#$MRZul z2+f6a4up^haqbV-0lXH9i}r79Y!`ldT!*eeNwdm zR}(n7h`wJ1rX_nZv$lw!{Jit<^N9)LlFc9U zI`~dTgGb~O9h2Ge@)Z~BC1az$y_>Yqx;(S#tNZBC*Zw8@S`0+55EOXHk}`0zS=X(L z!kMhRXXm7p)h98SXsz?+ZRx1r#FG7Sk0>nMJmxG}U)?Zor<*|-xVKJ3r7v(grXkpG zn{dBi{UkzRlku!ACBE#d#m|R;nDFSqgXl%??YqHy0w-1k(Ue_&pkYaX#%OkYA@T&0 z6Vx=WI8M*Y`WB}~05XfezZAG)Z*{y!2IJq-19ITo#*?lBkJ z`a0vw;!99C^Q9VJe0^DH#kwf8Z^Vr9ns*_R9+l9eS3j+du$R+SMVgcReNaxJCu=^J zcHbJ7>_2|GrE}z#&U{N42D`^i*m0kNM8*sC$-Yoeugh|z(hZtnSDPezvPHuTD2d%d z=i68Qm#Fk{r10H4WqixJIhP~$q~NLHE>*Sq9a)d8za@}jW|e7nb%NRh%_QppQXJns zY{d9~aHEb(YQ;ZGJ1%raRS*1%h_5zULg%Yja`-DKN9yVejkU#1n7lbL<NxWWSZ60U^v6lkRg&!KN$66B z8*9{#8AM2)OgaXU!_sHh>N#j3tC)-@{1t>Q6**IB{l)7T*;$`D@z$rlR`k=U`DaID zVNC8Y!mc`WZ8<7>j{;5G85}_uOJA@%RHhwXDz|)nOtZeB486T1FqqD9+t0+#Jntl7 zvm%!Ej9n89_JHH}PR)DkzF)tYSuRIkE#Ay}Lw-^Qgftqx_zh>wy_}$J*^fh^kA_$~ z-XYMrbAT14rZBrR`c#@XpI+6oJ!jGepUhh=vtkuXcISx4*FTH?39-o3`)cuJ%iTk7 z8j98F*25;UFTOo`r0QwmEK47n_~OK)S*a-$*vA*I$v#}WS^At-<*{4tn+WxbuwQPT z9jEsCn*EIV8}y zKKcyWDHIc;7rFcH2raW{9L$+Ai}#(V+SA*nHCmlb`5Lu}gYD|7fVr>JU;4nh8@ExO z2eGK!+?nmI@HF!Im{VDFDtG7S^Ha9fFUhguP{k&!NA$|{@E~LP*K4g>zZmVNiJ?vy@lB#}mkRh&RN7|80Z*v4v4SX} zckzSSr`_Ljk5&vn$Wf4`*JVjU>nhPM`pY!dV@WkM5#Y0?n7P?Gmv zy%W9QYgo!!qY3|To=#outn~UXU;L24quHhI+gBtu{*HHC-RO7p7!3E?IwU!E=wbqwm$PoPYT>0QzWna@-^uB(Z`?QErA~ON zxB0f@Zw%j(A^v-E>*_Tm(#MX|k7Z6=*ZhU0yl$MAgN9kE7@+B?7IKxlce>HZuBwZR zHV~^NXXE=7}d?s8&S2(zT!K$fa+8@Th!}V|Zn5M{;4)-x`lu?P2(yB|;2j$X(!@ zCOb7K1Pbjhcwco?{{F~BtEq3P(bb&M#k6SuXa)#oWu6sQv_0!G@>(X(o?-!Mhll7Y<~Ke_Qo@;OWU@ z;FHL;fX+=|AdsL7YZ*NfhhN!R3GF*tmangt{)pipaJ$~NuS-bJbY!w{xaEa{4SKeC z9`SO7epCOmAG^lUAkkgCjBNv4y@9d!(Y*ELQ*w7q>#32D5-IQ#gsZE0nx|(zWDKVA z6lS`&G%mPxbhV(0;%cP>O0Sk%a+l=^)_K=y32v0WC@8S2kWIx-TUoj6-JV@Fg1ZXY zYqfd4`OwNE>ee1uEpy=2J+&rr=P`>#XMg+cNb{gm)|EGoeciV8IS%{B^J@yF2YWlt z$prga&tw*K*h{sE+h!N{6`kg{2)OOi5o08~m+h)l`#-40IuQ_<|B#&ea5JTp7enR8n2)cdTB?aWNaQx3Zhcke4q4C&TE14(p(4u_qH zO^&{(C?{TMnn;u%FPr~V+AY63T~)E3kt;O3Kj8qjVop=`#S3q`ezVX5ftysJIF5U@ zPF#gZ7J>g!9WIO#5bu!{c+0QEotDh)ns|dH?FvQcVo$_xC8K=Z!ip@A zzorq!Vr-p0Jo15!Z@A|yeN-Fo&$GOli5w>x{0Q zezpOV6TM>)Ti1HFO)5vuop|1UFLUgcuzCH%{%p_g7YqYeKFu6s+vn|zZs&rU0i(o6 zo}Qjm#GA7C^SR;Eb)E@}nGCnj!OSUj?DKP9vVS+36n#T=t&>u4bQiMpbnG|tLtDIl3OBe+Vo7sYWQ1UZq-Ed%QLYj zvyN(A?olx?@G@YEdvyHaCQHlF8Z6k{92|0Z4JL!$&6-(Gx5#lmK3h84e>SA9K=&vd zWfJW9``)?!%2Ir6Zc%`1!~EPRMR|W_jmD6Ntuw0o9dlZOaK)9S-VI)F&(WzD8`7l} zY)u%>i{#1zH?tUL#+*NMGX2sSvTd>h#$}=!yO-B@cb0XjRi!F9?yHH5cCWcm<@UV9r0&Zrdf0lEW4xyu znBPC%t4SZ4c6ccCdAuRq;TjKWeTvn)>TLBKht*D{Sm>RfSxBi1VA{jxzsnE=hMvE> zKz^(OT&7RA+-2t2xlPtR#n_FE?QT4^gTPhI|lcxTMQER3M5WzZQc|f z^+oiQ>cw5If9jj`>_0%a9)G76AZDd2C%*AW>Y1e{%uU-|NOY`tooKAE7^Jf1Vd z-HW=f`f1!fMtgx#Qstm)hVfKHy-8EO^oUxIVDI|}CSP1$N(oA|QR7;k3dFiw9E?&STu>yOPRx#rE4^p#t7RB;qJ08AOt(K#_ zu^H&zYVvusU8m;F_ut|-iacy<2#&jZtWbLrf~+>&eF@q+CWWW@zZfVd^qAVJj+L>S zGMwSwX$95QVj7OB*8Xg!lG_jy4jMuiNtPz}(2+Ic&?NQ=wOh9iU+VAAlGIq?cMAhh#ctm}Q$f)l z9J0{h;Nbl$2!gWd5SIN)(1JOhU+3rH>i8tIw9e4fGw#S@md2qG6yZTbmpih~RUB1OLD$s(0Ki{|AZ=nQ-J&IJ^#XEPmOgZ()X2DBW z#1922YO4aIvQ<2JK0dP9Pf>jGPUXkbw7}1)lsVATqb2im4{W`A?6{nLn_XIZC11BX zCHRPEzJ-PG+*my73jwVZUp?yh@2?8*EBZ)}6qVC{yYzGN9tzUe@%PQGG+9^Lb3+m#6lKUnL_o?0&6k4>!fdvLPd{S=8YX`3ElS^2){4Zp z{%w6|O@n3>=FbMvp7+}MB}GN$IF+Sw`C)v@cW7i=cvu*>kdV&B>Al)mcfB?IURjzq zXaqg2zq~naoy$P*wwtbrKh>F!A`)SJSCYe4@nnUFQSSgHOQc(9A)hV-)7*NvOOE~f zDB8XsAOEH~)VxS2N>93?aYOjg&1R*kH66UCy9x&!GQ)KPz6Vk}&ssk%*86==2|Fu0 zW2ut2CHroPDX(KWV$z2vq+*+PujLi)yS*keM_2xS)o{w%$+yo9Y~&nU;?wTubXD{+ zYiFvf?6_arZZnu*>QBg)B?NPC5V3zorfXsdw+@FE!c+~d2e*dIddl=&i?7ZA<6*}K^ z*hgFS=P>NET>UEmum;Gva%aCT#k0wUwT3BjXeF4R~~aNWk6a~ zboWoQ2N7}Tp+9c_QI+ZPO*ZwJ0>mpCyeo)cBw65%E`Gf%*pmyk-GWbdBNy#DOfmf^03gPkd6 zZ(~nw9%dJ#VW;zqH26Ys1`9PBuV}!iOkp~pwzDxrI{EDInq-qcZrXr}0GCERwhh_i zIUJ+yj^|ZcF5%;yX$R|X*KKsUeb2(8?5EdncOLF-5ea7Vx#Aw`tx#H(BhnwUW7jU7 z=8n)|86l=BOZ%a~`$3+b;Zl4$URJ%n3m4hQ?Rc2G?TKnmU?SEf|?G}&yFONInz$H^`s^T9nfQad-m^j;Q2j~9bG#*|z*E_fV zX-huvW)Dy`~zSUo5+;-9vbU~~36Zh8bIi%bvC9sdRjUATS4<0_O z&?%9d8^gCPL6sxb9Y#rlQ1Z~pvQIwJ?uYI~D8?&qv+~Tl>ihz!=Tyt)nbI$1p~rjI zvA&;L%rx`y&hG3eZj}fbo+-n2Gq{6e*n!6UgTRK0NqId$(0Q2I-9%yBO@Z~U+ zf&E6=_gNl+Cc-1+oanm=#~)?r=v~-C>p~X|ShGvnMhzi_poVr;(At8*4(^6zq^ebNB%E$YFl^)G& zlg|%ny||!cU=U$Y5=gXoNKFIR0A}c0Tc_6DCepJ4_WwOM_hx^if*d+Bk5zgh8*Q zq(ozCSgp27Omef+LKt)RVKYz z4TT>F1U!W>)u|m1X{hzQ2&-kpfvcGO^3p!=SWl6T15&F2R~(dU^9ji24&^T0T>#mZ z&|~+M3%z!+u38m}atk*YfQN6fRDt+sGyUCeP#hPSZqa;QSw*EP{PSFvbE@` z&?V1V1OdSE35Z zHZju63+|*Ni1kQeOG`rzpWo}ld^MlBJA*oxDfjZv;*-_ODJw#|hQ&HxrUXSDO+0qK zp7~vAUwsC87`WsB1JNyG%{D-g0S&(eU`v3sPa5yRDj)iw?@ptCdU280)UU@?xOj^(oCNk}%0TJEK9TjY2s%Mk(|r^)1NbXlEg5MmFBk)yW1l0ivaX z2t5@P7t4c@TLsm{fq-~^G-qZL4ll&SctQ0=OJR&&PBzB=Q!rnR(Q(+$`j8_U+D`UE zWztm;?FGrbyTx6qn!G2oI#F6V+80lgq=$Ue$JMgeo4u&h_30B4>(ehh?KA|XXJh6spB zdDtINVmR5fU;Shw2P;>}`jy$GL0)MLC>QD$%3@ zxt5q`%Q0K>O&_&bzB+dGO-BNRvR@dg@LJb{~GVb<<{0Fo+|TT(LnrR~$y zlDhh{9H=7y`y>7oVMp%AP2WSsE^p-Ev~^y-QAnq3&!rzBGN5yJ!3&kvKzJDWE*h-P z`;_R`Jx3mkjZSBXJ5HG9)wQL=K4+)GycNI0o`6?O>=Po7A|!w(Pd*0Z7CGABpa5Cl zPw6(M*~3TKZQI_l65$VUhCij42NCrzI2^2r&MzuDG1&hxdmN%eRu?;L75s!uBbKh^YuyJSRoiR*Cocu1ecmz$U~{=eatE%? z&J@9ks(l;EXN(s*r=5I$Nh7IgYWPRk?JG31O32!_to(*~=T~QWhH#R2;xQ4BYQCKcU`$v(W6F}`8hBFC}jTl6qQJcC`? z=t;V9zmSNNNlHrIzJFgG8x7t&C%Q=F=;s7CczSqvm^+i4eNEC9u5dZ zGaib@CsC9qGAt~cCH)PvuXoK9<>yP&htTx-4o&b_+fC}zlQBP;vz*YRTLJlLR!jx( z%`euaDnYD)Hz{HpZ>PmG)v>H@u(?NiYe_#?H*!%;6wkA*R*x)Oa}?U_L-n?e+*Ilj zR7MPgq9HGt@ccRVS9ab$0S)cB#lx@9wj#W2Gf=+mwNS1swam4xOIa&$xelv+YpR{| zEly42iAPEsKEx*;W9es97;d)_?zv7d!YR%~!7o016HQqngo?H zpnIYa#0BiL<@kFJ8Xj#G_Qt2+z5@ezyr6&%UH0AKk&%ViXs}JH!RUcnuUln<1`R?M zAQ(56u5=DHOi=DGat8MCK;Tez?3Y&8n@f&9mR8FmB`CbH8JS zMJEE^#*G^hgm$HM1QrUggs)=pV$QL}n;sY)tx-{s2*;ayS6s}ead9NkYtSc63*|NJ zhstOYtqM8Jv`;qqB*eE3(PZ@l)W zMG(R!Hflu^LDY<;hS9AZ-9ScBES zV~P9@PR?!&Qqo3sZID%c#t_>$l)V+vrBKvynG>xw_%Dy-A=kL{lLOspv`Kd6xam@L3d0ga~*~27!^Fuhi}ycHUwEI5)exk{&T^2aL+T zf6BpJ68d>}l@t|Gmms^qquc0@G8rFeqf=Yh^X9j`>+5GRx!fc>HBb?0^07#zP{T32 zMA%us!Q9x#R;hPUT(Ou{-*v>-=LI!+cy^39y|~Nd5BY`2NW<(&TyIfM?Wa#5I96u{ z2o#fg2Aaw1fB~=49}bi0g02S`O zn22p@o10HZe|6Bvahlp+y%zJ+GcnOBJ8135ebx@242^vn=+c}`|8ZebYY?O=-amRI zFUnY7B>AgZ81E8|WXt-?c-^d;Eu&YCqe4_6&B#`^l#TIn9B6awT65s&<(%ACsSf)N z=R*JEd_Bv*I|sN(Ok|-v%rwR;BqUeWZ9|v7<22A;CqKg?5yQ$zY2DwDo4Wm@QtHKqMalR z*WdH<-D~XjH1;WaH|64)c%H>y~XGZMAv4 zJ*SL~E_!o*O^pW82pMis{ROv^y%9TIC7SsA()rM=8?fJ29jlIP2!rV-C|TSHh}!{f zG3zfL@TCcLaiOH8rbao>OcNmhi1s0~;3Q3b;+H{s8W7xsmQ1(E}Al?B1&(F z`M42*PJJINY4=44V_e{|mC&PdaxmnEYqkxO0EDC-@M}!BY`aZ0#vCC%!Z(+1#QfzP z{!(@rtvGZ-1Y;m(##ze22V=lWup7$+k?}lzx{1B}PKDu7WzFU$C{ZY*?}?}yRLAQD znKt6Xyy&e&AdeUgN)Sbr`};FP8=Yr#paxlv_9bSdWr*w$-v{d2cXA9GLb^=}YM-R} z3(+EAiR}tHBu#qnOZ?kO5@Xd~L^{O!uIS>aib(y5bw~xB*lF(98$6M#Mw9D=>;ghg ztQYs5=)B&x?Wb9U1T}aa3erH(MW{^QKjNs#Rvq%}685S@XcTuhjFiO$V!FO!HZkbO zi@-AtfcywH6;C|7?3rl?teQq&N<)B1PW(4n+TJ+ZUk=9~AMXs+#}21?j}Sv~n~+M-pX@k)DsAp6jFb*zgp5 zG^EvPFaq0|lr8G0PW{;2gk{##NEAjV%KsB1(6wu0MH}*`%zk9qnweLgw-*O6NSpz4 zq*A1XKV@VjKZc%BDT=OS)X*l-KQ~-{e0L{YMg4$U#F0=RlCE}LL}iFXh0w+DLRv~5 z5Biwb*y}o% zduf5=QC8wIh@&|nVCRTNNaSisJ5D?%u4GvwpG=uotf+yAK`4?zZp=A)(&<<8g^bIO zfN5AcJH;wuJOPc6v`>Sbvqv@b*zz~|`D;b_ zBC((pAe%$NL=VHTG6s6l-U3RZ5gMV}4Y${lTLgHse1o72vdA$l50EWQATQ9>n3F>q zijf&YgJAILcor|Z($&C{bebMFtcp@P47wxQuDPpivL5RF$1k)e1Fw8uLQ}?-P@~bRjYh*+2~`gV!WBFe{FTS6 z(~6kn95nCUdR|n-27dX#A=F61f^3$^IGUQ8(s%?<`sg`cn%GH$D^r|vUvhSo)XZ4E z6x#Xku3P~E#e!V0VFk?%rEKY{;j*lbNMoT(11}^W^ptz{vmx&p={Yqhz)Oo%2&zVF z{QC}g(a22U!tJ`C?RQq!=A?35N9Gp5XV8b3<^kTaPfQGy>x=D<3$u8^jeQ8YqcHp< zL&sX@l;2W!2dke6ibw69_vR$3llNTsdHi>$^m;}rI3sQAu6o-~$`ACU|I{uls{pWv z7$%SQKe3}iXQs~7QOOx<17u_D1;+Qfq!!{jggcD)s6tNy zQ8)nK>?jGU!TMf>gJ137>JsJUwq0)>kvwT9q;5dCzCjTj5)3?W45)9tY94Xs_{(;jn zePgOfYQAHhq(ACze@13ST2*|g(r(F6I3{);s^kcebkH?6?i}bBk@viCF=>)9T9sVb z($(Oc`T6rI>nVGB_J@J2X{({T>N8dmJZcGLevn*gjNdM+`~S!p%dm_knbnISfRTq8 zYYn0!jwxG?MxrwWdBDlhehs8}tZUb5G+jar6O0vxNQ+`Bla6!<(C=P)=lYCoQ+GFG zqllRi^d(5gGscdMMu)n;B=2o9Kalxj)71FmmK$i<(bz?d@k-LKjwe9+%d~+^N{NvV z`eb!PKsAKK?(u>qoE)Z?wrt)HkwGG1hI-geuoA-WK;Aw^YI^Ljh|mF zBL`JeFSQggInbP&;=!kKX~}8A_6L$*ZCo?)&oF;MEj@oh}B#B_vQ%kJC2p ziw?GZ1FRKtE6{^=$7!Bo@vR>m-X#Nslz`CKlcTYqhF_k`4Cf~lfeT}wO>M3`@yv5y z|HMouu(bS!%A>%*5I-8X<{H7HEyq@PF^*rf6Sh|S@}R}m-~8j}l&VN+=_#wS)dBl# z51#ogMI+q$=_#Fz9L*>4?3*8!8AZni?NW?4V{g@re}OEq6ZO@$kELpAF(0D;FkDb| zCCS&Wc#6qM3AJ!Zz5NPK?kSZ|jh(%zy;FNC#SQ9jQ*1gbk5x3VMbCV9NyOxmV{)C;G1-)94R6jjsbK=Bwg^H&m-%>>PBQgh}JHIBSPUdqcLzGaNR@i9@Ecm*|l?LYjbl5LJt%CH4#aMn#i`! zk(lSUmXVSV_ET|g?X!au(7$PH)KicceuSl&8(aNgHaz{*fEsxs_624eM&xoKmQ)T< zIsy&sT@a|O2CTJ|-CG5cN>thp{WxIpI@rTKp*I7mjn#mnp|fzDE;-h;V9>5OP(lkK zEW~bAFz_p}at+?D#Q=CTB}pQFvLPbn&zYGr2Z;aVrR^Iu(36N1GA=}2<%XMEK|@3K z%h9mZxiSJx2hZ?NjW;SYZQ90}v+2nWC-XIS-#vaaFMOW2ed6@NKHsDyYbaKQdwsVG zHLE+J4%&~UO?0eBzq(1W1H<~WYO3V4g)h(k@y?=nE$guw70rYTq0`B>$wW_DMZK!F zOV2^Ww0}j2*d-qAjp_}$r^e2zKArw1b$qxpqCO$sa)0pX#MoNFmfkXxZ?6-5h3&!? zNmLvaXy?;-J#Lh2UZdRaoJ7lC;drL6q{KOF(Mb2s{vDg_r_C!?ZyX7rb~-yx>+}Xq zGfvbvr$u&O;p99#zbb69Bi#P(=XdMB@)pdOnv|Y zg$)go8gc;rpdJOuP-!H$*WVdg1d-6n|gO<2Jqsu!;QmxP+P&2!W*YU-;-=U7%cNF+% zoTd-mrADKE-8;2auqKso1wKBzBj$WH5|iEZ>m3#5fRsNB9d+)ehZPm=BrKA1d&wHW zDr$P2{!r!o#rLLKfdrYEO#2(i03No}+`KWIy_ zzJB-!U_J`{LtR7`1xq<83dseEkKgYg@pE(YnT9yP!sDa44W3v(q5vP;`p=mCHm5)D zrEA`kAN;vZI<=;VPsGOh8?8B-eRTu*HEQA-Rl!g66~3&pwEq|wjB4O$Xc!o%hgjVR za<6`&BlRWk{kes^rcQu1$eST+r*T$dvCLL;*wz?xM3Jde=%4E#q0*c^6s;nPHXMpI`ucW zQmhjVVL%k4f}sz#I?VuWO3;4xA*dMYGF?z=8yK)p&F=nrZFsF57NvMqQ?S}4JAedl z^vVne>E-ODYM?L@8cU3B{6nfS*n7SyM)%a8Hf(b}nDA`O+)HvYDBPBzC#}DuNKO0d zbx^rFyLWB=J~Lg{qBk|Excq9fTB|m zA$e@5x&jFzA}$B)f+zgG?Ap1#km6DQ2Zc+AmP(X!)fgt`(?ytd;0_YrGPq>P-6H2N zo`-781!jLv=#vx(Yv zys&Pmy~&rq4=*p{Y3-FQfDcxWe}V$}7a&UEb!C5U?U4NopDSLled|_YsBr$%f}bAh z7XU&pB}i^-kGwI4_dSVO5KxUzgL!?J7%Twv3JTH-{Di){x1hyANK(YTaJ;3hEgXpY zapeacX&WIilnEt3^)HXnCJ5|@8_%17P^zkxI_#mDJiE6xLt_R_p7_h+tMPYCfiBrk z_NE+vmXYz35YdVSsK#5kV-_&};#VhB-YXq0)wdOTlS$;8l9*2-bnF&!Vg@lX11f0$f)uyTx zb3K|egta10_-uoj()h##peniXYG`Sh00OhLwXFeI0>3`0`vEMM7|WDu=xekOV40>VUe~*>8l%`U~^MI4Fg#xgx>io4~we zvzRq8MIrPCs5-@Ab&;X3;nR*n@eQ?DCxCk8zyG$~dTPRkR*wA=RTWNBbRf8%(^NW! zL%4B81UK4LYrZ3wA~rpMu?c*EaxmE1M^Voss2$>3Zs2uk85!NMx_v-}KETv)ymJ*a zO#5nWJbqfXnh&8@w(k`v5hTOIXKCd_ofXNbvA++J(u2WOX<~^a1Dc3i73%%>Sy^{g za_>T!7Uv?;bjfGUFD+*i<=5aEM-`{?<+TNacWa-WaU~p#moE=slQ>1#0;mAo7B2yQ zUx~Lzs1%@UXh2d6#eE(Ol9Wa#E!26Gp=ynwv#7&-uTLe0AbFzz1_y?RJ>A{iA;RR# z5P^gu>Ps>xxn*>8D+Si|dstk!@D#ud1M|UTu>Cy@OG*Vw#JAIk_7LFHp`i)|Qyhh) z1>~yGiNQdoNNPz)`k%n%OD^c@z#j-zY`{1W=VgiqRD>o@-@WH=6Pym1jwv8B2oNC4 zjheq;0Wq8bqF{_b15K-*RDwZ4mP*yYz#v+W=?{xJjhCAw(VN+{$zR+~8ByntIwN_k zAbSuIeliLWej9^K0SE$_4e|OwHBS>k=pVxs2)>EH7*q*mPn=LiJ_2Owp>jN)6bJ3OR&fF&khXIg~)@X1DItCYwfL^;xqaYHF;-T?oo$h$!VT{0XR=Wbl5& z;54}wWE7A;rBef>oOS60Fh(Bvg)9xR6(V!h!hRHlu?_&eQAhoW`j*X~4HFQYdi+lk zjmrQl+hdljSV6oB_MN-{QO?Lf`IH4-Ve_Yqk3Wj`O~iISU;WugGQ#xC&LGkytd_w5&9XFw~*s)P(U57!|Z~tePD4ESC%!pk5FT5hDX_7MW7qgLdml%0$mjU1mraXJiGg4D!==;tELLZRUZrcaIlH*d$ z!nv|;F1J{Jtd!8>YCmm(S|tY!jogX|*zrRqE-DS28i*`q@T-{ z1lkHyvIBvEknY?mB9ijN-|U99)S+BlgbFgt_oH2a0EpryRs-}$#OVmQOI+@GjB2qB z*3L0}{sSd%{(YNT8;-tyBHvHr)6@6t>v1@nu81_)0b zb0G;FHI$OOzmE?R?0}ThfmV{`3ppefdM}Iv29eOk_NAFAiLHs5KNvgD3GYg$ox+2#7e1x8KeeOmng{- z$_>#HMIdfPsr;5)|m!k+u6Y2|O>X2E70xl8L zMA3xUabQ7eV_rFP3MHYW%e|UJ{7*tc2Xjdk)WE(ETKF!xS8#_OeHd>)GFNwE#eV%$ zA{5GsL_8p})}bCk5T5L-X>clxmfmCso&m}mU&6h0%K^a&Uiy55sLr84kJ4Hdh$3oP z_BLQ`L6Pzp%6r8EaZ#9(<2k5CBNNObyd`LSK6>;>yajv{^XaF!WAGVbK?$%%V6g3d zMT{EnaL-2-*wlHz_6AoOhNS~Q=hZxB!!yw6TLIM#T9wy8WthaCa!gsy~M+bL}&_{$xnuDkmqYC7KYlR%3WfgP}h_fHW zc=s?xTM>q&uJe=)iBj6DOztpJM@Myo7?ZJrhhwpw+=gZ|FcoCtu=|*qY$PF*T#%#= zeuW%dH_jLO1DH|_Y+JVeQm{M$k&Kjd@7y^aXR7xr;oBk0#&gyCCLcrjH#Fy(`CpGk zf*C14aIwznv7pqgLT`5~8IoWp7 z;JA4-un}#9*pH8Ha{k(Mkfsc{;*Pe;tVDw3{)&NwZtFluD+ok*cI}dbgU!(xA5=Ms z%^?wdHm#9IvVl%Z-ZuHoS5dIA?+o{@Otb1pFl!4@@!i@i)m;(kZYR_{`TpEqVBCXZ1 zX6FzC>4040-#?F}zfbmpBC-9S9CpFIxq-+#qAE;zB;e-OfA^=~2>kfXVK=v>5E=aY zh~(dG=t!hc-fs0Z zEOhwe#`+}P(|hEe2tNr>SH5VSbbi?e*5%$VTw50NPEP*W_|Zk%tv=C#Z}Lh}&$5h3 ziN=11lHAGo$jzO_zvNO%m#bM(cm8uV!5OPi`4>^U_u$#TPnCQuR!wEr$6Qt{cAFRM z$G9nT^4~2jXFIeJdBj$(`A5v@<$Zkh0R7!vw>SR)Q_lph@l#48=-bX*`X}b>(JWD3 zL18bmefeO#k<*-Sc%52&zsvUH5{DvVqNBNo7b~j2aCa*d%4Yrd(0xnXqK$4jJ84Es z2j?bZF5A2Dsi~+`^g0=QiuB(;`{<`GMx#9Pu43eY1$>R8^J&}uxnGy8t+;3ysLr_@ zc3k2!>9${&-1$d}>2&`Rw|Oq^s_$ms@#HZ~f*w!hBe|bX|2+gg*v>ygIUR3ve)i)U z`3=oYlVQBG&yo~`G55hRVEwGK1nZLrmSKHt_LuMDl~8)si)Nqs4Y_QwuywOv(`{*7 zjrB?C*(X`;jgxTQ9+`cGT#~pwMZw9HvrlY=7wYolli%!<1ds!+t`D4jKx;no`nWH% z&i?Wsc{cKij#TmU?^gf&TCL}i&qgyp_*&vTZ8hufuLXv0qEMoomM>qP`_F^>vP~ZM z^!JYo&$j=6|5wymzz8ykP4)t1XU~?z{}1)nir_D~%Fg|LofPXe*Z2>xW_-TQn}8km zM1YtOP+SmnYibO^Aq2MjmQ_Pr>GP6#e}98Zn0x`m+QQd=tV#s#@Kif(uZ@k(DKte8 z>I(AB2rT!N+vorN4aIZw&$#f^R8~Gh+(xC54^caj`uh6%%I^Eui#6cLmSu`i1?t*+ zCf9AKtBRLaj$i;jaIZT5nSB&B*)7XTmkSCBg})ea_0-eS>K;1%_jz1+cKo{N%Od8; z#iKnRD`tNw&JZ&w*c4xDLpWt=|If@l;AwT8|A5H1*Q*rHVW&8idF4{H^gp*6yb1#& z{aSl_yP&Xe`y&;38r*%sTvzRS@b@_>U#JqtB`A~t#;;7fwNv>qB-RU=sMdA>Cf_xD z<)4qO_F3gNFXqFomL{sPkx>DN(&ZmMJjRh%D*PkQd*C@v7gHJcx#{{LsyNu+4`y8_ zW}s?_#G6+Ox1IU>pe|k1Ul&E~by&r;_V<4qdt=-WtP8%~bDT|k%fFhkQGDbTlm&{W z9{88hqbPydsusNclEt-u`%r?#Fq3$+_x!ksx(^??W*tNk2Vqt(5={*b3E7JDphBQ!` zqk%@vluF}%uhl{AzQ6bX_w#xC$2q(9^W4w<-1oZIy4JO>HFLPaQcV!6;#}iZ&Ko4) z20X!Qsy~hy@hJK;@F+JUFP{B#3yz>4KknG8%9|2bbBj(JaZy^E=LF?BABB%bp`6Ki zr@6S3ek5NN$DMj$d2T!~yUcN?+MT#lQ06L*JKb1`JIxiY=e(#dT{LLMsEguF+;CMG zby0a-bgA&Di%zAB2G?+2Q~(!UvS$kCq<^Ci)pl}z_=rAKxyJdS1%0UehV#R<_)t4s z>IKK@h+Kx<@OQZo$Aem|z?fz?xNv^I3GX~n&f0}zqO><-t7W`+mb!R&34A_{$yZ`< zS9pzBG3Hpd8!kzl>fSQ^KFRjxPJHof{?6g`mMe*2$}i=vup6<4tMHCyx=Ng?yfnOo zFDqg;8C4&<{u*8jGig~*c=3#X+3;utx$!XLFP*)zZNyDXF+%QX<7fMg7$F@?Ee!5f z=@B>X!i^uDm5A>capU(Gb@cTY&$LHeFuDz2?vvvy>m2cTbaF;}zIYZmVs1+8aOac- z+@dnWi~VT@R?F$%O;7fWSk|Z4o)LR0aPe%`_2D7v=HM<#3%Nyg&TT(c7H>LYpi^;) z=6@vO`^DF;Jq&?`{Kto0#4v+g)bf z^Va%_YJowk3kF~QzWUlvF2IkC{LvZ@BBaCzo?+eXlW7yZB+ZYwfNF5tB9GaVvCDx4 zJWLE@N%)fX=R*Wvj11t0ZXIu>R^me=;E=#btP?AthGJH<&!9HwmWcvO8eoxRPmPHNGrV9cNq)S~i5iB)Dw;XYt@t?=q;;UWCPClP8>R!%I?m!L&aTQx5v@B_ncNzAuJ>h$<8CF&%HWO<&Co!C8vlHLFilBun>ydCnY~ zqVaqu<-Bj*T66mQ7cI*|b*bC0lb~gfb0{b#y4tajyK)k+ovd@NgsfhL`}ayzhFZBRpGb*ci0uh~l;jT$gxcflP6 z?(MbTBw-1?(&m_~)rPtLbj-e-t&@%5$_IUQYwpVq7-?#1nmrpgJ0LbzoNgYOI@p_x zkTv1sKYzX-7+f}DO*0zyHU7q>5VF_wOV?o63HG}CK31B*gl{{tyB%`?W2$Iqcw#E( zBv)XO!-AuBCw{iN@Oe?06hK!+7S_eW`rGD8K$}^pGg^r>j1Fj=w>EQ2GC=ya4}^lXDD!w@Q}5%6Ecb@-0;hBjoceT*|44t@>?i*sS+kUh z-a9u?N4R0O*2zu4($PP%?l9msyUvFuxGR>4!K>@@{gAOWKtq6iF^8EbXS+3h@zxp? zZk}?HYK^A}nmYJ#s@aF*aZ8vc3AG-4mIb?Qz$QMc z@wM+N{nl0~OOJvgrueMx!=Oy|^w-O#=G&prvyqVj9`=D7|M?Nc+Sc0W@pj_(I=gv2x2IU)PV$XY8_YUG^m_47ez6OlNU9g@{`pVl{+c&0m+ogII zAFzvP>ChQy)L^pkew*8ST{IsV?uMj|H@Lqae9Rnwx;IW{u*!+C=g*&C4kr;3XW zZt^&p8zsc!f`XPJ8DqyfjC3561roIZvfl}?N^3xZkcQ2jD`RVcLO+&Hf7NW{fHHvo ztwCbhN2%QY%d_$3z(1BU-^z5Ck#z$@)uj0L1%9i&V1iysh7Gf+^D=19-8{9$;*CgD zGK-cqoeR{l2UNW{`IjgqDMg79_ULMWEd4dhWHV^G7(JejJ)T$Rtrw-;lXT>R8#r8n z4<4)ncrMtOk^A%ExZM9Pv>T%`_9wJbHo;4 zUa~Y&dka&%o1XivU$^cEayODy7Z;nMHMapWxKi5YFfNDjCg)0=)CG;2MRrG)-66XM zXT|AOb3a8EpLzDGp0fIlo_GmY1!Oj$#487>wbdvde%yCj!onn~uI|WikeAz1_z2Zi z9<~#ly54U**7A9S$csSuyv?lmZ;;r^LDDTB-Qr;Dna&P$T@3W}tAF!O4?|;19ueacS9*E(| zcKMqbC%yQTBBP$ zv;jA^E*&g4&cK!PM4<76uafx~Gx~?ynAv*t@1oaad`K4+2(m2}5ntW_u*Ki)lC$H8N6ziRowemg;5l5Mg)uK*Pl{pfH^6$ruGnJQcUJx#^plU7W1!=af zx*y`W;0d~*h7#umF+yQENsbFxp12)IzOyF3C{_| z^|;{PuT7nM319s6qb&dbxBtsMhmgV?AwW<! z4}h6vjJKHA^-GYHumg)TfAhti{FH`Z#a1M*)}6+gz6 z@1+==vu{nsD-lkVeQKhh0vRpx*kW z5UUE(MC0@ju@RFedq%AJUgTApB#SMos;XeVxiaY6tblFT1&od!6|UW@wqW?-gwM*& z>DnYT^l&Ze2(r-cIsn;6)FP$W>cf>#ntH@<#7h906YZ%mmnPdVDkvzZpvWP*9YsSv z0@)iM3_pI%1ZTaKH#XB8-;?=;%{Kov;>Lh>HvTR7l3w%sdq|iV_}M?sKU-=%{4i=u z@i3btU*S&@s)9p88rIDx|81bV(r<8}&-1iLWWT?Cv+U1>5Ue#uujHDU3qAYGcc72n zQ;;>BKjMzCD^_=A3_YsyjuE$kB|N=-$H?F6≷eCzj(yw0%P>PvxOyx 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
      d>rqW-t?HdZT;Jg^QWbwrtrl#0nQT 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

      fgT!B7(#7R+yEwfjW5m}ACDFzjR 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 Q1Z=0GsD8fS+?Ct^2i`2`Qvd(} literal 144759 zcmd>m2|Sc*`@fyYQjtQIBq7=NWXoRmeV27?BfGIDRI-F9S&JxHvSb~!ke!H0GRVFU zSrh*EFwSw#>HOYv&U@a@`}_au^QoEVx$oz>@9X|v`*qDtH5GYMVj5yBEG$w*g^Lx(7uAGWK8yfju`8{H%pmP)vrte%^rmo>z}3X6qLX7>{dpDg%n z2{E^EF?VF)<%U?Y@Lgo#<>i!xICJuN+v>@is@YkpLfz!7FM-Qogp;|W)$SNIa~ms8 z9`x0nhd6r4yO=xMUWQs)Ie>ANUb~}sxCI4wXIgsiUJ(@F-@RbtviG{3r|@#4FK9ww znCJ6wV}`jyEUjEIPusoZ28BAfL7abi(E{q^WM$#@i-G1YE>O>34zq?jU^a<)t25Zg zFD9WcXj+&%{PwCY#L~@ncdQUMW_jq*3RVys+r2mQ@7=j#ZeefZ0(E!7ypM&K&wA(2 z?j{_~_nw7$lB=z`CDe2G@jI7x=;{K6f+x%F9GQ%j1DdvbWZRu5_d9og+MbJ*6J~Ay z^8`WR-GzCWJGf)^zI(yd&3lg+JKM2BZ-|?PPukNK;%24kY;J)b?g@?`T(fm^bO7IY zz(;^J2TjaeSEvJe**n){pbk)%oqh6I3!wj^uS1>O_Fd=O`2()Iy178@t@hm!-1!6U zU>0lT;%2pnwcSh3=0ETF7x=V-I$F88cmoQ0e&--CPAQhIYO<0SxS$g#L=2_0zqdr=f3M+?)5i#C3(byX0;$z?gsJLKZuw6-ytSuE&I^th!M_cB>+}wZed~N>T2b}!Yk+i z7At)P{mBOX>5SDmn{!~qjx?78qg~l{H}+RW!S3weaOR&uCjXv7KpzO2jC=BGKQej0 zgG}MS0hl}(V(#1jA44XZE%$KwGfIDk7#gN~2m^8&gG}_KpK!U0?Hw%s1n6HN)5^(G z3a#h>T`(*Hu7FcR%V9LCf0y|Ck;}I`YCm#$Fe>E7LzhQjcQt>S+}!7Q|AHjgIi#QH zy+{0?59#-)ZQ)?<>I$*gnXpIj|4|PAD}rMV;l~pk^WyKw3QH?%h!eyO0(AoNz{Seh z#mW^x^Ir;$|2}!a|2<9ipOP267#sI50QfK97b94HG=edE+%GbC|1kt(7PAk*mga8e z0JFe?bhmJGcR_1BYhZWsa_^}@D<>NWFrt1}`)OU4-cf-6JeK)>Wt9o>e#a{NPhpu4 zqXzywmeGO{W0U=1-SN^ixw_c=5A=M{#$7Xy<&$B1%B_=t_k4o3URUl_sv0Y?t7!GEP=_pGs4aG z`>((l5r7|oh4FKI>>tE7uka5=H^wU1cOYn@0dim{^J@+D3(2q-U~q#tT5)=qyFftb z1M2i0h4lmJDIkOi6kq}j{D0KQ`g^3sf3vpQ7h(9fd|n=m_}DK|Foye&*Jyi-*@s{! zcSkE12#EbVprgbPCs#LfCpUo2|5g$3YrjC?kH*CQ7Rc@?$$jzy4YC+~ z?uRT7CQkhiU>38QeVE0#xtkiUqoSrTaH#aMEd>kW5on4^l%o-Feeg8oR;n(>5 zu?_-A#Qb{m|NmJBd-3l50LAS4$K!KvG5hf8Xl{?TUH1hJ|3N(d+FBLi}cg?D9-nf>8HKb?Bjn7;{zG+5Ar?#UTF}GlM^=G_;ubopj)ziAaHLxx-#{H%OM5yYCd`?iRrBbsq466W$fE|5h#Suk|3$ zcgwu@;`{r{y#J13+C2+-AH6Yw6KiyU8c=g5^0#NZqRG2^|JRnozfax2ftJ6P%|Diw z{1~qJ)3W*Jf-H3Cc~4UR9C`kJVe+m?{KE?kF{1bvW(CHW`tjE7o?_SsUr+lK>&ks#t^J5NVA6b6I)tD639>K!Lz+5_Mn8&|hdL z+Lt2vZqvpeYwX(JP{9A4WXWC&+CLyAf14~(b$4?Gy_{%k$=t%(-08PR23jDU(G5^e zP*8FKn*Vm2K|wbNpEQUc*f@b(4p!Fa4kcI6bqLI`OFQ2#f>OI*bRK@MG4Yq6M=QMj z2G?$LCa(p$nPYe4o+$?ISPEVd6c7Y={swb_mk(pzW7y<-6&ri;ll@$>$2tE_MdDuP z?SIx-_^)OX0gRT~Z%XV13VuA3?5$>>&XltBFn6*5S?ph%68{HyLxB5Nys_sg>}&Dc zizWZCmj0&gV%e_k{F}v2gk1W z&T1dUz8|~ZqvLmo^;cuEKd1V3h10*6*!yJ*8^{3sR(JkX6R_Vn{R^RM@bgzc|2Wys zZ(;5G1-P9bm;x8~ep_zu`%k|+r+}wBj)ldDrFc57CsE;@VwIJ=D0%G+v4R}-o!gUQrQcxNuX&65#Jl*q z_*@4>=-ppzglyP(1Q;9VPo>vK&~9@$Xay3hVY5r(XJb*N;b<}5rt%`{5OW!6;5Ip# zAcwiSGlZg%*mrFxOnM^1l4S7XlbzwaH?WB*V9aK*a3Y_amg;K1%)P0XX zl!lcqgd=!gIRodX*Ggt^NKQn!kqmzQvyXJp9{kcSk-%e7uaKjYn_<})^J1m74eSGD zf-RlD`f)*JZ#}DQScAD5nYWgvx|P2!O)@vfb26`W%4G-2P%&G5z9mqNv*cZJvD3wN)%odo+-n{(Oj)sazLI<|6vDY3tn5|{Rw552XO-VQJQa;vUGO?lh>%Evf@WX`EkvwsS!3tK19gc% z;Dcr6kBnc|s*Wq?)xVnAj@^EYon*i(&WP^%9&QFb}Ht;1KmVBbZ0_ z@t{h`Eyoe-wQDK((1V;rQEb|@>T>=$9Sf*5L3NGe#1ys(WU@5_lOmq@S06d{x zPA;67M;ZjFVFRj6a*l>*G0K5?{1m(Nx*?1&O$rur$mXF@0Rx!zRZ7c4arb!Iv>CO~ z`!;&;xkXwA?Avzchz;hR;2DZ3G%a5eeUN>_4!Gn)?)a-6F1eE_S%w7;yl^Hyr$iO3 z-p`aX+23wp&gClcMKlpl*u*472ctRAmQR>!msEgtappHa=9<6_ zn&LS*0Hemnuqea0&dz7xn4@2CNie2)hXF4Z;^3LAR;r>RMxgizdSh&9F2`%T+mtrI z#(}o<$)iXiU&clpwE{crAKja|wR;mM2P+7Xgv2v-6nPgDR3$#~8SyP`KF`G!q$u*9bMK=sN*gNd{<`g~`pZIsbxe?t$SsA|PY{3XIf zy!XKuG>Zj93CrwG;{XE7pycT@gA%2tIJP4Tun#O)wWUVoE`@HB8b=EXl(kld!F+gR zKaI-m-HoMzebsnKqL%ZkXZl6Z=Tts5y;s@9yS6KUv{TH+QoDr zAy`L{0O>WT+47ie5SoX)T18IoPJHtaO|6^XfS|n=@cGS6i@8v}ypwAmPcn;L%?2rg5xX)^QEURkMGyS6TO}?vh#`lGwDetq@We%UdD*O%Q{ROUttT3?=S-?>?eg$UHF*~vQh=kQz4oAOIl zPj}=+ zq(&|6U>#0dk_a=TGebR)PSh>CdWu2JYD!spZndJ6^l@XY?yQ za^FNT3=9jCi!Z6gwPB5at_$ZlqnmGHY*1{=6zH)O9O{kJHgsp1e_l5Hs3IUtrfTX1 zmwBZ4b`(IkA7ykY2Et_FbUF8(Z%43DnGk2jfY|Y=F(N z*!MiU=4BL^%dzBytkM^+MC6+$u%`NIMAD11H^#8=Q6aHZbF;2?KEAwxwFI7|X>pyF z-Q;$DTTo?p1WHPdj1nP!!QyfnW<8RfpUdXw)q5aZApngwFC6vUz` z(8m@FL{P!5u04YeR2MoANk1{Jd;!3#P{c=h&PcE4nLhLSmvIEHT^Koo4i_}v1b!vN z1j_;lsuC_QWnMOcHVeEUaW*xpodG59pr37+1+1@odRqMvbbFhUOWS zTLQM28ol_5gUppFZyCOd)TF2%KXCXIMFx$}YzcxAcBn7>o-Unb_}rwp^I#w>1iRVP ze|;1e(hQ_U*8M|R*{BQ{&0F>*|F4r-#8oL@#R9gftRn-dsY$uRgWXWpxYw_mwcCRK zmk#mWjwa_|Yrayb7AksiWmPCnS9OAPw14>14v#ZEuB~vN=J&xKJW@Mm&sQkK$bqZj z`LgdonY$yF7M+^UT={s_Q=`y>M5r}6Xu#%jLj3{SA-w9yBk%y#O>?6WLb*9J3x{D7 zx)_;k!YjA?TYGik5gVi^*&d?n+6C%CYG|!TT=_<1$9jAviCq~)9doRMWKj0fS;Ij} zpj=e!BQ2wVR`7ANwRL+IqU99tD<8t^9*_`bR#TyfAnB5QH;z#=PT-3cYQzv)`yIQy^R1evHVWTBp z56R6PRG^1*9^P;`=mQei6`^&BWj9dTU|ZEb&38l6(Hg&3OA?wmqdl@TWQAuc;lRP3 zG{is|a3h!LB2L&qixI7%smIo*CtrAb6b4INOM7)0fdNAxZ51hn@1Ow;jo4_i34#ui z{LP3Qi9^1d1q`vLFUJ57HZP7gTg<=}V^1dz9}VG9TQdj3Mx($?Dr$95d#f>))e3;Q z@Jb!=W&6-)6nUwA&*?nBz<|HiZRo%7Syn6*^@JQJUv+U|jFC-pf;0w27f=eI^JJ>=(VTKQZ+CFzbDd}*Nf&}Fn~Dp%H= zgwn%ypc=k3*(IOuv4$El2yvWx|0L$DKJRe}|EiJJ2fB|{Q}Z^{<9M3LfpK79-Inwu z8S(f*2Yj&2q!-{;EA){bF}rWebPh{KM74U5uc&yOAlp$ z;VOiZ?JfahIf{{$o~ojrb7$|eYj@)6ptTSTs-Ecx1QxGt2cy)>16l&Ow?8An#uP&j z9_O#ZUZv(TF`6f=I_ZX;j^9@bjbb-FSSEY7NYs65Lcro}q&Ad`QAG3crNz(TTuO*M zWBex{<)Y|0-$pSYTX{9(A>+Z$+2pWOT@-)e} zK!KJB%T{{KxoGhn}=`)5dVf5zvU3D&}7S5^rZM@L_Qxy&*nv+w6S^7QKMEIO<>147Jt=WGTkVjgu<84QPKL)Skmd9)#fN z^@^22Tm5bp!kp}bzy}4(k##^XhoBl|4b+1!c?Q$@G~0I%X{f{9EQvUd0jsUpu3O1; z8&`?^glNX|(rQWQ+^2G?ygMc>D6HW$LtqQLR}qlXOn7zY=pwjr=`xB)U{`HPuU2MX zBe?hWe6)R;OcR`u#uZvpWpWWT>Yn7sxFsvxCLa>^ z0NAohY=0}Fzv3D(8PV7keu=apDKOX$myk?r4eugtDsz`i_)$(uxkEK16nUmK)efJ^ z-cc^W5~<0$hEO!p$NaZ9JzIM#y}wu+BXaVqpW0u3AebBKIw(nh#s6v`c`r+JOsf(z z%0N9F5qb;ADYagt1rO%tXc!E*F^!DrKUR(Qgi-P z?R3KIl`Jx#{iXvi4^n0a>H~pjeb;5nT_+8JHP9s=ugnYcO|B_tIxm9*C4$mt(dU3g z>9Ksq*QymEUc-T}&TX=>swBr+wx5cBqbh#9Kz4H(F>si*ZPTP6kaTV^=$Ujr$J*)k zcXFRuy>V6xrAXWq-16zx4VMH4fBkhFNz=ANaSq!Nk|2agdVp5f4f;6mzh70~*KG zIa=>sxZ9HQNq%v>`S|b@FdA9E91S=p@U)uqz>zneKC`)fo+Hmk)<@rNa-7nv=U+8; zooG;9yn>usoM-TnQLy@=iQf@*Oi1;`+y{=yuFN>koOrd%#-@BeQ?u_qNk)1~whM9f z9It+6iP!R$*MoDO%4c08uivfZ>%oj)h42*_%YN43k=cjdoU=LI;X^Dcy0(wI`=nQ3 z*YX2OBWtHk{dy~1fm{)_;SxT=PLTta<2f8Ju2Ch;CsJke+_E`_=V<_LXjg{Hxvu)< z=#!Zb8dym}IX=ANGb+BMsSkH6*2?M9e=)ZvXYM`Rbbz?j;xuO*v|5~7g~Dvwlk{+V}<<;dE)%WysC6YWRE zOA{YZK^DAlnH*mEhCRu8*xF%!JF%geGj9bl1E9^OMt z#ywP&zS%cXHgMv?IR;;0H8T3I>P>MiT$>dEjH(wF?rG)rE3zkWojj7o*}V})zNXd` ze>QDd8u-MXrxF#(L~) zEhoinpZceWTJb_YO))F3TsnMQybSK}p-`ELZW{Z;QI3I2k8e+&zL^S>Mtav2U83oh zRiX;e?6l(Iuu>jNs1Rsw?0DzOA6f31a60C+J!IJdHxj$Rf|WC_IZMzUJBVMuS)pBp zbaYwQRRMt<9OU`JKI83Z|TB6%P$zox4l>-~-U{&S`NX~B62Sr26D^~zN0-~F#Pyz zj7%l9&voS5!mwCG7rXn|eP0#b7PM$nI)2_Ip02O-8}>whv3r#7bN>qelyBvwnfkAvcIGHs ztXprZH)vH;P&SgH9`5Z3x32RvS!qO2Ovnw9nSWPp_Koh3g^ah^EMzN)8I$@eNAH|4zDrZGEQk!yCe?J>j>{4tlEMO zIL3M4)?R0&WWpj?S~>AIM2vl4XMqtznk7rdm`hd-?l zrctXwt`iMb1AbA;bdh~uxuH=_!u7bZ^PHLVji|Y*Cqakzb z>1(UBq)fES7bciXo+iZe$n+7O;*&dtHb7TRuWw-?G2YL{1(6jB58}Z=$C&YH;fG~p z=|VFt;UvU1_|IUlnosVMi#7d{I-K;!sVa&62GcL!`*u0P-Lbd0LOTDB-%ua!+f%wh ziR6;dDzB@fQ}Y1|iCT?T<8#4N*mVJ_ClKPquyL*}Bk>F@NU?V~LMb;?oD%j0uB}Qu z+D=8)=Da+`;4;){)tJmTjoaa~@+tF76nC1Z^MDSG%nUKy7#7uqx)xGIa3<~KmatF! zZ4Febkw&qD=K7Z=rwHl`o?E*5FICuS1s15FrPr%vxZR; zfz(CMzg9MGP@&q?G_h*(J7Pv{kGP>&mO>MkovRP!1_}k}5yv~60{Zo?Y!SgE)7ikW zuw?m$dd|wxH|g46+38V;5=`RdT(m6c12CYw%$=FLtaNcv6Irq83amaT<6Q}<<`IbFkB zuzYzN+XPt95ljTfO-B6)s}bcTkgXKA`3}hxmF@UzF3E{jR7%+En?C%S6SJLpDND0G zT1U?rW-%Sm>#MkII+=JV9vqYan|Ar#dRAsioarSz_(Z;SiH1hL&U&_hnPUpC@dvA_ z>&Wf6ks(3*qoZ7dEK3G`VG5NrHTj*E`6si;xkyCU4#SvRzm_4Rb!urRLOfrnH*Xcr zbhWD6w=cUK)#tS-c*Q|;MGxE4w!}WJCmNgi^6~R^sAI$1eQxyUlyw~52U`(Y+!J|T zI^xUbs+D50L>QIBqZ7g#!q0(|Mol>K^!?PC2c>?w!-JB&o#is89H%IsOLJ8{pJ+xc zA=llr95!51wQ`qDkKyyO#rbbsj$lG=kNWn0X*CbAv!J`KbbkHKFfm+DjPbbhN4SN| z*L2%!R{0Lf)(IRXWqCDPpL`~no2`6444onVu-zP;q}soa$w)Gx?;}F7CDg{ zyi^X4pJ;r`&_OWhpmkNu{cDvqyVq8EF}=X~^=6Tq3|uK03Mos~_7oa{8}rCbOW#F42a3K0bc#+v&-!3psw+2C=79G9b-r{KFqhXcrxBWtEUHKVDtsGZfHW;8Q%3 zlhtc`uTPb=&2|F!)k){epX3?f^E6q;(@pVP+`1Bi(UtJ^R2HtEp$I#c3+1grHCLA2O=mvQ!j+TP%fg3yf&V#~;rjg^sVx zh|_6be>=7LE}eRnki_=@T*osCp&7&o9B9&qYh*Nu9n;=?MAh4nBOv~91g8G{>PH9Z z{Qi!OMDe(Mox#5^lw0#{Bfp_?xl$=bw-8eQ z0ACr=XmQ}k#Q8$2M#fk<_q)_7s;oq;l59(jdCf~= z8cn9uzfHWtJ~U^yz=3d}&Tu&IH+C%^_BP#Mf&8{(fY06w^}*CE}Zf{s1@_Z z+$>J-t(57Ap?u@=Vd&`HJHa0zb1v648BIl+ie;b^H+*O6n*7p2JfCWvVOJjimf+u_ z^aT(863bLN|8DYdiPoM|Pj)<|gLXsiBKsNvE}As2>#1UUrS@t8TJA&`t~AKQ6C1~A zt7GOiPZRN$Iv}eNj*6}s#=iWG(~0*p{c=8bR`gb`o@l8v??!~bzms)Xoasd0`#YEW zR&3+rbFxYs>Xda)DbKMRTqhuO6mw(9#CLk(_godbOub)eajVkGvLQQabSR9W(`ZC4 zWiAMAXnU(Zk+jl&@=FgxRSv%Y!vqI;+q+o{SDkw_4|wX@ODt}-$846^bw2L((Reh~ z*9k@Ox85$Y=;w#@ctpo1%PZ9%oYYjR%zKpKvVM~HswH(RA`%t)S$0wB)+MNN^XcwP zySh?0@c^TmCseGg?Rt6BCfu%pW0vwtZ(o~o-BFe3dwN$8agO0pFZ9`qduc))gUS;n z6+Trd#CYn!0ime#?C7{|;`-naWRUjk;4<~nE3tC>c28a(7ly_Zfk%&3TJG|Nzt8UL zc2xZG+9&?Z5{?meDpo1;K&qOBln%`Zjr?&}1dae3@7?GF%%VBeN3hPDeK-jB|8}D- zvHI#z=Q1%o(2;UN#Bc0jMyrloa}u+V*Uc;I#SP>%uXw*6Ogw4obZ_W|dtsf5(^ zza%i{2n>@}!2>tHNaV#Xc1yfJDJL!_)LD()fpWlY*gS+BtjLqB1H!&_b8bXitM(8& zl<}NYDitps@$KkE;x&RJXm+nsB205K5|Ki(e=~R=iX$rIxM3ieo0ttDuzx@Ouppdn zkl{r(8z+tK7roAJO?aY9}&$uk3yQiz83+Id9Rfki18^cQ)nCam!j}UX6>7;v@ zAu5f3@C@D(86A%%2_kJ1#8eg=H1pZ?s*TgjTkd~UeNE6I=-AI(*Cf6ou?mW+N=C7WF}b z`^>PF)Kd*h*FI=_1z^KZz;4HM$s;C~Lv5C`f>V`($NVzwLQ0G8kL4QXi(f%O&#?mK8JGb9!11x%MyVDakv%Z_hm3mizSOwg`uS+e_5atD;J$VckOa zSxWdA$P;QLtDhyRjTB%$kL$SJ)EXmrnOG!XNT|jK79sGJ7Kr8^f6qlbb_y*tDeeyN z1RsIrLS@UYytV9}&sdRObtdoR5c zzD7uNox{z^P>2||Ac>7azVNtJmB>+f@Wkfx+o=^75quttC+}A}N1S~ibP+^%m_yOgW_EZ5 zWGljbkQ?bkTJeOCrozl5=V(+M!o@~+VsotT(X}N@TQ9RU-*vrQrOCvsQe(s&-YcKZ z!wFoCxrdpJ$$NC4y5wZ@&I%LVP&*I@31A7hOj=VrbRnmeed%EUu5awPE%QQbm8H8v z6ygZXF*fFeI`%fG#agbZPv^!-M{F1qQ={AuqhKWP`g5!kA zypMZ|&E>viW~{dwmX5w@bd6yjv*dQ%{VRt@E%2=?1(>(3^n;25aY9N>&LM5q{+vrK&yUO24$w2HZ^ z0?~i@x=CM#rMy<{HV~AX`B%@}277Z%kUrs?Oi)!Voe!;?9H=Cn=r3Bf*d`l!iyZ0> z@uX%VC4v)WTCdKFN<+`gUKj_dNKo5`#M8p6+@`qr@M$`6wF(~6In;#CO@@asLz1L> zW#L+PVk^!Nb$ZXtma+T&K*^n7;U0l>YdFWw4No&h7VW4w*7WC!NQ1|g^?WR2{utu0hF5mP`s!q?1; z<>}W=FI`RJm%NlObf4BYIkmrLnh0J!M>L;>gRM2#x#jP&@v$A7?v9LzdJx`CUZuh$ z@g(7r=%m=Lmx#&QY0f1nCFqN+iHd-rD%HDXupC3OX%g)8hA2$;s@A^T0 zTl*^C$D5ktBXj*e2B?!1t?uV3Q4`4YARR?l(ItO=x6q>-l2^$pY0?qrSbE7pK?>6o zNsu+m1i2EBIeQtMB*8jsgr7uEosI)!KXf$l1fEHY5tq#!*&;$as{(6BLP&~!?P!D4 z>_}@BS92+0WTX7+$Ia8;E)Ab#?H@H^TU_!wI*;7Y{Oab2-cYpYWmsd`P%^*G?0bEV zkpwmEfx<_#tE}1D8I0JYw|?rN!f_cEX!n%XO&6V;d(2j5#9Wi z+95ux#TF}2ZDFcWWT3d7ZR46O@wB*Pj{j0y68wz7)A;D>;Kxl{T;n-e)Px%!J!x68 z=X5^jR#oWU1MWt7O`k9y`i!-LkZ_-7*LHDMP=rGEj>`F?iVoJGVTql#eu>>(kVHRi ztBH?_=g?J&KWEJD)!_;&f+5X75Mt4K;cKDu7?yXX-UV+qsJl0h;pxeb)z#1XmhRq8 zDUgh0SiAW0sad03jEJVeWXIRdGqu<0U61D7LE@e!QNqD>p)LKYK(Gv9OTO@onT4Yy z?v@pa@=GDpJLq7hU@NkV9lA-N+V%OUm$ro2hf9-4=(RX6j#Omu)uBm8t{WeBbaLTJ+ls~#t+dwgSwMnT+Zr-+z zzm}CBHkwI2^7`ECOhq_*C)^@ zNlS{No#0HKK07K6^+}m(WPQ9-DR`BmVd5sqNV72^B~0^FfPsc3%+Nsa$vJI~Y8Dwe zzq1XQw>92gH^~Z67Lh<#C!h;+-%%0?u7%Q|V8fHLWmt^lDE$0W>_ONR5mWXzn3B&w zejrVowh9sg#nIR7We$b#>nk^}GawY-(myJp<9Zs!nJT39Sv*;lqo_4y`@W9HhF#Ys zI2B81(byCAtVrL}u1BBU;fX!kw9X)bxORjHD!jOPV7Wsl-^i%`;B4CJ#V`{sN0|~q zx>v8ytLJ%bzhtnSdnjOVhtzTHLwTpoU>u0Mc?bBA-pjb#8Y8j#B5xjU$I_i+oGvF; z?(Cb)_?Wt5Vg7W&5W4EgT+{s47&n-eLzfV>}eB*X?r+lT~M&7AQ zTaRb@FUVgQKy~{?6WpLX@AAS#7r3|jay!ztQPY%GHKG4fp<0X9_>OAT{lR>jtjpFee8OS*Um$$(fqp98^v#l zc}L97J2h>uG#;a-w|fR$@+&nH!o~nZNx8Kvvwfqnsu%7YVxHl+JUK(<>hV&8v15?- zY3($}(n%*uEU)Dyoy%`S4!^#sK^4=k%lP2c0FN}+6=l^__!)ad09#FJFMIn1Qw?J3 zhFqSZ}0Vchavyz`+5k61IMtUe|R1Ho6lnrFi206PsmsRb1pnQoJvpfLUp%7{^S z^b9@@es%gmU`Rkgl$8Zi@o?(7?7>OE&Y7^}HkZ)^&c*JOU>@oDaavD1+lwVlb;}(e zHX?lNo^e&Bq8#SLP;km~-FiJ|X&;&0#C0SJ#_EuZl5%fFcD!gNipZ5chEr9O#{-{B zTOAx4?yQ<+#9zKWsX3Vx@NJU1w4<-2-Ai0Dv<&%}^r`K=*C0pI5HdW?;$3(OWb^~e zuELu-GyF#h3x(E*;lj0o(02yi?*EY4WSDKBj9c*oGZ_C$GP=6MN>R2&}pb-At)T?acwjIIS;d~-y#0aUdJ zkVQohuNh(o1VgiN^MyKm=8>s1p7vNHYi!|}vKve^UEzAYlfypj&VCEs{^BiEo09jQ zj|)q#yeY!B`i4@0eXy@jx_j;Ql>}AC_)|AiwelB$v3uYO zoCo>joUnI!TU%in%{n?CUp&k7V>B{*)zAiu<`i9`|4=Ro!aLcU*jU&F!gQ;%FD+aa zvX;s3WgU_W+=vdQP9vk(wti~_?-4lv63fynn*7A(Wc7LYF>*Di*Yk*=hgpKbS)K|t zJv8-$Q+)%7f7otnF?;fftIPivcnC8u4i)!I-p)*vrWgahY zNAYtId|CRsA#`XX3-nK%q&fzwD&sf}pE~~*swgEv9-|9l&!oaT{f!pk4$Ag@eu@bmYkV7bJuftRP$@%tZH^kuO#uWsMR{5 zNT~-)NMJI(Yti#U!{MU>e#gj)nNHB$GJ2n;=gjO41;>D*p9$>-P zT8;Z6v55EiHyx)>FMT}CBK8n$R*PzF;!%{_RDXp#NjHl# zlIdAs{vfSWt?~q^xrN8t`vY*Y0Y;T5AYQjQxNX|#dY@Z_UAn($`jJkMox&Es3V*;E z&qNxUmdoW*&%7kpX5yL&mf)p^RSqLOc#IK?91J^)|LhvH3@6{XBm8i!HQD|2#J+*; zZx$QorDnZ{-Y!4GN>AvKLRVUIIDdX~V0|hle*{$Fg?n+Hk#wfwCaE4eSmhzfMNC(9 zue9RZ##`huA<9fAiJ9|8=)X@yi(tWHcp&$3|b{9>|#@%O~=^M7WmIM>V zmF8JG=Cpjp!IrH_Qd0ucoDox6F`ff#Odv$Uh0gnrMIaApsWm6DB^NvDYrZA|NdgXw zF!%QlDTsp0{2*B{gF{H?&mBs~#m$3mCl*NdoMT|t&HHwUYK7aqhd7s4u)veVo5$z$ zW|dQ`3vpY@S8?YFLN-%)u5X2NS<-oT6PnT~E?V8o@0Fl(e6MKz*F4ITzK&j_%1YYIv*5U@^ECOJ*y(bL`$C|P9i}kcw;LpwzEK1B4iL49~*X$ncOAPg)&}*$7TXL zm=c4+Y&~MiGmOAKUqMjJBoOQA)M5>oz+sB17yMo!U)vK|Qo(H6jGtiL(x|cp+2;88 z*)R|hJsm3^tM@o!I$c5rGW3$!rt!I7b0)CIJF}fwIKKuif<$ZOdXb_=h8cZs)rw`) zH5a#eja-!Vo280Sp|bj=YT&T5sh>?BjJVD+uoo5>NC08e_D3q4c zmoEVT%f@?b@(ntdVsRZgJzm;89K{mXE;JWsH}C16R#jMe&*ahTVZsaVZn@lfPXxbH z+-JlAgcCiLS)K_^m?jn08n8`tVJkDodzeV`z8Z!gKSscsG;h-Dwx9h@ws*G9JWry{ zG3pS^nEbZ*k|ljFNKdUVmkwBfQsKpe8vnoXwPE+R64jlTDd(Mipm4AxFd^ zSiqK3rlmrH#{6>}xGs`=}IKg7w%V5(hFiktw{&q$n1Kb9Tx|PK0GxLWofy>oJ@V?N`ha7KG#KC?5dnV zB)ay5y+pKW%+YkZ-K6#I<@mM}Cn1_yY7>q|`7!UGZkb(zx<#GETIkHzeKff8Y29@6 z?l%E}XIH+V+SF4`L3T+=%Z`bVeDjz9(neXXaX5@erf2b8D4|}fWb53 z!Kl->G+R<7OjV_d)UjDXEw4Z+Eo*=X>%wqdEM;RX(i6AvDC6tb{gnZBIs^zUvoV-Q z0M*m#{%TiUsqPY{_!VFX92S~;6mweJMRS;m<+w%~i~m$3;n;jLo%j4UNC=0-> zU-E@RFCHYlGfP$Va(3V&bZwNKnDBiHbiwmO+P4tUI$>6Wqp&hiVy`Lah4+69P*Cx8>rXhqdTpuph;Ch z?5hRC&LRWZoEJwFL9Q%@yL2K$>3i1F2$X>GtzQjvy^b#G+Qm*`4f8{+cj`Vavj#LPj=}> zaSUIY3I=*lIKGUvcJWr7`O;r#)m%$dubwT51%?txT9N_dCvcl`VxXpGD`>7K_rW%S zdAnBaL!5Lxc%I3p)(1k$AX23_+VR={%VGz*gr8>PjXo&hACw9baUE-jJRx>H%-;5- zibm~ZhtKkq_GJNr{Bs{c8&c9KhH+{9Z=mZA!BtqO38Vo|CYI|mff6Lr0<=%caP@SS zU9}L8tJgE;5No!V0VRsiGxPN0Dqun2mC_(6&nDtBl178AgJt$s4!=qrYe0X;@FRi; z=_C!SW5*KvF1?dCa6Fg`LVD^vIp4n3d@!CGgp+yHNgRP)_P_7liwlqhp?ry6_rY$F zohDO81>z6uACo(1GcF$tHa`G2FSyB6h6kFuoM^Hxq2nJY9qa%pE~c@q6VEkmpsODt ztK}C?y)#LAA$Es{K^kf~!>4=v8u8#V+&Dj7#(6!dFqN{PE02gLMd)V7Mw(zTwS?aj z;2;9A_c-%2=;bURMnzepo8x+@7~fF9a`i}O>uwoT^)R4Op3c6Jfg^ZEa-u})qQ{Uf zJ`vA#6Al77pXU#c<}Zf<*au`cU3d~m3f4^XF?6yZoLjwCrSc^DIli2>TsrSRmu#U+ zOUbo+S<(2YO$Q?hf+%DrO0J9*Q*+oY7zBW)FNZyyiAA&0pc+_LQ13A@ce0k-n-aw7 zh;16z>-Rt*ai=tBXr?OoB2X-o&;Y@g!WNsoMIob_Pj%!mTo07Weg*BN`4U=IsamxJ z_!De}|6;uqFh`0z#%tQx(g)KKw}Rf8Jj<@L-vkZI2!^MHs_bULI3CrZFU&<}PF~t2 z-!|VZbT2lrGBQB7PG^<3D-XQ^D0k-qEq`%n`vY`ozj|}4%8+2TPORT6!4Hscwkt+_ zr}vUBKQI|eLBzA&HwO$-1<)nHqLL&G6bcTylKhlPlAhB*HD|!~W@(a$s~mm6x2tda z!X#J~d=|1QX8LXeI`grE0t0l<f~`o?a@d0!?f& zQ<**yt`(|}J1aW2`ohf1XL1x2Jgq>(ZHLXi?bQOH9*Cz4eOrm+0InGDnzU-${vUPk z71dfCNzsuNT>~nK2&ZUk;-uGQ)&S#eASqc=4NU%(&=?un)34^BPNF zyIl#k2U!TY;$;?!R9JbqH%#_S9Aza;!D}YTzsp8lazk>ojPf^?-8(f6uU;>oUP*A= zewz2fqf3a39IJg0wX@awH@3urp|w1YH~5-e6m7gif#sbD_DO40W+BJXJ3GnwGFDTz z3&iO$%w9AEttpkmLZBQH?khvW6HsCUx1+!Ylx&rr^pEGTg!qiEDrTNGnM){t^p6JY z`I*Wo!&Zr2YQJ$+s2Fkgn}<37a_%`P9Z!5`@?jzxc#s+IiR&#$_%c^2+oq6dl zfbnzZls7&hzLz4~BT)+6qorWMagKAtw!|4d#PMA*Afi%;Ro715Q zf9#&K_7D!+lXQHiDh+2GDHHggY$70vfV0g@r(ORQtdA#hlZU3fg0{iv z`vvEC`;%_iB{G5NyX^hDLmB&U&`7j#=gj9=9M{9<&vJ3m#HH$qq~sB(+OX35LhXm~ zJ)IcbNz2S+YQxa!3#FRL3Uk~C2T#+pPtjI%4op1lmLJvg=6+<{Hi0SAbaee?8IpQq zu(UWg=+)`E<`%)6H;CnS-C2RpKziT}B)9XN0MBGH2cAe?*y*sj>$IMN<09kYS-1fh z@dsr059dqJuPSnB#;asPI<3Qx&-yHM`Fth&^Lr)dCrB75-P4VDNt`i@_5YYLL~bpw~HdCH=3r^f<@6p>c@y>TV`od+Kv$mVZ< zR69Aydjt3t%LAFhAOgglZ2t!3^9vjo9V$Ik0(m^XH5l9*EfoQLo!vD>6D0)_9+N{E z-!hhX0YE~L^{e>j!G6!@`d@)`!q~^=&PJ^o*gOO)+4`s2Z)?VKCTN9cYVRj7CXXfG zA87pi{!xhN>A^EUO}uknDk{iEu66XcRf! zqo2DAJB_|82n}99Q4|8~jpv{MbK~#H@mc@XuNQrnZ;F_drqA#lEA2YhP_ao|`~7pt z5d<1fBrKI&$I4$>v_7zWdox2YaBJo{Kqh^Fnf=BfPa802LWx{T_vt2M>_=djzl2C_ z0^SBoe8?_dz<+HN>vbpWXzw|o(OPOJvFGOST@E!{9VroB8>=vt+(tu58~`f$39>CZ zfVcGq83FsbQ9pantZl8LwR{J-fF`T@(BJoe13HqwC7=$y2-)8(B4;0ieNJQ+5zGnw z`4C8K7y=4$ZWEE~pB=Q!G2#VCNw7hGK$v@DUhnyGZg$FvObJ*2D!`FwJ7zZBUA~># zgBvcTFW*tifho+bv(J)bAs9lfF^gY|Za)F+Ti21|=YWW9uH=X32h(YYKi+KKVs?SP zeaIwiQn3k^?2LJxyJ3@`Ytik3N3w3CH$Ug7ZS6JbJQtm=7mU6Dd0ApwA@Pv$tRWsw z*qm-Q1zD6Lt7#%b_`$(E`6Zu)Xd*Fao(cVM8}LL*>6;w;SUD6tvpPW6?e^;)WUtoJ zr{{1EYQT6ue|-4sDd0r)5_9P>Y*zY}WJZz}4f>rx($%8SUF?UHK|c)Mniw%<<9hqsm8%~1<~aLmxsK)}P0(644x7`9xRp-wJH zC7J+`nuwp>w+nZwuh7_bkye0o6PUZR;2fj@TC`3S84a@^boGFQmOn=LH~8?Mzs9UV z!Qhc1pu!;v$#!8z&>6Hd@UU-*`3S%AY|MV2Wa~4)KKuCgR%Xqt(A7tU`}-i7^((kw z07;%Z<`xuWe*uwRr4}6Qs@+aBit9Xc2S#nD_(R_2!xVw6jSu3vK3=6X1)R2iN4ALLOZoTl`TI%iB)?}D+O&~_rKoYV%p`j zNvI@oNE-*tsE&F!O-0Q=2iq(G^H^-??`nYMuK<=1#=bN0m9)L~uP>Z9s?@)WDQY!b zrudjK4z~*Z+{hDZeky#dGaxL70}dJ792E?9et9Z@Kkg&cI}ru!+0I^>9C+kxz~pn5 z`0b_KKJ!~B>p5nrQ^V&ivvTb*Fs=$u0y5obMBrI|sWklY4JKU$uLbNifE)mG@b(P{ zKqOo}t94UcC7qD19XFF+?mNv6!4K=HmGLfu^DEXMcm_ISg?hpuNDsbfo>W?WYb7VV z+?EaP+s}hJ1_O)O*!UCV+MZIIJ)#L_Ffjn7Ti2PEMhPNodiAVTjmu+=3g|pe5{Mrl zu-D3BQ>p%tpgo6P$MmQPm39!%WXs0XaM=H0*5=OOwN%7uZK2Eb^@tR)4hjesAg~c; zTLNrrs$S3DZmPyrNKq>Gz`Cns0b_D=OlwvpnwtL&WV-{RiLz%9O44-JM!_)5*|(8W z8kADxW6vC=)g>`ZwhZxXJy5KuadB_9YKZI=68(f6ffm?yQ+)U_po+>mbiOJG1d;@Y z^bq`qTvb%id@}G7wLND7HztjbI{q#~xDP+5t9DUInpGkzBAn2n7AqbAqRi~w0+^$b zum;RDLvW|ptfq$+V3tiue^#(NMI2Xlx`$(Lk`f+BnAhF>^`RsbB86cSpdk3YhH5Y{ zC8k?Pv$Rb;9NX)C3u)N#v)m<+IM^Tq;DBC`;+nuLvg^%$k+)&_M~pM8Piy>R)dznX zg74*p1T(}XtpdiZ zlX1P;;Un{(`E6~@c!~~w2)5TO;+Y%4LZ}70W8Qc_Kh_L2)sG}UtQ#&bMJ7q^t(2W> zk@O~>AhA8hh78{~*8owO>w5D*t#%D807-t4hM$#{eswa{avpRoy&Qpr|9Noy%M`BI zHvL@8lo_=>D{Z;bw>-zsqw+pLwu{$(6GxT7N1*Fqw!23V6?%FF!0TF@ShrPa?n2Ed zvLQJVjQz~euU#h|Ck)B)3ZzCfZO|hWffq8zE zYS~!}4R}06l5r_jgYNk;0V40vOJ6^B)D)K++WHS1D}}FY5*1uNPjh`vLW~z9D>0p8 zTBMx8tp^KK17b~w0^hMlgygGskntI6OJ$)V38wck6A)rB1eh!_>7 zzWu1FqVm(24G2#_K5mxRYYyvgbe^I_{gH^Bfd3>Zx!-Qf!9{e?cE-_->A0WAqq%EA zE8v)6_kn_n6EL}Bwd{>P2Bo>cof*D!x|IQRCzeY#Cf*hFXI;82?=@4}Ek(E_TGKw9 zm~-o;?DgR|fqh8?5@Wj#+(TcL{Rdg=O`rr(+dz7#&ME!9yL)XzJ&KI-=ik@yq@wTV z|IT+O&fp|_w4c|cAErFuD{g{jGoHDI@zpGH`Yu9i1kSE*+zhDCx}8;d(=t`MWeRnrW0!Ju^_EHdTndExj^2*B}L z?S-hZ&W8@g%3I$PGV_tlq?}y$0N_)_(T=EBkNX;L7w95zF6UDtTy6!ztu=S{WvFhywdjW!)<_p zQ-+=mjPvCx=(o}+J`x5?Swlk%>(@hRJlY<$Rx4nhNkDQ9^L2>y_v20~Nf8xC&`0!Q z7LY4|i+roI_}75a1CPsnDZ~1F)cZJ^l8KpHKY`8Wc9oqL{Hwxj=$qfdaCA)%B>RL77>h%JPyxY#!PfZH*CTc&IC^+Kjwry0bFIllp5m=7nG zTkVJX=_HFJg;m+3Fe=WHw#emkq!L;Iy(Kr;5Y=jlI~ez zPZW#JHIh&R0eY!~g|kHTvyvU63oQf78O(=Bg9O_OOHb(d4T`&yV5lgrKv}y2x#nre zc8Of)l7o5mQb3!pb{#$!mJV~!9^ClTK_%(3q&lv!&dKr{f-k%nFPFq218CIfo^+79 z@;kcp-Z^^|iu8A0EPR3A^o6J-l-sO>$wzbqOQ{(#T%lIwy|RsTHDXKp_~@U|uq!au z{z^6o$a<%I%N4IMQ{+dv15DxsDDOToI;bCzuqt)Q&9JqydfDus$NRHmA%qWgfX^Ly zzYEQQK93>iPgwPaAW#KTyej>zpc^i@U~U7Ro~0IUb>n0q<vI#`QS`h{kVY32CUOR?_^dRxfSryH?1{2XX$)KT zV!C^`Wdm$8q8=RkSPy}>W9S)x$5yilkl2n3sKRK=IVf#@nI)GyDVkDQOHR`W-x^^#mV`euF`oN(>%C+OsJv7&wuVw_QTKZ`IgeqvGkCL%#_-AKzxvryeEWXnPEVE zHd>JTZ#XQM3`?kk(&B*1Opy(YBAyltfg3r=fL2r+2>=sS-ycLJlDUv&b=Z^RyVp!f zs%IV?dBAOq^fVi;?8X8yo*1!S_KIdGBZrLB*E37n6j45Ev=`=CYN1=&MRtM5E5JIe z+V?Z0!flY9V(zGtSNI>bJ5(!lcvb(^XTd**=6GUOn7!yL3(k*G>ysAF3s5dq4naOu zlDBcSY(R`;qt*Fq;JCz`j9M1Wym1Vhmyk`t&=k;m$_FrXKa855Y|g65@A?Ma$U)@` zqJBg09N^Va_!Dtt|W@7^cpwUJ52xoMxZV)}1M zA>ebUZF`27H=Np(V@&hV`ZahQ6?^O$1q~(@pqBIaZoa?N92c*-;}YL*f8wIyMRxVU z0)VM0sK5@*#dM0`Od~AlX;!d z;AWMyD&-SkefQw_@KLlC2>1DLiR|v3I~3tHG*5*OmJjKE|Z${Z@M;q~UE({xoIAOW#rVxm% zAC8d{h5RZ5(TleuAfUAh9}`DaVGr#&$a|?A=o#kdfeIebkVKPhX9Fz}Qw*up+;b_6 z{ZyTY0VXlZ7a}~?sZHkId7C0NMwl&b8bV(l_h>Gn2P!D^3|5tuefWW0%%l`o3u%Me zP^>{NboNov$*W)?l8Z{F`gI{wAb9Ah*`d(@ezvNvw+5XGRiS9_OYDpqr$T-6v7wfS zg+%Z`g16@!M@{-c<5DcNzS2?UO1mczo-3iVLZL2l(q;?nvB{G(N7b|XeC&M(W6*?mZL8#~|HrSH|Z^!WwA_-*Nu!|~9s9PSW-`*I? zg2N$#5~@8Cj-VRyt>|!z8>C6$R`HCzI%NC+g_sRmc~v+zoPDTZRkYZpTGNe!c(%MJ zvk=H;#w-O}0!fNLb;I*OJSti+2~Z0b&>Xzk80iiJ+}y-fm0AW`2t+GtdY%((u*3ez zgsSV63SBb`;!$olE8McJR0#ZU+FUB5fV#uC`ay~v^|z@&PhxGwW3px_PN7W+!Za_f zeT}@FBz^({mvO_$M0^XfYRWhPqy58#J&o2y~c z0aJWd6N?azIeI#+@_-oqryzDM8dGh;cGsnu_<#6C7`Kz4n0i>0z_+e&{mGeoA?UzA z_6|YF@d2R}m7mB_a=o08yK%x?`@5IW++IS27bI~I?D2z_L?FE|aGAg>N}mZ_$vE@E zU=cEv_&I)+xeC`9gXJzjcz88Xuc1{=oM>`%jI2w~n*W=W$TTT^fXl{Jfts4V!tNxN zb#EmTcAEM-kms7Y1^))x@ADn?p>agE-AoU++5-#K4`}OmG~}uDX%AN|De{o+PW(hP z8*=&oh`)%?H#oTSvkI?SD#*0mIt@7mc4O;lPG6sK=AMs#J24Kpuyjd+g`yG`{Rl$E zJ2!ut2aIAjQhv!2pL;x!+u(b28lHAHiY@hP{AY>h0!Xq~@|#dt-6K=d;0gA@e>zXY zbI*Kt%sSk4(?(d1`+ex%gtJG+6OJRH-AO??aKVg#}{bzcdQ ziy9`utF`pvSbM8flO4&Fyem#~KIhz% zKd6m6QaSr`JU|u>sH`vz^v>S{3GyVBCKnSv{sCk~i6t}`6zRC0r%akj7Bh5AB{SP! z8*;9Vk1p{vo8t0n$#qN0Cc_m2{%1+T_Mr`!KbqK8?x%=!eg1J=;%mhDU33uUr!lD6 zI)Dcld|U-doAX4GvX(k6_{BE|8hrrNh7#JzBcEBGFH2Z;CfKz1#R$S)DfvHv!4ZXV zb>V+c?&i|)iv6L6Oo5VanNuu`1hi%iU-t0;8KqhzB!MxB`;wum$Ft_|2OYCqIeeZdio5_|p+ zH1i6JwCuA!4-kv9UWnLdem-VVvc}#UC4i>W zd($B*OB-Gw**NxVuK7CxJ2TFeP}Op@+582l_T==RMmTIa=Q=Zsh{~5(*?(d0W-=Ic9WJ#hgl_N%UUAoWn;^HoU; zFl`Y>qlQRcv>P)gL6%R29&7=!v0K{?J{@GW! zx1AnF_0#Rn7lb)<9A(}(fLHu7AiZ;l;Hx9CiK%Bl8*U$Ir4m2SiMnk|GR|ids@Xu| z&gF|_CoRr{h8j51<3K{z^2b<34l6R~=OG!g<2DGek-t@Ds(F%|Us+Qz~wY z+N39n#CBgtSFfqdv}mOR0W5QTcjr@uJu*6_PpBhKL5H3bkPLs&9F$`TT zP>;Cr=5Fmhx?VWH`(%yl*Of{=6>3|xA^HUSb7ZVuQjmJo4YZl*IOk!+O09mb#^DPR z92HkLJBz3h?quGlXAsg#5Ctlj^kmQ}vfTw|r*-CRE4_J|O%*KK)N6eJMPC~%CE0X9 zEJUsKDP-+W66@5oEED7`gn#9UKh1NYTtHxhskl5ecTC#bArw8lLoiz4MDs&Pd2f&e z)97)Qcdyx^WPJueei$R2Otvei{RPj5L-0q8ePjuq=O9^cTT9pj`P6$TQaki^BNR*c z6x2kE z2xM=rMFvtym!l~lmfZ;~ER&E{3N>Vwz#LOm$`IY?od3nm_K1sU@sqZNUrHT_exziJ zFN8Z<3E~H>MDy)FRQn0K#K->jh&k$w&8ObJiF0ccy$KD2CP8K?643=*xpetD&%c;{ zN0Wf;W!wztj(fBH-i&KO$cOF_k{$xYOeo{>AgcsHO)h-=dy{gDqKa+l7tg`&D&2Kn zzAHw*D7M?tnW*^$)`Z8GwnUZv5)hEa8bU0 zyvP{e{jGG8i}s|QY@4;`mv=!1HswL`X-aOUKVn}$VjNUndfw89>%t@7pqVgijXFMz zJmJjSS9y5NyI4Ag$-FhAoN1(+wap;a(rseY2WV%GxT(|u2#pQ3m&hEXjf-oy!;Q>~ zQ$v`VPKuUEpoDbUXY<=7)v6S4Xp7FC->Zz4rgTXpZ`ETLJlCCb^nYGX;r`VNkoJ(w zin_ZeJJCy_2k z5i~km83gTQ*QGI|z4?i8aw>(8YYg`yLvm&8%2N2$%IW0ebLGo@EhTyT33OopPx6I8 zx5V361gI4)Vh874$y{E0$*MdPDC2Nvri+H$m+eYg*bD+BJ_Vtg8^3@L)!*JGG*U7(?L$(#0%5x7i*)h@Yp7&^G-i= zoiQw#8!HeERqN#TmZJT8Ma_|KN*)uA`Z$-H$fx$;>k@&K#E%-i>u2eH+gIgZrQ=hG}2%KauPjxp{nrieH#xSW4E(A`>+pQ!>+ zpKpYuy!mA437_55Pkg#3)t$f3^yB}r`8AuB{ii`lR!QR8Fk6UC(v{9JG( zC9h5i2oW%P%Cr1t5$q*(Jw@tXpkyw zFbY@Xd1{1Fo7>=?wUTrd zjn-*IQFVx8xx4CHck0{q4MgYY0Hiw6e;#HCrSsUMeo z(ZDeQ9^>8E&?3c$DN(mTHhf6OyEi1U*rPWp;WZ1P`~krC7v0CD#GR9#Ii>TJw`bwV zY2|Z+IN2@*8K}~@Vp7f7F5NQVhc4y{(xt&{bWIud6sn_ z$5jM?^#;}9H;2`Epid`!Du-vymTyMP=Cq||UuOQ5Q5#X{|1LRktNfwi=~YNvHKpN{ z#XTtl3dQ*vk~(Zc;UdMgxk_ALZV4nat2}D(1XAuV)8wx{;`XfI0v$~7VTp76V9 zQQhS{bPv!`t;LWa;1{glTl`J_1@i-{s1;f;$W$*)k|4=>DToxjRFJu2`j3=Ef-Yo6 zI*@s#&Fs7iIw%jp4&k)2K+7$o<#HReK=dZZ<^#&<%r%b-*mr(9gODEd(QP6f_C1K` z=dp*7u9<4?7-e(YC4H#EX=fdpC*0BNVEY5#S>NUURfd>?;qz-$c(^`{)*zcjjr_{O zN}=u&=M0)asyEj7$^Kakyn*$CnSLKto+vkD7zm!Hzvlkg55&TkI6r+{2Cy!X0)r#< z14`~OH`Arm#omJ@z$N-fnbrWz=l1YUsOK(aikyaQz{X@g%Nk@Casu+aBKmm_7gajY z_WxxAEpEk0gx~9XZ*SUmT3Z>v`klq74cv}mwf1tGR+OF|uJMDP>&Y=4$Hgn1Y&iXV zLXl17G$CNH6lo z7EC~v@LUQ$3s)m%s7Tr5Q51UuR`k66?07Nw7?(45&cBaWc z%OP4QBPce0A|?&AmSCUAIP0ost^fJG!-t+>5i>!xeD?PpQfO&9AQyn6Zy?1MBJ(u2 zhVrhmGy%c&3IRGc35$49Q)+M(%GB^5Ail+}0Fq1R@md$8K20`hNFw+!H+Po$(pc^@ z=-NUQ>{39NN%2zF@dU_EOfQ;8umjDZRpk=3(_@)ctMvJt1+np=Pxn5LZ2__ONZ^rL zE=k1!!>E)^8>9$mjK)eqXrWjNI<)Ws2rVP&$)1e^?cj0^{$jWJVDu6Q5LB#Ini>i0 zLS)3G+mrJKf+7|Tm2Xv|6y!krQY48#p;xe_9c_iOV}MEf;q~E$U5h|h-lU>CS3z$_ zh6+pg*|9bS%00iFNZ#*nOcjtX5hg%Fm3fc&*5p0)r{!3+jlrCzYA@e^%CUByMpjzz zQjm+Nealwy7A8=FZq2m^tqh1~E?)0{zvWgz>0=ww^B}%=QIWh0@(XfTAoLUjQg)Eg zs}@2v(USKj1ps~MA>Ctg_qa#@eTO1Q;CTiOW=KumQX(dGhT(l7L+c~D%l z-w(l+&#!hAC2Ulh3fOG90mzR3xhq&|2mbe^Dr`qx^96aQa?;E05E`4w#yf50->Cayd)6DI zH)!nw9^Zo9xsar`nVtxp6}kO(cXm0UaK_1I^m8SL7(Elk(N0O*x{5aZ)a&JbGiv*< zjku@%RGljo>ooEg6fQKkL6sp&_YV%6LHyR=ewr2pro(dOLL#|bo*{ls$PO?l<$G<7`9{J`SZPIF?LDOY9ZI?cSB45;muoNiBQ zzNU_`y~>OKdpR9s%48YD5Ws(n9cMI-5+`NgJZIC`Kb-|lI^N5=pgm{p`}F)b!G8oW zT9>*rBWU|xy+ldLNIVqRP5*Z%97FJ{TIa`#T!0&~sWisTNHpS{ROkGnyXrzzuLJk{ z`5*Utv=i*6xuE^ehuRHgIrI`6I~&Cpql?gi2zsTKVR56)i^LtO=Wcc9om(ADx<>6? zaqP1HDXxfqi^!@SCbm?D#VM+Z+B^mQj+xPZE(%VVDBK!ufGWxrEUAVUySrb!WCV_D zfvfjNTm`X+FK-h!!PPNl1&*$X*J-QkWxtq;4O>U2^{v0-%2b8T32R<-w7(NbDfB7s zs>HCy{VXukA-P_O5of+dK>COYL<@FDk=MUgk7{O1U=iQcf1}Uuf_|{Z5!^8_@LXo4 zpZj>=?w$NCW#{h-N8k1v!5thvRcZH1@+X$^(HI5ErI89|{A*-oS%Jat^6%G7vu|3tT4{IM| zqr-(qG@ZeiYI+fUeR!y+CYxV^dfZvOi+F`zGh-J;u%(sVL}KFIVlEA+x)+7<$Xn)A zSfdy}-UCZ|*`MKbfZzWzV2)|?eooQ{=)qSldLg{O^sWCfhJZm2dbrSaidZsTo-g-% zx+P_k6L2=3VYjJ;tD23i)<_{57CA#-TzpGpP93UluEdaUt#rJgA`XW1R6Q+xwBD^H zK0=cVCL_A|-fh4OMdDuQUip!x+p4HXWk-tjS-*l?d;MfJ)(DB?GPJA}hZ~-s^W{?N zmeM&Y#MJ-#^1DV%y#h0~M53{`@cPASEcgK~4dYr7_#5OVf+E;lGD6RiR65csjm`3Z z8E_EY{7rb#2l5|taP#*67^6ab*kq@-&RDcwgV^+UW$N()OG@xw5?itV_p6^N77Fn( zxZ$sPo*z$>TRiy;#>-4!lh0&um_ID}c{q|?-HTfp%vIr=CIJ@u-B=@2j~fxph=Xsj zy3$EsE{lTI@w>dCgA>F1|3;@`8w&HA1en1%ePa(Ds7PuvnA*`BbGO;llfVRMrTs4h zF3(Hse?)Pk|Cs}};b;G23?1=dds10nQEYZmywU;W`~ua(7D z!)baj(VB(D^7$Z$Aocih<=KNDyNNqB{MtCK54vEkM2kGHfz2r}VmxKY9|OBwpL?4+ zuOliW&g(*&_DaUgl?>)?&+%^1ST5^m4cmHd06Z(!VkY_ zXalg&TL3Vfetr~I7``c!{2TO--yF>6~ zKR1ToNrS~P{8gFG-oj$pZ7x@En%)}d4eCUoi3fpJv0pY5#TLb4zZ- zXqkAoM%sRO!T~Tf{n5*kM@+k{t0V?d8P}ld6-gQhW+Q0M6b}B%F4a~pcU5=@bOe_{L)&!fT+TVS*?qgr(&CUir zO1pC=I(x$SO`0k-|f+eZH%bajC@d#u$2+8v3QT;FTE#s*91d~ zaB=R}3k31vKRR5;!MUk1g?LS+3TXBh8ssteO`Ul<$U~}r(~axdF=gzaD4wA&q7@~)<^9>|wI(rbQF5NP)u`BW_PvVCskpoqZrXAZ1d~< zv{9a;KhSMK1G7oL6ZO8j6~)I~orY2>GEoHIGGAld2WH3tgH#|6YoNFsNKYy~9bvk+ zpLhJx>f!;?h>ZtW;!nsoXy)@}FrNt1IcWjZL6d6PCcV&+?-9PR8QV~lKlz_#_+J*Z zLv9|y4VwkGLb{sz$Ffi499`9Twb5-UuFCLlm-}yTX~c9rLUV2{kYf!YUES&sU}PF{ z{q2iv#DDXHQ+K;++5`x{Y_k5ky?z89f5TJFzb5Vby&oU{tHA~962h)4@R9)M_9wH> z2{2ZOe|(&O#9o+*eRKPUUfh2=M06lLl;o;q``8Mt#w&j9y|k0(*VKvq-v-yEFHF4jXktb^)ZoVO!*y@j&Y~Ej?#HSmBv24ps zdBuB-+TyjG|Ng;L*h7|HF2!Qn_h6liDZo^BRd|k(m$*n;3YQ^b43RkV!X=!|8zh$H zJi^CF8n=(7sT#rU9KzpUcpW(M0^$f6iel%#!6im$wl-G;EXq83%Z*rM1Bp%O`iqnQr~4MgT+x zv;zD^;j3lHI}v$^IWfE(*a(9wDHsL5>5;V*@H|HvLkxy!awNVm_tA1Yk`te&4W75O z@2amI@=;Ix7x%xX7;I_1l&rXUl2)VQaAjsT9GV?_Gv@!6|< zVGd3&UC-_8O?hR4PU`8*>o2-bKG{jmuxit==mO(1|KbYC6WE9$=6oB&-I0qCA3(Kd zQ|r-1WKzj-*wIG)GES3Dt{^+OI3s8|V}Mq9-6mir$l3SvrImD3>#z~n+V`h=p1Nws ze!k$kt(S|Bls_l&D37|=O*k#bpn4TgZxUUtfVZgR1t zys5Zxt-QH^AyLxPvw1^)XF6~;!!mGsEeor1WI95%B?hFiwRkYZh z9FDcVB%xEm{n@K;t)(YknwL>KdbbaB2Yk6u-7Dx+QdV~nG@Q$m^Z2FSamkJ87cD8Z z`}_B7nuVQb%eMB;?yz2_xfgciayYnlaMBq%&*7Bbz7BL(mchg-T;=wO-gugGRz#~_ zP2KB>@rcZ2U^fqqZhKsZrksLG#nSOxXQ7YAli3>VhT1y|ELk}0o=nbG_er?fJ4Bd% zaxHk}#A@khuvQEA1{eQX%KUiq^dq#yb+- z!?o(tx+2%w4Mo5#j@);x^f$!xlTSPWM{~5kcW(an;%kkYoJX0SO+WhYIKOg~|K{CE z`DUT71FV@F-D?BF@_RxD71P%Y0C5Hiavt1k_XoFJErY%%g`EL~oL|fenp@x>Uhdgz-mT=&+GdJR*(s~g%B&XF|i6-+UB+GU_AL%5k7&727Se|S%EV_toNYy z3;vn8Y2M@-JSZ}=w~!H_)>r=nG;pmtioQN(8L~2@DkIYYF=o9{Uw-@Obs|8y^Wz1p zVl}6;$uTp7O6a8V5PwhHv5(XS=)kQJX^RKbJQ(iT{tGNnCM5V$P0yfb!#o*P8K`1P z6H(cG%If_-ac8=g$3?bjmI2bvSw68IYnfi}r2+Y+W*htNvrMa+xcR}!7VwcfEw3YI+n_Zk>t8W0DNM+=`Ne)lF&@EWW1F87^KTsQL_knQ+PlU9x|PC_AsUC7Wb*(KM-M7R?%3lKrEm~f@Y(xo9pvA%-IXJygK%L?8Un}?- z%|2mW{k(tpATE zK38`@F!2%)^TxexyA#C9UC>td_ath+8ZHA~qdc%74VN}*hS%Y&7M6d28v4tyy@?Mq z)!Gkcn>U;KW_kj?Kfe0%>N>Q$Fdg3I;1RwXxAF^+$Z8d; z4c=yYD*1w2E2sK|(KC9Ne|Md%gAlJK`&&3Tk9(tXn7}xm04bsg#(LWUhNrZJjr5fJ zyNuhqm%3GeFWZx^19wt>s7o{@-cTq7JfEk;JWcYAte9g*TS@7D6XScOz)1BZRur+B z6OGh$=a+KcXLy{Av-@Dg4eApAvGW9Nu2O~a%Ig?ukKTh+pu+e<3F=2BUaiNYH>JCo z_x{eJ7L!?nx5qtehP0V7z5}lAz0H;#-L(_8N?M|kc=zGx1~%d7tm z1u$O`-=W-&%+!SKj1EyM0)Zugp3&}SwOUIh2^n?eJjK&n@)^Ir0$;xyyFK^(Ebl%L z3aERl(@SEDz+e9+fukb8-go`d&R%fN2^;NK-Ty{L2wSiE;I1OIv=#ohnI_0AXz97{ z{;u|x)wLrkS4qb|>$voHrbt{Sc6rqR!E6=Lf)*;0B3_J~EE%@l%{Gt)aEAI}0RRf0 zc{Yj6_fF;1YR3P#kC%ijp?2`p#QOrESJ2kC7 zi#>NBs;h;0{qj@p_MX^ZCS0wF3NJKWq7`gzgUi;MCP1yIY+xGLb0^SC!5aKZ zC8=}>h{vR1}rKJxvPu_ItGN z3)jBM2|J1-DqBgPnX@C0VUK}jJ>7qV7>2{F^uG-r2O}^0j1w?OZ_YkK1@5S(HO~{R%AC9)>P_j*^D|(M3^rstWAh zX|}u&>Zj!>fNH$i6MV3be|!f2Q^s zNdyS40xYS-i*oxxQewTx5B)q!&3*PhMSSR+taxE3MlT^C3ZCBb7UFL&a3x~Hw(j}E z0@7fpm2T_Pz8A6vs1Jl1=*aHx;hdANEdYtJ%D&#!>oP9~c*7}Cysk3H^kJ@rTUqV8 zdcGFxoi6v<5fdkHL1`V$t`z_d8v;W7oLjt8L%YLf6i+dhDgoU0I+V$Gzwj@LIq(kjHqws2;MfUFBx;S4U4?`n6|pRO09ydqK8pruJeQx`|!<} zCNF?q!OXXBU5}_1p=VPR#CH!-s$`Vm#{fd+?4O>5R^lE+p8;!txTk<&_->c~xcs z3g$y73hI2qe|4T~1H+~#Rj`*qWiKd3!bT6lswAeR(I;7-{7jSA&0t@2!aMSAtopy> zvz{|x#wWXlPy4z42^(6lv+UU{Dc_O}9%}e9nS&V{LzKLF1CG^G(SnsXNE;$@tmiDI z^1jlj^YHfB@8~EClxl<(Z5Fr*2{??Y19RG!&=QpK!!j&rfc_9AQLqpcn;NN&_mF!J zY~X0m+yVz#A*Gm^-;mZuK6EG_Qifrp^Rqt`Zt3m%bN^bhrd!wA3~(T$VgrK(8BUza zX^ObMTv?l};e)JY;pm7)2sSzpN1MwTE#+OpHumwp53~{)wybM*wK>@5rl>d(%F5C& z+J*c0K?6R=~Yl?5D6~qTQe) z+Kp(H-WQ~b5S1?%4SG|NWN`jRxAAWfog`G(o-plCx>d-@d{)dGXhF)8BC2Z8(s%gK z+UV!XCf!pgZ? z1X2w?8|`>GIVQ@J`|j)1VnlC8<_#{fvYIE!EBWg+!@BYzB)rx?YZ$ZJZIKIVQzIlD zi_c`|7z&OX{GVvtWIS4vKSz6Qq5A{tf#6f)`MEO!V#!srcYaNm=cM9p$rp1kTNBX}*)8G+YBnRFR|ka$z9r8EBX+MULQ|?&;tQtJ~kMgcT&iDb8rKv3q=u zp8MKpztrR5sZx=2smV66lC&pjpB)n*-X)s{|53`B^Fipdhh?-1{Rxt%1&d_I=*m;P zXZI^+3%tO9O5#!Gk-e~p<2n|}ean4@&^D7?e1^mPW{+YJqGs`not!MTa@BO(~pAFV@t#R|E zKPAy5^4wA&^?ru@^odrQl4x;E<0&1(yP`>AqAMWLD5dLcA7c2qgSn;cqNkf<0i*n@s)phC7FdhCO=O zYdY?F4w|AuTOCuU)}alt2w;gK&?C#f%rNYP&Bw641Zr60o7a}eTjVvp3jBZyt*>BGR0nqk-ONgW>#l~T z*KRi?B`}MJT;0o7P5YjVl)9J4i@Qu^%`o?)Cs8ldo`{YZqUFxsAe%azP8h7@V2h{p z2v5fK<)8Z&&D#BR(U{m-K2REwRyQs1$q9eL;=j9OLuh{MviFt;8K1pe7rELlYP08#Z0YBC!)xG>G z%Z)j)INaL3@bW#z4yO?(Ebt?vA;)q@Ffn>q@Y1$8AyS!kfJKozGsReyvMU;9-JyIf z|AT;RwY|45AM#oq^~H0kWzpXTUwv76Y#UV)4bllFN8ES#2#h3Hq}<<}oZAh-Toqzc zI_?`FOZ7#5g9mb}^?MPqgU6$eMgsLv<Ju)JRkdcUf=ga&4eD2SE ze~;hq@82Kae>yr2*LA&~<9t2G`55O^{znhJ1_loU2NP}ka4kp;U%#qL(gArTCLJE* zWYh!#e>4T}*q_#|Ni|2-KNH_NSD-j zKG#a$A2X9nldEtQ7z=rJcYEo)^$jiU?HaA&i8`GZV_u$Gx1WDHPM*|B7F|8VKd22@ z*P05+r>mHDA!n|b>@oQ1F4XJtp(Z$9i9R@gS7OwR>&J^b#c88(cDZBTGf94F&~hq& ztRubx!8!E&`%!WcHdyN&HI7kgAAzbzZD-xmBXn*jGMsizu;<;%GE8e%%8S4EGX+(i zcRg1)%kem~+yUDX6gQ!_Y#3?MXpC&5sfu4WX|n9IUQpI(DHwOnO6wgwfjRIx$Gyn& zTEMLe=RluZ==!Bah#LiV9@0b7I-UUcvDl;EFVil%RWu&Jo%Oq|Q5_!|i%=q(rXp0L z2nThtuFj>P;;4n&x`W=*Nb3$P;kF57S?Nt`+>zFldljvQs+O9^9Z!At-I-N1o7b7AU0-_GYU_5)P6|tIMeWwzc<{ zNg%(h@N3$CR5qR=?P*QNw4UUI;~woXB({Ct#kfdmQ*{PS2ou=+K4VAubhdDiVynLQ zT+RJpSk^6;O}GOaj6WFL-^KNOE6j(00sYHXA! z2H9^E`3Bwv=g4*|H=kfqw{T&9-h%0+!Aza0nN|BVTF=*forREmjIS=)Wr@vhX!WSf zV1W8`Q-i*Rj3h9<5tJK(o0?6$j3n6v*DRMzxE*sw1~A@TTKj51*ep-G2{p(YqPJf?ZSoQsg*aZ{QI32+krd~>`@9A_sssW_*lh*MxEyMRX+^A zISbAzCvc#UOn}0ulGi5<$cWtD}MI9(9 zdKJ>~<9sMU+8ngB{501N0G|Re~feHKNIYzf0BJ;WHR?U%wNuQis9fDPHG|aQp zd=KgsC%^Pk`;LL>g@og0_H9DDQ_*jeh|k{JR``;nx>kW|Cb!~KUXx@EuV7ea+_?2? zX6*P+k6>l3=*k-3HIasCzxaMRKk%l@0PDGlS2x5`Hr4JOzq%h;|FXE~JVC`}XH$%b zZczG!C=NOCSdP4X-^5WiOIP;*u2Thvjs0D2``5$5bgC0@t#G3IpJXf~*%q@%BgeVI zO_#3if}p;iO0P7N5Q{3TCnlZtwpdI$OUTnO=`MI7X!F`3_x+cs*4mfxN+ELO@~9Sr zAUvhs0dTqqVzN*z(xnBzFM3TLs~F3%IP_PxbyUHcYZ+uhBokO;rCyBhx(aQcz92j_ zLv`f?hwdboHJef|o9oaa)!)`>zupOvkkE~m2Kj5j9aI`-mHyjluWL6Zqc7wJSVaMuDwh87gx^R*PX84XPyr5Redl_Ys+ipr>B zeFt+~jk6g8EYasU;40#K?fzvpsp;!cce*%KW76FUGP+nVa+Tq}I;%>wRU6@PVW(RC z4=ei;ySc+o8ck|MQ zzaI`fQ4In;rq-^cqg%N7q>g~W&UGi)>`emQBt-e=Y%$_<^Wox4KBAC$m)#LN&fC+F zq_nry%GBVXo1rtcva5-^9&{=xuk6*rJxOa8(K_%wJq~(x>+7?dYj4EJ6CN5?((L%T zk$64&o)MKe@^BXaS0=995b8EU^fA=2hPhe>zme0ynS}kII<)!TyLk|r<|4>U+0nZ zZdp7(#q8f_2QK*;s}k?qkAJ>QyUO`(z6XJOrjy-OjQ@dqbdw`vH+)m-TI3?QqD_L8 zFMnL8P~us;1_|A|S%+%1a{xf>&#)iQ1 z#wK5h))JbTW9L1M{XOq+NOL~X4N%v_%b~K;e`D8()ydjRD})Ph^MObVT!8J%qSQIi zJce+oxWN}3PtKaN{H%6H4#UM*bEUb533v(Ksp$z@x=`d1-5Z;Qmhuu~W8eW8TbuXmkT@18Wwddrns?na z#-A$^*b%F&9{&uKCYK*AzidKDEt^$L2n;i=ArUxxxsDIA)w}h1?tD4W!05N%MENN# zkQF88odwV4dzJVug?lLGkLiFqJY+98HOE94V$LR<^=c(I9-HnCHOMsg7SyaH3@^gd z^DV{5UI`;O_Xq77u?P(li@_@oBe*)S@dU{78PENyX&;cW^8nTs_Sg0Hdm{(n$3Mp% z5+xuN(7@mQzDDN=u*9zAO|(_xpGa&o+)_$|VI}ZJ@4b=Tg7_lEL8m zg((C#c;5W?_)uu)1N?vQvvBj8z2x{cymm70=U`I_)~umeWgn{b%D6r1g%!AMObf zClLNxaxB9Yp(Gud?Y;{Lt#Eyb)+!QHm|pq-F>b0kNoscbhe3`zU1~O{-6jUpa1)Md zl86|pe<^cx*zzj8fCmTB2C`ZZPCUkX16sKJg9f;4a|Y^n<4KqwrUDD--YHSHb&6L@ z1Pw5imK6FwrWz{m*vh@a5xR4p zNK6W+<=A6-j1Y!?Jo!cp#7298BzigfK%;bb zHG&6C$|Bpk4&h20fZBQCN<_|r`^b=u&@@qtM@3@+nZY=)Av{}*uE>u>euwnCmx=AI z-7QlJyg?X@#5=Qs6|deRya35dk#Bc9I{qTL%H^UL542CUobWH~6iVD>DIv9xW)iQY15 zD%bChT2Ad>%RaMm3ur5znjOGQk!QUDpr0e@#3wE4S$%>-!Kjv$Zx=Ki0+8E4MMhQ< zD&vHW{RsEw%31!PFY-TL9i`z)FMR0Cw=rkuxqDO>Y4etj$RSRy1v>QjH&}fLwJH4m z^2p>Owhq{URdy5rU*w*-c%$e@rt0-44)ydwBJ$S{aU)e`U?RrL>L=alldZRR{$cP4 zZ$zx7>cFnoII8oa{2|IX(k4}iI4H%g5qMD?wdZI$qH7fw0^m``7XZJ1BJ=g>FsojC zPb}MQS9mJT1<>>BZu;LI;E=+$LW!%_86m61G(rX$Yn7Mxl3hi2^yiT~)PsNUge#(E z+O#rffS!x}1`j#TBSB$Q!xTn@Q0o{h54*I#9>bCweDs;>Gg_`vHDTp4kYhVnWgxbZy1 zO(c^XZ?n2N6HowFCy+&)J1u=ZyK&*3;AP-~`7y9N>eRJ|IORf z#PuO@M6(9q%(}+^h6wvrtq>3aWKag9s)SE(Z2lDIr>gv03}Dz+ipqKOLby$EMb{Z( z!Ldm~k^BFJYw7$ch;OrqX0!S#JY5eVgZ|AO>$Vvx&8PqGA7rI1I#jMVp*|o;Y{IQ1 zta+M!w+X0tBG1qx{We3z`Gx;Oh_F*n{}Ji=Fq0b#60XzUtF9v5BSVk=GiG3i19^ao z#6xzC~|YsOSYaNRALc}}%+oDExbbNQv_QwF-W40#>j z&Fk{oC=j@T7=aszLT2cBP>GPYO$R!h$DelOh>DPr{}p4?jK=E^;A7z_8hc|$P7nVV z!~8BC>Ti7KI~w}N@YM~ZQBn_{f&T5Pwkg9L zi@0mj8A;FdGoal+BOg-R)}U;t>~ryOXxNZF9l1nDkyW>mk%BG>G!6ggl2C{)sc|eJ z;$~mED6%Il1F9|s>>$d0l8~7Mtt*TRh1>6NfZE7HW#fMJx+%o}Lq zj${!%qNvD5x*svDBX{<{J{ZFXKjyK})Ed$70l4V8E>R&CB=|iUWm*|e*rpe<|i8|k0ITuXfqlxUSWYilt_cdwB0mW#z`KHeWIH6`D2@9lj zQGv@q)-v44#et;!47s88UAg^}ra|4f`2{L6oo(siQ9!u4qsWYedj%C^(078?NSm8X zpJ)u(X-*_FVsoe)ks1t%P_JdNE77@!zfin+tvY@er}Xq9ylJ8|7AdfEF!6-?9^yCs zUe4@CNE=Wk!QtT7#>U?D$jN|L)nfBeth`( zZ)z|JV<;h-C&Q6?nCxP%h@$UmC)E^kto5Z8@_8nGqO&!A>-9yAaJB>ka?*YUV@r}0 z6I^8tw>06=wne4bY^l_gk`yJ3G$aw8>5o_A_&jbi9n73t8s#!%Cq>3f<0aP>A9}&G zD{Q{mQ4b-t&Jg!8ts-oVv(#pB91vGWi67Vlex^+mH0Vr%&9MiMQP#AZw~}-R4ZI%3 z#<9-Ox%VgMEG zfyryLb>P1+gz}HB{YfZTYqJbQ-pd-Ny0bvsL52=Pd=V>rWu6h9acUA7w)<<9*LfBL zlzi6)I&HAz73>Md=%C(HF#VC(Q;d+eDv!wKs(t&ro{bP0te)AV4GZI{U($P~E-U4X zo2hir0pmYAAEBBXDXe@WlJU}m94qDZWM5>O;X|e?d2MG3@F;XJ9nTiW7 zce-689>_ljJn?qrGHR0qJeJG7U&$j$LGz>SdX-%D0rG@}!GN5SQI?>1a_E9ei!kGT zTB1T2x~KJOt}^5|K=bm_aD-VEdU|n(B#5_m9V(~Ho;rQL4dHC;K$#WF7?BmB4l!3s z!)x~cgwXcqzsIJArdKi2gKs{8aFVoRMaNW)Y$JU5-btS%>+rGVrf`K4ToIbe%-g0iS^+}xDJ(HHGr=$`LtYxEE|&3aSaZ+VB6X!dmo~x z=Ol`YMqd2-7HE8yeCsANyhJ>fk7>`LUXPqf z=8V8e{AE5_I=*!h<+-ICbGr2^!N__t(e#avBnH#Yw*DH_&|PtyrIL zTq=FDjD}d6D|kVkrzD03_IQSOz%2pt8~dYOX!{;=kl%z>euFxIzX8Q;QQClCaQ9F9 z0aBG^q*~JKEd1N-HH9dF5Rz&?5K(=A0CBMf^br|I(og(&`qx2IEyKHGtF_Mjjj~l| zTx?%HRS1$ZX@AfLS`g3MTk*E(h;r!m1hMsklF#(b+!?cNSc80Adkqc^+5gF&j_gz+ zYY-zt+!neeNSZ=^N5AHip-u)+)rZePdWs(B*XQu?ZO|n>$x)4c@GIR07=X?&c^41~ z1;Q-u10tkj2qY&}&p`PhJ4CQ#G5FNsflMD%;<1-+0bR`NK4`oVJ6UW`#o1;I>IG}th#6c)VEuHY-&@L_4Ndm zds;;!@J1R@eF0y~PwVx~C0;Ez7t7j_4>99Z zb26T|&F^jx$b4n?z%$@qkfkzZYr8quh6y}2x4Oo$lF%$x!iyF&LAW=nztAjadb5An z1KaYV>p!oU$f1MmU#Zu>6VOS(pC4btZhtsY9svUsYfOSe;J5XD@7wUZ~7<+UDD`J6a>9Ap~HebIcj%^9a_~2rp4Tl?Jn2OYs%?-K#oR zL~;mDQjK0pVUXIdUc?Onr!4gvxFKgeQrS zPW?*xs{n-ieuQ})kp+ddhGLX(s)CO7r04NvZ72VN5~C|t{y);rap+xWln&{K^~q01lI=m@CP;J+QK z_{R=)f{UM;r+9vAFCzoQW}f@)Ag_~P^#*P)qeBQts?4{6Xf}#cxN{pf^kDJGXGC~v zSwWlmP>^fq0LNwrNaGq)lk~QOlN0**EudMvJ+&E=+d`wY#*duDdCXaW#-&{ZJ7RSF z^7${)g9daf3FJ3K*>E0}kw#PMM|1%9Y0`U(rd}x>2kpb3bU7VIi8#U=7%h7@nR!Cz zFob1+1s$*-Kr{t3k`Wo1De+PPnl2<(0np7wc8y9b8zi}~EslpF z+v5Jg&>hqG^SCOfIai+9-{bFU_rh;cI`>kujQ!3gzFsR0QzL?Nwkvh~FeC!SUOPoU zuuOd9^E>oWg|rJ^cMm&_rb$A!@)VBu7wK#H8LV$SOqf}+wzRuL*N5x`NDAgq_U&2M z%=2-2gF0AHlzjMps-;bi{mOfMwApJ|O+2w82Mz9~gHs0yyTsTXrNE@X`c6F+S*ClW zEovdWLw{I^$)HQ(r7v9am6Xu;t|ukniFuYUNOFQKMo7X^SC@0+eXn^T>($_Q|17}d002ic1@kdX zeSeQW|DAX>>XH@Qb^R4td^d z5w@yUY!reX$Ztlcr45oD(Cu_Jgne$)83-$Ocd=YksjbOTUs>v<_UKlA*G2s+{+h|g zrLcUVj?q~pL8E{WC8cZp>!y{sdy_# zx;Q;<{(jdTLXp+y(tX~G`IWA>BUDzX2}6eOYYZN$p2oX~myIKWLxO{P+Qw7$vZDp8 zBAV%ms=P?f_hN*xZzImzPUpU5po%yXy1;JW-y`Oklte`S#(+pDJ3Kor%AffaS8A$C z0r3I55nh%J4ngI0|I0o#8mU2c4Nk9 z-`!Vk!xG@eu+e}7wN>lc-=~d|B~@^I#2*CKk95aG*encoqg1*m$oYS&m56$oGq3dj ze5RQjx_u!Bj#b|s0zUl<9*#+K43&vIzrZFVdgUIkoF;SEyi1V9*2U{ML|4!@yvK1r zIqsWs39K9P`v$!b4KTWq9rrbtR@bwC!Hu9m=30j&2Wuii#z*Oi3)>66INmCwdVVG$ zjUtR($T!<1L1OeJ@6WrxJP9*)G|ut(rZC5#+{a5E2ve}@(dxTPiQtfKuupwuRzl^H z%Y311o_SE1MyraD^H?a7BEM2y8$gAe97E4mDY8}4MZCwHr#_IHeWm`2IN1AlB>f$+ zOGi|A@#S`0paQ)waY~pH=jrdaBW4k{+2}+u^D_~uYV(<0i69?w?q{zC}n`6rMan=CGc9lgyWdf02`ji;j8# zFEVV!Pk<%qiz=fJT~fR$X#}1juFCO>dcL&)x13c%aVDdDN5VW=kZgqd0x!JL223^Y z728A^8Nyd!U|8`4@23S_KHjG>)#_nq&s5claF%_4|6uV9`*RsTlIQCV^%WS8h~gQn zv^<5Q(91TIWHZiyuNhRwxVQff_uPWW-=Ar(%{Jyi=k^xxoUg=6vJdXbWCOgs?Gw39@uw?)Ts6`ik;LXebIG|RSXbOW z<%5#p6)J|djc&u7>;58t!ds^Cy@YG&&vA<(VQh*a_Z(80tI+{)A;`&fq1!V86c>QlEFOp?!jjUX*Wt#KOujEZsy@`y( z<)=W=@Pe14`KtDPUf~_o-7lB&DQ%S9`JfYTy#!adZzpHo->k3hPxwl|;Ul$@u&}a{ z_{k{x(shh-3Ob_P3H!R-@X|vzItpPW9lPF)@__8L&kuX+8V7}Ew}EXtTw%q|)16jv z*+x8IK@C%_QW-JU`;*Y8g8}jvQ6i5oCZW1%H@`jHCT@uqnj2YSn>F+9Kncja9R9&> z35~L1r(_7Z`z;~)PqUIpRb5c_Hk#M)gsJpxC1T6_ixGg(ceCk{~VC?cl$mo)9 zCWBiQI9a;bNj238YUgyaSzrffSI?`H%cz+S#q8|x)O3wrfh&8BJRRK9wC@Vq-vT)Ry}{ov(g|{HPse#ZBOa}GaN(8w;g~C^(3>ao1%ZP1V@`o4 zo|H#_^HMqUP_6_apF=m~nW*2C+fl9fp`ITX(M%41ABKl2j68iM)OyZ$rD`kh_BiXK z1{=wh_2v15M_%Z07J$t9Q@4J-{yP)HuV#D0l0QSTGq-p3C5>7=i$dwdI*Bo(k+J%R z$KJ)9!}qy`V3dc8Do^$($Yb#C?$q}T>uBP!6*zLwz3v`W`Z0XQ<>|*|)ulEQ|H~k^5g7nrU%t3Oi8L<|FP?6zHb8{$GiPr+M-?tqH0^m zhlOL1`3u3jjdR+;sBEhN7Rw8{2Fu?97!S*Y!$DYq`m9T(Z5b z*}e1v1efMahW+a=+sXGz?7Vds;~9&;5Q%WyVxA4~oAtZ?P=u}T?sTgoF(Nbel#{CZ z;z&3jPr295!1J>p8P^fKk0i%H@w?fKC_We-)6RHk|)>vS3B#54J?~Y1*gAUNV;)%t?y-Mv~H3{ zhHBZ`xqPjvI2z3KAoI>r?|I1%^XR~0CZ=K4T=eyRJDb2qFTJ#7L%lvd_=PrGF6-co zKWBe4S2Lj=NEP*u(+BHJiaE97)yJ8>aTDvj5Nd7nB|77RPmbY3JIXfysI8#dPiA9! zr#j8Kl2Dqms)b1woTCZR5mw`lh0M_^K}&~|pPFd4PKfOBU~=IZcJ?2PgPq?87`PXD zyVvD_RTyfHpLK-j;UdLmw~#C6vF{8hY1eXeg-dX}F?i(uKbh@>aEQ_fH}6m;9>YD{ zZeLZW32Sn+z=EG8eYSg>kS*-xdfFBx{Uu?K`l9YL)BGdWi~Bea%XF9zdznIHgM+M% zP9=*l6D{?U|LqUam?z6Scln9^!%d=A9Nf4szY2ux{TutKrd&t7Z}bP$F6Skm?T8m? zSi51TSiV;GTPBbm=VtaZkA~m!4A>hN9Vo3UFPHRmU40n6d~GbuZVH6Q>k|~hW^c0i z`zVUO9(lTq&3H-^!wc11T=H25b0Q98dGo+yFXm}Mg*Oc!aN~FcEj+NYXC1)ycJ-k3IV$C1RU7G~ga*RrvySBK`KmU|? zl0wsx&n_Uih%1uy?kqn6aRTb{MAcnG3VV$R_aWN&ae)E9O8sznpNlT`2=2m5-*4(qF91n_Yp$Kc4))>35QRyN0h}s!X_@6NWJD}5y?3hojE7uCEVtiOdA!=R~I## z$4|dCzBuxARD;C!$>wM^m(sXuY2a41B>h_lO@6I$ocu>Ek_sA{WVUlJEw!IAamp(D zys&n79sPGxK_AB~(13{0-DPYo8K;x>TE?$jV>?~qs=1_q2F{iBf~4CbXg?5UWIo#D zUN;$(-FmAAwU+!myw;exYiDTyUp=8rvHZ#Q!ih!R)&IEvB3_X*=@sYE$je z8iPEu4#$~HI2cp%OFuvV+2Rq;HGZumIZtKq8l){=J4n|uLLqbZJNb);m4R%~;DkHd z1?Rq?qWIey%-+yP=1DKY7A>Q5AY#bh)1elfPU(uO>6of_Z#WYIg@T>hg*mVI$cA#y z)>lz3C~K12Nzy9O73r>i^Q!fjz{itpZ zo;j#}g;m25ioJ=GWK#SEhh!nJm{iVRS0a`Z<7~K(E5vMb-+ObB(xCY^afT~l%O7Rl zB^C#qK9#(t?cMs5;N6m_=n9;~h%^ZL`R97vQ(o|Vq~g;|8z0?xO?8}p!c}^351bOr zM11^BGWcwRFYKt>KFuAB4BYEUlkt4o8~y&GG7)M6R}nAp?niEJrokz*N&};X1k3z; zXR#dQJ3k-P*LcXaKf5QmIh2|$)sdMmnXiOP_a5(JbyAM#6-R&Sy)JCJCzoks2j;;_ z?-lE6sY`?LW2r(mB;i=x$3-|7BhY7SIBNsp z$Nr&|70+5wqkk5acx~IH`7}nU*V6WFj_ukLcAt0^7?)_04+liqcxU66O-qb6%@sO{PxRa zs@Nd2V|t-~L5CqWo6*7Ix6G_+L{3%hYQvNU;T(C#rTNpXMl%856@P3jj@wdYH_+@m zXV{+~sdv{S%^RAK#cLHMVkS)_-Pdh*8HmsZ8;+!I zKKcGt-_Nqnx~~1)>ALm8_zH<*86uA%3cQ!vf_e1QO79JZkX(N=_L;Pn9BI(U&FU?G zEDN)1o8Y@t&MXN-jVkW3bl&9g^Lhdr!R8Z@%dZ;)PY^GM@O zrtl-z!nx!bzCdQzTp0%&{dw=_flDY+wZB7jAQ0OxjPtPiB6}(mh>++b$FtMVALjKf>Rj6<%QV0L+M3NRB25wik&3K?R1^zsQM+}U2_6uEw#txE53qNY)|V)`5JZD3{uJ} z5?pLh!_{fJxD(7m0riU)9MPsvzf;!s+w0I8w=A!V z18u>l&%1&?CnMh<&}MZ?X_dF_F@-gju8k*KCo@Ms?g7-K8OhLCkWu?;<=PL=cGclH^=h|T?KZjE_;%9q`^ z70|cRAW_T4kyS;ay~ig^SP10ja~-eCe751(HLgelF(9JZC4bd+8GI@P}e2bq?fyrOM}wka7dtK1+d%=#SiUFfhTFqrdOtnU$^j`g!nLsn`^ z*5ayg{{jG{$w28f4-Ovk8-6dQQjGL6dTHSeG00(fgWo#S zA2TEJwzK-20tP_FrJiCD{-hd|Q_QTynAPw(NSaDUWAq8IHHZmQ3umg)PlGJYh1gY9(Jv0w6&R*d72CS5tKV;5CK z+#bbIxCp7zlatzUjhipN;qWU?-cGZu&dvMl+og=XxRvsZ{JCU-<6;{Xf5|Hm z?^KHup6%NjMjttkhUMv?E|=5h?Bi-&7l?yjJffY=2QKMA%rhs0Fg!SP#~T~5A+Q1P zE4aq_E%suhvp_+L^UP6qPBrQ6H+wO$9q_;4(M$Y!DOup&R9!B%EMcA|nl~v-{-CW; zzFIb^CC55T)0-5P^5TX;PzIc{G! zu z*edZt05ZCDn9qH$-8=0`g*k&5W^*axYH;lqc+gGsa$ls&PTTj_>&44TIq^v%c{8le z14CId}UCA7qa>ENqMs$>CVyg*~z5R$K z+IO{vDm$q^%o~cbOw$+RTz*00>b^0`Ovf8Vl)luO8vj5ZtT8UsmO7!k$c*1iOJ(Ih zm!3%qn99^O9a@qgbjzF6%Vav39l-vFJ!)DrJdC#x<~53LJ1R|hwjvBh$YNEBSiNQfM0?t9f zN*XxkpB$Oc3rd8EC%AL>&OYGIg*ui#c!5aP^aP1?1e82+byPe&SG^sADHXiCs@ z?jYmP#yq-9b)2Y0?hwqXhE#}WTh76q2xtZ8-py82>d!IVv3^Ly5ZYGSgkU$DrGdp~ zWP(U{)c2-O55uWOpHos2Pzyn_gbm!k`3`ByP}}-P0x)I=LsWCbi%u(uHrnP@H{cI7}tQO4o)* zA)>D}PoRXu354{)hpmk*iPC=cRl%$)+%$NzlYHE4>okcUIAq_l5RKe5T%|I6lPWeC z|10q_CrEJo#U#a5e7@v=;A+Kem_kez9?cb!f@(T?P#9oNoJ z?${*s&!-w+DWnSr$wP<>-CSa^qzQyyScB$pnQqhEnYbh~)Z9W*!B%aK1H_L2W3aSI zF$3cAT?Ub^!9^tvZ~)kBkBZSC**{BAZ|&aHC2s5a4V7qMCG1-d08W7C3U^-QCK}rg z^+e+upQN3~eS;`Oa(jEng@K$*>*FU%dK! zW+6i(=PfJSK}wc{HtT%A-AU_zY4A8Vn=XV%Y%OO@puDT!?vST#B>zJ@+J@@79PWQgE? zF5Qg3CVt&o_Kdgmd5tjXJr(*#d{~U?_4TzR!8loEk$TNC84ei*?bJ5GmE9_PcU2O6 zLnXoq6tU$;XCUir6gMkf9v$qpl5x`uTSRc5!dYR782~Z7-=yo~cXbyB*)#lJBI&7IGXYJW*52zfz!3(*PhI%~|@fBrsG{9lKe2lC#wt#71A8t;h*x#MF0aB3NH76|Oz!O{`|$|sfmiq7*QFaxscNjtvaw)}bX^}5-jiD&bt8xJL; ziLP(cX5T9aGU5^`6*-D9BWIr1`tk9_JN*7+Rtk!M#rcZ@IG3Ortf#%w8B2e=KIS}2 z;N0ILlL(YOL167~KJ``mJU8XO|IF}0eWq~W7J`SAk(Z$Z_3ocHmmAjt)sVk>qF&&5 z3ENxuT)CC7d2OSg$a(Dv-EfT|`%7fIKjY3iNM*(;@fo@5XE&Xky5!a;YbvmwIf$qGFTY5HO^+Z^_{4dIfd(%&GwJz_?9JNEZDU_Hnir~ z_XBPdw{4qnJMdQmqG#PJLwucqI8lP%5`g|JPM;uD>5$p(zCv zre9aB5p#OWBm24&hDN?w&38FQjE~E$dnh<^h_x6qdaMJ}a@4!qNLQNtM&4?6XZ!!b zdJ14mXltlbS3|*M+>|u|_u@=sGBTpGxf=5O@)qIPq;zs@dVee?8Z~|KE^;qOp1Nds z;~cP6vAM8-NZs-MZoscE2}~R(OVNfc;0nQw_gS^ntKy6@N5rO% zCPQ#o3psz8{~z?ZE-2j4O}4X*dVL*gv>3@|gBL7?^9!3R2epJ)s~N?8UtFFnBi#XL~649$F-@ zpGpj~AMnR<=&1*yqMg)%WGS%d>M?RIFzcX5F~J7$ z9|GOYqwOqnpq0t)m=K5c5i+l>RdTV;Nu>)oulRBPXkpo`lAgz2w3$!@xJr8T^Q)-m%<#Y7RbhLsCWbJo?!ylr`h_@fkY8jU7AR zhBX9DanSd?MbJq_=VPZzkQw7Nd)38`o;-2W_`tb6-vVT$bVz3p?yODJd@vkuJOqbW z)f4Ma@U5WPhOFqw?f!U9rHAYJe2k(Gu=1_OXc)YQWmXQr?*;xkA!|@}F!)#Z1Al;~ zEU{XA4MbV*DG{FhHZU5}7ZVj*&*>Qb_NAY&xyN(cX3o+HLY=<9M}igfijn+-4L-It zz$bVDE`!2mY)-DPga@19K$H0+v2MdD?3WfbP@Q3BX7OirmqSWfr z%%TR04Nc}O385*=jvcEeBpn8Ag&M+o;Q>W2#GP-OHWpF7xaxF!Y{Jw-t&YKVn{-6a zb+eol=0O#h&5x0iDmx*RRcbWa4$D`@)Z@0sN-RpEP&2~SMNtI8{-@Bb0k9BO#&;H1 z(7rdrba}ZRAHoh?!!-^YO+PLn^R>U0iZ{nF;?D8HO+^T=&ZE-B;t~B$f$_`M0n(6} zv0178Q8I|=ACiTQkV5a*9C3$rr`&?f+I}aukg6w=tn+lrm5_s zvN+@bBXRW#jD+CE69HrM;4wX}UxiBetw8p=fLI1Xc?rp*gWc^hH_$krY4oJutTlbFK+P6(Qt!~ugh(jBN5zbl-1@{GPwBX5 z8!xM=OK_qEDq+F>sZ&M_^>gM*B8Ji%1yX%(!4H{+?+T-PV1H;hCz$VxRM#nnSxzmU zF1~0~QhoL3H#>fPCO8k&nY-&=CDFuXK&N!|sg;L)hlaNvl-}gm|5u47Bk+{&qtj=s z>EpinL%+D^Dfck=W7fmf*Q0so$t#!$Ut0L9w7`<7Msd&2VHa&x6Np;w+ z>4stUeHfhHSMH28Cq=1XdVdRH3?DBQwuGdjoJ^oa%Ok_%AfI_}48hW?aU14KuRf@9oZp_aBKh}Qm4)XDm+l#Nb z>NMd|!J4l`2kVE49i0Rr1dN~YTnv-B-)qo!)W9wy+Eny zIV_W4fz)mI_}dH&J9|0M&TC0lgHQfWh`yKJb&h{?nHYLeFm@RpT8rd{1d6`G2qi&+ zoX>)>rnW7Ua9_YU!$uUo6b7={-JD!-@`D(^p`Xp?))$8BGdia;I|mNC86K4_CV_A! zjG)y7Lusk18zImWTz*PhOnLoe&(4pTsr#ozIgeFYJq`$FfiGIzBgYu|BpD@vgx6x{ z*e4{s^e}S6>32}GWk|oOYmC6%wFI%J?kO#8{3qgnYC6y}2B{+5qU(v4IL)u`4vRv* zjRKhs^r4zR$%(P1EJErmG~0aQoYmrE)W3}pplL-#DDyngUibkF{DAQe_h4CbJjm{1PP_TAtE{LklK=^hbM$4t+L)h60g3?&w>)UW)vFj2w?1L) zrV8lp6fXc>;^xzUj~FT8eb`VkmmvjtQX2wcxZa08ej1k$;!lF$m}TxX9XsiRCZ^1w)PMWw=v6z1HVMIIA}oI;6#$p?LJ||Do%xjR!&+B_X_w%_wzx#Rr z^^$YXoS9?hm>K=~W&KIe2_*wavyK}dztKfl$x~^9qxXh%1Qy{`3mw9Jf;qo#4V@$L z=B&5~w~+ed_eKmeZlo=s+}WEkh?Kw>R-7>T7lw7X< zXVbz!4j~$5XTtr@rYTWCHOm~Jt{A}TyZKL_4!|#W`}H;N?u$#`yoP0HxW?B!5_9|Dr};4J=AoEvBncNxbl`vG&d;lcyc8MnQI%IY2= z2hedv3ZfD6Voj4Fu>#-)`p~H==U-xj;S9H@-IotuNErPVNEE^r zWjX6WLSaTLy!88)Z*tMC;kz>vx+0(sCZy0oqdI3`BZBP#-L6(Bns*Ky9$K$}l82Bt zFpd#!44zM(KgBQrH!U4CT0obYsDm%2W=4OZEd|bPe|~F~2)UFgq~t&95mfWDq^7zD z&aO#_W5x}=Lpa^kLYvhsJ17ZoyRuj3{md}t5lvtW!0P^Q=s6gW-ydWf;vMZnr zO2!pJ#^}v^KZJ$Y_%u5 zAfY0ws`YIA1}N7(fSClo{eSXiK_-nP)Ha|W%Ef@1{w(`uvC|wBrWsIahFgN$T`}VA zQVm^Il(Nx`!YO{JA9^zRf77hoZ|R8CrO0kJ|Goc)O`#9UdZ}zVa+Ux@`)m;8$Qm)T z>c8-OSONb-)ot?M87K}Q{vdI@+1eTa)z$xzulkR%kMcj5jp_$WamoRm+5TbNvj7{} zp)k`y9MG=g-J)86obaw$tM1JTJza(GH3Fwz&l*4sOMnSndsF`LH!=xO@i7~~wLXM* zxb@L*z*y>oe01lb5_r+Zg(N6#KTi@Pb$8}^_c2GEN(|8siscd$akA4=57u@2h- zM<)&SUuXpJi3-QP4BPlyQ1|vsyCf}F)^^4z&@<>MDA5Dur;MNHJi};-1YJZ!#nK=Y zet+1Ps2$XO`9y3JqG1I}+sFOW83||!r!yeh^@-5mNahW{yPyFjJ`;eM2N}~|ovc?X zV%ww>T@jiT!=T=&M@5=dGGs;T5Mu~EKz4!PEJ+GfxaSVIXM5+;2!ztfmg-QMdDV8# z6qEqAH*<|xUzC&LrKkbaID`4B*NCZkCXlR7Tv}xC3zp2b9^8LyygC^AKX$UY5HvPk znDa+&3k`xKkx|M?YT$q-_B>Qzvv23#GP+WlSW6c4uNvSAkju~Zoum$s2_RR9bnD&^ z%1d;LKPV8deiC1U$|I@1b)}#B0G`1ZO(YqD-2Q?Yb3Wtc9iVAkRT%}KvYNAG-{$G+ z!Jjy#|9Z4ShyiaY9!i1V-4dt1)4=Rjn+O%Y>=OgoU-3Tzfiok+2Wx{xBzA&St9vW?XcFsz~lGNG%Vc*dM=o)j{kKb?N%!W8$>KF^J_zLe27|M#Dhn#y+<7Yy^rw zJ0RMw^ySwr`F8Vo&AsAhYL^JJhpW%5>S)z*6PLaRW}uu)326BU1>#%6r3aot6@{$& zdHchov?ck{hT zKR7d&7f7AyT*0D}+OVJpZYtpjguPBBM*-`OzDqg$PMGuyC*sxT^e#wE?$wJHJZTRe zzv2ic4|>s6dJm;62qZ|F_t@4mPEn(sR)K*F*Yyra`nVGCZW-ldi zHPIAEAnd;llbRK}nQm|cWQHs`DjjF8zOO%nTDX0>{}{Z~kLS6n&W0f~zEnxLS*XS@ zs2}>MKv7kJXNw9|4qP=I&y3*zGa#Pi&0Fp2OF z)WU7(RU$QL<>gR=y$%&&+k%|@oN)B^pLoy-4My@s*U|b`U%zt{ z8=@FD`MCkzl+oO1cmETJeN&~YLV#;@HnQ&D8G|(efm)LF(;>&1z)Lu~^mlPos<{KI zAOqR5Zq%@Gp6Sh7oh-KbK_KzsCe$PiNM8?EC2Q3?pbQBsP2%0u6H?)zyrZekSJ zPHe}4dCV7@U;#2t8XeF}#Bi{=Jpfi{TW z%A`VfVKm~ZW!LRU={rERNLdqV27^ipH}HhtftNNtVhR+415&~KqQ3Oxv&f_0!&f7u zJ%1AIhQUH2N&P{pue7x%L97y(lhjH6t!9FQ4rVCLAw!EV^BW?N5$6Aha;D8QM|~!{ z=xD|2pT?omR_KvzaO-(ZL0$&b+72`%nCWx;v4$gNBR9=s30O#6f=b)N9<(LV5K?gH zkbME|lWr2=y=})Kr(o5M9MJI1&8E4X7B9tSaM2*we~x^$-;EdzdX%M8y=yC@r)XM?GToGAq^6Y#2Uqt}mB3+!kNyp5{4o`(@>(Gl^GM{7nhJV;8C&`EpyH08qr zhlN^_8i5uaucKO$ok8p0tJ7Wcck4l{Vmj;06bP@D*N|N_; z{~B;_o#rV;fW`_5C3%AK3|$_cWaIbFfMys`kY?NcMA!meFI!3^39t7~&Q#r#vsm75 zgC2QzruF0M^I#;%e*yJ?t*+j9kVjN!R$(c``-KnP^|e1mFxgpN4hM3mi(l_>Ez5XJ zH49B&Yiuz@HGrxY)sMcC@TLiLv45{4rl3#w_0jg&Gwrv>>qOXo<~9Z02O$FODpSY! zc|HQVn{UHfzezhIv5EM{ek5uIw3+>HWowdP%|Gf9yUx4SR+^g82(1bul(x2WB1!ms zs27#5#UBhRM%&7h2c2NMu&rX^RUv{V5WLiNtnlnlf_71{LH_fkUpAk8$?d8^sCWe> zkG#*p@4q@)q5W)o?)Sm={K)3x@3Bq?cgE$ATSrt1@_YoXGfkNZVVz4`PyiBmdnnmjO zxuDUpfKxoPbmgekPJjYM)nH=vUAhlM;AD6UU%lV~C&Nr25mPnxAk*m%JxrpXq*I0P zJGeYdz1ir*3*=_>$XQfYN%R)rKV!K)3Mq?15|b{Pl$nEYpzgY~A-eUMW0z z{9w$SXJb;}_8HLR9b^S&2{td~u*b&}#d0a*Bwyb?<|LpadqVhyqs9MLf38(*Ky{cv zbB@8hBo9pn7&ctCs#%zhgFuOf?Adn5$8Qr29_CRtGQt;0sZBH(=f6pZGE!{zUjG#E zA@@#;-{|zvqgQ9P-?fCgr`$QVljPy^X`g7RF`NY2VvHo@2^Yu@5|lY&s;=T#LG3OU zdEfi*h?WOcW>1xl80)2ySk&bYpUT}B+TVsp7Jn@4n~a}x$+Q?!G|;Z{H7nO5z(kS(R=YMg5Amd=GY@EgkW=G zx**DY$hJ)AE@Ei0oHIRM1a;ut>5!R~0_(lcK&nm1^N^SD>?)i^^^z9Uh6|~iZN{n^ zoBRV14n)BDrifV87pw9X>b)z~(|K{Q)SaxMF--1)7w`M$n){e=gD9B(4U^qmA%V0bv8m&h#Rx;lP84nk{{>#`Hz-cytb-$ z9kT=doQx_gC1t%AqMHF4Sc!h97P^hGNEI?QQHb`=6t_<~;d)M)uYQ+_$k#m#Kj#N5 z-avcB9~(WX!lztldf4(CQk3~IJL?`sXM1FzUr16AZI&q*pG>6LR!wxm*^)3NY+fmF zw@wT4Kk=KEnZ@y-8H7v>1S#_K^Gm0`y}FY6$#-Q~OFLauWH49ByqYq6Ami5iq$ysV zw5~`hW|P-M6e}F(4a(t!;-DneR}@&|(a$`v#`oN%c%z`Yni>^d?J5R`SGM3yez-&M zzQeo>KSwOJR{P=M7sAxz7y*ynl@U`RA`H_9Vhb>R(~z6U>zKg#?^LODAAMFw$$qjb zg)}F*4&T~Qm!j~%zac?MBkt8R#UsNEjT|T!5&lz_5LfXBBoEuQgQC*brOc(70AY>wickubh|X?(hF8 zMaWcJtJ?4wZ{%O6z?1@Wduz~}39Uoo_EWHqOvfl&Zt&V=2=BeFGV|}kD)?=@{T9z^ zg+b{Q!SrfUts30BL0pyR=~7=}Nc{9Xue4fljYGc>5ac7{@AOv;{p)OzcxT5&ZG>RU zxNKG(9|~J8@zo0wNjC^2URR|)H1?m&76H!{_ESr&!(3iRVibwb@-ma*qvHq<<5%9^ zb$u<^yz%rs7^P4h8YLjB0*vA+(Y_6&0}&MvY=OqQvp#RqE2L27^3oOm9R2WpaP$sY zRPhl+njC+~41(%(Wh2a)`8DbU(Bor`+-UGBtox0?t4Pb7k%7cu8W4lK!^ZyTz}vA2 zQ;cGs{@pH9R3=en0B^$&_ovTqzv;KZp$=AVI>||C;YMghvw=e7AgCY`Xo=SX3$5f> z-fd2t+E*;)ULJraDh-|jfbq@uSNBg_M;`tB@tx_@;Hk@_og(+MT1-Mt0vc{)OPd(? zGRD8`eIJ;9dnC?M(BUmIsqjay+sT7ItuTzsK)`GL)wxkdhmQ%w&{H%TDKcL>CD2aKu`Bn(Fl7=CrSu`uw*lmL3sQa7$8qFS7710ZU={kRs&=roatpJpl_)D*cx2T#A@j7>W!_=IOp(Y~%y$6pjvxzD8eeOnZXYXX-Wu)(}zMW=kJ)N|>>)7XWKirStl(%<+@AjmM@^F(j zq%U;SQDUwi1xxELiNVCKW6-=#Mnx%s`|MBR@d(v-m{lqT+e#Fk>1!q$9D72T1K}6Z z7J68}j=HhBK9W(k$*I~j!D$|$37nlsqkdoS%^owYl$$Mdv$2grI@wF`F4E2Agm{H} zz5PlaW%x*I_R*v&XLEzOqphE|T3Zgc#+n_f=`iBCbt!_jO*$AJa+JfVsJN4fxuALB zhj03(CR5#6umG?sfoph1r2hiA$WU}bUHA-D8HdC7n4$8y289$!iI>8 zq45Dx;NY(E(3H>V%_{jyIdDyCwFHH#3HckyahNyePlz3rdVdp!wS2BjC;FYdnXJfTiqJfzPM{8*z$B9 zt+&eR4u*Sc@bp@Mg3R zQy$qabqy#)38J&iv}eo<@FZs}f2o0nym6o7!MF7GvS}_KbA;PZGflSv*UaIz_;+`T z3L3HJ2ZnsQaIRKXMM2^AsIuvPt6IGl=W&V0`;nJ*hSL!~LfYB!AcuA@VCO7wY$02e z+it%~!|qFYxDdA9?1gBOaNrV{ZQufFcM7Ku56-61p^Qc58CO9r{4z^&uep11uG@iA zx~qk>`iKVXrf-5kf>=>X>9Do4nJ~ATRdW!57_$FD<1W@lyDFr&*GR+|AtGtRasz0D zn{>eDLT#;lE}cxBYNW(!_j-`@!Wl!-9(;UU`h`+fcM6@(j(x@$-w;9+JHo9vea%jnz4I<*KIC&nYxv%~s)Z4Y3@55k)b zPRp&N%&J?H4SUSUQ%Cz<%KmsYmR8uUHVGPWz(udi9R*%j^7&{j+o<P2D!8NOi}*#&f}C{044dcxrR|vKq8)%o|m_EN%yW7)R#^HzYbYkfx*vCILZ6 z;LP~S1?y45xD@|RrYt0?`Oz?3+%bA1FLS6&{&AIj9v%^wK=qI0xkW^%8zk8Ly`0$( zWxiTG0B;8B?pva?F)#KRw8Vv#U8|&9r&cS(wMrh7iFR67%G?I6Q(N7qjeU}M^sbdK zRknKlt?wa@{54MDnkX)sJ576qpzRlcy{Sd!cLXIT3{V)Hw{mU0f|= zpbeHsXnEijNX@CVs}7g5F)nAab7bJUYI4vKl*NF@^@~^+CmAV?!d@Gg!mHqRBiX6| zLIS2Z?FVRgJgUm2S#Lcehilx9j{%l)9Z*z;ApbUDON-+@_mrH)yRkz-vaS@Qj zh3PDiN{&@R+Ib2bfuC@8T)hw3 zm)9-p98CTho3BF2UAt;``A4iDK)&1IfH-{ZrdZ<#^AQeesYG*QdUgekCfmiNi9*Hyl0W))5JX5Z?iK zd_-&E6L>{1PG#I;0T~7KABW2SW#?Mf@wLM0SNgnugGV?(W`Jmvdj9+Zz&E#DQlXIw z0m8-A&6N`mWMod!$UJo_l+VX!IZ4S}odo~6?!Au&w46pu$Xs@oxcXpz@rbhg@OJN{ z0is|03QJTa*!l78CShnpT`NqIF^^DV6wM#KPl>1AX3fS6pdB)Z_&aev1FiL{ZI@FpO0i0YbcT?T6;-S411RL~qEE*=jde6XtJq1?z|4u=3 zvy_`%Gl@qFLLEkp-u5Hghkuv3L-tzFPX{x;d|<2sb3wOv#-Mq?0>ub+yrZC(cMW=Z zE`7IE5uH_9ockM-$)LfumE5yGd2-a0l*z!oQI-oTrRm7|ZIp*hwT6bu0w*Hf!lmot znhoxg!-LBM@5(|Pl9vIV#%3@-6r+U(y#y|7IM^BHClWo!UYgJkwEsw&y09Jbqri2# z#b#p)U1oRf{AFZhX|5j*qplvc{TYN8O~z}T-iJ^N=197vCCw%z&^Tc$-yH=2cGe

      a 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%0A2UxMOtHeB!jXj2d*po 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 Date: Fri, 12 Jan 2024 09:29:23 -0500 Subject: [PATCH 280/332] fix index/ledger regression --- R/link.R | 4 ++-- R/mp.R | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/link.R b/R/link.R index a541a9f7..87938a4f 100644 --- a/R/link.R +++ b/R/link.R @@ -128,8 +128,8 @@ FrameGetter = function(link, dimension_name) { self$get_index = function() { Index( self$get_partition(), - self$link$labelling_column_names_list[[self$dimension_name]], - self$link$reference_index_list[[self$dimension_name]] + labelling_column_names = self$link$labelling_column_names_list[[self$dimension_name]], + reference_index = self$link$reference_index_list[[self$dimension_name]] ) } self$get_labels = function() { diff --git a/R/mp.R b/R/mp.R index 4fa90dcd..300e6243 100644 --- a/R/mp.R +++ b/R/mp.R @@ -84,11 +84,11 @@ mp_square = function(x, suffixes = c("A", "B")) { n2 = sprintf("%s%s", names(x), suffixes[2L]) x = (x$partition$frame() |> setNames(n1) - |> Index(l1) + |> Index(labelling_column_names = l1) ) y = (x$partition$frame() |> setNames(n2) - |> Index(l2) + |> Index(labelling_column_names = l2) ) mp_cartesian(x, y) } From 97b3d943f74b37e32326fe423751cb754428981a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 09:49:45 -0500 Subject: [PATCH 281/332] address #154 with managing out-of-date vignettes --- _pkgdown.yml | 8 ++------ {vignettes => misc/old-vingettes}/hello_products.Rmd | 0 {vignettes => misc/old-vingettes}/model_definitions.Rmd | 0 3 files changed, 2 insertions(+), 6 deletions(-) rename {vignettes => misc/old-vingettes}/hello_products.Rmd (100%) rename {vignettes => misc/old-vingettes}/model_definitions.Rmd (100%) diff --git a/_pkgdown.yml b/_pkgdown.yml index cde0d6c6..b41d3bb8 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -18,17 +18,14 @@ articles: navbar: ~ contents: - quickstart - - quickstart2 - - quickstart_products + - example_models - time_varying_parameters - calibration - - example_models - - flows + - engine_agnostic_grammar - title: Specs desc: Specification documents contents: - cpp_side - - model_definitions - vignette_status - title: Developer desc: Vignettes aimed at package developers @@ -39,4 +36,3 @@ articles: - elementwise_binary_operators - state_dependent_rates - standard_expressions - - hello_products diff --git a/vignettes/hello_products.Rmd b/misc/old-vingettes/hello_products.Rmd similarity index 100% rename from vignettes/hello_products.Rmd rename to misc/old-vingettes/hello_products.Rmd diff --git a/vignettes/model_definitions.Rmd b/misc/old-vingettes/model_definitions.Rmd similarity index 100% rename from vignettes/model_definitions.Rmd rename to misc/old-vingettes/model_definitions.Rmd From 857634ee20386706434a0379eaa7bac613d4504f Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Fri, 12 Jan 2024 09:53:40 -0500 Subject: [PATCH 282/332] fix typo in dir name --- misc/{old-vingettes => old-vignettes}/hello_products.Rmd | 0 misc/{old-vingettes => old-vignettes}/model_definitions.Rmd | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename misc/{old-vingettes => old-vignettes}/hello_products.Rmd (100%) rename misc/{old-vingettes => old-vignettes}/model_definitions.Rmd (100%) diff --git a/misc/old-vingettes/hello_products.Rmd b/misc/old-vignettes/hello_products.Rmd similarity index 100% rename from misc/old-vingettes/hello_products.Rmd rename to misc/old-vignettes/hello_products.Rmd diff --git a/misc/old-vingettes/model_definitions.Rmd b/misc/old-vignettes/model_definitions.Rmd similarity index 100% rename from misc/old-vingettes/model_definitions.Rmd rename to misc/old-vignettes/model_definitions.Rmd From 87d269ea12e92b343856a8c01daa160e01aea591 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 12:04:31 -0500 Subject: [PATCH 283/332] allow missing row/col in params frame --- R/opt_params.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/opt_params.R b/R/opt_params.R index 4fb0fa13..43895e6c 100644 --- a/R/opt_params.R +++ b/R/opt_params.R @@ -106,6 +106,8 @@ OptParamsFrame = function(frame, .dimnames = list()) { for (c in names(frame)) { if (is.character(frame[[c]])) frame[[c]] = trimws(frame[[c]]) } + if (is.null(frame$col)) frame$col = 0L + if (is.null(frame$row)) frame$row = 0L row_col_ids = make_row_col_ids(frame$mat, frame$row, frame$col, .dimnames) args = c( as.list(as.numeric(frame$default)), From 26b3484e3767c657b3d19807b55d1a27b9330cb7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 12:53:52 -0500 Subject: [PATCH 284/332] text in time-varying vignette good enough for #154 --- vignettes/time_varying_parameters.Rmd | 118 +++++++++++++++++--------- 1 file changed, 76 insertions(+), 42 deletions(-) diff --git a/vignettes/time_varying_parameters.Rmd b/vignettes/time_varying_parameters.Rmd index 64cafb3f..600762c4 100644 --- a/vignettes/time_varying_parameters.Rmd +++ b/vignettes/time_varying_parameters.Rmd @@ -31,7 +31,7 @@ knitr::knit_hooks$set(basefig = basefig_setup) ## Baseline SIR Model -Here we modify an [SIR](https://canmod.github.io/macpan2/articles/quickstart) model so that transmission rate is time-varying. +Here we modify an [SIR](https://github.com/canmod/macpan2/tree/main/inst/starter_models/sir) model so that transmission rate is time-varying. ```{r baseline_sir} state_labels = c("S", "I", "R") @@ -45,7 +45,8 @@ simulator = ("starter_models" (simulator |> mp_trajectory() |> mutate(state = factor(matrix, state_labels)) - |> ggplot() + geom_line(aes(time, value, colour = state)) + |> ggplot() + + geom_line(aes(time, value, colour = state)) ) ``` @@ -82,9 +83,10 @@ simulator$insert$expressions( ) ``` -And that's it. Now we plot the updated simulations using these change-points, which we highlight with vertical lines. +Now we plot the updated simulations using these change-points, which we highlight with vertical lines. ```{r time_varying_graph} s = mp_trajectory(simulator) +cp = simulator$get$initial("beta_changepoints") (s %>% mutate(state = factor(matrix, state_labels)) %>% ggplot() @@ -93,19 +95,20 @@ s = mp_trajectory(simulator) aes(xintercept = x), linetype = "dashed", alpha = 0.5, - data = data.frame(x = simulator$get$initial("beta_changepoints")) + data = data.frame(x = cp) ) ) ``` +The clear kinks at times 10 and 15 are due the drop and then lift of the transmission rate at these times. + ## Calibrating Time Variation Parameters First we simulate data to fit our model to, to see if we can recover the time-varying parameters. ```{r noisy_data, basefig = TRUE} set.seed(1L) -I_observed = rpois( - 50, - filter(s, matrix == "I")$value +I_observed = rpois(50 + , filter(s, matrix == "I")$value ) plot(I_observed) ``` @@ -150,14 +153,22 @@ simulator$insert$expressions( simulator$replace$obj_fn(~ -sum(log_lik)) ``` -Next we declare the beta values as parameters to be optimized on the log scale. +Next we declare the beta values as parameters to be optimized on the log scale. The clearest way to do this is to form a data frame with one row for each parameter to be fitted. +```{r params_frame} +default_beta = mean(simulator$get$initial("beta_values")) +params_to_fit = data.frame( + mat = "log_beta_values" + , row = 0:2 + , default = log(default_beta) +) +print(params_to_fit) +``` +There are a couple potentially confusing aspects to this data frame. First, we want to fit the `beta` values on the log scale, and we indicate this by prepending `log_` in front of the name of the `beta_values` matrix that is in the model. We will more explicitly add the `log_beta_values` matrix to the model in the next code chunk. Second, the word `row` corresponds to an index for the change points. In particular, `row = 0` corresponds to the initial `beta`, `row = 1` to the first change point and `row = 2` to the second. This is because all quantities passed to `macpan2` engines are matrices, and so in this case different rows of this `log_beta_values` matrix (actually column vector) correspond to different change points. If we were dealing with matrices with more than one column we would need to include a `col` column in this data frame to indicate the matrix columns of the matrix entries that correspond to parameters to be fitted. + +Now we log transform the `beta_values` matrix entries and declare them as parameters. ```{r params} simulator$add$transformations(Log("beta_values")) -simulator$replace$params( - default = log(mean(simulator$get$initial("beta_values"))), - mat = rep("log_beta_values", 3L), - row = 0:2 -) +simulator$replace$params_frame(params_to_fit) ``` Finally we fit the model back to the simulation data. @@ -165,14 +176,20 @@ Finally we fit the model back to the simulation data. simulator$optimize$nlminb() ``` -We can see that the optimizer converges (i.e. `$convergence = 0`) in 26 iterations. +```{r optimization_check, echo = FALSE} +if (simulator$optimization_history$get()[[1L]]$convergence != 0) { + stop("time-varying optimization example is no longer converging") +} +``` + +We can see that the optimizer converges (i.e. `$convergence = 0`) in `r simulator$optimization_history$get()[[1L]]$iterations` iterations. On the log scale we see that the optimizer finds different values (`current`) than it started at (`default`). ```{r log_parameters} simulator$current$params_frame() ``` -More importantly the beta values on the untransformed scale recover to the values used in the simulations, although the second fitted beta value is much smaller than the true value. +More importantly the beta values on the untransformed scale recover reasonable values that are qualitatively consistent with the values used in the simulations. ```{r recovered_parameters} data.frame( fitted = formatC( @@ -182,6 +199,7 @@ data.frame( true = simulator$get$initial("beta_values") ) ``` +Note however that the second fitted beta value is much smaller than the true value, which is potentially interesting. ## Radial Basis Functions for Flexible Time Variation (In-Progress) @@ -190,11 +208,14 @@ This section uses radial basis functions (RBFs) to generate models with a flexib Before we can add the fancy radial basis for the transmission rate, we need a base model. We use an SIR model that has been modified to include waning. ```{r} -sir = mp_tmb_library("starter_models", "sir_waning", package = "macpan2") +sir = mp_tmb_library("starter_models" + , "sir_waning" + , package = "macpan2" +) ``` -The `macpan2::rbf` function can be used to produce a matrix giving the values of each basis function (each column) at each time step (each row). Using this matrix, $X$, and a weights vector, $b$, we can get a flexible output vector, $y$, with a shape that can be modified into a wide variety of shapes by changing the weights vector. +The `macpan2::rbf` function can be used to produce a matrix giving the values of each basis function (each column) at each time step (each row). Using this matrix, $X$, and a weights vector, $b$, we can get a flexible output vector, $y$, with a shape that can be modified by changing the weights vector. $$ y = Xb @@ -208,18 +229,31 @@ d = 20 n = 2500 X = rbf(n, d) b = rnorm(d, sd = 0.01) -par(mfrow = c(3, 1), mar = c(0.5, 4, 1, 1) + 0.1) -matplot(X, type = "l", lty = 1, col = 1, ylab = "basis functions", axes = FALSE) +par(mfrow = c(3, 1) + , mar = c(0.5, 4, 1, 1) + 0.1 +) +matplot(X + , type = "l", lty = 1, col = 1 + , ylab = "basis functions" + , axes = FALSE +) axis(side = 2) box() -barplot(b, xlab = "", ylab = "weights") +barplot(b + , xlab = "" + , ylab = "weights" +) par(mar = c(5, 4, 1, 1) + 0.1) -plot(X %*% b, type = "l", xlab = "time", ylab = "output") +plot(X %*% b + , type = "l" + , xlab = "time" + , ylab = "output" +) ``` Here `d` is the dimension of the basis, or number of functions, and `n` is the number of time steps. By multiplying the uniform basis matrix (top panel) by a set of weights (middle panel), we obtain a non-uniform curve (bottom panel). Note how the peaks (troughs) in the output are associated with large positive (negative) weights. -Now we want transform the output of the (matrix) product of the RBF matrix and the weights vector into a time-series for the transmission rate, $\beta$. Although we could just use the output vector as the $\beta$ time series, it is more convenient to transform it so that the $\beta$ values yield more interesting dynamics in an SIR model. In particular, our model for $\beta_t$ as a function of time, $t$, is +Now we want to transform the output of the (matrix) product of the RBF matrix and the weights vector into a time-series for the transmission rate, $\beta$. Although we could just use the output vector as the $\beta$ time series, it is more convenient to transform it so that the $\beta$ values yield more interesting dynamics in an SIR model. In particular, our model for $\beta_t$ as a function of time, $t$, is $$ \log(\beta_t) = \log(\gamma_t) + \log(N) - \log(S_t) + x_tb @@ -231,7 +265,7 @@ $$ \frac{\beta_t S_t}{N} = \gamma_t $$ -This condition assures that the number of infected individuals remains constant at time, $t$. This means that positive values of $b$ will tend to generate outbreaks and negative values will tend to reduce transmission. Because of the local nature of RBFs, we don't need to set all coefficients of $b$ to zero to achieve the same results in practice -- we only need to work with the coefficients of the basis functions that are appreciably greater than zero at $t$. **fixme**: clarify last sentence? +This condition assures that the number of infected individuals remains constant at time, $t$. This means that positive values of $b$ will tend to generate outbreaks and negative values will tend to reduce transmission. **fixme**: I (BMB) understand why you're setting the model up this way, but it's an odd/non-standard setup - may confuse people who are already familiar with epidemic models (it confused me initially). @@ -281,11 +315,11 @@ print(simulator) ### Calibration -Now we're going to try to calibrate this model to data. The main innovation here is that we will use a +Now we're going to calibrate this model to data. The main innovation here is that we will use a built-in feature of [TMB](https://cran.r-project.org/package=TMB) (on which `macpan2` is constructed), estimation of latent variables by [Laplace approximation](https://en.wikipedia.org/wiki/Laplace%27s_approximation) to fit the time series efficiently without overfitting (see section 5.10 of @madsenIntroduction2011, @kristensenTMB2016, or the [TMB documentation](https://kaskr.github.io/adcomp/_book/Tutorial.html#statistical-modelling) for more detail). -The next few steps will follow the first example in the [Calibration](./calibration.html) vignette: +The next few steps will roughly follow the first example in the [Calibration](./calibration.html) vignette: TODO: make sure to line this state up with the specific code in the calibration vignette. 1\. Simulate from the model and add some noise: @@ -301,23 +335,22 @@ plot(obs_I, xlab = "time", ylab = "prevalence") 2\. Add calibration information. - We start by adding standard boilerplate stuff to include the observed data and store/return the results. ```{r add_calib_info} ## copied from 'calibration/"hello world"' example simulator$add$matrices( - I_obs = obs_I, - I_sim = empty_matrix, - log_lik = empty_matrix, - .mats_to_save = c("I_sim"), - .mats_to_return = c("I_sim") - ) + I_obs = obs_I + , I_sim = empty_matrix + , log_lik = empty_matrix + , .mats_to_save = c("I_sim") + , .mats_to_return = c("I_sim") +) simulator$insert$expressions( - I_sim ~ I, - .phase = "during", - .at = Inf - ) + I_sim ~ I + , .phase = "during" + , .at = Inf +) ``` Now we start to deviate from the previous example: in addition to @@ -340,7 +373,7 @@ The $\phi$ vector is a set of *fixed-effect* (unpenalized) parameters; in this c Although this looks awful, (1) the high-dimensional integral over $\mathbf b$ can be separated into a product of one-dimensional integrals and (2) the Laplace approximation gives us a quick, reasonable approximation to the one-dimensional integrals. -The `rbf_sd` parameter can be interpreted as standard deviation on a Gaussian random effect +The `rbf_sd` parameter can be interpreted as a standard deviation on a Gaussian random effect or as approximately $1/\sqrt{\lambda}$ where $\lambda$ is a ridge penalty. Continuing with the coding, we add the parameters and negative log-likelihood to the model, @@ -354,8 +387,9 @@ simulator$add$matrices( , rbf_sd = 1 ) simulator$insert$expressions( - log_lik ~ -sum(dnorm(I_obs, rbind_time(I_sim), I_sd)) + - -1*sum(dnorm(b, 0.0, rbf_sd)), + log_lik ~ + -sum(dnorm(I_obs, rbind_time(I_sim), I_sd)) + + -1*sum(dnorm(b, 0.0, rbf_sd)), .phase = "after" ) ## initially forgot this: maybe we could warn when someone is missing this???? @@ -366,9 +400,9 @@ simulator$add$transformations(Log("rbf_sd")) params <- read.delim(sep = "|", header = TRUE, strip.white = TRUE, ## important! text = " -mat | row | col | default -log_I_sd | 0 | 0 | 0 -log_rbf_sd | 0 | 0 | 1 +mat | default +log_I_sd | 0 +log_rbf_sd | 1 ") simulator$replace$params_frame(params) ``` From ef4ab4bca52182bf390024bbca9122147bb2197c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 13:16:51 -0500 Subject: [PATCH 285/332] better parameterization for the sir_waning model --- inst/starter_models/sir_waning/tmb.R | 4 +-- vignettes/calibration.Rmd | 50 ++++++++------------------- vignettes/time_varying_parameters.Rmd | 2 +- 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/inst/starter_models/sir_waning/tmb.R b/inst/starter_models/sir_waning/tmb.R index 2ec6b8ee..89156f52 100644 --- a/inst/starter_models/sir_waning/tmb.R +++ b/inst/starter_models/sir_waning/tmb.R @@ -1,7 +1,7 @@ library(macpan2) computations = list( - N ~ sum(S, I, R) + S ~ N - I - R ) flow_rates = list( @@ -25,7 +25,7 @@ spec = mp_tmb_model_spec( ) # defaults , default = list( - S = 99, I = 1, R = 0 + N = 100, I = 1, R = 0 , beta = 0.2, gamma = 0.2, phi = 0.01 ) ) diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index 0b126afa..9643e30c 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -44,13 +44,16 @@ We'll do the first thing you should always do when trying out a new fitting proc ### Step 0: set up simulator and generate 'data' -First set up the model from the quickstart guide. For convenience (since we will be using several different versions of this model along the way, and modifying existing models is not quite as simple as it could be), we'll encapsulate this in a function so we can easily re-run it later to generate a new model. (To allow for some flexibility later on, I'm also making the initial state an argument to the function.) - -```{r sir_setup} +We will be using several different versions of the SIR model, all of which can be derived from the specification in the model library. +```{r sir_spec} sir_spec = mp_tmb_library("starter_models" , "sir" , package = "macpan2" ) +``` + +From this specification we derive our first version of the model, which we use to generate synthetic data to see if optimization can recover the parameters that we use when simulating. +```{r sir_setup} sir_simulator = mp_simulator(sir_spec , time_steps = 100 , outputs = c("S", "I", "R") @@ -648,9 +651,9 @@ sir_simulator$add$transformations(Log("I_sd")) sir_simulator$add$transformations(Log("beta")) params <- read.delim(sep = "|", header = TRUE, text = " -mat | row | col | default -log_I_sd | 0 | 0 | 0 -log_beta | 0 | 0 | 1 +mat | default +log_I_sd | 0 +log_beta | 1 ") sir_simulator$replace$params_frame(params) ``` @@ -693,43 +696,20 @@ measles$date = as.Date(sprintf( plot(measles$date, measles$cases, type = "l") ``` -We need to slightly extend the standard SIR model to include waning immunity. +We need to use a slightly extended version of the SIR specification that includes waning immunity. -```{r sir_waning} -computations = list( - S ~ N - I - , R ~ 0 -) -flow_rates = list( - infection ~ S * I * beta / N - , recovery ~ gamma * I - , waning_immunity ~ phi * R -) -state_updates = list( - S ~ S - infection + waning_immunity - , I ~ I + infection - recovery - , R ~ R + recovery - waning_immunity -) -sir = mp_tmb_model_spec( - before = computations - , during = c( - flow_rates - , state_updates - ) - # defaults - , default = list( - N = 100000, I = 1 - , beta = 0.2, gamma = 0.2, phi = 0.01 - ) +```{r sir_waning_spec} +sir_waning = mp_tmb_library("starter_models" + , "sir_waning" + , package = "macpan2" ) ``` We use [radial basis functions](https://canmod.github.io/macpan2/articles/time_varying_parameters.html#radial-basis-functions-for-flexible-time-variation-in-progress) to model time-variation in the transmission rate. We also make a variety of questionable assumptions (TODO: fix these), but the point at the moment is just to illustrate usage and provide a proof of concept. ```{r rbf} -#set.seed(1L) d = 100 n = nrow(measles) -simulator = (sir +simulator = (sir_waning |> mp_simulator( time_steps = n , outputs = c("S", "I", "R", "infection") diff --git a/vignettes/time_varying_parameters.Rmd b/vignettes/time_varying_parameters.Rmd index 600762c4..027ae718 100644 --- a/vignettes/time_varying_parameters.Rmd +++ b/vignettes/time_varying_parameters.Rmd @@ -277,7 +277,7 @@ simulator = mp_simulator(sir , time_steps = n , outputs = c("S", "I", "R", "infection", "beta") , default = list( - S = 100000 - 500, I = 500, R = 0 + N = 100000, I = 500, R = 0 , beta = 1, gamma = 0.2, phi = 0.01 , X = rbf(n, d) , b = rnorm(d, sd = 0.01) From bc55b87006fcc55c8812fcafb527bd79a4e3181c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 13:51:54 -0500 Subject: [PATCH 286/332] closes #154 --- vignettes/calibration.Rmd | 89 ++++++--------------------------------- 1 file changed, 14 insertions(+), 75 deletions(-) diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index 9643e30c..76a80917 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -44,7 +44,7 @@ We'll do the first thing you should always do when trying out a new fitting proc ### Step 0: set up simulator and generate 'data' -We will be using several different versions of the SIR model, all of which can be derived from the specification in the model library. +We will be using several different versions of the SIR model, all of which can be derived from the SIR specification in the model library. ```{r sir_spec} sir_spec = mp_tmb_library("starter_models" , "sir" @@ -62,25 +62,6 @@ sir_simulator = mp_simulator(sir_spec sir_results = mp_trajectory(sir_simulator) ``` -```{r sir_setup_old, eval = FALSE} -mk_sim <- function(init_state = c(S = 99, I = 1, R = 0)) { - sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) - #sir = Compartmental2(system.file("model_library", "sir", package = "macpan2")) - sim <- mp_tmb_simulator(sir, - time_steps = 100, - vectors = list( - state = init_state, - flow_rates = c(foi = NA, gamma = 0.1), - trans_rates = c(beta = 0.2) - ) - ) - return(sim) -} -sir_simulator <- mk_sim() -## `.phases = "during"` is important so that the number of observations matches the number of time steps -sir_results = sir_simulator$report() -``` - Add some noise to the prevalence (`I`) value: ```{r sir_noise} @@ -125,7 +106,7 @@ A sanity check: make sure that the starting values give a reasonable-looking tra ) ``` -The simulated trajectory is **not** sensible in this case - the `log_beta` value is way too large (we set the default to $\exp(1) \approx `r round(exp(1), 2)$), while we know in this case that the true value is 0.2). Hopefully in a real system we would know enough to get reasonable order-of-magnitude starting values. (If we really knew that $\gamma \approx 0.1$, we would know that a starting value of $\log(\beta)=1$ would correspond to an ${\cal R}_0 \approx 28$, clearly unrealistic for most infectious diseases ... +The simulated trajectory is **not** sensible in this case - the `log_beta` value is way too large (we set the default to ${\exp(1) \approx}$ `r round(exp(1), 2)`), while we know in this case that the true value is 0.2). Hopefully in a real system we would know enough to get reasonable order-of-magnitude starting values. (If we really knew that $\gamma \approx 0.1$, we would know that a starting value of $\log(\beta)=1$ would correspond to an ${\cal R}_0 \approx 28$, clearly unrealistic for most infectious diseases ... Setting $\log(\beta)=0$ instead gives us a trajectory that is still very unrealistic (the peak of our observed prevalence is only 16.7), but at least it's smooth. As it turns out this will be good enough, but finding appropriate starting values (based on external information and some trial and error) is often a significant part of a modeling workflow. @@ -139,7 +120,10 @@ Setting $\log(\beta)=0$ instead gives us a trajectory that is still very unreali Let's replace the starting value for `log_beta` with 0: ```{r repl_param} -sir_simulator$replace$params(c(0, 1), c("log_beta", "log_I_sd")) +sir_simulator$replace$params( + c(0 , 1 ) + , c("log_beta", "log_I_sd") +) ``` (In a proper workflow we might prefer to go back upstream to wherever we defined the default values, rather than resetting the value on the fly ...) @@ -190,8 +174,9 @@ These correspond to true values of 0.2, 1, so pretty close. The best-fit parameters are stored *internally*, so if we re-run the `$report()` method we will get information about the predicted best-fit trajectory: ```{r plot_results} -sim_vals <- (sir_simulator$report(.phases = "during") - |> filter(matrix == "state", row == "I") +sim_vals <- (sir_simulator + |> mp_trajectory() + |> filter(matrix == "I") ) gg0 + geom_line(data = sim_vals, aes(y= value), colour = "red") ``` @@ -216,21 +201,6 @@ macpan2helpers::mk_calibrate(sir_simulator sir_simulator$objective(log(c(1, 0.5, 1))) ``` - - -```{r next_example_old, eval=FALSE} -library(outbreaks) -sir_simulator <- mk_sim(init_state = c(S = 760, I = 3, R = 0)) -mk_calibrate(sir_simulator, - data = data.frame(I_obs = influenza_england_1978_school$in_bed), - params = list(trans_rates = c(beta = 1), flow_rates = c(gamma = 0.5), I_disp = 1), - transforms = list(trans_rates = c(beta = "log"), flow_rates = c(gamma = "log"), I_disp = "log"), - exprs = list(log_lik ~ dnbinom(I_obs, I, I_disp)) - ) -## basic sanity check -sir_simulator$objective(log(c(1, 0.5, 1))) -``` - How does this look with our default parameters? (To save typing I'm writing a generic function that runs the model with a specified set of parameters, then plots the results along with the data.) **fixme**: some basic plotting functionality in `macpan2helpers` ? @@ -289,34 +259,6 @@ The fit isn't perfect, but we can think of a number of reasons for that (people While this code isn't horribly complex, hopefully it will all be somewhat more streamlined/automated in the future. Computing confidence or prediction intervals is a fairly big subject; see Bolker (2008) chapter 7 for an introduction to some of the issues. -```{r sdreport_old, eval = FALSE} -## sd vector corresponds to the same information as the -## internal 'values' object, which consists of 4 columns -## of indices {matrix index, time, row index, column index} -## and a 5th column that is the actual values ... -#sdr <- sir_simulator$report_with_sd() -#ss <- sdr$sd |> matrix(ncol = 5) -#timevec <- (sdr$value |> matrix(ncol = 5))[, 2] - -## observed values -obs <- data.frame(time = seq(nrow(influenza_england_1978_school)), - value = influenza_england_1978_school$in_bed) - -## a little tricky: we don't yet know how to specify .phases = "during" so that -## we only get values relating to time points, not also to "before sim" and "after sim" -## so we need to drop points 0 and (n+1) -#ss <- ss[timevec >= 1 & timevec <= nrow(obs),] -#res <- (cbind(sir_simulator$report(), sd = ss[,5]) -# |> mutate(var = factor(matrix, levels = c("S", "I", "R")), -# lwr_delta = value - 1.96*sd, -# upr_delta = value + 1.96*sd) -# |> select(-c(matrix, row, col)) -# ## don't necessarily need to keep sd column but ... -#) -``` - -Now plot the results: - ```{r sdreport_plot} obs <- data.frame( time = seq(nrow(influenza_england_1978_school)) @@ -347,7 +289,7 @@ print(gg_ci1) - These are confidence intervals, not prediction intervals (so we don't necessarily expect the points to lie within the envelope); prediction intervals for non-Normal errors are a little bit tricky (maybe something about this in Bolker 2008?), not worth doing right now. -We can also generate intervals based on multivariate normal sampling, which relaxes the second assumption but not the first. Sometimes `sdreport()` is memory-hungry, so below we show how to get the covariance matrix of the parameters directly from the fit (it does take some extra computation). +We can also generate intervals based on multivariate normal sampling, which relaxes the second assumption but not the first. Sometimes calculating the standard deviations of the predictions is memory-hungry, so below we show how to get the covariance matrix of the parameters directly from the fit (it does take some extra computation). ```{r ci_ensemble, eval = require("numDeriv", quietly = TRUE)} sdr = sir_simulator$sdreport() @@ -395,7 +337,7 @@ stopCluster(cl) stopifnot(all.equal(sim_ensemble, sim2)) ## identical ``` -We can also compute prediction intervals via ensemble, by adding the appropriate amount of negative binomial noise to each simulation. However, it only makes sense to add this *observation error* to the I time series (if we had more than one observed time series we would probably want to estimate separate dispersion parameters for each series), so we'll adapt the ensemble code to pick out only the I values before adding noise: +We can also compute prediction intervals via ensemble, by adding the appropriate amount of negative binomial noise to each simulation. However, it only makes sense to add this *observation error* to the `I` time series (if we had more than one observed time series we would probably want to estimate separate dispersion parameters for each series), so we'll adapt the ensemble code to pick out only the `I` values before adding noise: ```{r pred_int} ## negative binomial dispersion ('size') parameter @@ -549,7 +491,7 @@ This section explains what is going on under the hood in `macpan2helpers::mk_cal ### Step 0: recreate the simulator -Since trying to add the same matrix to a simulator twice causes an error, we'll re-run the `mk_sim()` function to create a new instance of the simulator: +Since trying to add the same matrix to a simulator twice causes an error, we'll create a new instance of the simulator: ```{r rebuild1} sir_simulator = mp_simulator(sir_spec @@ -561,7 +503,7 @@ sir_simulator = mp_simulator(sir_spec ### Step 1: add observed data and slots for history etc. -While the files specified in the model definition (`variables.csv`, `derivations.csv`, `settings.json`, `flows.csv`) are sufficient to define a simulator, we now need to add more structure to the model object so we can do the calibration - specifically, both whatever observed data we want to compare against, and whatever new variables ("matrices") and expressions we will evaluate to compute the goodness of fit (aka the loss function or objective function) of a particular set of parameters. +While the files in the model library are sufficient to define a simulator, we now need to add more structure to the model object so we can do the calibration - specifically, both whatever observed data we want to compare against, and whatever new variables ("matrices") and expressions we will evaluate to compute the goodness of fit (aka the loss function or objective function) of a particular set of parameters. If we have a `TMBSimulator` object (i.e., `sir_simulator` in this example), the `$add$matrices()` method will add new variables to the space where the object has already stored the state variables, etc. (you use `sir_simulator$matrix_names()` to list the existing matrices, although this produces a long, scary list of internal variables that `macpan2` has constructed) @@ -633,8 +575,7 @@ sir_simulator$insert$expressions( ) ``` -Define the objective function (which will almost always be the sum -of the negative log-likelihoods for each point): +Define the objective function (which will almost always be the sum of the negative log-likelihoods for each point): ```{r add_loglik} sir_simulator$replace$obj_fn(~ -sum(log_lik)) @@ -813,8 +754,6 @@ Not a perfect fit, but not bad for now (TODO: work on this, without papering ove Here we consider the problem of fitting an SIR model to a simulated dataset from this model, such that the simulations pose challenges to the fitting machinery. ```{r logistic_ex} -#sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) -#sir$flows_expanded() sir_simulator = mp_simulator(sir_spec , time_steps = 100 , outputs = c("S", "I", "R") From f5362d3e5d6aa7f5c7fec43dd93b977204bfd7f4 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Fri, 12 Jan 2024 14:27:12 -0500 Subject: [PATCH 287/332] add jen freeman to authors list and revert version untill we are really ready to bump --- DESCRIPTION | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index efb81e88..418a96a7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,14 +1,15 @@ Package: macpan2 Title: Fast and Flexible Compartmental Modelling -Version: 1.0.0 +Version: 0.0.4 Authors@R: c( person("Steve Walker", email="swalk@mcmaster.ca", role=c("cre", "aut")), person("Irena Papst", role="ctb"), person("Michael Li", role="ctb"), person("Weiguang Guan", role="aut"), person("Ben Bolker", role="aut"), + person("Jen Freeman", role="aut"), person("Darren Flynn-Primrose", role="aut") - ) + ) Description: Fast and flexible compartmental modelling with Template Model Builder. License: GPL-3 Depends: From 03e9289662820521dd323bf1003a6fbac71d207d Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Sun, 14 Jan 2024 14:30:08 -0500 Subject: [PATCH 288/332] almost done #150 --- .../competition/calibration_example.R | 26 +-- .../predator_prey/calibration_example.R | 162 +++++++++++------- .../lotka_volterra/predator_prey/tmb.R | 80 ++++----- .../macpan_base/calibration_example.R | 51 ++++-- .../starter_models/seir/calibration_example.R | 19 +- .../sir_waning/calibration_example.R | 23 +-- inst/starter_models/ww/calibration_example.R | 24 +-- 7 files changed, 229 insertions(+), 156 deletions(-) diff --git a/inst/starter_models/lotka_volterra/competition/calibration_example.R b/inst/starter_models/lotka_volterra/competition/calibration_example.R index 817f18f5..ef7f2010 100644 --- a/inst/starter_models/lotka_volterra/competition/calibration_example.R +++ b/inst/starter_models/lotka_volterra/competition/calibration_example.R @@ -29,7 +29,9 @@ lv_comp = mp_simulator( ## ------------------------- # interested in estimating ayx - effect of species X on Y -lv_comp$replace$params(spec$default$ayx,"ayx") +lv_comp$update$transformations(Log("ayx")) +lv_comp$replace$params(log(spec$default$ayx),"log_ayx") +lv_comp ## ------------------------- ## specify objective function @@ -56,7 +58,7 @@ lv_comp$replace$obj_fn(obj_fn) true_ayx = 0.8/200 ## simulate observed data using true parameters -observed_data = lv_comp$report(true_ayx) +observed_data = lv_comp$report(log(true_ayx)) ## compute observed data - convert density to number of individuals Y_obs = rpois(time_steps, subset(observed_data, matrix == "Y", select = c(value)) %>% pull()) @@ -83,24 +85,24 @@ lv_comp$update$matrices( if (interactive()) { - ayx = seq(from = 0.1, to = 1.5, length = 100)/200 + log_ayx = log(seq(from = 0.1, to = 1.5, length = 100)/200) ll = vapply( - ayx + log_ayx , lv_comp$objective , numeric(1L) ) - dat_for_plot <- (cbind(ayx, ll) + dat_for_plot <- (cbind(log_ayx, ll) %>% data.frame() ) - ggplot(dat_for_plot, aes(ayx, ll)) + + ggplot(dat_for_plot, aes(log_ayx, ll)) + geom_line()+ ## add true parameter values to compare - geom_vline(xintercept = true_ayx, col='red')+ + geom_vline(xintercept = log(true_ayx), col='red')+ theme_bw()+ - xlab("ayx") + xlab("log(ayx)") } @@ -116,8 +118,8 @@ if (interactive()) { ## estimate is close to true print(lv_comp$current$params_frame()) - print(paste0("default ayx ",lv_comp$current$params_frame()$default)) - print(paste0("current ayx ",lv_comp$current$params_frame()$current)) + print(paste0("exp(default ayx) ",exp(lv_comp$current$params_frame()$default))) + print(paste0("exp(current ayx) ",exp(lv_comp$current$params_frame()$current))) print(paste0("true ayx ",true_ayx)) data_to_plot <- (cbind(as.numeric(Y_obs), Y_obs_times) @@ -146,12 +148,14 @@ if (interactive()) { ggplot(lv_comp$report() %>% select(time,value,matrix), aes(time,value,col=matrix))+ geom_line()+ theme_bw()+ + # plot carrying capacity of each species geom_hline(aes(yintercept = lv_comp$get$initial("axx")^(-1),col='X'), linetype="dashed")+ annotate("text",label="Kx",x=time_steps,y=lv_comp$get$initial("axx")^(-1),vjust=-1)+ geom_hline(aes(yintercept = lv_comp$get$initial("ayy")^(-1),col='Y'), linetype="dashed")+ annotate("text",label="Ky",x=time_steps,y=lv_comp$get$initial("ayy")^(-1),vjust=-1)+ - guides(label=FALSE,vjust=FALSE)+ + + guides(label="none")+ labs(col = "species")+ ylab("individuals") diff --git a/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R b/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R index fcc7b5f8..108040b7 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R +++ b/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R @@ -18,7 +18,7 @@ spec time_steps = 100L # simulator object -pred_prey = mp_simulator( +lv_pred_prey = mp_simulator( model = spec , time_steps = time_steps , outputs = c("X","Y") @@ -29,9 +29,10 @@ pred_prey = mp_simulator( ## ------------------------- # interested in estimating delta - predator growth from predation -pred_prey$replace$params(spec$default$delta,"delta") +lv_pred_prey$update$transformations(Log("delta")) +lv_pred_prey$replace$params(log(spec$default$delta),"log_delta") +lv_pred_prey -pred_prey ## ------------------------- ## specify objective function @@ -41,13 +42,13 @@ pred_prey obj_fn = ~ -sum(dpois(X_obs, rbind_time(X, X_obs_times))) # update simulator to create new variables -pred_prey$update$matrices( +lv_pred_prey$update$matrices( X_obs = empty_matrix , X_obs_times = empty_matrix ) # update simulator to include this function -pred_prey$replace$obj_fn(obj_fn) +lv_pred_prey$replace$obj_fn(obj_fn) ## ------------------------- @@ -55,10 +56,10 @@ pred_prey$replace$obj_fn(obj_fn) ## ------------------------- # delta value to simulate data with -true_delta = 2/200 +true_delta = 2.5/10 ## simulate observed data using true parameters -observed_data = pred_prey$report(true_delta) +observed_data = lv_pred_prey$report(log(true_delta)) ## compute incidence for observed data X_obs = rpois(time_steps, subset(observed_data, matrix == "X", select = c(value)) %>% pull()) @@ -73,68 +74,105 @@ if (interactive()) { ## update simulator with fake data to fit to ## ------------------------- -# pred_prey$update$matrices( -# X_obs = X_obs -# , X_obs_times = X_obs_times -# ) +lv_pred_prey$update$matrices( + X_obs = X_obs + , X_obs_times = X_obs_times +) ## ------------------------- ## plot likelihood surface (curve) ## ------------------------- -# -# if (interactive()) { -# -# Ca_seq = seq(from = 0.1, to = 1, length = 100) -# -# ll = vapply( -# Ca_seq -# , pred_prey$objective -# , numeric(1L) -# ) -# dat_for_plot <- (cbind(Ca_seq, ll) -# %>% data.frame() -# -# ) -# -# ggplot(dat_for_plot, aes(Ca_seq, ll)) + -# geom_line()+ -# ## add true parameter values to compare -# geom_vline(xintercept = true_Ca, col='red')+ -# xlab("Ca") -# -# } + +if (interactive()) { + + log_delta = seq(from = log(0.05), to = log(0.5), length = 100) + + ll = vapply( + log_delta + , lv_pred_prey$objective + , numeric(1L) + ) + dat_for_plot <- (cbind(log_delta, ll) + %>% data.frame() + + ) + + ggplot(dat_for_plot, aes(log_delta, ll)) + + geom_line()+ + ## add true parameter values to compare + geom_vline(xintercept = log(true_delta), col='red')+ + xlab("log(delta)") + +} ## ------------------------- ## fit parameters ## ------------------------- -# -# ## optimize and check convergence -# pred_prey$optimize$nlminb() -# -# ## plot observed vs predicted -# if (interactive()) { -# -# ## estimate is close to true -# print(pred_prey$current$params_frame()) -# print(paste0("default Ca ",pred_prey$current$params_frame()$default)) -# print(paste0("current Ca ",pred_prey$current$params_frame()$current)) -# -# data_to_plot <- (cbind(as.numeric(X_obs),1:time_steps) -# %>% data.frame() -# %>% setNames(c("value","time")) -# %>% mutate(type="observed") -# ) %>% union(pred_prey$report() -# %>% filter(matrix=="Ia") -# %>% select(time,value) -# %>% mutate(type="predicted") -# ) -# -# ggplot(data_to_plot, aes(x=time, y=value, col=type))+ -# geom_line()+ -# theme_bw()+ -# ylab("Ia") -# -# } -# +## optimize and check convergence +lv_pred_prey$optimize$nlminb() + +## plot observed vs predicted +if (interactive()) { + + print(lv_pred_prey$current$params_frame()) + print(paste0("exp(default delta) ",exp(lv_pred_prey$current$params_frame()$default))) + print(paste0("exp(current delta) ",exp(lv_pred_prey$current$params_frame()$current))) + print(paste0("true delta ",true_delta)) + + data_to_plot <- (cbind(as.numeric(X_obs),1:time_steps) + %>% data.frame() + %>% setNames(c("value","time")) + %>% mutate(type="observed") + ) %>% union(lv_pred_prey$report() + %>% filter(matrix=="X") + %>% select(time,value) + %>% mutate(type="predicted") + ) + + ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + geom_line()+ + theme_bw()+ + ylab("X") + +} + + +## ------------------------- +## exploring +## ------------------------- + +## dynamics +if (interactive()) { + ggplot(lv_pred_prey$report() %>% select(time,value,matrix), aes(time,value,col=matrix))+ + geom_line()+ + theme_bw()+ + ylab("individuals") + +} + +## exponential prey growth instead of logistic +## set K_inverse=0 +lv_pred_prey$get$initial("K_inverse") + +## define new simulator and update default +lv_exp_prey = mp_simulator( + model = spec + , time_steps = time_steps + , outputs = c("X","Y") + , default = list("K_inverse" = 0, "alpha"=0.1) +) +lv_exp_prey$get$initial("K_inverse") + +## better way to get initial population size +#lv_exp_prey$report() %>% filter(matrix=="X" & time==0) %>% select(value) +## set Y0=0 +ggplot(lv_exp_prey$report() %>% filter(matrix=="X"), aes(time,value))+ + geom_line()+ + theme_bw()+ + ylab("prey") + + + +## Holling functions diff --git a/inst/starter_models/lotka_volterra/predator_prey/tmb.R b/inst/starter_models/lotka_volterra/predator_prey/tmb.R index 92ddeb63..c6aca72d 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/tmb.R +++ b/inst/starter_models/lotka_volterra/predator_prey/tmb.R @@ -1,5 +1,36 @@ library(macpan2) +## functional responses + +#' Holling type I +#' +#' @param a predator attack rate; prey/predator/time_unit +#' +holling_i <- function(a, X){ + engine_eval(~ a * X, a=a, X=X) +} + +#' Holling type II +#' +#' @param a predator attack rate; prey/predator/time_unit +#' @param h handling time (ex. 1 day) +#' +holling_ii <- function(a, h, X){ + a * X / (1 + a * h * X) +} + +#' Holling type III +#' +#' @param a predator attack rate; prey/predator/time_unit +#' @param h handling time (ex. 1 day) +#' @param k exponent of prey density, k > 1 +#' +holling_iii <- function(a, h, X, k){ + a * (X ^ k) / (1 + a * h * (X ^ k)) +} + + +## model initialize_state = list( X ~ 100 # prey , Y ~ 100 # predator @@ -13,10 +44,11 @@ flow_rates = list( , mortality_y ~ gamma * Y ## effects from predation ## compute functional response (f(X) = X, default) - , functional_response ~ holling_i(a = a, X = X) + , functional_response ~ a * X - # , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) + # , functional_response ~ holling_i(a = a, X = X) # , functional_response ~ holling_ii(a = 5, h = 1, X = X) + # , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) ## mortality rate of prey (due to predation) , mortality_x ~ functional_response * Y @@ -31,13 +63,13 @@ state_updates = list( ## set defaults default = list( - alpha = 1e-3 # prey growth - #, beta = 1/100 # prey loss from predation - , gamma = 0.2 # predator mortality - , delta = 1/200 # predator gain from predation - , K_inverse = 1/1000 # prey carrying capacity - , a = 1e-3 # predator attack rate - , h = 1 # handling time + alpha = 1/5 # prey growth + #, beta = 1/100 # prey loss from predation + , gamma = 1/50 # predator mortality + , delta = 1/10 # predator gain from predation + , K_inverse = 1/500 # prey carrying capacity + , a = 1/500 # predator attack rate + , h = 1 # handling time ) @@ -49,36 +81,6 @@ spec = mp_tmb_model_spec( ) -## functional responses - -#' Holling type I -#' -#' @param a predator attack rate; prey/predator/time_unit -#' -holling_i <- function(a, X){ - a * X -} - -#' Holling type II -#' -#' @param a predator attack rate; prey/predator/time_unit -#' @param h handling time (ex. 1 day) -#' -holling_ii <- function(a, h, X){ - a * X / (1 + a * h * X) -} - -#' Holling type III -#' -#' @param a predator attack rate; prey/predator/time_unit -#' @param h handling time (ex. 1 day) -#' @param k exponent of prey density, k > 1 -#' -holling_iii <- function(a, h, X, k){ - a * (X ^ k) / (1 + a * h * (X ^ k)) -} - - diff --git a/inst/starter_models/macpan_base/calibration_example.R b/inst/starter_models/macpan_base/calibration_example.R index e4b04aaf..2a47ebbb 100644 --- a/inst/starter_models/macpan_base/calibration_example.R +++ b/inst/starter_models/macpan_base/calibration_example.R @@ -29,8 +29,8 @@ macpan_base = mp_simulator( ## ------------------------- # interested in estimating asymptomatic relative transmission rate -macpan_base$replace$params(spec$default$Ca,"Ca") - +macpan_base$update$transformations(Log("Ca")) +macpan_base$replace$params(log(spec$default$Ca),"log_Ca") macpan_base ## ------------------------- @@ -59,7 +59,7 @@ macpan_base$replace$obj_fn(obj_fn) true_Ca = 0.8 ## simulate observed data using true parameters -observed_data = macpan_base$report(true_Ca) +observed_data = macpan_base$report(log(true_Ca)) ## compute incidence for observed data Ia_obs = rpois(time_steps, subset(observed_data, matrix == "Ia", select = c(value)) %>% pull()) @@ -86,23 +86,23 @@ macpan_base$update$matrices( # plot surface as contours if (interactive()) { - Ca_seq = seq(from = 0.1, to = 1, length = 100) + log_Ca_seq = seq(from = log(0.1), to = log(1), length = 100) ll = vapply( - Ca_seq + log_Ca_seq , macpan_base$objective , numeric(1L) ) - dat_for_plot <- (cbind(Ca_seq, ll) + dat_for_plot <- (cbind(log_Ca_seq, ll) %>% data.frame() ) - ggplot(dat_for_plot, aes(Ca_seq, ll)) + + ggplot(dat_for_plot, aes(log_Ca_seq, ll)) + geom_line()+ ## add true parameter values to compare - geom_vline(xintercept = true_Ca, col='red')+ - xlab("Ca") + geom_vline(xintercept = log(true_Ca), col='red')+ + xlab("log(Ca)") } @@ -118,8 +118,8 @@ if (interactive()) { ## estimate is close to true print(macpan_base$current$params_frame()) - print(paste0("default Ca ",macpan_base$current$params_frame()$default)) - print(paste0("current Ca ",macpan_base$current$params_frame()$current)) + print(paste0("exp(default Ca) ",exp(macpan_base$current$params_frame()$default))) + print(paste0("exp(current Ca) ",exp(macpan_base$current$params_frame()$current))) data_to_plot <- (cbind(as.numeric(Ia_obs),1:time_steps) %>% data.frame() @@ -131,7 +131,7 @@ if (interactive()) { %>% mutate(type="predicted") ) - ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + ggplot(data_to_plot, aes(x=time, y=value, col=type, linetype = type))+ geom_line()+ theme_bw()+ ylab("Ia") @@ -142,6 +142,33 @@ if (interactive()) { ## exploring ## ------------------------- +## plotting Ia(t) vs. Ia(t+1) - should be linear? +if (interactive()) { + + Ia_shifted <- (macpan_base$report() + %>% filter(matrix=="Ia") + %>% mutate(Ia_lead = lead(value)) + %>% rename(Ia=value) + ) + + # linear model might not be appropriate + # what are the assumptions about error here? + lm_fit <- lm(data = Ia_shifted,Ia_lead ~ Ia) + summary(lm_fit) + + ggplot(Ia_shifted, aes(Ia, Ia_lead))+ + geom_line()+ + + #add lm + # geom_abline(intercept = lm_fit$coefficients[1], + # slope = lm_fit$coefficients[2], col="red")+ + + geom_smooth(method = "lm", se = FALSE, col="blue")+ + theme_bw() + +} + + ## all infectious compartments if (interactive()) { ggplot(macpan_base$report() %>% select(time,value,matrix), aes(time,value,col=matrix))+ diff --git a/inst/starter_models/seir/calibration_example.R b/inst/starter_models/seir/calibration_example.R index 19c08c95..24d80f56 100644 --- a/inst/starter_models/seir/calibration_example.R +++ b/inst/starter_models/seir/calibration_example.R @@ -48,8 +48,9 @@ seir$replace$obj_fn(obj_fn) # choose which parameter(s) to estimate # 1/alpha = time spent in E compartment -seir$replace$params(spec$default$alpha,"alpha") - +seir$update$transformations(Log("alpha")) +seir$replace$params(log(spec$default$alpha),"log_alpha") +seir ## ------------------------- ## simulate fake data @@ -59,7 +60,7 @@ seir$replace$params(spec$default$alpha,"alpha") true_alpha = 1/5 ## simulate observed data using true parameters -observed_data = seir$report(true_alpha) +observed_data = seir$report(log(true_alpha)) ## compute exposure for each time step E_obs = rpois(time_steps, subset(observed_data, matrix == "E", select = c(value)) %>% pull()) @@ -85,14 +86,14 @@ seir$update$matrices( ## ------------------------- if (interactive()) { - alphas = seq(from = 1/100, to = 1, length = 100) + log_alphas = seq(from = log(1/100), to = log(1), length = 100) ll = vapply( - alphas + log_alphas , seir$objective , numeric(1L) ) - plot(alphas, ll, type = "l", las = 1) - abline(v = true_alpha) + plot(log_alphas, ll, type = "l", las = 1) + abline(v = log(true_alpha)) } ## ------------------------- @@ -106,8 +107,8 @@ seir$optimize$nlminb() ## plot observed vs predicted if (interactive()) { print(seir$current$params_frame()) - print(paste0("default alpha ",seir$current$params_frame()$default)) - print(paste0("current alpha ",seir$current$params_frame()$current)) + print(paste0("exp(default alpha) ",exp(seir$current$params_frame()$default))) + print(paste0("exp(current alpha) ",exp(seir$current$params_frame()$current))) plot(E_obs, type = "l", las = 1) lines(seir$report() %>% filter(matrix=="E") %>% select(time,value), col = "red") } diff --git a/inst/starter_models/sir_waning/calibration_example.R b/inst/starter_models/sir_waning/calibration_example.R index ebefb181..5eaa6add 100644 --- a/inst/starter_models/sir_waning/calibration_example.R +++ b/inst/starter_models/sir_waning/calibration_example.R @@ -45,11 +45,12 @@ sir_waning$replace$obj_fn(obj_fn) ## parameterize model ## ------------------------- +# choose which parameter(s) to estimate - log(beta) and phi sir_waning$update$transformations(Log("beta")) +sir_waning$update$transformations(Log("phi")) -# choose which parameter(s) to estimate - log(beta) and phi -sir_waning$replace$params(c(log(spec$default$beta),spec$default$phi), - c("log_beta","phi") +sir_waning$replace$params(c(log(spec$default$beta),log(spec$default$phi)), + c("log_beta","log_phi") ) sir_waning @@ -66,7 +67,7 @@ true_beta = 0.3 true_phi = 0.09 ## simulate observed data using true parameters -observed_data = sir_waning$report(c(log(true_beta),true_phi)) +observed_data = sir_waning$report(c(log(true_beta),log(true_phi))) ## compute incidence for observed data I_obs = rpois(time_steps, subset(observed_data, matrix == "I", select = c(value)) %>% pull()) @@ -93,22 +94,22 @@ sir_waning$update$matrices( # plot surface as contours if (interactive()) { log_betas = seq(from = log(0.1), to = log(1), length = 100) - phis = seq(from = 1e-3, to = 0.2, length = 100) - x_y = expand.grid(log_betas, phis) %>% setNames(c("log_betas","phis")) + log_phis = seq(from = log(1e-3), to = log(0.2), length = 100) + x_y = expand.grid(log_betas, log_phis) %>% setNames(c("log_betas","log_phis")) ll = apply( x_y , 1 - , function(z) {sir_waning$objective(z["log_betas"], z["phis"])} + , function(z) {sir_waning$objective(z["log_betas"], z["log_phis"])} ) dat_for_plot <- cbind(x_y, ll) - ggplot(dat_for_plot, aes(log_betas, phis, z=ll)) + + ggplot(dat_for_plot, aes(log_betas, log_phis, z=ll)) + geom_contour_filled()+ ## add true parameter values to compare geom_vline(xintercept = log(true_beta), col='red')+ - geom_hline(yintercept = true_phi, col='red') + geom_hline(yintercept = log(true_phi), col='red') } @@ -128,8 +129,8 @@ if (interactive()) { print(sir_waning$current$params_frame()) print(paste0("exp(default beta) ",exp(sir_waning$current$params_frame()$default[1]))) print(paste0("exp(current beta) ",exp(sir_waning$current$params_frame()$current[1]))) - print(paste0("default phi ",sir_waning$current$params_frame()$default[2])) - print(paste0("current phi ",sir_waning$current$params_frame()$current[2])) + print(paste0("exp(default phi) ",exp(sir_waning$current$params_frame()$default[2]))) + print(paste0("exp(current phi) ",exp(sir_waning$current$params_frame()$current[2]))) data_to_plot <- (cbind(as.numeric(I_obs),1:time_steps) diff --git a/inst/starter_models/ww/calibration_example.R b/inst/starter_models/ww/calibration_example.R index a41a02b3..984e6d3a 100644 --- a/inst/starter_models/ww/calibration_example.R +++ b/inst/starter_models/ww/calibration_example.R @@ -29,7 +29,8 @@ ww = mp_simulator( ## ------------------------- # interested in estimating asymptomatic relative transmission rate -ww$replace$params(spec$default$Ca,"Ca") +ww$update$transformations(Log("Ca")) +ww$replace$params(log(spec$default$Ca),"log_Ca") ww ## ------------------------- @@ -58,7 +59,7 @@ ww$replace$obj_fn(obj_fn) true_Ca = 0.8 ## simulate observed data using true parameters -observed_data = ww$report(true_Ca) +observed_data = ww$report(log(true_Ca)) ## compute incidence for observed data Ia_obs = rpois(time_steps, subset(observed_data, matrix == "Ia", select = c(value)) %>% pull()) @@ -82,26 +83,25 @@ ww$update$matrices( ## plot likelihood surface (curve) ## ------------------------- -# plot surface as contours if (interactive()) { - Ca_seq = seq(from = 0.1, to = 1, length = 100) + log_Ca_seq = seq(from = log(0.1), to = log(1), length = 100) ll = vapply( - Ca_seq + log_Ca_seq , ww$objective , numeric(1L) ) - dat_for_plot <- (cbind(Ca_seq, ll) + dat_for_plot <- (cbind(log_Ca_seq, ll) %>% data.frame() ) - ggplot(dat_for_plot, aes(Ca_seq, ll)) + + ggplot(dat_for_plot, aes(log_Ca_seq, ll)) + geom_line()+ ## add true parameter values to compare - geom_vline(xintercept = true_Ca, col='red')+ - xlab("Ca") + geom_vline(xintercept = log(true_Ca), col='red')+ + xlab("log(Ca)") } @@ -117,8 +117,8 @@ if (interactive()) { ## estimate is close to true print(ww$current$params_frame()) - print(paste0("default Ca ",ww$current$params_frame()$default)) - print(paste0("current Ca ",ww$current$params_frame()$current)) + print(paste0("exp(default Ca) ",exp(ww$current$params_frame()$default))) + print(paste0("exp(current Ca) ",exp(ww$current$params_frame()$current))) data_to_plot <- (cbind(as.numeric(Ia_obs),1:time_steps) %>% data.frame() @@ -130,7 +130,7 @@ if (interactive()) { %>% mutate(type="predicted") ) - ggplot(data_to_plot, aes(x=time, y=value, col=type))+ + ggplot(data_to_plot, aes(x=time, y=value, col=type, linetype = type))+ geom_line()+ theme_bw()+ ylab("Ia") From 82160e12f0b6982d59e1454bd87ad8c368557ec0 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Sun, 14 Jan 2024 17:44:56 -0500 Subject: [PATCH 289/332] .Rbuildignore cached files with long pathnames --- .Rbuildignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index a698a372..c736a897 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -29,3 +29,5 @@ drat/ ^vignettes/quickstart_products[.]Rmd$ ^vignettes/sir_radial_basis_transmission[.]Rmd$ ^vignettes/time_varying_parameters[.]Rmd$ + +.*/html/.* From b26d6291202767f8836cb24c2b2a95f0a1089ca1 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 14 Jan 2024 20:41:40 -0500 Subject: [PATCH 290/332] clean up examples for #155 --- NAMESPACE | 3 - R/calibration.R | 299 ------------------ .../lotka_volterra/competition/README.md | 2 +- .../lotka_volterra/predator_prey/README.md | 2 +- inst/starter_models/sir_demo/README.md | 2 +- man/Compartmental.Rd | 47 --- man/CompartmentalSimulator.Rd | 58 ---- man/macpan2-package.Rd | 1 + man/mk_calibrate.Rd | 95 ------ misc/build/run_examples.R | 10 +- {R => misc/old-r-source}/compartmental.R | 0 11 files changed, 13 insertions(+), 506 deletions(-) delete mode 100644 man/Compartmental.Rd delete mode 100644 man/CompartmentalSimulator.Rd delete mode 100644 man/mk_calibrate.Rd rename {R => misc/old-r-source}/compartmental.R (100%) diff --git a/NAMESPACE b/NAMESPACE index 3a6789aa..d0778f6c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -105,9 +105,7 @@ S3method(to_positions,numeric) export(BinaryOperator) export(CSVReader) export(Collection) -export(Compartmental) export(Compartmental2) -export(CompartmentalSimulator) export(DerivationExtractor) export(Derivations) export(Derivations2ExprList) @@ -178,7 +176,6 @@ export(labelled_frame) export(labelled_zero_vector) export(labelling_column_names) export(make_expr_parser) -export(mk_calibrate) export(model_starter) export(mp_aggregate) export(mp_calibrator) diff --git a/R/calibration.R b/R/calibration.R index a9cbe87d..000d8f15 100644 --- a/R/calibration.R +++ b/R/calibration.R @@ -92,305 +92,6 @@ TMBOptimizationHistory = function(simulator) { } -add_slot <- function(sim, x, value = empty_matrix, save_x = FALSE, return_x = FALSE) { - args <- list() - argstr <- list() - if (save_x) { - args <- c(args, list(.mats_to_save = x)) - argstr <- append(argstr, sprintf(".mats_to_save = %s", x)) - } - if (return_x) { - args <- c(args, list(.mats_to_return = x)) - argstr <- append(argstr, sprintf(".mats_to_return = %s", x)) - } - args <- c(list(value), args) - argstr <- append(argstr, sprintf("%s = %s", x, deparse(substitute(value)))) - names(args)[1] <- x - do.call(sim$add$matrices, args) - argstr <- sprintf("sim$add$matrices(%s)", - do.call(paste, c(list(unlist(argstr)), list(collapse = ", ")))) - return(invisible(argstr)) -} - -## Note: example is failing with bf1de7f99 -## with: Error in valid$consistency_params_mats$check(self$model) : -## optimization parameters are not consistent with matrices -## but validity could not be checked because: -## Error in if (any(!valid_pars)) { : missing value where TRUE/FALSE needed - -##' Add calibration information to a simulator -##' -##' ## To do/FIXME -##' * see hacks for getting simulation variables, state variables -##' * modularize? -##' * switch for enabling a differenced/incidence class (add a flow/accumulator var to model; add a differencing step)? -##' * allow setting clamp tolerance? allow specified list of variables to clamp rather than all or nothing -##' * rename and move into macpan2 -##' * document that 'log-likelihood' means -1*(loss function) (e.g. for SSQ, chi-squared fits) -##' @param sim A \code{macpan2} simulator (i.e., a \code{TMBSimulator} object). -##' @param params a list of parameters with default/starting values. -##' @param transforms TODO. -##' @param data A data frame containing data to add (i.e., observed variables that will be compared with simulations). If the data frame contains a column called "time" or "date" (any capitalization), it will be used. -##' @param start_time A time or date, overriding first time in data; set to 1 otherwise. -##' @param end_time A time or date, overriding last time in data; set to number of time steps otherwise. -##' @param exprs A list of expressions to add. -##' @param debug (logical) Print debugging information? -##' @param clamp_vars (logical) Force state variables to be positive in likelihood expression? -##' @return This function modifies the simulator object **in place**. It also returns (invisibly) a character vector of the lower-level operations it performs. -##' @export -#' @examples -#' ## it's convenient to have a function that sets up a fresh simulation -#' ## (since adding already-existing components to a simulation object throws an error) -#' library(dplyr) -#' setup_sim <- function() { -#' m <- Compartmental(system.file("starter_models", "sir", package = "macpan2")) -#' sim <- m$simulators$tmb( -#' time_steps = 100, -#' state = c(S = 99, I = 1, R = 0), -#' flow = c(foi = NA, gamma = 0.1), -#' beta = 0.2, -#' N = empty_matrix -#' ) -#' } -#' sim <- setup_sim() -#' if (require(outbreaks)) { -#' I_obs <- influenza_england_1978_school[["in_bed"]] -#' } else { -#' set.seed(101) -#' I_obs <- (sim$report(.phases = "during") -#' |> filter(row == "I") -#' |> mutate(obs = rnbinom(100, mu = value, size = 2)) -#' |> pull(obs) -#' ) -#' } -#' m1 <- mk_calibrate(sim, -#' data = data.frame(I_obs), -#' params = list(beta = 0.2, I_sd = 1), -#' transforms = list(beta = "log", I_sd = "log"), -#' exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), -#' ) -#' cat(m1, sep = "\n") -#' sim$optimize$nlminb() -#' sim <- setup_sim() ## refresh -#' mk_calibrate(sim, -#' data = data.frame(I_obs), -#' params = list(beta = 0.2, gamma = 0.05), -#' transforms = list(beta = "log", gamma = "log"), -#' exprs = list(log_lik ~ dpois(I_obs, I)) -#' ) -#' sim$optimize$nlminb() -#' ## warning about NA/NaN function evaluation is probably harmless ... -mk_calibrate <- function(sim, - params = list(), - transforms = list(), - data = NULL, - start_time = NULL, - end_time = NULL, - exprs = list(), - debug = FALSE, - clamp_vars = FALSE) { - ## how do I get these programmatically from sim? - ## is there a better/easier way to get state names?? - ## these are present in 'Compartmental' objects; - ## easy to get with sim$labels$state(). Should they be carried along - ## somehow? - data_sub <- deparse(substitute(data)) - desc <- list() - - ## if only one data frame is passed, assume that it is - ## referencing state variables to be fitted. in the future - ## we will allow a list of data frames, with one per vector. - if (inherits(data, "data.frame")) data = list(state = data) - - logit <- plogis ## ugh; better way to handle transformations? - - ## for testing! - - cap <- function(s) paste0(toupper(substring(s, 1, 1)), substring(s, 2)) - - ## add log-likelihood slot - ## add comments ??? - desc <- append(desc, "# add log_lik matrix (empty)") - - desc <- append(desc, add_slot(sim, "log_lik")) - ## added_vars <- character(0) - - ## add data - if (!is.null(data)) { - for (vctr in names(data)) { - - if (!is.data.frame(data[[vctr]])) stop("'data' argument must be a data frame or list of data frames") - timecol <- grep("([Tt]ime|[Dd]ate)", names(data[[vctr]])) - if (length(timecol) > 1) stop("multiple time/date columns detected") - if (length(timecol) == 0 && (!is.null(start_time) || !is.null(end_time))) { - stop("if start_time or end_time are specified, data must include a time/date column") - } - if (length(timecol) > 0) { - timevec <- data[[vctr]][[timecol]] - if (length(unique(timevec)) < length(timevec)) stop("time steps in data must be unique") - if (any(diff(timevec) < 0)) stop("data must be sorted by time") - if (any(diff(timevec) < 1)) stop("time steps must be equal to 1") - if (is.null(start_time)) start_time <- timevec[1] - if (is.null(end_time)) end_time <- tail(timevec, 1) - ts <- as.numeric(end_time - start_time) + 1 ## check on +1/edge effect issues? - ## generate time index that matches up with data frame - timevec <- as.integer(timevec - start_time + 1) - data[[vctr]] <- data[[vctr]][, -timecol, drop = FALSE] - } else { - ts <- nrow(data[[vctr]]) - timevec <- seq(ts) - } - irreg_time <- any(is.na(data[[vctr]])) || any(diff(timevec) > 1) - cur_ts <- sim$tmb_model$time_steps$time_steps - if (ts != cur_ts) { - if (debug) cat(sprintf("resetting number of time steps (%d -> %d)\n", - cur_ts, nrow(data[[vctr]]))) - sim$replace$time_steps(nrow(data[[vctr]])) - desc <- append(desc, sprintf("sim$replace$time_steps(%d)", nrow(data[[vctr]]))) - } - for (nm in names(data[[vctr]])) { - if (debug) cat(sprintf("add data matrix: %s\n", nm)) - do.call(sim$add$matrices, data[[vctr]][nm]) - desc <- append(desc, sprintf("sim$add$matrices(%s[['%s']]", data_sub, nm)) - } - } - } - - ## ?? needs to go before expressions get added? - for (p in setdiff(names(params), sim$matrix_names())) { - ## add params if not already in model (e.g. dispersion parameter) - if (debug) cat("add param (scalar placeholder value): ", p, "\n") - desc <- append(desc, add_slot(sim, p, 1.0)) - } - - ## add _sim analogues for state variables referred to in expressions; - ## substitute rbind_time(*_sim) in expressions - for (i in seq_along(exprs)) { - ee <- exprs[[i]] - if (debug) cat("process expression: ", deparse(ee), "\n") - all_vars <- all.vars(ee) - ## create a placeholder - ## FIXME: change logic to allow time indices for derived variables as well as state variables? - ## should have a separate loop that checks for 'specials' (dnorm, dpois, etc.) and uses those arguments - ## to match sim vs obs for time-index-matching - ## right now this *won't* work for the combination of derived variables (e.g. sum of hospital classes, incidences) + irregular time - for (vctr in names(data)) { - for (v in intersect(all_vars, labels(sim)[[vctr]])) { - ph <- paste0(v, "_sim") - if (debug) cat("add (empty matrix): ", ph, "\n") - desc <- append(desc, add_slot(sim, ph, save_x = TRUE)) - newexpr <- reformulate(v, response = ph, env = emptyenv()) - if (debug) cat("add ", deparse(newexpr), "\n") - sim$insert$expressions( - newexpr, - .phase = "during", - .at = Inf) - desc <- append(desc, sprintf("sim$insert$expressions(%s, .phase = 'during', .at = Inf", deparse(newexpr))) - if (irreg_time) { - ## could skip indices for any data elements that are regular? - ## need to match observed var with sim var explicitly - ## for now let's hope there's a one-to-one match between expressions in the state vector - ## and observed data ... - data_var <- intersect(all_vars, names(data[[vctr]])) - t_ind <- timevec[!is.na(data[[vctr]][[data_var]])] - t_indnm <- paste0(v, "_tind") - do.call(sim$add$matrices, setNames(list(t_ind), t_indnm)) - if (debug) cat(sprintf("add data matrix: %s", t_indnm)) - desc <- append(desc, sprintf("%s <- %s", t_indnm, deparse(t_ind))) - desc <- append(desc, sprintf("sim$add$matrices(%s)", t_indnm)) - bind_var <- sprintf("rbind_time(%s, %s)", ph, t_indnm) - } else { - bind_var <- sprintf("rbind_time(%s)", ph) - } - ## convert to parsed expression, then get rid of expression() - newsym <- parse(text = bind_var)[[1]] - exprs[[i]] <- do.call(substitute, - list(ee, setNames(list(newsym), v))) - ## substitute clamp(*_sim) [INSIDE] rbind_time() - if (clamp_vars) { - clamp_var <- sprintf("clamp(%s)", ph) - newsym <- parse(text = clamp_var)[[1]] - exprs[[i]] <- do.call(substitute, - list(exprs[[i]], setNames(list(newsym), ph))) - } - } - } - if (debug) cat("add ", deparse(exprs[[i]]), "\n") - sim$insert$expressions(exprs[[i]], .phase = "after") - desc <- append(desc, sprintf("sim$insert$expressions(%s, .phase = 'after', .at = Inf", deparse(exprs[[i]]))) - } - - ## modify names for transforms, apply transform to specified values - pp = function(...) { - l = list(...) - l = l[!vapply(l, is.null, logical(1L))] - if (length(l) == 1L) return(l[[1L]]) - if (length(l) == 0L) return("") - do.call(paste, c(l, sep = "_")) - } - trans_params = character() - for (m in names(params)) { - tr_params = names(transforms[[m]]) - if (!is.null(tr_params)) { - trans_params = append(trans_params, - paste( - unname(transforms[[m]]), - names(params[[m]][tr_params]), - sep = "_" - ) - ) - } else { - trans_params = append(trans_params, - paste(transforms[[m]], names(params[m]), sep = "_") - ) - } - } - - # trpars <- transforms != "" - # trp <- params[trpars] - # names(trp) <- paste(transforms[trpars], names(trp), sep = "_") - ## trp <- Map(function(x, tr) get(tolower(tr))(x), trp, transforms[trpars]) - pframe <- data.frame(mat = trans_params, row = 0, col = 0, default = unlist(trp)) - rownames(pframe) <- NULL ## cosmetic - - desc <- append(desc, sprintf("pframe <- data.frame(mat = %s, row = 0, col = 0, default = %s)", - deparse(names(trp)), deparse(unname(unlist(trp))))) - if (debug) { - cat("param_frame:\n") - print(pframe) - } - - ## FIXME: handle no transformation case - if (debug) cat("adding transformations\n") - add_trans <- function(tr, nm) { - if (debug) cat("add transformation: ", cap(tr)," ", nm, "\n") - sim$add$transformations(get(cap(tr))(nm)) - ## does package checking complain about <<- ? could use assign(..., parent.frame()) - desc <<- append(desc, sprintf("sim$add$transformations(%s(\"%s\"))", cap(tr), nm)) - } - - ## add transformations - Map(add_trans, transforms[trpars], names(params)[trpars]) - - ## now add param frame (does order matter??) - ## - ## FIXME: refresh_param not found - sim$replace$params_frame(pframe) - desc <- append(desc, sprintf("sim$replace$params_frame(pframe)")) - - if (debug) cat("set obj_fn to -sum(log_lik)\n") - sim$replace$obj_fn(~ -sum(log_lik)) - desc <- append(desc, "sim$replace$obj_fn(~ -sum(log_lik))") - ## add transformations - ## add parameters - ## for now, assume all parameters are scalar? - - ## everything is done as a side effect (mutating state of sim) - return(invisible(unlist(desc))) - -} - - #' Pick out obs/location pairs from terms involving probability distributions #' find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) #' find_obs_pairs(stuff ~ other_stuff + more_stuff) diff --git a/inst/starter_models/lotka_volterra/competition/README.md b/inst/starter_models/lotka_volterra/competition/README.md index fe806293..b8c3f173 100644 --- a/inst/starter_models/lotka_volterra/competition/README.md +++ b/inst/starter_models/lotka_volterra/competition/README.md @@ -41,4 +41,4 @@ $$ # References -Hastings, A. (1997). Competition. In: *Population Biology*. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_7 \ No newline at end of file +Hastings, A. (1997). Competition. In: *Population Biology*. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_7 diff --git a/inst/starter_models/lotka_volterra/predator_prey/README.md b/inst/starter_models/lotka_volterra/predator_prey/README.md index 17d8940c..41125f50 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/README.md +++ b/inst/starter_models/lotka_volterra/predator_prey/README.md @@ -84,4 +84,4 @@ $$ f(X) = \frac{a X^k}{1 + ah X^k} \:, k > 1$$ # References Bolker, B. (2008). *Ecological Models and Data in R*. Princeton: Princeton University Press. https://doi.org/10.1515/9781400840908 -Hastings, A. (1997). Predator-Prey Interactions. In: *Population Biology*. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_8 \ No newline at end of file +Hastings, A. (1997). Predator-Prey Interactions. In: *Population Biology*. Springer, New York, NY. https://doi.org/10.1007/978-1-4757-2731-9_8 diff --git a/inst/starter_models/sir_demo/README.md b/inst/starter_models/sir_demo/README.md index b20a114f..a29b8d2a 100644 --- a/inst/starter_models/sir_demo/README.md +++ b/inst/starter_models/sir_demo/README.md @@ -38,4 +38,4 @@ $$ # References -Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 \ No newline at end of file +Earn, D.J.D. (2008). A Light Introduction to Modelling Recurrent Epidemics. In: Brauer, F., van den Driessche, P., Wu, J. (eds) Mathematical Epidemiology. Lecture Notes in Mathematics, vol 1945. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-540-78911-6_1 diff --git a/man/Compartmental.Rd b/man/Compartmental.Rd deleted file mode 100644 index b1534449..00000000 --- a/man/Compartmental.Rd +++ /dev/null @@ -1,47 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/compartmental.R -\name{Compartmental} -\alias{Compartmental} -\title{Compartmental Model} -\usage{ -Compartmental(model_directory) -} -\arguments{ -\item{model_directory}{String giving a path to a directory containing -the following files, \code{variables.csv}, \code{derivations.json}, \code{flows.csv}, -and \code{settings.json}, described by -\href{https://canmod.github.io/macpan2/articles/model_definitions}{this spec}.} -} -\value{ -An object with the following methods and fields. -\subsection{Methods}{ -\itemize{ -\item \verb{$variables$all()}: \code{\link{Partition}} object of all variables. -\item \verb{$flows()}: Data frame with rows giving all groups of flows. -\item \verb{$flows_expanded()}: Data frame with rows giving all individual flows. -\item \verb{$variables$state()}: \code{\link{Partition}} object of all state -variables. -\item \verb{$variables$flow()}: \code{\link{Partition}} object of all flow variables. -\item \verb{$all_labels()}: Character vector giving the labels of all variables. -\item \verb{$labels$state()}: Character vector giving the labels of all state -variables. -\item \verb{$labels$flow()}: Character vector giving the labels of all flow variables. -\item \verb{$labels$other()}: Character vector giving the labels of all variables -that are neither state nor flow variables. -\item \verb{$expr_list()}: \code{\link{ExprList}} object containing all expressions. -} -} - -\subsection{Fields}{ -\itemize{ -\item \verb{$def}: The \code{\link{ModelFiles}} object representing the directory -that defines the model. -\item \verb{$simulators}: A \code{\link{Simulators}} instance containing methods for -generating simulators, which are objects that can generate simulations from -the model. -} -} -} -\description{ -Create an object for containing a compartmental model. -} diff --git a/man/CompartmentalSimulator.Rd b/man/CompartmentalSimulator.Rd deleted file mode 100644 index a4ab38f8..00000000 --- a/man/CompartmentalSimulator.Rd +++ /dev/null @@ -1,58 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/compartmental.R -\name{CompartmentalSimulator} -\alias{CompartmentalSimulator} -\title{Compartmental Model Simulator (experimental)} -\usage{ -CompartmentalSimulator( - model_directory, - time_steps, - .mats_to_save = .mats_to_return, - .mats_to_return = "state", - .dimnames = list(), - .tmb_cpp = getOption("macpan2_dll"), - .initialize_ad_fun = TRUE -) -} -\arguments{ -\item{model_directory}{Path to a set of model definition files, defaults -and parameterizations.} - -\item{time_steps}{Number of simulation steps.} - -\item{.mats_to_save}{Character vector of names of matrices to save. Defaults -to \code{"state"}, which is the state vector. Other useful names include -\code{"total_inflow"} (the incidence associated with each state variable) -and \code{"total_outflow"} (the total leaving each state variable at each -time step). One may also add any variable in \code{model$other_labels()}. -See \code{\link{MatsList}} for details.} - -\item{.mats_to_return}{Character vector of names of matrices to return. Defaults -to \code{"state"}, which is the state vector. Other useful names include -\code{"total_inflow"} (the incidence associated with each state variable) -and \code{"total_outflow"} (the total leaving each state variable at each -time step). One may also add any variable in \code{model$other_labels()}. -See \code{\link{MatsList}} for details.} - -\item{.dimnames}{Named list of \code{\link{dimnames}} for matrices that change -their dimensions over the simulation steps. These names correspond to the -names of the matrices. The output of the simulations will try their best -to honor these names, but if the shape of the matrix is too inconsistent -with the \code{\link{dimnames}} then numerical indices will be used instead. -For matrices that do not change their dimensions, set \code{\link{dimnames}} -by adding \code{\link{dimnames}} to the matrices passed to \code{...}.} - -\item{.tmb_cpp}{Name of a \code{C++} program defining the engine. Typically -you just want to use the default, which is \code{macpan2}, unless you -are extending the -\href{https://canmod.github.io/macpan2/articles/cpp_side.html}{engine} -yourself.} - -\item{.initialize_ad_fun}{Should the automatic differentiation function (\code{ad_fun}) -be produced with TMB? Choosing \code{FALSE} can be useful if this is -expensive and you want to build up your simulator in steps without having -to recreate the \code{ad_fun} more than necessary.} -} -\description{ -Compartmental Model Simulator (experimental) -} diff --git a/man/macpan2-package.Rd b/man/macpan2-package.Rd index 29ed036d..55d8d638 100644 --- a/man/macpan2-package.Rd +++ b/man/macpan2-package.Rd @@ -26,6 +26,7 @@ Authors: \itemize{ \item Weiguang Guan \item Ben Bolker + \item Jen Freeman \item Darren Flynn-Primrose } diff --git a/man/mk_calibrate.Rd b/man/mk_calibrate.Rd deleted file mode 100644 index 1b410072..00000000 --- a/man/mk_calibrate.Rd +++ /dev/null @@ -1,95 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/calibration.R -\name{mk_calibrate} -\alias{mk_calibrate} -\title{Add calibration information to a simulator} -\usage{ -mk_calibrate( - sim, - params = list(), - transforms = list(), - data = NULL, - start_time = NULL, - end_time = NULL, - exprs = list(), - debug = FALSE, - clamp_vars = FALSE -) -} -\arguments{ -\item{sim}{A \code{macpan2} simulator (i.e., a \code{TMBSimulator} object).} - -\item{params}{a list of parameters with default/starting values.} - -\item{transforms}{TODO.} - -\item{data}{A data frame containing data to add (i.e., observed variables that will be compared with simulations). If the data frame contains a column called "time" or "date" (any capitalization), it will be used.} - -\item{start_time}{A time or date, overriding first time in data; set to 1 otherwise.} - -\item{end_time}{A time or date, overriding last time in data; set to number of time steps otherwise.} - -\item{exprs}{A list of expressions to add.} - -\item{debug}{(logical) Print debugging information?} - -\item{clamp_vars}{(logical) Force state variables to be positive in likelihood expression?} -} -\value{ -This function modifies the simulator object \strong{in place}. It also returns (invisibly) a character vector of the lower-level operations it performs. -} -\description{ -\subsection{To do/FIXME}{ -\itemize{ -\item see hacks for getting simulation variables, state variables -\item modularize? -\item switch for enabling a differenced/incidence class (add a flow/accumulator var to model; add a differencing step)? -\item allow setting clamp tolerance? allow specified list of variables to clamp rather than all or nothing -\item rename and move into macpan2 -\item document that 'log-likelihood' means -1*(loss function) (e.g. for SSQ, chi-squared fits) -} -} -} -\examples{ -## it's convenient to have a function that sets up a fresh simulation -## (since adding already-existing components to a simulation object throws an error) -library(dplyr) -setup_sim <- function() { - m <- Compartmental(system.file("starter_models", "sir", package = "macpan2")) - sim <- m$simulators$tmb( - time_steps = 100, - state = c(S = 99, I = 1, R = 0), - flow = c(foi = NA, gamma = 0.1), - beta = 0.2, - N = empty_matrix - ) -} -sim <- setup_sim() -if (require(outbreaks)) { - I_obs <- influenza_england_1978_school[["in_bed"]] -} else { - set.seed(101) - I_obs <- (sim$report(.phases = "during") - |> filter(row == "I") - |> mutate(obs = rnbinom(100, mu = value, size = 2)) - |> pull(obs) - ) -} -m1 <- mk_calibrate(sim, - data = data.frame(I_obs), - params = list(beta = 0.2, I_sd = 1), - transforms = list(beta = "log", I_sd = "log"), - exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)), -) -cat(m1, sep = "\n") -sim$optimize$nlminb() -sim <- setup_sim() ## refresh -mk_calibrate(sim, - data = data.frame(I_obs), - params = list(beta = 0.2, gamma = 0.05), - transforms = list(beta = "log", gamma = "log"), - exprs = list(log_lik ~ dpois(I_obs, I)) -) -sim$optimize$nlminb() -## warning about NA/NaN function evaluation is probably harmless ... -} diff --git a/misc/build/run_examples.R b/misc/build/run_examples.R index 32ae3697..c8c8a715 100644 --- a/misc/build/run_examples.R +++ b/misc/build/run_examples.R @@ -1,14 +1,22 @@ library(macpan2) run_eg = function(f) { + print(f) tmp = tempfile() tools::Rd2ex(f, tmp) if (file.exists(tmp)) { + print("Found examples ...") r = try(source(tmp), silent = TRUE) if (inherits(r, "try-error")) { + print("FAIL") print(tools::Rd2ex(f)) stop(r) } } } -.trash = lapply(list.files("man", full.names = TRUE), run_eg) +rd_files = list.files("man" + , full.names = TRUE + , pattern = "*.Rd" + , include.dirs = FALSE +) +.trash = lapply(rd_files, run_eg) print("success!") diff --git a/R/compartmental.R b/misc/old-r-source/compartmental.R similarity index 100% rename from R/compartmental.R rename to misc/old-r-source/compartmental.R From 0e91b6ebecfab41cecb634e9ede0c832784f729a Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 14 Jan 2024 21:30:41 -0500 Subject: [PATCH 291/332] check index range in c++ assign function --- misc/dev/dev.cpp | 21 ++++++++++++++------- src/macpan2.cpp | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index 57231cfc..a68f6ac4 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -2248,18 +2248,25 @@ class ExprEvaluator { SetError(255, "Assignment value matrices must have a single column", row); return m; } + + // apparently we still need this check + v1 = args.get_as_int_vec(1); + v2 = args.get_as_int_vec(2); + printIntVector(v1); + printIntVector(v2); + err_code = args.check_indices(0, v1, v2); // CheckIndices(args[0], args[1], m1); // err_code = CheckIndices(args[0], args[1], args[2]); - // if (err_code) { - // SetError(MP2_ASSIGN, "Illegal index used in assign", row); - // return m; - // } + if (err_code) { + SetError(MP2_ASSIGN, "Illegal index used in assign", row); + return m; + } rows = args[3].rows(); // err_code1 = RecycleInPlace(args[1], rows, cols); // err_code2 = RecycleInPlace(args[2], rows, cols); - v1.push_back(1); - v1.push_back(2); - args = args.recycle_to_shape(v1, rows, cols); + v3.push_back(1); + v3.push_back(2); + args = args.recycle_to_shape(v3, rows, cols); err_code = args.get_error_code(); // err_code = err_code1 + err_code2; if (err_code != 0) { diff --git a/src/macpan2.cpp b/src/macpan2.cpp index a34c2ea3..ac696a91 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -2249,18 +2249,25 @@ class ExprEvaluator { SetError(255, "Assignment value matrices must have a single column", row); return m; } + + // apparently we still need this check + v1 = args.get_as_int_vec(1); + v2 = args.get_as_int_vec(2); + printIntVector(v1); + printIntVector(v2); + err_code = args.check_indices(0, v1, v2); // CheckIndices(args[0], args[1], m1); // err_code = CheckIndices(args[0], args[1], args[2]); - // if (err_code) { - // SetError(MP2_ASSIGN, "Illegal index used in assign", row); - // return m; - // } + if (err_code) { + SetError(MP2_ASSIGN, "Illegal index used in assign", row); + return m; + } rows = args[3].rows(); // err_code1 = RecycleInPlace(args[1], rows, cols); // err_code2 = RecycleInPlace(args[2], rows, cols); - v1.push_back(1); - v1.push_back(2); - args = args.recycle_to_shape(v1, rows, cols); + v3.push_back(1); + v3.push_back(2); + args = args.recycle_to_shape(v3, rows, cols); err_code = args.get_error_code(); // err_code = err_code1 + err_code2; if (err_code != 0) { From 1803f7ff218fa74edd2af39e0ca0a525860eef20 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 14 Jan 2024 21:39:29 -0500 Subject: [PATCH 292/332] remove debug instrumentation --- misc/dev/dev.cpp | 4 ++-- src/macpan2.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index a68f6ac4..b08f6f67 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -2252,8 +2252,8 @@ class ExprEvaluator { // apparently we still need this check v1 = args.get_as_int_vec(1); v2 = args.get_as_int_vec(2); - printIntVector(v1); - printIntVector(v2); + //printIntVector(v1); + //printIntVector(v2); err_code = args.check_indices(0, v1, v2); // CheckIndices(args[0], args[1], m1); // err_code = CheckIndices(args[0], args[1], args[2]); if (err_code) { diff --git a/src/macpan2.cpp b/src/macpan2.cpp index ac696a91..f928dbcc 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -2253,8 +2253,8 @@ class ExprEvaluator { // apparently we still need this check v1 = args.get_as_int_vec(1); v2 = args.get_as_int_vec(2); - printIntVector(v1); - printIntVector(v2); + //printIntVector(v1); + //printIntVector(v2); err_code = args.check_indices(0, v1, v2); // CheckIndices(args[0], args[1], m1); // err_code = CheckIndices(args[0], args[1], args[2]); if (err_code) { From 750fe89ee90f6e86f89b9544cdd1ae57f07ef46b Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 14 Jan 2024 22:03:42 -0500 Subject: [PATCH 293/332] cleaning up tests for #155 --- tests/testthat/test-c.R | 12 ++- tests/testthat/test-change-points.R | 10 +-- tests/testthat/test-example-models.R | 127 ++++++++------------------- 3 files changed, 53 insertions(+), 96 deletions(-) diff --git a/tests/testthat/test-c.R b/tests/testthat/test-c.R index a3cae180..f1b6b772 100644 --- a/tests/testthat/test-c.R +++ b/tests/testthat/test-c.R @@ -1,16 +1,22 @@ test_that("concatenation works with many different shapes of input", { set.seed(1L) + z = rnorm(4L) + w = matrix(rnorm(12L), 3L, 4L) + answer = c(pi, z, w) m = TMBModel( init_mats = MatsList( answer = empty_matrix , x = empty_matrix , y = pi - , z = rnorm(4L) - , w = matrix(rnorm(12L), 3L, 4L) + , z = z + , w = w , .mats_to_return = "answer" ), expr_list = ExprList(before = list(answer ~ c(x, y, z, w))) ) s = TMBSimulator(m) - s$matrix(matrix_name = "answer", time_step = 1L) + expect_equal( + s$matrix(matrix_name = "answer", time_step = 1L, .phases = "after"), + matrix(answer) + ) }) diff --git a/tests/testthat/test-change-points.R b/tests/testthat/test-change-points.R index c26d2d4f..50a4c0ba 100644 --- a/tests/testthat/test-change-points.R +++ b/tests/testthat/test-change-points.R @@ -1,11 +1,11 @@ test_that("change pointers can be incremented", { sims = simple_sims( - iteration_exprs = list( - pointer ~ time_group(pointer, change_points) - ), + iteration_exprs = list(pointer ~ time_group(pointer, change_points)), time_steps = 10, - pointer = 0, - change_points = c(0, 4, 7) + mats = list( + pointer = 0, + change_points = c(0, 4, 7) + ) ) expect_equal( sims[sims$matrix == "pointer", "value"], diff --git a/tests/testthat/test-example-models.R b/tests/testthat/test-example-models.R index a27fe444..7908a033 100644 --- a/tests/testthat/test-example-models.R +++ b/tests/testthat/test-example-models.R @@ -1,93 +1,44 @@ test_that("example models are properly parsed", { - f = system.file("starter_models", "sir_vax", package = "macpan2") - m = Compartmental(f) - s = m$simulators$tmb( - time_steps = 80, - state = c( - S.unvax = 99, - I.unvax = 1, - R.unvax = 0, - S.vax = 0, - I.vax = 0, - R.vax = 0 - ), - flow = c( - infection.unvax = 0, - infection.vax = 0, - gamma.unvax = 0.1, - gamma.vax = 0.3, - .vax_rate = 0.1 - ), - beta.unvax = 0.2, - beta.vax = 0.01, - sigma.unvax = 1, - sigma.vax = 0.2, - N.unvax = empty_matrix, - N.vax = empty_matrix, - foi.unvax = empty_matrix, - foi.vax = empty_matrix, - foi. = empty_matrix + r = ("starter_models" + |> mp_tmb_library("sir", package = "macpan2") + |> mp_simulator(time_steps = 80, outputs = "I" + , default = list(beta = 0.5, gamma = 0.2, N = 100, I = 1) + ) + |> mp_trajectory() ) - expected_head = structure(list(matrix = c("state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state"), time = c(0L, 0L, 0L, 0L, 0L, 0L, 1L, 1L, 1L, 1L, 1L, - 1L, 2L, 2L, 2L, 2L, 2L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 4L, 4L, 4L, - 4L, 4L, 4L, 5L, 5L, 5L, 5L, 5L, 5L, 6L, 6L, 6L, 6L), row = c("S.unvax", - "I.unvax", "R.unvax", "S.vax", "I.vax", "R.vax", "S.unvax", "I.unvax", - "R.unvax", "S.vax", "I.vax", "R.vax", "S.unvax", "I.unvax", "R.unvax", - "S.vax", "I.vax", "R.vax", "S.unvax", "I.unvax", "R.unvax", "S.vax", - "I.vax", "R.vax", "S.unvax", "I.unvax", "R.unvax", "S.vax", "I.vax", - "R.vax", "S.unvax", "I.unvax", "R.unvax", "S.vax", "I.vax", "R.vax", - "S.unvax", "I.unvax", "R.unvax", "S.vax"), col = c("", "", "", - "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", - "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", - "", "", "", "", ""), value = c(99, 1, 0, 0, 0, 0, 88.902, 1.098, - 0.1, 9.9, 0, 0, 79.7951198756937, 1.20488012430633, 0.2098, 18.7853741620422, - 0.00482583795782464, 0, 71.5786247459434, 1.32137525405659, 0.330288012430633, - 26.7537280311629, 0.0145362050190736, 0.00144775138734739, 64.1620591000826, - 1.44794089991735, 0.462425537836292, 33.8922515639728, 0.0295142852977935, - 0.00580861289306946, 57.464079777476, 1.58492022252397, 0.607219627828027, - 40.2786893040758, 0.0504281696137448, 0.0146628984824075, 51.411617437995, - 1.73248256200496, 0.765711650080424, 45.9821923276533)), row.names = c(NA, - 40L), class = "data.frame") - - expected_tail = structure(list(matrix = c("state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state", "state", "state", "state", "state", "state", "state", - "state"), time = c(75L, 75L, 75L, 75L, 76L, 76L, 76L, 76L, 76L, - 76L, 77L, 77L, 77L, 77L, 77L, 77L, 78L, 78L, 78L, 78L, 78L, 78L, - 79L, 79L, 79L, 79L, 79L, 79L, 80L, 80L, 80L, 80L, 80L, 80L, 81L, - 81L, 81L, 81L, 81L, 81L), row = c("R.unvax", "S.vax", "I.vax", - "R.vax", "S.unvax", "I.unvax", "R.unvax", "S.vax", "I.vax", "R.vax", - "S.unvax", "I.unvax", "R.unvax", "S.vax", "I.vax", "R.vax", "S.unvax", - "I.unvax", "R.unvax", "S.vax", "I.vax", "R.vax", "S.unvax", "I.unvax", - "R.unvax", "S.vax", "I.vax", "R.vax", "S.unvax", "I.unvax", "R.unvax", - "S.vax", "I.vax", "R.vax", "S.unvax", "I.unvax", "R.unvax", "S.vax", - "I.vax", "R.vax"), col = c("", "", "", "", "", "", "", "", "", - "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", - "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - value = c(11.11904089301, 69.5471580869164, 0.0360129884910043, - 19.260789183079, 0.00746993224426355, 0.0258290314088979, - 11.1219103069748, 69.5407769633133, 0.032420686432392, 19.2715930796263, - 0.00671945255000025, 0.023249614737845, 11.1244932101157, - 69.5350325503671, 0.0291858866732368, 19.2813192855561, 0.00604468410605343, - 0.0209274764530073, 11.1268181715895, 69.5298614441577, 0.0262731721356657, - 19.290075051558, 0.00543792953346754, 0.0188370149696871, - 11.1289109192348, 69.5252065301769, 0.0236506028864187, 19.2979570031987, - 0.00489228524182762, 0.0169551648110116, 11.1307946207317, - 69.5210163641336, 0.0212893810171205, 19.3050521840647, 0.00489228524182762, - 0.0169551648110116, 11.1307946207317, 69.5210163641336, 0.0212893810171205, - 19.3050521840647)), row.names = 453:492, class = "data.frame") + expected_head = structure( + list( + matrix = c("I", "I", "I", "I", "I", "I") + , time = 1:6 + , row = c(0, 0, 0, 0, 0, 0) + , col = c(0, 0, 0, 0, 0, 0) + , value = c( + 1.295, 1.673819875, 2.15811605601715 + , 2.77369837437042, 3.55034660108989 + , 4.52082543972383 + ) + ) + , row.names = c(NA, 6L) + , class = "data.frame" + ) - r = s$report() - expect_equal(head(r, 40L), expected_head) - expect_equal(tail(r, 40L), expected_tail) + expected_tail = structure( + list( + matrix = c("I", "I", "I", "I", "I", "I") + , time = 75:80 + , row = c(0, 0, 0, 0, 0, 0) + , col = c(0, 0, 0, 0, 0, 0) + , value = c( + 0.00460845639937598, 0.00389989999247531 + , 0.00330028093582821, 0.00279285178324815 + , 0.00236343941648981, 0.00200004942841338 + ) + ) + , row.names = 75:80 + , class = "data.frame" + ) + + expect_equal(head(r, 6L), expected_head) + expect_equal(tail(r, 6L), expected_tail) }) From ee92c23b3525a805214aebb297bb9b5f6f820574 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 14 Jan 2024 22:47:14 -0500 Subject: [PATCH 294/332] cleaning up tests for #155 --- tests/testthat/test-expr-list.R | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/testthat/test-expr-list.R b/tests/testthat/test-expr-list.R index 4b314ceb..fc9549a7 100644 --- a/tests/testthat/test-expr-list.R +++ b/tests/testthat/test-expr-list.R @@ -4,24 +4,20 @@ test_that("expressions are inserted and printed", { l = ExprList(during = list(a ~ 1, b ~ 2, c ~ 3)) - expect_output( - (l - $insert(X ~ 500, Y ~ 600, .phase = "before") - $insert(A ~ 0, .phase = "after") - $print_exprs() - ), - "---------------------\\nBefore the simulation loop \\(t = 0\\):\\n---------------------\\n1: X ~ 500\\n2: Y ~ 600\\n\\n---------------------\\nAt every iteration of the simulation loop \\(t = 1 to T\\):\\n---------------------\\n1: a ~ 1\\n2: b ~ 2\\n3: c ~ 3\\n\\n---------------------\\nAfter the simulation loop \\(t = T\\):\\n---------------------\\n1: A ~ 0\\n" - ) + l = l$insert(X ~ 500, Y ~ 600, .phase = "before") + l = l$insert(A ~ 0, .phase = "after") + expect_snapshot(l$print_exprs()) + #"---------------------\\nBefore the simulation loop \\(t = 0\\):\\n---------------------\\n1: X ~ 500\\n2: Y ~ 600\\n\\n---------------------\\nAt every iteration of the simulation loop \\(t = 1 to T\\):\\n---------------------\\n1: a ~ 1\\n2: b ~ 2\\n3: c ~ 3\\n\\n---------------------\\nAfter the simulation loop \\(t = T\\):\\n---------------------\\n1: A ~ 0\\n" }) test_that("formula validity is enforced", { - expect_error( - TMBModel( - init_mats = MatsList(a = 1), - expr_list = ExprList(before = list(a[0] ~ 1)) - ), - "without subsetting on the left-hand-side" - ) + # expect_error( + # TMBModel( + # init_mats = MatsList(a = 1), + # expr_list = ExprList(before = list(a[0] ~ 1)) + # ), + # "without subsetting on the left-hand-side" + # ) expect_error( TMBModel( init_mats = MatsList(a = 1), @@ -38,6 +34,6 @@ test_that("proper error message comes when output matrices are not initialized", ) expect_error( m$data_arg(), - "some expressions are saved to matrices that are not initialized" + "The expression given by" ) }) From 8840be3badd526f5a7ac46ff12fce4d5a3d0084e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Sun, 14 Jan 2024 22:49:47 -0500 Subject: [PATCH 295/332] cleaning up tests for #155 --- tests/testthat/test-expr-parser.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-expr-parser.R b/tests/testthat/test-expr-parser.R index 93fb580f..73a56b4d 100644 --- a/tests/testthat/test-expr-parser.R +++ b/tests/testthat/test-expr-parser.R @@ -1,5 +1,5 @@ test_that("missing variables in expressions are handled", { - expect_error(engine_eval(~x), "the expression given by") + expect_error(engine_eval(~x), "The expression given by") }) test_that("parse_tables ...", { From 428af06167f5d30e2776066faf3d2277f92ad0d2 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 10:05:10 -0500 Subject: [PATCH 296/332] predator prey calibration not fully working (yet) --- .../predator_prey/calibration_example.R | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R b/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R index 108040b7..a3b46287 100644 --- a/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R +++ b/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R @@ -152,21 +152,25 @@ if (interactive()) { } ## exponential prey growth instead of logistic -## set K_inverse=0 +## set K_inverse=0, and initial predators to 0 lv_pred_prey$get$initial("K_inverse") -## define new simulator and update default +## define new simulator and update defaults lv_exp_prey = mp_simulator( model = spec , time_steps = time_steps , outputs = c("X","Y") - , default = list("K_inverse" = 0, "alpha"=0.1) -) + , default = list("K_inverse" = 0,"Y"=0) +) lv_exp_prey$get$initial("K_inverse") - +lv_exp_prey$get$initial("Y") +lv_exp_prey ## better way to get initial population size -#lv_exp_prey$report() %>% filter(matrix=="X" & time==0) %>% select(value) -## set Y0=0 +#lv_exp_prey$matrix(matrix_name = "X", time_step=0, .phases = "before") +## set Y[0]=0 + +#lv_exp_prey$insert$expressions(X ~ 0, .phase="before",.at=1) +#lv_exp_prey$replace$e ggplot(lv_exp_prey$report() %>% filter(matrix=="X"), aes(time,value))+ geom_line()+ theme_bw()+ From b7f920f178e17fb7ed9a0c6161d806008e5d82b5 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 10:09:41 -0500 Subject: [PATCH 297/332] removing LV model nested directory structure --- .../calibration_example.R | 0 .../competition => lotka_volterra_competition}/tmb.R | 0 .../calibration_example.R | 0 .../predator_prey => lotka_volterra_predator_prey}/tmb.R | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename inst/starter_models/{lotka_volterra/competition => lotka_volterra_competition}/calibration_example.R (100%) rename inst/starter_models/{lotka_volterra/competition => lotka_volterra_competition}/tmb.R (100%) rename inst/starter_models/{lotka_volterra/predator_prey => lotka_volterra_predator_prey}/calibration_example.R (100%) rename inst/starter_models/{lotka_volterra/predator_prey => lotka_volterra_predator_prey}/tmb.R (100%) diff --git a/inst/starter_models/lotka_volterra/competition/calibration_example.R b/inst/starter_models/lotka_volterra_competition/calibration_example.R similarity index 100% rename from inst/starter_models/lotka_volterra/competition/calibration_example.R rename to inst/starter_models/lotka_volterra_competition/calibration_example.R diff --git a/inst/starter_models/lotka_volterra/competition/tmb.R b/inst/starter_models/lotka_volterra_competition/tmb.R similarity index 100% rename from inst/starter_models/lotka_volterra/competition/tmb.R rename to inst/starter_models/lotka_volterra_competition/tmb.R diff --git a/inst/starter_models/lotka_volterra/predator_prey/calibration_example.R b/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R similarity index 100% rename from inst/starter_models/lotka_volterra/predator_prey/calibration_example.R rename to inst/starter_models/lotka_volterra_predator_prey/calibration_example.R diff --git a/inst/starter_models/lotka_volterra/predator_prey/tmb.R b/inst/starter_models/lotka_volterra_predator_prey/tmb.R similarity index 100% rename from inst/starter_models/lotka_volterra/predator_prey/tmb.R rename to inst/starter_models/lotka_volterra_predator_prey/tmb.R From 44217a57f8351b0b2c4df9713eaaa10bf7601252 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 10:11:37 -0500 Subject: [PATCH 298/332] fix small typo in quickstart vignette --- vignettes/quickstart.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/quickstart.Rmd b/vignettes/quickstart.Rmd index a275e2cd..7b52761b 100644 --- a/vignettes/quickstart.Rmd +++ b/vignettes/quickstart.Rmd @@ -75,7 +75,7 @@ The first step is to produce a simulator object, which can be used to generate s - `model` : A model specification object, such as `si`. - `time_steps`: How many time steps should the epidemic simulator run for? - `outputs` : The model variables to return in simulation output. -- `default` (optional) : Allows one to update the default parameter values and initial conditions provided in the model specification (see the argument `default` above in the `mp_tmb_model_spec` function). Any variables that are not specified in `default` will be left at the the values in the specification. +- `default` (optional) : Allows one to update the default parameter values and initial conditions provided in the model specification (see the argument `default` above in the `mp_tmb_model_spec` function). Any variables that are not specified in `default` will be left at the values in the specification. ```{r tmb-si-simulator} si_simulator = mp_simulator( From 721ad7905a0d6e74cd67dacb06443e6cbfa03c1c Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 10:18:51 -0500 Subject: [PATCH 299/332] moving LV model documentation --- .../lotka_volterra/competition/model.R | 57 -------------- .../lotka_volterra/predator_prey/model.R | 76 ------------------- .../README.md | 0 .../README.md | 0 4 files changed, 133 deletions(-) delete mode 100644 inst/starter_models/lotka_volterra/competition/model.R delete mode 100644 inst/starter_models/lotka_volterra/predator_prey/model.R rename inst/starter_models/{lotka_volterra/competition => lotka_volterra_competition}/README.md (100%) rename inst/starter_models/{lotka_volterra/predator_prey => lotka_volterra_predator_prey}/README.md (100%) diff --git a/inst/starter_models/lotka_volterra/competition/model.R b/inst/starter_models/lotka_volterra/competition/model.R deleted file mode 100644 index 8bedf395..00000000 --- a/inst/starter_models/lotka_volterra/competition/model.R +++ /dev/null @@ -1,57 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -## none - -## absolute flow rates (per time only) -flow_rates = list( - ## growth rate of species X and Y - growth_x ~ rx * X - , growth_y ~ ry * Y - ## intraspecific effect - ## species X on X - , intraspecific_x ~ growth_x * axx * X - ## species Y on Y - , intraspecific_y ~ growth_y * ayy * Y - ## interspecific effect - ## species Y on X - , interspecific_xy ~ growth_x * axy * Y - ## species X on Y - , interspecific_yx ~ growth_y * ayx * X -) - -## alternate parameterization of model by carrying -## capacity and relative interspecific effects -flow_rates = list( - ## growth rate of species X and Y - growth_x ~ rx * X - , growth_y ~ ry * Y - ## intraspecific effect - ## species X on X - , intraspecific_x ~ growth_x * X / Kx - ## species Y on Y - , intraspecific_y ~ growth_y * Y / Ky - ## interspecific effect - ## species Y on X - , interspecific_xy ~ growth_x * alpha_xy * Y / Kx - ## species X on Y - , interspecific_yx ~ growth_y * alpha_yx * X / Ky -) - - -## state updates -state_updates = list( - X ~ X + growth_x - intraspecific_x - interspecific_xy - , Y ~ Y + growth_y - intraspecific_y - interspecific_yx -) - -## simple unstructured scalar expression -expr_list = ExprList( - during = c( - flow_rates - , state_updates - ) -) diff --git a/inst/starter_models/lotka_volterra/predator_prey/model.R b/inst/starter_models/lotka_volterra/predator_prey/model.R deleted file mode 100644 index 95393c18..00000000 --- a/inst/starter_models/lotka_volterra/predator_prey/model.R +++ /dev/null @@ -1,76 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience (none) - -## functional responses -## Holling type I is trivial (modify beta/delta as needed?) - -#' Holling type II -#' -#' @param a predator attack rate; prey/time unit? -#' @param h handling time (ex. 1 day) -#' -holling_ii <- function(a, h, X){ - a * X / (1 + a * h * X) -} - -#' Holling type III -#' -#' @param a predator attack rate; prey/time unit? -#' @param h handling time (ex. 1 day) -#' @param k exponent of prey density, k > 1 -#' -holling_iii <- function(a, h, X, k){ - a * (X ^ k) / (1 + a * h * (X ^ k)) -} - - -## absolute flow rates (per time only) - -## exponential prey growth -flow_rates = list( - ## growth rate of prey - growth_x ~ alpha * X - ## mortality rate of predator - , mortality_y ~ gamma * Y - ## effects from predation - ## mortality rate of prey (due to predation) - , mortality_x ~ beta * X * Y - ## growth rate of predator (due to predation) - , growth_y ~ delta * X * Y -) - -## alternate parameterization of model -## logistic prey growth, and non-linear functional responses -flow_rates = list( - ## growth rate of prey - growth_x ~ alpha * X * (1 - (X/K)) - ## mortality rate of predator - , mortality_y ~ gamma * Y - ## effects from predation - ## compute functional response - , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) - #, functional_response ~ holling_ii(a = 5, h = 1, X = X) - ## mortality rate of prey (due to predation) - , mortality_x ~ beta * functional_response * Y - ## growth rate of predator (due to predation) - , growth_y ~ delta * functional_response * Y -) - - -## state updates -state_updates = list( - X ~ X + growth_x - mortality_x - , Y ~ Y + growth_y - mortality_y -) - -## simple unstructured scalar expression -expr_list = ExprList( - during = c( - flow_rates - , state_updates - ) -) diff --git a/inst/starter_models/lotka_volterra/competition/README.md b/inst/starter_models/lotka_volterra_competition/README.md similarity index 100% rename from inst/starter_models/lotka_volterra/competition/README.md rename to inst/starter_models/lotka_volterra_competition/README.md diff --git a/inst/starter_models/lotka_volterra/predator_prey/README.md b/inst/starter_models/lotka_volterra_predator_prey/README.md similarity index 100% rename from inst/starter_models/lotka_volterra/predator_prey/README.md rename to inst/starter_models/lotka_volterra_predator_prey/README.md From 36b9795c714dd370275dd58433dcf9d6319b96ac Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 10:38:24 -0500 Subject: [PATCH 300/332] removing hard coding of initial state values in model library --- .../lotka_volterra_competition/tmb.R | 11 +++----- .../lotka_volterra_predator_prey/tmb.R | 11 +++----- inst/starter_models/macpan_base/tmb.R | 23 +++++++++------- inst/starter_models/seir/tmb.R | 8 +++--- inst/starter_models/si/tmb.R | 5 ++-- inst/starter_models/sir/tmb.R | 4 +-- inst/starter_models/sir_demo/tmb.R | 9 ++++--- inst/starter_models/ww/tmb.R | 26 ++++++++++++------- 8 files changed, 51 insertions(+), 46 deletions(-) diff --git a/inst/starter_models/lotka_volterra_competition/tmb.R b/inst/starter_models/lotka_volterra_competition/tmb.R index f9d5c507..406bfd94 100644 --- a/inst/starter_models/lotka_volterra_competition/tmb.R +++ b/inst/starter_models/lotka_volterra_competition/tmb.R @@ -1,10 +1,5 @@ library(macpan2) -initialize_state = list( - X ~ 100 - , Y ~ 100 -) - flow_rates = list( ## growth rate of species X and Y growth_x ~ rx * X @@ -33,12 +28,14 @@ default = list( , ayy = 1/50 # ayy = 1/Ky, Ky = carrying capacity of Y , axy = 0.8/50 # axy = alpha_xy/Ky, alpha_xy=competition coeff of Y on X, Ky = carrying capacity of Y , ayx = 1.5/200 # axy = alpha_yx/Kx, alpha_yx=competition coeff of X on Y, Kx = carrying capacity of X + # initalize state + , X = 100 + , Y = 100 ) ## model spec spec = mp_tmb_model_spec( - before = initialize_state - , during = c(flow_rates, state_updates) + during = c(flow_rates, state_updates) , default = default ) diff --git a/inst/starter_models/lotka_volterra_predator_prey/tmb.R b/inst/starter_models/lotka_volterra_predator_prey/tmb.R index c6aca72d..6d38aba6 100644 --- a/inst/starter_models/lotka_volterra_predator_prey/tmb.R +++ b/inst/starter_models/lotka_volterra_predator_prey/tmb.R @@ -31,11 +31,6 @@ holling_iii <- function(a, h, X, k){ ## model -initialize_state = list( - X ~ 100 # prey - , Y ~ 100 # predator -) - flow_rates = list( ## growth rate of prey @@ -70,13 +65,15 @@ default = list( , K_inverse = 1/500 # prey carrying capacity , a = 1/500 # predator attack rate , h = 1 # handling time + # initialize state + , X = 100 # prey + , Y = 100 # predators ) ## model specification spec = mp_tmb_model_spec( - before = initialize_state - , during = c(flow_rates, state_updates) + during = c(flow_rates, state_updates) , default = default ) diff --git a/inst/starter_models/macpan_base/tmb.R b/inst/starter_models/macpan_base/tmb.R index d21cfeac..bc36d43c 100644 --- a/inst/starter_models/macpan_base/tmb.R +++ b/inst/starter_models/macpan_base/tmb.R @@ -1,13 +1,5 @@ library(macpan2) -initialize_state = list( - S ~ 1.00E+06, E ~ 1 - , Ia ~ 0, Ip ~0, Im ~ 0, Is ~ 0 - , R ~ 0 - , H ~ 0, ICUs ~ 0, ICUd ~ 0, H2 ~ 0 - , D ~ 0 -) - computations = list( N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) ) @@ -74,11 +66,24 @@ default = list( , c_delay_cv = 0.25 # coefficient of variation of testing delay , proc_disp = 0 # dispersion parameter for process error (0=demog stoch only) , zeta = 0 # phenomenological heterogeneity parameter + # initial states + , S = 1.00E+06 + , E = 1 + , Ia = 0 + , Ip = 0 + , Im = 0 + , Is = 0 + , R = 0 + , H = 0 + , ICUs = 0 + , ICUd = 0 + , H2 = 0 + , D = 0 ) ## model specification spec = mp_tmb_model_spec( - before = c(initialize_state, computations) + before = computations , during = c(flow_rates, update_state) , default = default ) diff --git a/inst/starter_models/seir/tmb.R b/inst/starter_models/seir/tmb.R index 8b8f0a3b..6a94c53f 100644 --- a/inst/starter_models/seir/tmb.R +++ b/inst/starter_models/seir/tmb.R @@ -1,10 +1,7 @@ library(macpan2) initialize_state = list( - I ~ 1 - , R ~ 0 - , E ~ 0 - , S ~ N - I + S ~ N - I - R - E ) computations = list( @@ -30,6 +27,9 @@ default = list( , alpha = 1/2 , gamma = 0.1 , N = 100 + , I = 1 + , E = 0 + , R = 0 ) ## model specification diff --git a/inst/starter_models/si/tmb.R b/inst/starter_models/si/tmb.R index 7fc603f1..25173763 100644 --- a/inst/starter_models/si/tmb.R +++ b/inst/starter_models/si/tmb.R @@ -2,8 +2,7 @@ library(macpan2) integrator = "euler" ## could be euler, hazard, or rk4 (need to add demo_stoch) initialize_state = list( - I ~ 1 - , S ~ N - 1 + S ~ N - I ) flow_rates = list( @@ -52,7 +51,7 @@ specs = lapply(flow_rates, \(flow_rates) { mp_tmb_model_spec( before = initialize_state , during = c(flow_rates, update_state) - , default = list(N = 100, beta = 0.2) + , default = list(N = 100, beta = 0.2, I = 1) ) }) diff --git a/inst/starter_models/sir/tmb.R b/inst/starter_models/sir/tmb.R index 33d47cb2..d46d45c1 100644 --- a/inst/starter_models/sir/tmb.R +++ b/inst/starter_models/sir/tmb.R @@ -1,8 +1,7 @@ library(macpan2) initialize_state = list( - R ~ 0 - , S ~ N - I + S ~ N - I - R ) flow_rates = list( @@ -22,6 +21,7 @@ default = list( , gamma = 0.2 , N = 100 , I = 1 + , R = 0 ) ## model specification diff --git a/inst/starter_models/sir_demo/tmb.R b/inst/starter_models/sir_demo/tmb.R index 55d82b0c..7978f698 100644 --- a/inst/starter_models/sir_demo/tmb.R +++ b/inst/starter_models/sir_demo/tmb.R @@ -1,9 +1,7 @@ library(macpan2) initialize_state = list( - I ~ 1 - , R ~ 0 - , S ~ N - I + S ~ N - I - R ) computations = list( @@ -26,9 +24,12 @@ update_state = list( default = list( beta = 0.2 , gamma = 0.1 - , N = 100 , birth_rate = 0.1 , death_rate = 0.08 + , N = 100 + , I = 1 + , R = 0 + ) ## model specification diff --git a/inst/starter_models/ww/tmb.R b/inst/starter_models/ww/tmb.R index c0962ae8..be51f0e9 100644 --- a/inst/starter_models/ww/tmb.R +++ b/inst/starter_models/ww/tmb.R @@ -1,14 +1,5 @@ library(macpan2) -initialize_state = list( - S ~ 1.00E+06, E ~ 1 - , Ia ~ 0, Ip ~0, Im ~ 0, Is ~ 0 - , R ~ 0 - , H ~ 0, ICUs ~ 0, ICUd ~ 0, H2 ~ 0 - , D ~ 0 - , W ~ 0, A ~ 0 -) - computations = list( N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) ) @@ -84,11 +75,26 @@ default = list( , zeta = 0 # phenomenological heterogeneity parameter , nu = 0.1 # something to do with waste-water , xi = 0.5 # something to do with waste-water + # initial states + , S = 1.00E+06 + , E = 1 + , Ia = 0 + , Ip = 0 + , Im = 0 + , Is = 0 + , R = 0 + , H = 0 + , ICUs = 0 + , ICUd = 0 + , H2 = 0 + , D = 0 + , W = 0 + , A = 0 ) ## model spec spec = mp_tmb_model_spec( - before = c(initialize_state, computations) + before = computations , during = c(flow_rates, state_updates) , default = default ) From aaaff41cc801ee07360f0b296797db2415303b80 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 14:52:19 -0500 Subject: [PATCH 301/332] last calibration example #150 --- .../calibration_example.R | 4 +- .../calibration_example.R | 60 ++++++++-- .../lotka_volterra_predator_prey/tmb.R | 109 ++++++++++-------- 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/inst/starter_models/lotka_volterra_competition/calibration_example.R b/inst/starter_models/lotka_volterra_competition/calibration_example.R index ef7f2010..0481bff7 100644 --- a/inst/starter_models/lotka_volterra_competition/calibration_example.R +++ b/inst/starter_models/lotka_volterra_competition/calibration_example.R @@ -1,4 +1,4 @@ -#source("inst/starter_models/lotka_volterra/competition/tmb.R") +#source("inst/starter_models/lotka_volterra_competition/tmb.R") library(macpan2) library(ggplot2) library(dplyr) @@ -7,7 +7,7 @@ library(dplyr) ## get model spec from library ## ------------------------- -spec = mp_tmb_library("starter_models","lotka_volterra","competition",package="macpan2") +spec = mp_tmb_library("starter_models","lotka_volterra_competition",package="macpan2") spec ## ------------------------- diff --git a/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R b/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R index a3b46287..d5c8d653 100644 --- a/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R +++ b/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R @@ -1,4 +1,4 @@ -source("inst/starter_models/lotka_volterra/predator_prey/tmb.R") +source("inst/starter_models/lotka_volterra_predator_prey/tmb.R") library(macpan2) library(ggplot2) library(dplyr) @@ -7,7 +7,7 @@ library(dplyr) ## get model spec from library ## ------------------------- -#spec = mp_tmb_library("starter_models","lotka_volterra","predator_prey",package="macpan2") +#spec = mp_tmb_library("starter_models","lotka_volterra_predator_prey",package="macpan2") spec ## ------------------------- @@ -152,31 +152,67 @@ if (interactive()) { } ## exponential prey growth instead of logistic -## set K_inverse=0, and initial predators to 0 +## set K_inverse=0 lv_pred_prey$get$initial("K_inverse") +lv_pred_prey$get$initial("Y") ## define new simulator and update defaults lv_exp_prey = mp_simulator( model = spec - , time_steps = time_steps + , time_steps = 10L , outputs = c("X","Y") - , default = list("K_inverse" = 0,"Y"=0) + # by setting initial predator population to 0 + # prey dynamics should be strictly exponential growth + , default = list("K_inverse" = 0,"Y" = 0) ) lv_exp_prey$get$initial("K_inverse") lv_exp_prey$get$initial("Y") lv_exp_prey -## better way to get initial population size -#lv_exp_prey$matrix(matrix_name = "X", time_step=0, .phases = "before") -## set Y[0]=0 -#lv_exp_prey$insert$expressions(X ~ 0, .phase="before",.at=1) -#lv_exp_prey$replace$e +## compute exponential function +exp_growth <- (cbind(1:10, as.numeric(lv_exp_prey$get$initial("X"))*(1+as.numeric(lv_exp_prey$get$initial("alpha")))^c(1:10)) + %>% data.frame() + %>% setNames(c("time","value")) +) + +if (interactive()) { ggplot(lv_exp_prey$report() %>% filter(matrix=="X"), aes(time,value))+ - geom_line()+ + geom_point()+ + geom_line(data=exp_growth,col="red")+ theme_bw()+ ylab("prey") +} + +## Holling functions + +## check current functional response +spec$during$fr + +## simulating data with different functional responses +fr_names <- c("holling_1", "holling_2", "holling_3") + +fr_sims <- lapply(fr_names,function(x){ + (mp_simulator( + model = specs[[x]] + , time_steps = 100L + , outputs = c("X","Y")) + %>% mp_trajectory() + %>% mutate(matrix = case_match(matrix + , "X" ~ "Prey" + , "Y" ~ "Predator"), + response = x) + ) + } +) %>% bind_rows() + +## visualizing simulated data +if (interactive()) { + ggplot(fr_sims, aes(time,value,linetype=matrix,col=response))+ + geom_line()+ + theme_bw()+ + ylim(c(0,1/spec$default$K_inverse)) #ymax = carrying capacity of prey +} -## Holling functions diff --git a/inst/starter_models/lotka_volterra_predator_prey/tmb.R b/inst/starter_models/lotka_volterra_predator_prey/tmb.R index 6d38aba6..4a362657 100644 --- a/inst/starter_models/lotka_volterra_predator_prey/tmb.R +++ b/inst/starter_models/lotka_volterra_predator_prey/tmb.R @@ -1,56 +1,36 @@ library(macpan2) +functional_response = "holling_1" # one of "holling_1", "holling_2" or "holling_3" -## functional responses - -#' Holling type I -#' -#' @param a predator attack rate; prey/predator/time_unit -#' -holling_i <- function(a, X){ - engine_eval(~ a * X, a=a, X=X) -} - -#' Holling type II -#' -#' @param a predator attack rate; prey/predator/time_unit -#' @param h handling time (ex. 1 day) -#' -holling_ii <- function(a, h, X){ - a * X / (1 + a * h * X) -} - -#' Holling type III -#' -#' @param a predator attack rate; prey/predator/time_unit -#' @param h handling time (ex. 1 day) -#' @param k exponent of prey density, k > 1 -#' -holling_iii <- function(a, h, X, k){ - a * (X ^ k) / (1 + a * h * (X ^ k)) -} - - -## model - -flow_rates = list( +## LV predator prey flow rates with type 1 +## Holling functional response +holling_1_flow_rates = list( ## growth rate of prey growth_x ~ alpha * X * (1 - K_inverse*(X)) ## mortality rate of predator , mortality_y ~ gamma * Y - ## effects from predation - ## compute functional response (f(X) = X, default) - , functional_response ~ a * X - - # , functional_response ~ holling_i(a = a, X = X) - # , functional_response ~ holling_ii(a = 5, h = 1, X = X) - # , functional_response ~ holling_iii(a = 5, h = 1, X = X, k = 3) + ## effects from predation + ## compute functional response + ## not sure if named list elements will create problems + , fr = functional_response ~ a * X ## mortality rate of prey (due to predation) , mortality_x ~ functional_response * Y ## growth rate of predator (due to predation) , growth_y ~ delta * functional_response * Y ) +flow_rates = list( + + ## type 1 Holling response + holling_1 = holling_1_flow_rates + + ## type 2 Holling response + , holling_2 = modifyList(holling_1_flow_rates, list(fr = functional_response ~ a * X / (1 + a * h * X))) + + ## type 3 Holling response + , holling_3 = modifyList(holling_1_flow_rates, list(fr = functional_response ~ a * (X ^ k) / (1 + a * h * (X ^ k)))) +) + state_updates = list( X ~ X + growth_x - mortality_x , Y ~ Y + growth_y - mortality_y @@ -63,21 +43,58 @@ default = list( , gamma = 1/50 # predator mortality , delta = 1/10 # predator gain from predation , K_inverse = 1/500 # prey carrying capacity - , a = 1/500 # predator attack rate - , h = 1 # handling time + , a = 1/500 # predator attack rate (Holling type 1,2,3) + , h = 1 # handling time (Holling type 2,3) + , k = 2 # Holling type 3 exponent parameter (k > 1) # initialize state - , X = 100 # prey - , Y = 100 # predators + , X = 100 # prey + , Y = 100 # predators ) ## model specification -spec = mp_tmb_model_spec( +specs = lapply(flow_rates, \(flow_rates) { + mp_tmb_model_spec( during = c(flow_rates, state_updates) , default = default -) +)}) +spec = specs[[functional_response]] +## functional responses +## these functions are not currently used +## instead see flow_rates list for all +## three types of functional responses + +#' Holling type I +#' +#' @param a predator attack rate; prey/predator/time_unit +#' +#' @return +holling_i <- function(a, X){ + # return("a * X") + a * X +} + +#' Holling type II +#' +#' @param a predator attack rate; prey/predator/time_unit +#' @param h handling time (ex. 1 day) +#' +holling_ii <- function(a, h, X){ + a * X / (1 + a * h * X) +} + +#' Holling type III +#' +#' @param a predator attack rate; prey/predator/time_unit +#' @param h handling time (ex. 1 day) +#' @param k exponent of prey density, k > 1 +#' +holling_iii <- function(a, h, X, k){ + a * (X ^ k) / (1 + a * h * (X ^ k)) +} + From 34f7a4ef3fc5e80d21496f4e0af22c93e4bf7a59 Mon Sep 17 00:00:00 2001 From: Jennifer Freeman Date: Mon, 15 Jan 2024 15:29:01 -0500 Subject: [PATCH 302/332] closes #150 --- .../macpan_base/derivations.json | 86 ------------- inst/starter_models/macpan_base/flows.csv | 15 --- inst/starter_models/macpan_base/model.R | 54 -------- inst/starter_models/macpan_base/settings.json | 35 ------ inst/starter_models/macpan_base/variables.csv | 50 -------- inst/starter_models/seir/derivations.json | 14 --- inst/starter_models/seir/flows.csv | 4 - inst/starter_models/seir/model.R | 33 ----- inst/starter_models/seir/settings.json | 6 - inst/starter_models/seir/variables.csv | 10 -- inst/starter_models/sir/derivations.json | 14 --- inst/starter_models/sir/flows.csv | 3 - inst/starter_models/sir/model.R | 68 ---------- inst/starter_models/sir/settings.json | 9 -- .../sir/transmission_dimensions.csv | 1 - .../sir/transmission_matrices.csv | 2 - inst/starter_models/sir/variables.csv | 9 -- inst/starter_models/sir_demo/derivations.json | 14 --- inst/starter_models/sir_demo/flows.csv | 9 -- inst/starter_models/sir_demo/model.R | 36 ------ inst/starter_models/sir_demo/settings.json | 6 - inst/starter_models/sir_demo/variables.csv | 10 -- .../sir_waning/derivations.json | 14 --- inst/starter_models/sir_waning/flows.csv | 4 - inst/starter_models/sir_waning/model.R | 45 ------- inst/starter_models/sir_waning/settings.json | 6 - inst/starter_models/sir_waning/variables.csv | 9 -- inst/starter_models/ww/derivations.json | 116 ------------------ inst/starter_models/ww/flows.csv | 20 --- inst/starter_models/ww/model.R | 59 --------- inst/starter_models/ww/settings.json | 42 ------- inst/starter_models/ww/variables.csv | 59 --------- 32 files changed, 862 deletions(-) delete mode 100644 inst/starter_models/macpan_base/derivations.json delete mode 100644 inst/starter_models/macpan_base/flows.csv delete mode 100644 inst/starter_models/macpan_base/model.R delete mode 100644 inst/starter_models/macpan_base/settings.json delete mode 100644 inst/starter_models/macpan_base/variables.csv delete mode 100644 inst/starter_models/seir/derivations.json delete mode 100644 inst/starter_models/seir/flows.csv delete mode 100644 inst/starter_models/seir/model.R delete mode 100644 inst/starter_models/seir/settings.json delete mode 100644 inst/starter_models/seir/variables.csv delete mode 100644 inst/starter_models/sir/derivations.json delete mode 100644 inst/starter_models/sir/flows.csv delete mode 100644 inst/starter_models/sir/model.R delete mode 100644 inst/starter_models/sir/settings.json delete mode 100644 inst/starter_models/sir/transmission_dimensions.csv delete mode 100644 inst/starter_models/sir/transmission_matrices.csv delete mode 100644 inst/starter_models/sir/variables.csv delete mode 100644 inst/starter_models/sir_demo/derivations.json delete mode 100644 inst/starter_models/sir_demo/flows.csv delete mode 100644 inst/starter_models/sir_demo/model.R delete mode 100644 inst/starter_models/sir_demo/settings.json delete mode 100644 inst/starter_models/sir_demo/variables.csv delete mode 100644 inst/starter_models/sir_waning/derivations.json delete mode 100644 inst/starter_models/sir_waning/flows.csv delete mode 100644 inst/starter_models/sir_waning/model.R delete mode 100644 inst/starter_models/sir_waning/settings.json delete mode 100644 inst/starter_models/sir_waning/variables.csv delete mode 100644 inst/starter_models/ww/derivations.json delete mode 100644 inst/starter_models/ww/flows.csv delete mode 100644 inst/starter_models/ww/model.R delete mode 100644 inst/starter_models/ww/settings.json delete mode 100644 inst/starter_models/ww/variables.csv diff --git a/inst/starter_models/macpan_base/derivations.json b/inst/starter_models/macpan_base/derivations.json deleted file mode 100644 index 4b57e75e..00000000 --- a/inst/starter_models/macpan_base/derivations.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "simulation_phase" : "before", - "output_names" : ["EIa"], - "arguments" : ["alpha", "sigma"], - "expression" : "alpha * sigma" - }, - { - "simulation_phase" : "before", - "output_names" : ["EIp"], - "arguments" : ["alpha", "sigma"], - "expression" : "(1-alpha)*sigma" - }, - { - "simulation_phase" : "before", - "output_names" : ["IaR"], - "arguments" : ["gamma_a"], - "expression" : "gamma_a" - }, - { - "simulation_phase" : "before", - "output_names" : ["IpIm"], - "arguments" : ["mu", "gamma_p"], - "expression" : "mu*gamma_p" - }, - { - "simulation_phase" : "before", - "output_names" : ["ImR"], - "arguments" : ["gamma_m"], - "expression" : "gamma_m" - }, - { - "simulation_phase" : "before", - "output_names" : ["IpIs"], - "arguments" : ["mu", "gamma_p"], - "expression" : "(1-mu)*gamma_p" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsICUs"], - "arguments" : ["nonhosp_mort", "phi1", "phi2", "gamma_s"], - "expression" : "(1-nonhosp_mort)*(1-phi1)*(1-phi2)*gamma_s" - }, - { - "simulation_phase" : "before", - "output_names" : ["ICUsH2"], - "arguments" : ["psi1"], - "expression" : "psi1" - }, - { - "simulation_phase" : "before", - "output_names" : ["H2R"], - "arguments" : ["psi3"], - "expression" : "psi3" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsH"], - "arguments" : ["nonhosp_mort", "phi1", "gamma_s"], - "expression" : "(1-nonhosp_mort)*phi1*gamma_s" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsICUd"], - "arguments" : ["nonhosp_mort", "phi1", "phi2", "gamma_s"], - "expression" : "(1-nonhosp_mort)*(1-phi1)*phi2*gamma_s" - }, - { - "simulation_phase" : "before", - "output_names" : ["ICUdD"], - "arguments" : ["psi2"], - "expression" : "psi2" - }, - { - "simulation_phase" : "before", - "output_names" : ["HR"], - "arguments" : ["rho"], - "expression" : "rho" - }, - { - "simulation_phase" : "during_pre_update", - "output_names" : ["SE"], - "arguments" : ["Ia", "Ip", "Im", "Is", "beta0", "N", "Ca", "Cp", "Cm", "Cs", "iso_m", "iso_s"], - "expression" : "(beta0/N)*(Ia*Ca+Ip*Cp+Im*Cm*(1-iso_m)+Is*Cs*(1-iso_s))" - } -] \ No newline at end of file diff --git a/inst/starter_models/macpan_base/flows.csv b/inst/starter_models/macpan_base/flows.csv deleted file mode 100644 index b708a688..00000000 --- a/inst/starter_models/macpan_base/flows.csv +++ /dev/null @@ -1,15 +0,0 @@ -from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition -S ,E ,SE ,per_capita ,Epi ,Epi ,Epi , , ,Null -E ,Ia ,EIa ,per_capita ,Epi ,Epi ,Epi , , ,Null -E ,Ip ,EIp ,per_capita ,Epi ,Epi ,Epi , , ,Null -Ia ,R ,IaR ,per_capita ,Epi ,Epi ,Epi , , ,Null -Ip ,Im ,IpIm ,per_capita ,Epi ,Epi ,Epi , , ,Null -Im ,R ,ImR ,per_capita ,Epi ,Epi ,Epi , , ,Null -Ip ,Is ,IpIs ,per_capita ,Epi ,Epi ,Epi , , ,Null -Is ,ICUs ,IsICUs ,per_capita ,Epi ,Epi ,Epi , , ,Null -ICUs ,H2 ,ICUsH2 ,per_capita ,Epi ,Epi ,Epi , , ,Null -H2 ,R ,H2R ,per_capita ,Epi ,Epi ,Epi , , ,Null -Is ,H ,IsH ,per_capita ,Epi ,Epi ,Epi , , ,Null -Is ,ICUd ,IsICUd ,per_capita ,Epi ,Epi ,Epi , , ,Null -ICUd ,D ,ICUdD ,per_capita ,Epi ,Epi ,Epi , , ,Null -H ,R ,HR ,per_capita ,Epi ,Epi ,Epi , , ,Null diff --git a/inst/starter_models/macpan_base/model.R b/inst/starter_models/macpan_base/model.R deleted file mode 100644 index 74464e57..00000000 --- a/inst/starter_models/macpan_base/model.R +++ /dev/null @@ -1,54 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -computations = list( - N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) -) - -## absolute flow rates (per time only) -flow_rates = list( - S.E ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) - , E.Ia ~ E * alpha * sigma - , E.Ip ~ E * (1 - alpha)* sigma - , Ia.R ~ Ia * gamma_a - , Ip.Im ~ Ip * mu * gamma_p - , Im.R ~ Im * gamma_m - , Ip.Is ~ Ip * (1 - mu) * gamma_p - , Is.ICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s - , Is.ICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s - , ICUs.H2 ~ ICUs * psi1 - , H2.R ~ H2 * psi3 - , Is.H ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s - #HICUd? in variables.csv but not flows.csv - #HICUs - shouldn't this be a flow? or does IsICUs contain this flow - do they skip H? - , ICUd.D ~ ICUd * psi2 - , H.R ~ H * rho -) - -## state updates -state_updates = list( - S ~ S - S.E - , E ~ E + S.E - E.Ia - E.Ip - , Ia ~ Ia + E.Ia - Ia.R - , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is - , Im ~ Im + Ip.Im - Im.R - , Is ~ Is + Ip.Is - Is.ICUs - Is.H - Is.ICUd - , H ~ H + Is.H - H.R - , ICUs ~ ICUs + Is.ICUs - ICUs.H2 - , ICUd ~ ICUd + Is.ICUd - ICUd.D - , H2 ~ H2 + ICUs.H2 - H2.R - , R ~ R + Ia.R + Im.R + H.R + H2.R - , D ~ D + ICUd.D # where is IsD? (severely infected that die before they get hospitalized) -) - -## simple unstructured scalar expression -expr_list = ExprList( - before = computations - , during = c( - flow_rates - , state_updates - ) -) diff --git a/inst/starter_models/macpan_base/settings.json b/inst/starter_models/macpan_base/settings.json deleted file mode 100644 index 87b87374..00000000 --- a/inst/starter_models/macpan_base/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "required_partitions" : ["Epi"], - "null_partitions" : ["Null"], - "state_variables" : [ - "S", - "E", - "Ia", - "Ip", - "Im", - "Is", - "R", - "H", - "ICUs", - "ICUd", - "H2", - "D" - ], - "flow_variables" : [ - "SE", - "EIa", - "EIp", - "IaR", - "IpIm", - "ImR", - "IpIs", - "IsICUs", - "IsICUd", - "ICUsH2", - "H2R", - "IsH", - "HICUd", - "ICUdD", - "HR" - ] -} \ No newline at end of file diff --git a/inst/starter_models/macpan_base/variables.csv b/inst/starter_models/macpan_base/variables.csv deleted file mode 100644 index 28f8bb5d..00000000 --- a/inst/starter_models/macpan_base/variables.csv +++ /dev/null @@ -1,50 +0,0 @@ -Epi -S -E -Ia -Ip -Im -Is -R -H -ICUs -ICUd -H2 -D -alpha -beta0 -sigma -mu -gamma_a -gamma_p -gamma_m -gamma_s -nonhosp_mort -phi1 -phi2 -psi1 -psi2 -psi3 -rho -Ca -Cp -Cm -Cs -iso_m -iso_s -N -SE -EIa -EIp -IaR -IpIm -ImR -IpIs -IsICUs -IsICUd -ICUsH2 -H2R -IsH -HICUd -ICUdD -HR diff --git a/inst/starter_models/seir/derivations.json b/inst/starter_models/seir/derivations.json deleted file mode 100644 index 4fc25525..00000000 --- a/inst/starter_models/seir/derivations.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "simulation_phase" : "before", - "output_names" : ["N"], - "arguments" : ["S", "E", "I", "R"], - "expression" : "sum(S, E, I, R)" - }, - { - "simulation_phase" : "during_pre_update", - "output_names" : ["foi"], - "arguments" : ["I", "beta", "N"], - "expression" : "I * beta / N" - } -] diff --git a/inst/starter_models/seir/flows.csv b/inst/starter_models/seir/flows.csv deleted file mode 100644 index 4cd8cf42..00000000 --- a/inst/starter_models/seir/flows.csv +++ /dev/null @@ -1,4 +0,0 @@ -from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition -S ,E ,foi ,per_capita ,Epi ,Epi ,Epi , , ,Null -E ,I ,alpha ,per_capita ,Epi ,Epi ,Epi , , ,Null -I ,R ,gamma ,per_capita ,Epi ,Epi ,Epi , , ,Null diff --git a/inst/starter_models/seir/model.R b/inst/starter_models/seir/model.R deleted file mode 100644 index fed9c28c..00000000 --- a/inst/starter_models/seir/model.R +++ /dev/null @@ -1,33 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -computations = list( - N ~ sum(S, E, I, R) -) - -## absolute flow rates (per time only) -flow_rates = list( - exposure ~ S * I * beta / N - , infection ~ alpha * E - , recovery ~ gamma * I -) - -## state updates -state_updates = list( - S ~ S - exposure - , E ~ E + exposure - infection - , I ~ I + infection - recovery - , R ~ R + recovery -) - -## simple unstructured scalar expression -expr_list = ExprList( - before = computations - , during = c( - flow_rates - , state_updates - ) -) diff --git a/inst/starter_models/seir/settings.json b/inst/starter_models/seir/settings.json deleted file mode 100644 index 94f52fe6..00000000 --- a/inst/starter_models/seir/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "required_partitions" : ["Epi"], - "null_partition" : ["Null"], - "state_variables" : ["S", "E", "I", "R"], - "flow_variables" : ["foi", "alpha", "gamma"] -} diff --git a/inst/starter_models/seir/variables.csv b/inst/starter_models/seir/variables.csv deleted file mode 100644 index 9254856f..00000000 --- a/inst/starter_models/seir/variables.csv +++ /dev/null @@ -1,10 +0,0 @@ -Epi -S -E -I -R -beta -alpha -gamma -N -foi diff --git a/inst/starter_models/sir/derivations.json b/inst/starter_models/sir/derivations.json deleted file mode 100644 index 81c90eb0..00000000 --- a/inst/starter_models/sir/derivations.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "output_names" : ["N"], - "simulation_phase" : "before", - "arguments" : ["S", "I", "R"], - "expression" : "sum(S, I, R)" - }, - { - "output_names" : ["foi"], - "simulation_phase" : "during_pre_update", - "arguments" : ["I", "beta", "N"], - "expression" : "I * beta / N" - } -] diff --git a/inst/starter_models/sir/flows.csv b/inst/starter_models/sir/flows.csv deleted file mode 100644 index 09da3cb5..00000000 --- a/inst/starter_models/sir/flows.csv +++ /dev/null @@ -1,3 +0,0 @@ -from ,to ,flow ,type -S ,I ,foi ,per_capita -I ,R ,gamma ,per_capita diff --git a/inst/starter_models/sir/model.R b/inst/starter_models/sir/model.R deleted file mode 100644 index f9c23304..00000000 --- a/inst/starter_models/sir/model.R +++ /dev/null @@ -1,68 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -computations = list( - N ~ sum(S, I, R) -) - -## absolute flow rates (per time only) - -## engine-agnostic (not done) -## infection = mp_flow(~ I * beta / N, from = S, to = I) -## recovery = mp_flow(~ gamma, from = I, to = R) -## state_update = mp_update(type = "R4K") - -## engine-specific -flow_rates = list( - infection ~ S * I * beta / N - , recovery ~ gamma * I -) - -## -state_updates = list( - S ~ S - infection - , I ~ I + infection - recovery - , R ~ R + recovery -) - -model_evaluation = list( - log_likelihood ~ dpois(I_obs, rbind_time(I, I_obs_times)) -) - -## simple unstructured scalar expression -expr_list = ExprList( - before = computations - , during = c(flow_rates, state_updates) - , after = model_evaluation -) - -## defaults -init_mats = MatsList( - S = 99 - , I = 1 - , R = 0 - , beta = 0.2 - , gamma = 0.1 - , N = 100 - , infection = empty_matrix - , recovery = empty_matrix - , log_likelihood = empty_matrix - , I_obs = empty_matrix - , I_obs_times = empty_matrix - , .mats_to_save = "I" - , .mats_to_return = "I" -) - -obj_fn = ObjectiveFunction(~ -sum(log_likelihood)) - -## simulator -################################################### - -tmb_simulator = TMBModel( - init_mats = init_mats - , expr_list = expr_list - , obj_fn = obj_fn -)$simulator() diff --git a/inst/starter_models/sir/settings.json b/inst/starter_models/sir/settings.json deleted file mode 100644 index f4eb7eb6..00000000 --- a/inst/starter_models/sir/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "required_partitions" : ["Epi"], - "null_partition" : "Null", - "vector_partition" : ["Vec"], - "state_variables" : ["state"], - "flow_variables" : ["flow_rates"], - "infection_flow_variables" : ["foi"], - "infectious_state_variables" : ["I"] -} diff --git a/inst/starter_models/sir/transmission_dimensions.csv b/inst/starter_models/sir/transmission_dimensions.csv deleted file mode 100644 index 66afd4cd..00000000 --- a/inst/starter_models/sir/transmission_dimensions.csv +++ /dev/null @@ -1 +0,0 @@ -matrix ,matrix_partition ,infection_partition ,infectious_partition diff --git a/inst/starter_models/sir/transmission_matrices.csv b/inst/starter_models/sir/transmission_matrices.csv deleted file mode 100644 index bb320c55..00000000 --- a/inst/starter_models/sir/transmission_matrices.csv +++ /dev/null @@ -1,2 +0,0 @@ -matrix ,infection_flow ,infectious_state -transmission ,infection ,I diff --git a/inst/starter_models/sir/variables.csv b/inst/starter_models/sir/variables.csv deleted file mode 100644 index 16fa0419..00000000 --- a/inst/starter_models/sir/variables.csv +++ /dev/null @@ -1,9 +0,0 @@ -Epi ,Vec -S ,state -I ,state -R ,state -N , -beta , -foi ,flow_rates -gamma ,flow_rates -per_capita_transmission , diff --git a/inst/starter_models/sir_demo/derivations.json b/inst/starter_models/sir_demo/derivations.json deleted file mode 100644 index 81c90eb0..00000000 --- a/inst/starter_models/sir_demo/derivations.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "output_names" : ["N"], - "simulation_phase" : "before", - "arguments" : ["S", "I", "R"], - "expression" : "sum(S, I, R)" - }, - { - "output_names" : ["foi"], - "simulation_phase" : "during_pre_update", - "arguments" : ["I", "beta", "N"], - "expression" : "I * beta / N" - } -] diff --git a/inst/starter_models/sir_demo/flows.csv b/inst/starter_models/sir_demo/flows.csv deleted file mode 100644 index e26f446d..00000000 --- a/inst/starter_models/sir_demo/flows.csv +++ /dev/null @@ -1,9 +0,0 @@ -from ,to ,flow ,type -S ,I ,foi ,per_capita -I ,R ,gamma ,per_capita -S ,S ,birth ,per_capita_inflow -I ,S ,birth ,per_capita_inflow -R ,S ,birth ,per_capita_inflow -S ,S ,death ,per_capita_outflow -I ,I ,death ,per_capita_outflow -R ,R ,death ,per_capita_outflow diff --git a/inst/starter_models/sir_demo/model.R b/inst/starter_models/sir_demo/model.R deleted file mode 100644 index b0e59856..00000000 --- a/inst/starter_models/sir_demo/model.R +++ /dev/null @@ -1,36 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -computations = list( - N ~ sum(S, I, R) -) - -## absolute flow rates (per time only) -flow_rates = list( - birth ~ birth_rate * N - , infection ~ S * I * beta / N - , recovery ~ gamma * I -) - -## state updates -state_updates = list( - S ~ S - infection + birth - death_rate * S - , I ~ I + infection - recovery - death_rate * I - , R ~ R + recovery - death_rate * R -) - -## simple unstructured scalar expression -## in the general case, -## birth rate and death rate can differ, N might not be constant -## nothing can be computed before simulation loop -expr_list = ExprList( - during = c( - computations - , flow_rates - , state_updates - ) -) - diff --git a/inst/starter_models/sir_demo/settings.json b/inst/starter_models/sir_demo/settings.json deleted file mode 100644 index f686907b..00000000 --- a/inst/starter_models/sir_demo/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "required_partitions" : ["Epi"], - "null_partition" : "Null", - "state_variables" : ["S", "I", "R"], - "flow_variables" : ["foi", "gamma", "birth", "death"] -} diff --git a/inst/starter_models/sir_demo/variables.csv b/inst/starter_models/sir_demo/variables.csv deleted file mode 100644 index 2cc66a3a..00000000 --- a/inst/starter_models/sir_demo/variables.csv +++ /dev/null @@ -1,10 +0,0 @@ -Epi -S -I -R -N -beta -foi -gamma -birth -death diff --git a/inst/starter_models/sir_waning/derivations.json b/inst/starter_models/sir_waning/derivations.json deleted file mode 100644 index 81c90eb0..00000000 --- a/inst/starter_models/sir_waning/derivations.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "output_names" : ["N"], - "simulation_phase" : "before", - "arguments" : ["S", "I", "R"], - "expression" : "sum(S, I, R)" - }, - { - "output_names" : ["foi"], - "simulation_phase" : "during_pre_update", - "arguments" : ["I", "beta", "N"], - "expression" : "I * beta / N" - } -] diff --git a/inst/starter_models/sir_waning/flows.csv b/inst/starter_models/sir_waning/flows.csv deleted file mode 100644 index 3bb52294..00000000 --- a/inst/starter_models/sir_waning/flows.csv +++ /dev/null @@ -1,4 +0,0 @@ -from ,to ,flow ,type -S ,I ,foi ,per_capita -I ,R ,gamma ,per_capita -R ,S ,wane ,per_capita diff --git a/inst/starter_models/sir_waning/model.R b/inst/starter_models/sir_waning/model.R deleted file mode 100644 index a6472af0..00000000 --- a/inst/starter_models/sir_waning/model.R +++ /dev/null @@ -1,45 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -computations = list( - N ~ sum(S, I, R) -) - -## absolute flow rates (per time only) -flow_rates = list( - infection ~ S * I * beta / N - , recovery ~ gamma * I - , waning_immunity ~ phi * R -) - -## state updates -state_updates = list( - S ~ S - infection + waning_immunity - , I ~ I + infection - recovery - , R ~ R + recovery - waning_immunity -) - -## simple unstructured scalar expression -expr_list = ExprList( - before = computations - , during = c( - flow_rates - , state_updates - ) -) - -## default values -init_mats = MatsList( - S = 99 - , I = 1 - , R = 0 - , beta = 0.2 - , gamma = 0.2 - , wane = 0.01 - , N = 100 - , .mats_to_save = "I" - , .mats_to_return = "I" -) diff --git a/inst/starter_models/sir_waning/settings.json b/inst/starter_models/sir_waning/settings.json deleted file mode 100644 index 7adf6dd9..00000000 --- a/inst/starter_models/sir_waning/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "required_partitions" : ["Epi"], - "null_partition" : "Null", - "state_variables" : ["S", "I", "R"], - "flow_variables" : ["foi", "gamma", "wane"] -} diff --git a/inst/starter_models/sir_waning/variables.csv b/inst/starter_models/sir_waning/variables.csv deleted file mode 100644 index b640f900..00000000 --- a/inst/starter_models/sir_waning/variables.csv +++ /dev/null @@ -1,9 +0,0 @@ -Epi -S -I -R -N -beta -foi -gamma -wane diff --git a/inst/starter_models/ww/derivations.json b/inst/starter_models/ww/derivations.json deleted file mode 100644 index 1d5fc154..00000000 --- a/inst/starter_models/ww/derivations.json +++ /dev/null @@ -1,116 +0,0 @@ -[ - { - "simulation_phase" : "before", - "output_names" : ["EIa"], - "arguments" : ["alpha", "sigma"], - "expression" : "alpha * sigma" - }, - { - "simulation_phase" : "before", - "output_names" : ["EIp"], - "arguments" : ["alpha", "sigma"], - "expression" : "(1-alpha)*sigma" - }, - { - "simulation_phase" : "before", - "output_names" : ["IaR"], - "arguments" : ["gamma_a"], - "expression" : "gamma_a" - }, - { - "simulation_phase" : "before", - "output_names" : ["IpIm"], - "arguments" : ["mu", "gamma_p"], - "expression" : "mu*gamma_p" - }, - { - "simulation_phase" : "before", - "output_names" : ["ImR"], - "arguments" : ["gamma_m"], - "expression" : "gamma_m" - }, - { - "simulation_phase" : "before", - "output_names" : ["IpIs"], - "arguments" : ["mu", "gamma_p"], - "expression" : "(1-mu)*gamma_p" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsICUs"], - "arguments" : ["nonhosp_mort", "phi1", "phi2", "gamma_s"], - "expression" : "(1-nonhosp_mort)*(1-phi1)*(1-phi2)*gamma_s" - }, - { - "simulation_phase" : "before", - "output_names" : ["ICUsH2"], - "arguments" : ["psi1"], - "expression" : "psi1" - }, - { - "simulation_phase" : "before", - "output_names" : ["H2R"], - "arguments" : ["psi3"], - "expression" : "psi3" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsH"], - "arguments" : ["nonhosp_mort", "phi1", "gamma_s"], - "expression" : "(1-nonhosp_mort)*phi1*gamma_s" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsICUd"], - "arguments" : ["nonhosp_mort", "phi1", "phi2", "gamma_s"], - "expression" : "(1-nonhosp_mort)*(1-phi1)*phi2*gamma_s" - }, - { - "simulation_phase" : "before", - "output_names" : ["ICUdD"], - "arguments" : ["psi2"], - "expression" : "psi2" - }, - { - "simulation_phase" : "before", - "output_names" : ["HR"], - "arguments" : ["rho"], - "expression" : "1-exp(-rho)" - }, - { - "simulation_phase" : "before", - "output_names" : ["IaW"], - "arguments" : ["nu"], - "expression": "nu" - }, - { - "simulation_phase" : "before", - "output_names" : ["IpW"], - "arguments" : ["nu"], - "expression": "nu" - }, - { - "simulation_phase" : "before", - "output_names" : ["ImW"], - "arguments" : ["nu"], - "expression": "nu" - }, - { - "simulation_phase" : "before", - "output_names" : ["IsW"], - "arguments" : ["nu"], - "expression": "nu" - }, - { - "simulation_phase" : "before", - "output_names" : ["WA"], - "arguments" : ["xi"], - "expression": "xi" - }, - { - "simulation_phase" : "during_pre_update", - "output_names" : ["SE"], - "arguments" : ["Ia", "Ip", "Im", "Is", "beta0", "N", "Ca", "Cp", "Cm", "Cs", "iso_m", "iso_s"], - "expression" : "(beta0/N)*(Ia*Ca+Ip*Cp+Im*Cm*(1-iso_m)+Is*Cs*(1-iso_s))" - } -] \ No newline at end of file diff --git a/inst/starter_models/ww/flows.csv b/inst/starter_models/ww/flows.csv deleted file mode 100644 index 5f3352ae..00000000 --- a/inst/starter_models/ww/flows.csv +++ /dev/null @@ -1,20 +0,0 @@ -from ,to ,flow ,type ,from_partition ,to_partition ,flow_partition ,from_to_partition ,from_flow_partition ,to_flow_partition -S ,E ,SE ,per_capita ,Epi ,Epi ,Epi , , , -E ,Ia ,EIa ,per_capita ,Epi ,Epi ,Epi , , , -E ,Ip ,EIp ,per_capita ,Epi ,Epi ,Epi , , , -Ia ,R ,IaR ,per_capita ,Epi ,Epi ,Epi , , , -Ip ,Im ,IpIm ,per_capita ,Epi ,Epi ,Epi , , , -Im ,R ,ImR ,per_capita ,Epi ,Epi ,Epi , , , -Ip ,Is ,IpIs ,per_capita ,Epi ,Epi ,Epi , , , -Is ,ICUs ,IsICUs ,per_capita ,Epi ,Epi ,Epi , , , -ICUs ,H2 ,ICUsH2 ,per_capita ,Epi ,Epi ,Epi , , , -H2 ,R ,H2R ,per_capita ,Epi ,Epi ,Epi , , , -Is ,H ,IsH ,per_capita ,Epi ,Epi ,Epi , , , -Is ,ICUd ,IsICUd ,per_capita ,Epi ,Epi ,Epi , , , -ICUd ,D ,ICUdD ,per_capita ,Epi ,Epi ,Epi , , , -H ,R ,HR ,per_capita ,Epi ,Epi ,Epi , , , -Ia ,W ,IaW ,per_capita_inflow ,Epi ,Epi ,Epi , , , -Ip ,W ,IpW ,per_capita_inflow ,Epi ,Epi ,Epi , , , -Im ,W ,ImW ,per_capita_inflow ,Epi ,Epi ,Epi , , , -Is ,W ,IsW ,per_capita_inflow ,Epi ,Epi ,Epi , , , -W ,A ,WA ,per_capita ,Epi ,Epi ,Epi , , , diff --git a/inst/starter_models/ww/model.R b/inst/starter_models/ww/model.R deleted file mode 100644 index e8d784fa..00000000 --- a/inst/starter_models/ww/model.R +++ /dev/null @@ -1,59 +0,0 @@ -library(macpan2) - -## expression lists -################################################### - -## free form computations for convenience -computations = list( - N ~ sum(S, E, Ia, Ip, Im, Is, R, H, ICUs, ICUd, H2, D) -) - -## absolute flow rates (per time only) -flow_rates = list( - S.E ~ S * (beta0 / N) * (Ia * Ca + Ip * Cp + Im * Cm * (1 - iso_m) + Is * Cs *(1 - iso_s)) - , E.Ia ~ E * alpha * sigma - , E.Ip ~ E * (1 - alpha)* sigma - , Ia.R ~ Ia * gamma_a - , Ip.Im ~ Ip * mu * gamma_p - , Im.R ~ Im * gamma_m - , Ip.Is ~ Ip * (1 - mu) * gamma_p - , Is.ICUs ~ Is * (1 - nonhosp_mort) * (1 - phi1) * (1 - phi2) * gamma_s - , Is.ICUd ~ Is * (1 - nonhosp_mort) * (1 - phi1) * phi2 * gamma_s - , ICUs.H2 ~ ICUs * psi1 - , H2.R ~ H2 * psi3 - , Is.H ~ Is * (1 - nonhosp_mort) * phi1 * gamma_s - , ICUd.D ~ ICUd * psi2 - , H.R ~ H * rho - , Ia.W ~ Ia * nu - , Ip.W ~ Ip * nu - , Im.W ~ Im * nu - , Is.W ~ Is * nu - , W.A ~ W * xi -) - -## state updates -state_updates = list( - S ~ S - S.E - , E ~ E + S.E - EI.a - EI.p - , Ia ~ Ia + E.Ia - Ia.R - , Ip ~ Ip + E.Ip - Ip.Im - Ip.Is - , Im ~ Im + Ip.Im - Im.R - , Is ~ Is + Ip.Is - Is.ICUs - Is.H - Is.ICUd - , H ~ H + Is.H - H.R - , ICUs ~ ICUs + Is.ICUs - ICUs.H2 - , ICUd ~ ICUd + Is.ICUd - ICUd.D - , H2 ~ H2 + ICUs.H2 - H2.R - , R ~ R + Ia.R + Im.R + H.R + H2.R - , D ~ D + ICUd.D - , W ~ W + Ia.W + Ip.W + Im.W + Is.W - W.A - , A ~ A + W.A -) - -## simple unstructured scalar expression -expr_list = ExprList( - before = computations - , during = c( - flow_rates - , state_updates - ) -) diff --git a/inst/starter_models/ww/settings.json b/inst/starter_models/ww/settings.json deleted file mode 100644 index d6f5e55c..00000000 --- a/inst/starter_models/ww/settings.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "required_partitions" : ["Epi"], - "null_partitions" : ["Null"], - "state_variables" : [ - "S", - "E", - "Ia", - "Ip", - "Im", - "Is", - "R", - "H", - "ICUs", - "ICUd", - "H2", - "D", - "W", - "A" - ], - "flow_variables" : [ - "SE", - "EIa", - "EIp", - "IaR", - "IpIm", - "ImR", - "IpIs", - "IsICUs", - "IsICUd", - "ICUsH2", - "H2R", - "IsH", - "HICUd", - "ICUdD", - "HR", - "IaW", - "IpW", - "ImW", - "IsW", - "WA" - ] -} \ No newline at end of file diff --git a/inst/starter_models/ww/variables.csv b/inst/starter_models/ww/variables.csv deleted file mode 100644 index 717dc61c..00000000 --- a/inst/starter_models/ww/variables.csv +++ /dev/null @@ -1,59 +0,0 @@ -Epi -S -E -Ia -Ip -Im -Is -R -H -ICUs -ICUd -H2 -D -W -A -alpha -beta0 -sigma -mu -gamma_a -gamma_p -gamma_m -gamma_s -nonhosp_mort -phi1 -phi2 -psi1 -psi2 -psi3 -rho -Ca -Cp -Cm -Cs -iso_m -iso_s -nu -xi -N -SE -EIa -EIp -IaR -IpIm -ImR -IpIs -IsICUs -IsICUd -ICUsH2 -H2R -IsH -HICUd -ICUdD -HR -IaW -IpW -ImW -IsW -WA From cb41351b2f89c0a8ce1c003d8c1b64a9a7c2ed1e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Mon, 15 Jan 2024 15:46:15 -0500 Subject: [PATCH 303/332] test fixes for #155 --- tests/testthat/test-insert-state-vars.R | 36 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/testthat/test-insert-state-vars.R b/tests/testthat/test-insert-state-vars.R index 2af1f345..ee32a933 100644 --- a/tests/testthat/test-insert-state-vars.R +++ b/tests/testthat/test-insert-state-vars.R @@ -1,16 +1,30 @@ test_that("inserted expressions are able to refer to single states/flows", { - m = Compartmental(system.file("starter_models", "sir", package = "macpan2")) - s = m$simulators$tmb(time_steps = 3 - , state = c(S = 99, I = 1, R = 0) - , flow = c(foi = NA, gamma = 0.2) - , beta = 0.4 - , N = empty_matrix - , test_ratio = empty_matrix - , .mats_to_return = c("test_ratio", "state") + spec = mp_tmb_model_spec( + before = list(N ~ sum(state)) + , during = list( + per_capita[foi] ~ beta * state[I] / N + , flow_rates ~ per_capita * state[from] + , inflow ~ group_sums(flow_rates, to, state) + , outflow ~ group_sums(flow_rates, from, state) + , state ~ state + inflow - outflow + ) + , default = list( + state = c(S = 99, I = 1, R = 0) + , per_capita = c(foi = NA, gamma = 0.2) + , beta = 0.4 + ) + , integers = list( + from = c(0, 1) + , to = c(1, 2) + ) ) - s$insert$expressions(test_ratio ~ I / S, .at = Inf, .phase = "during") - s$insert$expressions(foi ~ (I/S)^0.5 * beta, .at = 2L, .phase = "during") - v = s$report(.phases = "during") + s = mp_simulator(spec + , time_steps = 50 + , outputs = c("S", "I") + ) + s$add$matrices(test_ratio = empty_matrix, .mats_to_save = "test_ratio", .mats_to_return = "test_ratio") + s$insert$expressions(test_ratio ~ state[I] / state[S], .at = Inf, .phase = "during") + v = mp_trajectory(s) expect_equal( v[v$row == "I", "value"] / v[v$row == "S", "value"], v[v$matrix == "test_ratio", "value"] From 4cc06556f7b86b353129e36d9dddb2e05e44901c Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Mon, 15 Jan 2024 20:48:51 -0500 Subject: [PATCH 304/332] partial buildignore/docs cleanup --- .Rbuildignore | 2 ++ DESCRIPTION | 2 +- NAMESPACE | 8 ++++++++ R/engine_methods.R | 2 +- R/enum_methods.R | 6 +++--- R/index_to_tmb.R | 3 ++- R/indices.R | 3 +++ R/labelled_partitions.R | 5 +++++ R/tmb_model.R | 2 ++ man/MethListFromEngineMethods.Rd | 19 ------------------- man/MethodPrototype.Rd | 23 ----------------------- man/MethodTypeUtils.Rd | 12 ------------ man/mk_meth_cls.Rd | 17 ----------------- man/mp_report.Rd | 16 ++++++++++++++++ vignettes/tmb_model.Rmd | 2 +- 15 files changed, 44 insertions(+), 78 deletions(-) delete mode 100644 man/MethListFromEngineMethods.Rd delete mode 100644 man/MethodPrototype.Rd delete mode 100644 man/MethodTypeUtils.Rd delete mode 100644 man/mk_meth_cls.Rd create mode 100644 man/mp_report.Rd diff --git a/.Rbuildignore b/.Rbuildignore index a698a372..b7dfe5d3 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -19,6 +19,7 @@ LICENSE ^doc$ ^Meta$ drat/ +.*\.vscode ^vignettes/calibration[.]Rmd$ ^vignettes/example_models[.]Rmd$ @@ -29,3 +30,4 @@ drat/ ^vignettes/quickstart_products[.]Rmd$ ^vignettes/sir_radial_basis_transmission[.]Rmd$ ^vignettes/time_varying_parameters[.]Rmd$ +## should we ignore tmb_model.Rmd as well ... ?? \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index efb81e88..c384a3f3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -48,5 +48,5 @@ Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3.9000 +RoxygenNote: 7.3.0 URL: https://canmod.github.io/macpan2/, https://github.com/canmod/macpan2 diff --git a/NAMESPACE b/NAMESPACE index 3a6789aa..3a8ce872 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,8 +10,12 @@ S3method(Vector,numeric) S3method(as.data.frame,Index) S3method(as.data.frame,Ledger) S3method(as.matrix,Vector) +S3method(by_,"NULL") +S3method(by_,character) +S3method(by_,formula) S3method(c,String) S3method(c,StringData) +S3method(col_labels,character) S3method(head,Ledger) S3method(labelling_column_names,Index) S3method(labelling_column_names,Ledger) @@ -32,8 +36,10 @@ S3method(mp_extract,ModelDefRun) S3method(mp_extract_exprs,DynamicModel) S3method(mp_extract_exprs,ExprList) S3method(mp_extract_exprs,list) +S3method(mp_final,TMBSimulator) S3method(mp_index,character) S3method(mp_index,data.frame) +S3method(mp_initial,TMBSimulator) S3method(mp_labels,Index) S3method(mp_labels,Ledger) S3method(mp_reference,Index) @@ -72,6 +78,7 @@ S3method(print,TMBModelSpec) S3method(print,TMBSimulator) S3method(print,Vector) S3method(print,summary.Ledger) +S3method(row_labels,character) S3method(split_by,character) S3method(split_by,formula) S3method(str,Ledger) @@ -165,6 +172,7 @@ export(all_consistent) export(all_equal) export(all_not_equal) export(atomic_partition) +export(by_) export(cartesian) export(cartesian_self) export(empty_matrix) diff --git a/R/engine_methods.R b/R/engine_methods.R index 5caeb335..111dc57f 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -40,7 +40,7 @@ EngineMethods = function(exprs = list(), int_vecs = IntVecs()) { #' the two. #' #' @param engine_methods An object of class EngineMethods -#' @nord +#' @noRd MethListFromEngineMethods = function(engine_methods) { l = list() for (nm in names(engine_methods$exprs)) { diff --git a/R/enum_methods.R b/R/enum_methods.R index 0cbd3497..aad4cfc6 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -13,7 +13,7 @@ #' @param int_vec_arg_nms Character vector naming the integer-vector-valued #' arguments. #' -#' @nord +#' @noRd MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { self = Base() self$formula = formula @@ -64,7 +64,7 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { #' @param cls_nm Character string giving the name of the class. #' @param meth_type_id Integer giving the associated ID of the method type. #' -#' @nord +#' @noRd mk_meth_cls = function(cls_nm, meth_type_id) { pf = parent.frame() force(pf) @@ -83,7 +83,7 @@ mk_meth_cls = function(cls_nm, meth_type_id) { #' This class is here so that `MethodTypes`, which is automatically generated #' from the C++ code, can inherit from it. #' -#' @nord +#' @noRd MethodTypeUtils = function() { self = Base() diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 2552dc39..3c773bd4 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -122,7 +122,8 @@ mp_tmb_simulator.ModelDefRun = function(dynamic_model do.call(mp_tmb_simulator, args) } -#' @param parameter_vector Numeric vector equal in length to +#' reporting function +#' @param parameter_vector Numeric vector equal in length to (?) #' @param simulator Object produced by \code{\link{tmb_simulator}}. #' @export mp_report = function(simulator, parameter_vector = numeric(), phases = "during") { diff --git a/R/indices.R b/R/indices.R index 16a7d877..d1e589c6 100644 --- a/R/indices.R +++ b/R/indices.R @@ -5,7 +5,10 @@ col_labels = function(x) { UseMethod("col_labels") } +#' @export row_labels.character = function(x) valid$char$assert(x) + +#' @export col_labels.character = function(x) valid$char$assert(x) diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index b8a3912a..88ce96f0 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -360,12 +360,16 @@ join_partitions = function(x, y, by = "") { ) } +#' @export by_ = function(by) UseMethod("by_") + +#' @export by_.character = function(by) { if (identical(nchar(by), 0L) | isTRUE(is.na(by))) return(list()) by = process_by_char(by) list(x = by, y = by) } +#' @export by_.formula = function(by) { if (is_one_sided(by)) return(by_(rhs_char(by))) list( @@ -373,6 +377,7 @@ by_.formula = function(by) { y = process_by_char(rhs_char(by)) ) } +#' @export by_.NULL = function(by) list() suffixes_ = function(x, y) { s = c(names(x)[ncol(x)], names(y)[ncol(y)]) diff --git a/R/tmb_model.R b/R/tmb_model.R index 874d977c..8205aa90 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -401,6 +401,7 @@ mp_initial = function(model_simulator, ...) { UseMethod("mp_initial") } +#' @export mp_initial.TMBSimulator = function(model_simulator, matrices, params = NULL) { (model_simulator $replace @@ -418,6 +419,7 @@ mp_final = function(model_simulator, ...) { } +#' @export mp_final.TMBSimulator = function(model_simulator, time_steps, outputs, ...) { (model_simulator $replace diff --git a/man/MethListFromEngineMethods.Rd b/man/MethListFromEngineMethods.Rd deleted file mode 100644 index fb97310d..00000000 --- a/man/MethListFromEngineMethods.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/engine_methods.R -\name{MethListFromEngineMethods} -\alias{MethListFromEngineMethods} -\title{Construct a Method List from Engine Methods} -\usage{ -MethListFromEngineMethods(engine_methods) -} -\arguments{ -\item{engine_methods}{An object of class EngineMethods} -} -\description{ -Alternative constuctor for \code{MethList}. -} -\details{ -The engine interacts with \code{MethList} objects, but the user interacts -with \code{EngineMethods} objects. This allows developers to convert between -the two. -} diff --git a/man/MethodPrototype.Rd b/man/MethodPrototype.Rd deleted file mode 100644 index 9c3b8c25..00000000 --- a/man/MethodPrototype.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enum_methods.R -\name{MethodPrototype} -\alias{MethodPrototype} -\title{Method Prototype} -\usage{ -MethodPrototype(formula, mat_arg_nms, int_vec_arg_nms) -} -\arguments{ -\item{formula}{Formula for defining a method type using a prototype.} - -\item{mat_arg_nms}{Character vector naming the matrix-valued arguments.} - -\item{int_vec_arg_nms}{Character vector naming the integer-vector-valued -arguments.} -} -\description{ -Define a method type using a prototype. These prototypes can be compared -with methods defined in R to see if they are consistent with a method -type that has been defined in C++. All -arguments are automatically derived through comments in the C++ code where -the method types are defined. -} diff --git a/man/MethodTypeUtils.Rd b/man/MethodTypeUtils.Rd deleted file mode 100644 index 0e535e7f..00000000 --- a/man/MethodTypeUtils.Rd +++ /dev/null @@ -1,12 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enum_methods.R -\name{MethodTypeUtils} -\alias{MethodTypeUtils} -\title{Method Type Utilities} -\usage{ -MethodTypeUtils() -} -\description{ -This class is here so that \code{MethodTypes}, which is automatically generated -from the C++ code, can inherit from it. -} diff --git a/man/mk_meth_cls.Rd b/man/mk_meth_cls.Rd deleted file mode 100644 index a3d4e6d6..00000000 --- a/man/mk_meth_cls.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enum_methods.R -\name{mk_meth_cls} -\alias{mk_meth_cls} -\title{Make Method Class} -\usage{ -mk_meth_cls(cls_nm, meth_type_id) -} -\arguments{ -\item{cls_nm}{Character string giving the name of the class.} - -\item{meth_type_id}{Integer giving the associated ID of the method type.} -} -\description{ -Place a method object in the package namespace for a given method type -defined in the C++ code. -} diff --git a/man/mp_report.Rd b/man/mp_report.Rd new file mode 100644 index 00000000..569838f7 --- /dev/null +++ b/man/mp_report.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/index_to_tmb.R +\name{mp_report} +\alias{mp_report} +\title{reporting function} +\usage{ +mp_report(simulator, parameter_vector = numeric(), phases = "during") +} +\arguments{ +\item{simulator}{Object produced by \code{\link{tmb_simulator}}.} + +\item{parameter_vector}{Numeric vector equal in length to (?)} +} +\description{ +reporting function +} diff --git a/vignettes/tmb_model.Rmd b/vignettes/tmb_model.Rmd index c12e98f1..f75cfb55 100644 --- a/vignettes/tmb_model.Rmd +++ b/vignettes/tmb_model.Rmd @@ -70,7 +70,7 @@ sir = TMBModel( sir ``` -```{r} +```{r badchunk, eval = FALSE} (sir$simulator()$report() %>% mutate(state = factor(row, levels = names(state_vector))) %>% ggplot() From 97426c9303dce77336cc718c7cfbed6dc779dfb3 Mon Sep 17 00:00:00 2001 From: Ben Bolker Date: Mon, 15 Jan 2024 21:19:53 -0500 Subject: [PATCH 305/332] import head() --- NAMESPACE | 1 + R/products.R | 1 + 2 files changed, 2 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index ef9666ec..c6604bb9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -277,5 +277,6 @@ importFrom(stats,as.formula) importFrom(stats,na.omit) importFrom(stats,setNames) importFrom(tools,file_ext) +importFrom(utils,head) importFrom(utils,read.table) useDynLib(macpan2) diff --git a/R/products.R b/R/products.R index bb11c881..d2a4911e 100644 --- a/R/products.R +++ b/R/products.R @@ -43,6 +43,7 @@ cartesian = function(x, y) x$products$cartesian(y) #' @param .rm Character string giving the dot-concatenated names of columns #' to remove from `x`. #' +#' @importFrom utils head #' @export cartesian_self = function(x, left_filter, right_filter, .wrt, .rm = .wrt) { left_prefix = var_case_to_cls_case(left_filter) From c52f2c54e61dfdb7948e986aa77968432d0810d0 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 16 Jan 2024 11:53:21 -0500 Subject: [PATCH 306/332] refine inference of output types --- R/tmb_model.R | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/R/tmb_model.R b/R/tmb_model.R index 874d977c..f2c9abda 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -265,8 +265,17 @@ TMBModelSpec = function( ) { initial_mats = self$matrices() initial_mats[names(default)] = default + initial_rownames = (initial_mats + |> lapply(as.matrix) + |> lapply(rownames) + |> unlist(use.names = FALSE, recursive = TRUE) + |> unique() + ) matrix_outputs = intersect(outputs, names(initial_mats)) - row_outputs = setdiff(outputs, matrix_outputs) + row_outputs = (outputs + |> setdiff(matrix_outputs) + |> intersect(initial_rownames) + ) mats_to_return = (initial_mats |> lapply(names) |> Filter(f = is.character) From 352f0e88138359ff9563bbeddbd8ae43d181d6b6 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Tue, 16 Jan 2024 13:37:46 -0500 Subject: [PATCH 307/332] more against #155 --- NAMESPACE | 20 +-- R/index_to_tmb.R | 2 +- R/vector.R | 40 +++--- man/mp_vector.Rd | 2 +- .../old-vignettes}/flow_types.Rmd | 0 .../old-vignettes}/quickstart2.Rmd | 0 .../old-vignettes}/quickstart_products.Rmd | 0 vignettes/calibration.Rmd | 6 +- vignettes/development_patterns.Rmd | 2 +- vignettes/sir_radial_basis_transmission.Rmd | 128 ------------------ 10 files changed, 36 insertions(+), 164 deletions(-) rename {vignettes => misc/old-vignettes}/flow_types.Rmd (100%) rename {vignettes => misc/old-vignettes}/quickstart2.Rmd (100%) rename {vignettes => misc/old-vignettes}/quickstart_products.Rmd (100%) delete mode 100644 vignettes/sir_radial_basis_transmission.Rmd diff --git a/NAMESPACE b/NAMESPACE index c6604bb9..75715d3a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,13 +3,13 @@ S3method(Index,Index) S3method(Index,Partition) S3method(Index,data.frame) -S3method(Vector,Index) -S3method(Vector,Vector) -S3method(Vector,data.frame) -S3method(Vector,numeric) +S3method(StructuredVector,Index) +S3method(StructuredVector,StructuredVector) +S3method(StructuredVector,data.frame) +S3method(StructuredVector,numeric) S3method(as.data.frame,Index) S3method(as.data.frame,Ledger) -S3method(as.matrix,Vector) +S3method(as.matrix,StructuredVector) S3method(by_,"NULL") S3method(by_,character) S3method(by_,formula) @@ -28,7 +28,7 @@ S3method(labels,ModelDefRun) S3method(labels,TMBCompartmentalSimulator) S3method(labels,TMBDynamicSimulator) S3method(labels,VariableLabels) -S3method(length,Vector) +S3method(length,StructuredVector) S3method(mp_combine_exprs,list) S3method(mp_extract,DynamicModel) S3method(mp_extract,Ledger) @@ -52,7 +52,7 @@ S3method(mp_union,Index) S3method(mp_union,Ledger) S3method(mp_vector,Index) S3method(mp_vector,Link) -S3method(mp_vector,Vector) +S3method(mp_vector,StructuredVector) S3method(mp_vector,character) S3method(mp_vector,data.frame) S3method(mp_vector,numeric) @@ -64,7 +64,7 @@ S3method(names,Ledger) S3method(names,MatsList) S3method(names,MethList) S3method(names,Partition) -S3method(names,Vector) +S3method(names,StructuredVector) S3method(print,DynamicModel) S3method(print,ExprList) S3method(print,Index) @@ -74,9 +74,9 @@ S3method(print,Partition) S3method(print,Quantities) S3method(print,String) S3method(print,StringData) +S3method(print,StructuredVector) S3method(print,TMBModelSpec) S3method(print,TMBSimulator) -S3method(print,Vector) S3method(print,summary.Ledger) S3method(row_labels,character) S3method(split_by,character) @@ -157,6 +157,7 @@ export(StandardExpr) export(StandardExprHazard) export(StringDataFromDotted) export(StringDataFromFrame) +export(StructuredVector) export(TMBModel) export(TMBModelSpec) export(TMBSimulator) @@ -164,7 +165,6 @@ export(TXTReader) export(Time) export(Transform) export(UserExpr) -export(Vector) export(VectorList) export(all_consistent) export(all_equal) diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 3c773bd4..845ee956 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -47,7 +47,7 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model } } else { for (v in names(vectors)) { - vectors[[v]] = Vector( + vectors[[v]] = StructuredVector( vectors[[v]], dynamic_model$init_vecs[[v]]$index ) diff --git a/R/vector.R b/R/vector.R index f474b5e0..70baf88a 100644 --- a/R/vector.R +++ b/R/vector.R @@ -1,8 +1,8 @@ #' @export -Vector = function(x, ...) UseMethod("Vector") +StructuredVector = function(x, ...) UseMethod("StructuredVector") #' @export -Vector.data.frame = function(x, index = NULL, values_name = "values", ...) { +StructuredVector.data.frame = function(x, index = NULL, values_name = "values", ...) { nms = setdiff(names(x), values_name) if (is.null(index)) index = mp_index(x[, nms, drop = FALSE]) bad_names = !nms %in% names(index) @@ -47,13 +47,13 @@ Vector.data.frame = function(x, index = NULL, values_name = "values", ...) { values[is.na(values)] = 0 ## that's the way we roll f = f[sorted_positions, index_names, drop = FALSE] index = Index(f, labelling_column_names = index$labelling_column_names) - v = Vector(index) + v = StructuredVector(index) v$set_all_numbers(values) } #' @export -Vector.numeric = function(x, index, ...) { - v = Vector(index) +StructuredVector.numeric = function(x, index, ...) { + v = StructuredVector(index) index_name = to_name(index$labelling_column_names) names(x) = extrapolate_dots(names(x), index_name) args = setNames(list(x), index_name) @@ -61,12 +61,12 @@ Vector.numeric = function(x, index, ...) { } #' @export -Vector.Vector = function(x, index, ...) { - Vector(x$numbers(), index, ...) +StructuredVector.StructuredVector = function(x, index, ...) { + StructuredVector(x$numbers(), index, ...) } #' @export -Vector.Index = function(x, ...) { +StructuredVector.Index = function(x, ...) { self = Base() self$index = x self$.numbers = zero_vector(self$index$labels()) @@ -111,10 +111,10 @@ Vector.Index = function(x, ...) { } self$length = function() length(self$.numbers) self$clone = function() { - new = Vector(self$index) + new = StructuredVector(self$index) new$set_all_numbers(self$numbers()) } - return_object(self, "Vector") + return_object(self, "StructuredVector") } @@ -130,16 +130,16 @@ process_grouping_dots = function(...) { } #' @export -length.Vector = function(x) x$length() +length.StructuredVector = function(x) x$length() #' @export -print.Vector = function(x, ...) print(x$numbers()) +print.StructuredVector = function(x, ...) print(x$numbers()) #' @export -names.Vector = function(x) x$numbers() |> names() +names.StructuredVector = function(x) x$numbers() |> names() #' @export -as.matrix.Vector = function(x, ...) { +as.matrix.StructuredVector = function(x, ...) { x$numbers() |> as.matrix() } @@ -154,7 +154,7 @@ zero_vector = function(labels) setNames(rep(0, length(labels)), labels) #' #' These labels can be used to create 'multidimensional' names for the elements #' of vectors. Here is the above example expressed in vector form. #' ```{r, echo = FALSE} -#' v = Vector(prod) +#' v = StructuredVector(prod) #' v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") #' ``` #' This example vector could be stored as a 3-by-2 matrix. But other examples @@ -189,16 +189,16 @@ zero_vector = function(labels) setNames(rep(0, length(labels)), labels) mp_vector = function(x, ...) UseMethod("mp_vector") #' @export -mp_vector.Vector = function(x, ...) x +mp_vector.StructuredVector = function(x, ...) x #' @export -mp_vector.Index = Vector.Index +mp_vector.Index = StructuredVector.Index #' @export -mp_vector.data.frame = Vector.data.frame +mp_vector.data.frame = StructuredVector.data.frame #' @export -mp_vector.numeric = Vector.numeric +mp_vector.numeric = StructuredVector.numeric #' @export mp_vector.character = function(x, ...) zero_vector(x) @@ -232,7 +232,7 @@ VectorList = function() { for (nm in names(new_vecs)) { if (nm %in% names(self$list)) { msg( - "Vector", nm, "is already in the list.", + "StructuredVector", nm, "is already in the list.", "Overwriting the existing one." ) |> message() } diff --git a/man/mp_vector.Rd b/man/mp_vector.Rd index bf6b4173..932b7665 100644 --- a/man/mp_vector.Rd +++ b/man/mp_vector.Rd @@ -14,7 +14,7 @@ See issue #131 #' These labels can be used to create 'multidimensional' names for the elements of vectors. Here is the above example expressed in vector form. -\if{html}{\out{

      }}\preformatted{v = Vector(prod) +\if{html}{\out{
      }}\preformatted{v = StructuredVector(prod) v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") }\if{html}{\out{
      }} diff --git a/vignettes/flow_types.Rmd b/misc/old-vignettes/flow_types.Rmd similarity index 100% rename from vignettes/flow_types.Rmd rename to misc/old-vignettes/flow_types.Rmd diff --git a/vignettes/quickstart2.Rmd b/misc/old-vignettes/quickstart2.Rmd similarity index 100% rename from vignettes/quickstart2.Rmd rename to misc/old-vignettes/quickstart2.Rmd diff --git a/vignettes/quickstart_products.Rmd b/misc/old-vignettes/quickstart_products.Rmd similarity index 100% rename from vignettes/quickstart_products.Rmd rename to misc/old-vignettes/quickstart_products.Rmd diff --git a/vignettes/calibration.Rmd b/vignettes/calibration.Rmd index 76a80917..9b4bed8d 100644 --- a/vignettes/calibration.Rmd +++ b/vignettes/calibration.Rmd @@ -89,7 +89,7 @@ macpan2helpers::mk_calibrate(sir_simulator, data = data.frame(I_obs = sir_prevalence$obs_val), params = list(beta = 1, I_sd = 1), transforms = list(beta = "log", I_sd = "log"), - exprs = list(log_lik ~ dnorm(I_obs, rbind_time(I), I_sd)) + exprs = list(log_lik ~ dnorm(I_obs, I, I_sd)) ) ``` @@ -196,7 +196,7 @@ macpan2helpers::mk_calibrate(sir_simulator , data = data.frame(I_obs = influenza_england_1978_school$in_bed) , params = list(beta = 1, gamma = 0.5, I_disp = 1) , transforms = list(beta = "log", gamma = "log", I_disp = "log") - , exprs = list(log_lik ~ dnbinom(I_obs, rbind_time(I), I_disp)) + , exprs = list(log_lik ~ dnbinom(I_obs, I, I_disp)) ) sir_simulator$objective(log(c(1, 0.5, 1))) ``` @@ -570,7 +570,7 @@ iteration. ```{r compute_loglik} sir_simulator$add$matrices( I_sd = 1 ) sir_simulator$insert$expressions( - log_lik ~ dnorm(I_obs, rbind_time(I_sim), I_sd), + log_lik ~ dnorm(I_obs, I_sim, I_sd), .phase = "after" ) ``` diff --git a/vignettes/development_patterns.Rmd b/vignettes/development_patterns.Rmd index b8a53007..1274ca8c 100644 --- a/vignettes/development_patterns.Rmd +++ b/vignettes/development_patterns.Rmd @@ -34,7 +34,7 @@ To understand `macpan2`, users and developers need to understand how to construc The `macpan2` package comes with a set of `vignette("example_models", package = "macpan2")`. One simple example is an SIR model stored here. ```{r sir_path} -(sir_path = system.file("starter_models", "sir", package = "macpan2")) +(sir_path = system.file("starter_models", "sir_vax", package = "macpan2")) ``` This path points to a directory with the following files. diff --git a/vignettes/sir_radial_basis_transmission.Rmd b/vignettes/sir_radial_basis_transmission.Rmd deleted file mode 100644 index 062eea78..00000000 --- a/vignettes/sir_radial_basis_transmission.Rmd +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: "Radial Basis Functions for Time Varying Transmission Rates" -header-includes: - - \usepackage{amsmath} -output: - rmarkdown::html_vignette: - toc: true -vignette: > - %\VignetteIndexEntry{Radial Basis Functions for Time Varying Transmission Rates} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - - - -```{r pkgs, include = FALSE} -library(macpan2) -library(macpan2helpers) -library(dplyr) -library(ggplot2) -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -cat_file = function(...) cat(readLines(file.path(...)), sep = "\n") -``` - - -## Getting and the Base Model - -Before we can add the fancy radial basis for the transmission rate, we need a base model. We use an SIR model that has been modified to include waning. - -```{r} -source(system.file("starter_models", "sir_waning", "model.R", package = "macpan2")) -tmb_simulator = TMBModel( - expr_list = expr_list -) -expr_list -``` - - -## Radial Basis Functions - -The `macpan2::rbf` function can be used to produce a matrix giving the values of each basis function (each column) at each time step (each row). Using this matrix, $X$, and a weights vector, $b$, we can get a flexible output vector, $y$, with a shape that can be modified into a wide variety of shapes by changing the weights vector. - -$$ -y = Xb -$$ - -The following code illustrates this approach. - -```{r, fig.height=8, fig.width=6} -set.seed(1L) -d = 20 -n = 2500 -X = rbf(n, d) -b = rnorm(d, sd = 0.01) -par(mfrow = c(3, 1)) -plot(c(1, n), range(X), type = "n", - xlab = "time", ylab = "basis functions") -for (i in 1:d) lines(X[,i]) -barplot(b, xlab = "", ylab = "weights") -plot(X %*% b, type = "l", xlab = "time", ylab = "output") -``` - -Here `d` is the dimension of the basis, or number of functions, and `n` is the number of time steps. By multiplying the uniform basis matrix (top panel) to a set of weights (middle panel), one can obtain a non-uniform curve (bottom panel). Note how the peaks in the output are associated with large positive weights. - -## Radial Transmission Rate Basis - -To transform the output of the matrix multiplication of the radial basis function matrix and the weights vector into a time-series for the transmission rate, $\beta$. Althought we could just use the output vector as the $\beta$ time series, it is more convenient to transform it a little so that the $\beta$ values yield more interesting dynamics in an SIR model. In particular, our model for $\beta_t$ as a function of time, $t$, is as follows. - -$$ -\log(\beta_t) = \log(\gamma_t) + \log(N) - \log(S_t) + x_tb -$$ - -Here we have the recovery rate, $\gamma_t$, total population, $N_t$, and number of susceptibles, $S_t$, at time, $t$, and the $t$th row of $X$, $x_t$. To better understand the rationale for this equation note that if every element of $b$ is set to zero, we have the following condition. - -$$ -\frac{\beta_t S_t}{N} = \gamma_t -$$ - -This condition assures that the number of infected individuals remains constant at time, $t$. This means that positive values of $b$ will tend to generate outbreaks and negative values will tend to reduce transmission. - -## Simulations - -```{r} -set.seed(1L) -simulator = sir$simulators$tmb( - time_steps = n - , state = c(S = 100000 - 500, I = 500, R = 0) - , flow = c(foi = 0, gamma = 0.2, wane = 0.01) - , beta = 1 - , N = 100000 - , X = rbf(n, d) - , b = rnorm(d, sd = 0.01) - , incidence = empty_matrix - , eta = empty_matrix - , .mats_to_save = c("state", "incidence", "beta") - , .mats_to_return = c("state", "incidence", "beta") -)$insert$expressions( - eta ~ gamma * exp(X %*% b) - , .phase = "before" - , .at = Inf -)$insert$expressions( - beta ~ eta[time_step(1)] / clamp(S/N, 1/100) - , .phase = "during" - , .at = 1 -)$insert$expressions( - incidence ~ I - , .vec_by_states = "total_inflow" - , .phase = "during" - , .at = Inf -)$replace$params( - default = rnorm(d, sd = 0.01) - , mat = rep("b", d) - , row = seq_len(d) - 1L -) -``` - -```{r, fig.height=8, fig.width=6} -set.seed(5L) -(simulator$report(rnorm(d, sd = 0.01), .phases = "during") - %>% mutate(variable = if_else(matrix == "state", row, matrix)) - %>% ggplot() - + facet_wrap(~ variable, ncol = 1, scales = 'free') - + geom_line(aes(time, value)) -) -``` From d971be6216617254b6a476b8379106560aa2fbdf Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 06:42:53 -0500 Subject: [PATCH 308/332] testing readers --- R/readers.R | 4 ++-- tests/testthat/test-no-args.R | 17 ----------------- tests/testthat/test-no-derivations.R | 20 -------------------- tests/testthat/test-readers.R | 2 +- 4 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 tests/testthat/test-no-args.R delete mode 100644 tests/testthat/test-no-derivations.R diff --git a/R/readers.R b/R/readers.R index 094a3f9b..45d4e6d2 100644 --- a/R/readers.R +++ b/R/readers.R @@ -23,9 +23,9 @@ Reader = function(...) { } ## wrapper for handling errors in the reading functions self$read = function() { - x = try(self$read_base()) + x = try(self$read_base(), silent = TRUE) if (inherits(x, "try-error")) { - stop("\nCouldn't read this file:\n", self$file) + stop(x, "\nCouldn't read this file:\n", self$file) } x } diff --git a/tests/testthat/test-no-args.R b/tests/testthat/test-no-args.R deleted file mode 100644 index 46e212a6..00000000 --- a/tests/testthat/test-no-args.R +++ /dev/null @@ -1,17 +0,0 @@ -test_that("arguments and argument dots produce the same results", { - dot_args = system.file("testing_models", "dot_args", package = "macpan2") - nodot_args = system.file("testing_models", "nodot_args", package = "macpan2") - dot_args = Compartmental(dot_args) - nodot_args = Compartmental(nodot_args) - dot_sims = dot_args$simulators$tmb(time_steps = 3 - , state = c(S = 99, I = 1, R = 0) - , flow = c(alpha = 0.1, beta = 0.1, gamma = 0.1) - , pop = empty_matrix - )$report() - nodot_sims = nodot_args$simulators$tmb(time_steps = 3 - , state = c(S = 99, I = 1, R = 0) - , flow = c(alpha = 0.1, beta = 0.1, gamma = 0.1) - , pop = empty_matrix - )$report() - expect_equal(dot_sims, nodot_sims) -}) diff --git a/tests/testthat/test-no-derivations.R b/tests/testthat/test-no-derivations.R deleted file mode 100644 index 22cd15f8..00000000 --- a/tests/testthat/test-no-derivations.R +++ /dev/null @@ -1,20 +0,0 @@ -library(macpan2) -library(dplyr) -library(ggplot2) -library(oor) -age = Compartmental(system.file("starter_models", "age", package = "macpan2")) -# s = age$simulators$tmb(time_steps = 100L -# , state = c(age_0_19 = 100, age_20_39 = 0, age_40_59 = 0, age_60_79 = 0, age_80_99 = 0, age_100_plus = 0) -# , flow = c( -# birth_rate_0_19 = 0, birth_rate_20_39 = 0.09, birth_rate_40_59 = 0.02, -# birth_rate_60_79 = 0, birth_rate_80_99 = 0, birth_rate_100_plus = 0, -# death_rate_0_19 = 0.01, death_rate_20_39 = 0.03, death_rate_40_59 = 0.05, -# death_rate_60_79 = 0.1, death_rate_80_99 = 0.2, death_rate_100_plus = 0.5, -# age_rate = 1 / 20 -# ) -# ) -# (s$report() -# |> filter(matrix == "state", row != "dead") -# |> ggplot() -# + geom_line(aes(time, value, colour = row)) -# ) diff --git a/tests/testthat/test-readers.R b/tests/testthat/test-readers.R index 5bbb78c7..e4178d1f 100644 --- a/tests/testthat/test-readers.R +++ b/tests/testthat/test-readers.R @@ -6,7 +6,7 @@ test_that("attempts to construct abstract readers give appropriate instructions" } expect_error( Reader(tmp_file(".csv"))$read(), - "Please try a specific reader like CSVReader" + regexp = "Please try a specific reader like CSVReader" ) expect_error( Reader(tmp_file(".json"))$read(), From 8e478b4125e0e4cf885ae815c72ef034af46d313 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 06:43:51 -0500 Subject: [PATCH 309/332] test c++ side recycling --- misc/dev/dev.cpp | 23 +++++++++++++++++------ src/macpan2.cpp | 23 +++++++++++++++++------ tests/testthat/test-recycle.R | 10 +++++----- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/misc/dev/dev.cpp b/misc/dev/dev.cpp index b08f6f67..61e70d15 100644 --- a/misc/dev/dev.cpp +++ b/misc/dev/dev.cpp @@ -504,7 +504,9 @@ class ArgList { } - // Method to recycle elements of all arguments so that they match a given shape + // Method to recycle elements of all arguments + // pointed at by `indices` so that they match a + // given shape given by `rows` and `cols`. ArgList recycle_to_shape(const std::vector& indices, int rows, int cols) const { ArgList result = *this; // Create a new ArgList as a copy of the current instance @@ -514,6 +516,7 @@ class ArgList { matrix mat = result.get_as_mat(index); if (mat.rows() == rows && mat.cols() == cols) { + // std::cout << "no action" << std::endl; // No further action needed for this matrix continue; } @@ -521,28 +524,34 @@ class ArgList { matrix m(rows, cols); if (mat.rows() == 1 && mat.cols() == 1) { + // std::cout << "scalar in" << std::endl; m = matrix::Constant(rows, cols, mat.coeff(0, 0)); } else if (mat.rows() == rows) { if (mat.cols() == 1) { + // std::cout << "good column vector" << std::endl; for (int i = 0; i < cols; i++) { m.col(i) = mat.col(0); } } else { + // std::cout << "bad column vector" << std::endl; error_code = 501; - break; // Exit the loop on error + //break; // Exit the loop on error } } else if (mat.cols() == cols) { if (mat.rows() == 1) { + // std::cout << "good row vector" << std::endl; for (int i = 0; i < rows; i++) { m.row(i) = mat.row(0); } } else { + // std::cout << "bad row vector" << std::endl; error_code = 501; - break; // Exit the loop on error + //break; // Exit the loop on error } } else { + // std::cout << "really bad" << std::endl; error_code = 501; - break; // Exit the loop on error + //break; // Exit the loop on error } if (error_code != 0) { @@ -2363,8 +2372,10 @@ class ExprEvaluator { return m2; // empty matrix case MP2_RECYCLE: - rows = CppAD::Integer(args[1].coeff(0,0)); - cols = CppAD::Integer(args[2].coeff(0,0)); + //rows = CppAD::Integer(args[1].coeff(0,0)); + //cols = CppAD::Integer(args[2].coeff(0,0)); + rows = args.get_as_int(1); + cols = args.get_as_int(2); v1.push_back(0); args = args.recycle_to_shape(v1, rows, cols); err_code = args.get_error_code(); diff --git a/src/macpan2.cpp b/src/macpan2.cpp index f928dbcc..00ec8f40 100644 --- a/src/macpan2.cpp +++ b/src/macpan2.cpp @@ -505,7 +505,9 @@ class ArgList { } - // Method to recycle elements of all arguments so that they match a given shape + // Method to recycle elements of all arguments + // pointed at by `indices` so that they match a + // given shape given by `rows` and `cols`. ArgList recycle_to_shape(const std::vector& indices, int rows, int cols) const { ArgList result = *this; // Create a new ArgList as a copy of the current instance @@ -515,6 +517,7 @@ class ArgList { matrix mat = result.get_as_mat(index); if (mat.rows() == rows && mat.cols() == cols) { + // std::cout << "no action" << std::endl; // No further action needed for this matrix continue; } @@ -522,28 +525,34 @@ class ArgList { matrix m(rows, cols); if (mat.rows() == 1 && mat.cols() == 1) { + // std::cout << "scalar in" << std::endl; m = matrix::Constant(rows, cols, mat.coeff(0, 0)); } else if (mat.rows() == rows) { if (mat.cols() == 1) { + // std::cout << "good column vector" << std::endl; for (int i = 0; i < cols; i++) { m.col(i) = mat.col(0); } } else { + // std::cout << "bad column vector" << std::endl; error_code = 501; - break; // Exit the loop on error + //break; // Exit the loop on error } } else if (mat.cols() == cols) { if (mat.rows() == 1) { + // std::cout << "good row vector" << std::endl; for (int i = 0; i < rows; i++) { m.row(i) = mat.row(0); } } else { + // std::cout << "bad row vector" << std::endl; error_code = 501; - break; // Exit the loop on error + //break; // Exit the loop on error } } else { + // std::cout << "really bad" << std::endl; error_code = 501; - break; // Exit the loop on error + //break; // Exit the loop on error } if (error_code != 0) { @@ -2364,8 +2373,10 @@ class ExprEvaluator { return m2; // empty matrix case MP2_RECYCLE: - rows = CppAD::Integer(args[1].coeff(0,0)); - cols = CppAD::Integer(args[2].coeff(0,0)); + //rows = CppAD::Integer(args[1].coeff(0,0)); + //cols = CppAD::Integer(args[2].coeff(0,0)); + rows = args.get_as_int(1); + cols = args.get_as_int(2); v1.push_back(0); args = args.recycle_to_shape(v1, rows, cols); err_code = args.get_error_code(); diff --git a/tests/testthat/test-recycle.R b/tests/testthat/test-recycle.R index bb387ceb..5c5ae5eb 100644 --- a/tests/testthat/test-recycle.R +++ b/tests/testthat/test-recycle.R @@ -22,17 +22,17 @@ test_that("recycling is up to spec", { ## fail to recycle a row vector to add matrix columns expect_error( - engine_eval(~ recycle(t(x), 1, 2), x = x), + engine_eval(~ recycle(t(x), 1, 4), x = x), "The following error was thrown by the TMB engine" ) - + ## recycle a row vector to add matrix rows expect_equal( engine_eval(~ recycle(t(x), 4, 3), x = x), matrix(x, 4, 3, byrow = TRUE) ) - ## fail to recycle a column vector to add matrix rows + ## fail to recycle a column vector to subtract matrix rows expect_error( engine_eval(~ recycle(x, 2, 1), x = x), "The following error was thrown by the TMB engine" @@ -46,11 +46,11 @@ test_that("recycling is up to spec", { ## fail if one of the requested dimensions is just totally off, ## either multiplicatively or otherwise - expect_error( + expect_error( ## otherwise engine_eval(~ recycle(A, 5, 3), A = A), "The following error was thrown by the TMB engine" ) - expect_error( + expect_error( ## multiplicatively engine_eval(~ recycle(A, 4, 4), A = A), "The following error was thrown by the TMB engine" ) From 0f26edfdec44b590157b26849388829e58e1963c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 06:44:30 -0500 Subject: [PATCH 310/332] test cleanup for #155 --- tests/testthat/test-labelled-partitions.R | 6 ++++-- tests/testthat/test-rbind-time-lag.R | 2 +- tests/testthat/test-rcbind.R | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/testthat/test-labelled-partitions.R b/tests/testthat/test-labelled-partitions.R index 20d195e7..d85eb1f3 100644 --- a/tests/testthat/test-labelled-partitions.R +++ b/tests/testthat/test-labelled-partitions.R @@ -1,4 +1,5 @@ test_that("model files can be read in and used", { + skip("Skipping because we are still reworking this test for new approach") f = model_starter("seir_symp_vax" , file.path(tempdir(TRUE), paste0(sample(LETTERS, 50, TRUE), collapse = "")) ) @@ -59,7 +60,7 @@ test_that("labels, name, and names conversion is correct", { expect_identical(to_names("A"), "A") expect_identical(to_names(c("A", "B")), c("A", "B")) expect_identical(to_names("A.B"), c("A", "B")) - expect_error(to_names(c("A.B", "C.D")), "vector contained strings with dots") + expect_error(to_names(c("A.B", "C.D")), "vector contained invalid strings") expect_identical(to_names(p$.partition), c("A", "B")) expect_identical(to_names(dotted_scalar), c("a", "z")) expect_identical(to_names(undotted_vector), c("a", "z")) @@ -70,7 +71,7 @@ test_that("labels, name, and names conversion is correct", { expect_identical(to_name(p), "A.B") - expect_error(to_name(c("a.z", "b.y")), "vector contained strings with dots") + expect_error(to_name(c("a.z", "b.y")), "vector contained invalid strings") expect_identical(to_name("a.z"), "a.z") expect_identical(to_name(c("a", "z")), "a.z") expect_identical(to_name(p$.partition), "A.B") @@ -82,6 +83,7 @@ test_that("labels, name, and names conversion is correct", { }) test_that("labels are appropriately generated when null partitions are involved", { + skip("Skipping because we are still reworking this test for new approach") m = Compartmental(system.file("starter_models", "sir_vax", package = "macpan2")) v = m$variables$all() s = m$variables$state() diff --git a/tests/testthat/test-rbind-time-lag.R b/tests/testthat/test-rbind-time-lag.R index 1e4c2cd8..843e6303 100644 --- a/tests/testthat/test-rbind-time-lag.R +++ b/tests/testthat/test-rbind-time-lag.R @@ -17,7 +17,7 @@ test_that("a selection of the iterations in the simulation history of a matrix t ), time_steps = Time(steps) )$simulator() - y_tmb = s$matrix(time_step = 11, matrix_name = "y") + y_tmb = s$matrix(time_step = 11, matrix_name = "y", .phases = c("before", "during", "after")) y_r = matrix(numeric(), nrow = 0, ncol = 3) for (i in 1:steps) { diff --git a/tests/testthat/test-rcbind.R b/tests/testthat/test-rcbind.R index 9ab75e2f..07098911 100644 --- a/tests/testthat/test-rcbind.R +++ b/tests/testthat/test-rcbind.R @@ -11,9 +11,9 @@ test_that("concatenation works with many different shapes of input", { good = TMBSimulator(TMBModel(mats, expr_good)) bad = TMBSimulator(TMBModel(mats, expr_bad)) - answer = good$matrix(matrix_name = "answer", time_step = 1L) - z = good$matrix(matrix_name = "z", time_step = 1L) - w = good$matrix(matrix_name = "w", time_step = 1L) + answer = good$matrix(matrix_name = "answer", time_step = 1L, .phases = c("before", "during", "after")) + z = good$matrix(matrix_name = "z", time_step = 1L, .phases = c("before", "during", "after")) + w = good$matrix(matrix_name = "w", time_step = 1L, .phases = c("before", "during", "after")) expect_identical(answer, cbind(z, t(w))) expect_identical(bad$error_code(), 27L) From 6c7b17b239f825eda7a5a03be6ff966e854a017c Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 06:47:23 -0500 Subject: [PATCH 311/332] cleanup --- R/enum_methods.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/enum_methods.R b/R/enum_methods.R index aad4cfc6..0cbd3497 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -13,7 +13,7 @@ #' @param int_vec_arg_nms Character vector naming the integer-vector-valued #' arguments. #' -#' @noRd +#' @nord MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { self = Base() self$formula = formula @@ -64,7 +64,7 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { #' @param cls_nm Character string giving the name of the class. #' @param meth_type_id Integer giving the associated ID of the method type. #' -#' @noRd +#' @nord mk_meth_cls = function(cls_nm, meth_type_id) { pf = parent.frame() force(pf) @@ -83,7 +83,7 @@ mk_meth_cls = function(cls_nm, meth_type_id) { #' This class is here so that `MethodTypes`, which is automatically generated #' from the C++ code, can inherit from it. #' -#' @noRd +#' @nord MethodTypeUtils = function() { self = Base() From 336da61a7f01e949fb9ef2ea8b7618644edf13b7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 08:56:49 -0500 Subject: [PATCH 312/332] skip some outdated tests and fix error message changes --- tests/testthat/test-standard-expr.R | 1 + tests/testthat/test-state-flow-order.R | 2 ++ tests/testthat/test-subsetting.R | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-standard-expr.R b/tests/testthat/test-standard-expr.R index 232c110a..ef3de60b 100644 --- a/tests/testthat/test-standard-expr.R +++ b/tests/testthat/test-standard-expr.R @@ -1,4 +1,5 @@ test_that("all flow types can be used", { + skip("test depends on old 'Compartmental' model approach") state = setNames(rep(2, 7), LETTERS[1:7]) flow = setNames(rep(0.1, 6), letters[1:6]) m = Compartmental(system.file("testing_models", "all_flow_types", package = "macpan2")) diff --git a/tests/testthat/test-state-flow-order.R b/tests/testthat/test-state-flow-order.R index 5c1a5fb9..35c7910c 100644 --- a/tests/testthat/test-state-flow-order.R +++ b/tests/testthat/test-state-flow-order.R @@ -1,4 +1,5 @@ test_that("the order of state variables and flow variables is defined by settings not code", { + skip("test depends on old 'Compartmental' model approach") library(macpan2) sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) N = 100 @@ -23,6 +24,7 @@ test_that("the order of state variables and flow variables is defined by setting }) test_that("the order of state variables and flow variables in settings does not impact simulations", { + skip("test depends on old 'Compartmental' model approach") library(macpan2) library(dplyr) sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) diff --git a/tests/testthat/test-subsetting.R b/tests/testthat/test-subsetting.R index e6988ff3..c7dba7d8 100644 --- a/tests/testthat/test-subsetting.R +++ b/tests/testthat/test-subsetting.R @@ -8,7 +8,7 @@ test_that("subsetting of matrices is _roughly_ similar to base R", { expect_error( engine_eval(~ A[0, ], A = A), - "the expression given by" + regexp = "The expression given by" ) }) @@ -24,11 +24,11 @@ test_that("index bounds are checked", { x = 0.1 * (1:5) expect_error( engine_eval(~x[-1], x = x), - "The following error was thrown by the TMB engine" + regexp = "Illegal index to square bracket" ) expect_error( engine_eval(~x[-5], x = x), - "The following error was thrown by the TMB engine" + regexp = "Illegal index to square bracket" ) expect_equal( engine_eval(~x[0], x = x), From c362a1c4b616658f08e7ff28785b170d1cf01037 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 08:59:26 -0500 Subject: [PATCH 313/332] behaviour of group_sums has changed --- tests/testthat/test-sums.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-sums.R b/tests/testthat/test-sums.R index f72d292f..3db1dd0d 100644 --- a/tests/testthat/test-sums.R +++ b/tests/testthat/test-sums.R @@ -4,7 +4,7 @@ test_that("summation works correctly", { expect_equal(engine_eval(~row_sums(A), A = A), cbind(c(15, 18, 21, 24))) expect_equal(engine_eval(~col_sums(A), A = A), rbind(c(10, 26, 42))) expect_equal( - engine_eval(~ group_sums(x, f, n), x = 1:10, f = rep(0:3, 1:4), n = 4), + engine_eval(~ group_sums(x, f, rep(0, n)), x = 1:10, f = rep(0:3, 1:4), n = 4), matrix(c(1, 5, 15, 34)) ) }) From 553b659b39e69450cc43aa9dd0ce24616fb68fe7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 09:02:10 -0500 Subject: [PATCH 314/332] update tests with better error messages, for #155 --- tests/testthat/test-tmb-simulator.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-tmb-simulator.R b/tests/testthat/test-tmb-simulator.R index 6eb58be0..e78040fc 100644 --- a/tests/testthat/test-tmb-simulator.R +++ b/tests/testthat/test-tmb-simulator.R @@ -1,10 +1,10 @@ test_that("informative messages are given when non-existant functions or variables are used", { expect_error( engine_eval(~ f(1)), - "that were not found in the list of available functions" + regexp = "that were not found in the list of available functions" ) expect_error( engine_eval(~ x), - "that were not found in the list of available symbols" + regexp = "but no variables were declared in the model" ) }) From d8596d3cdf63a14267447b8aff98c4cb47613745 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 09:05:14 -0500 Subject: [PATCH 315/332] skip/rm outdated tests ... for #155 --- tests/testthat/test-user-expr.R | 14 -------------- tests/testthat/test-var-order.R | 1 + tests/testthat/test-variable-products.R | 1 + 3 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 tests/testthat/test-user-expr.R diff --git a/tests/testthat/test-user-expr.R b/tests/testthat/test-user-expr.R deleted file mode 100644 index 65be25cd..00000000 --- a/tests/testthat/test-user-expr.R +++ /dev/null @@ -1,14 +0,0 @@ -library(macpan2) -library(testthat) -f = system.file("testing_models", "all_derivations_fields", package = "macpan2") -m = Compartmental(f) - -s = m$simulators$tmb(time_steps = 10 - , state = macpan2:::constant_named_vector(1, m$labels$state()) - , flow = macpan2:::constant_named_vector(0.5, m$labels$flow()) - , a.n = empty_matrix - , b.n = empty_matrix - , c.n = empty_matrix - , d.n = empty_matrix -) -s$report() diff --git a/tests/testthat/test-var-order.R b/tests/testthat/test-var-order.R index 43045583..15e78e4e 100644 --- a/tests/testthat/test-var-order.R +++ b/tests/testthat/test-var-order.R @@ -1,4 +1,5 @@ test_that("arguments and argument dots produce the same results", { + skip("test uses Compartmental") dot_args = system.file("testing_models", "dot_args", package = "macpan2") rand_order_args = system.file("testing_models", "vars_rand_order", package = "macpan2") dot_args = Compartmental(dot_args) diff --git a/tests/testthat/test-variable-products.R b/tests/testthat/test-variable-products.R index f1305962..572a384b 100644 --- a/tests/testthat/test-variable-products.R +++ b/tests/testthat/test-variable-products.R @@ -1,4 +1,5 @@ test_that("state and flow variables products are correct", { + skip("test uses Compartmental") sir = Compartmental(system.file("starter_models", "sir", package = "macpan2")) vax = Compartmental(system.file("starter_models", "vax", package = "macpan2")) From 64ac5f9609f947464b97481146330d3863dde4eb Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 13:57:11 -0500 Subject: [PATCH 316/332] file reorg --- NAMESPACE | 12 - R/calibration.R | 16 - R/chain.R | 0 R/defaults.R | 74 -- R/derivation_expander.R | 0 R/dsl_expr.R | 16 - R/dynamic_model.R | 15 - R/engine_defs.R | 11 - R/model_def_run.R | 139 --- R/mp.R | 898 +----------------- R/mp_bulk_indices.R | 145 +++ R/mp_cartesian.R | 164 ++++ R/mp_decompose.R | 39 + R/mp_dynamic_model.R | 117 +++ R/mp_group.R | 120 +++ R/{index.R => mp_index.R} | 53 ++ R/mp_join.R | 170 ++++ R/mp_labels.R | 10 + R/mp_library.R | 19 + R/mp_subset.R | 60 ++ R/mp_union.R | 60 ++ R/tmb_model.R | 1 + R/tmb_model_editors.R | 15 + R/trajectories.R | 6 - R/vec_utils.R | 27 + man/Clause.Rd | 11 - man/DerivationExtractor.Rd | 25 - man/Derivations2ExprList.Rd | 35 - man/MethodPrototype.Rd | 23 + man/MethodTypeUtils.Rd | 12 + man/Scalar2Vector.Rd | 21 - man/SimulatorConstructor.Rd | 20 - man/StandardExpr.Rd | 14 - man/StandardExprHazard.Rd | 14 - man/UserExpr.Rd | 14 - man/dot-known_dist.Rd | 20 - man/mk_meth_cls.Rd | 17 + man/mp_aggregate.Rd | 2 +- man/mp_cartesian.Rd | 3 +- man/mp_dynamic_model.Rd | 2 +- man/mp_index.Rd | 3 +- man/mp_join.Rd | 2 +- man/mp_linear.Rd | 2 +- man/mp_lookup.Rd | 2 +- man/mp_rename.Rd | 23 - man/mp_square.Rd | 2 +- man/mp_subset.Rd | 3 +- man/mp_symmetric.Rd | 2 +- man/mp_triangle.Rd | 2 +- man/mp_union.Rd | 3 +- .../old-r-source}/alt_model_constructors.R | 0 {R => misc/old-r-source}/derivations.R | 0 {R => misc/old-r-source}/files_to_models.R | 0 53 files changed, 1065 insertions(+), 1399 deletions(-) delete mode 100644 R/chain.R delete mode 100644 R/defaults.R delete mode 100644 R/derivation_expander.R delete mode 100644 R/dsl_expr.R delete mode 100644 R/dynamic_model.R delete mode 100644 R/engine_defs.R create mode 100644 R/mp_bulk_indices.R create mode 100644 R/mp_cartesian.R create mode 100644 R/mp_decompose.R create mode 100644 R/mp_dynamic_model.R create mode 100644 R/mp_group.R rename R/{index.R => mp_index.R} (90%) create mode 100644 R/mp_join.R create mode 100644 R/mp_labels.R create mode 100644 R/mp_library.R create mode 100644 R/mp_subset.R create mode 100644 R/mp_union.R delete mode 100644 R/trajectories.R create mode 100644 R/vec_utils.R delete mode 100644 man/Clause.Rd delete mode 100644 man/DerivationExtractor.Rd delete mode 100644 man/Derivations2ExprList.Rd create mode 100644 man/MethodPrototype.Rd create mode 100644 man/MethodTypeUtils.Rd delete mode 100644 man/Scalar2Vector.Rd delete mode 100644 man/SimulatorConstructor.Rd delete mode 100644 man/StandardExpr.Rd delete mode 100644 man/StandardExprHazard.Rd delete mode 100644 man/UserExpr.Rd delete mode 100644 man/dot-known_dist.Rd create mode 100644 man/mk_meth_cls.Rd delete mode 100644 man/mp_rename.Rd rename {R => misc/old-r-source}/alt_model_constructors.R (100%) rename {R => misc/old-r-source}/derivations.R (100%) rename {R => misc/old-r-source}/files_to_models.R (100%) diff --git a/NAMESPACE b/NAMESPACE index 75715d3a..48ce53e5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -112,10 +112,6 @@ S3method(to_positions,numeric) export(BinaryOperator) export(CSVReader) export(Collection) -export(Compartmental2) -export(DerivationExtractor) -export(Derivations) -export(Derivations2ExprList) export(DynamicModel) export(EngineMethods) export(Euler) @@ -150,11 +146,7 @@ export(Products) export(Quantities) export(RReader) export(Reader) -export(Scalar2Vector) -export(SimulatorConstructor) export(Simulators) -export(StandardExpr) -export(StandardExprHazard) export(StringDataFromDotted) export(StringDataFromFrame) export(StructuredVector) @@ -164,7 +156,6 @@ export(TMBSimulator) export(TXTReader) export(Time) export(Transform) -export(UserExpr) export(VectorList) export(all_consistent) export(all_equal) @@ -196,7 +187,6 @@ export(mp_combine_exprs) export(mp_decompose) export(mp_default) export(mp_dynamic_model) -export(mp_expr_binop) export(mp_expr_group_sum) export(mp_expr_list) export(mp_extract) @@ -206,7 +196,6 @@ export(mp_final) export(mp_group) export(mp_index) export(mp_indexed_exprs) -export(mp_indicator) export(mp_indices) export(mp_initial) export(mp_join) @@ -216,7 +205,6 @@ export(mp_library) export(mp_linear) export(mp_lookup) export(mp_reference) -export(mp_rename) export(mp_report) export(mp_set_numbers) export(mp_setdiff) diff --git a/R/calibration.R b/R/calibration.R index 000d8f15..60d47b9c 100644 --- a/R/calibration.R +++ b/R/calibration.R @@ -90,19 +90,3 @@ TMBOptimizationHistory = function(simulator) { self$save = function(opt_obj) self$.history = append(self$.history, list(opt_obj)) return_object(self, "TMBOptimizationHistory") } - - -#' Pick out obs/location pairs from terms involving probability distributions -#' find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) -#' find_obs_pairs(stuff ~ other_stuff + more_stuff) -.known_dist <- c("dnorm", "dpois", "dgamma", "dlnorm", "dnbinom") -find_obs_pairs <- function(form, specials = .known_dist, top = TRUE) { - if (is.symbol(form) || length(form) == 1) return(NULL) - if (deparse(form[[1]]) %in% specials) { - return(list(deparse(form[[2]]), deparse(form[[3]]))) - } - res <- lapply(form[-1], find_obs_pairs, top = FALSE) - ## drop NULL elements - res <- res[!vapply(res, is.null, logical(1))] - if (!top) res else res[[1]] -} diff --git a/R/chain.R b/R/chain.R deleted file mode 100644 index e69de29b..00000000 diff --git a/R/defaults.R b/R/defaults.R deleted file mode 100644 index fc809a03..00000000 --- a/R/defaults.R +++ /dev/null @@ -1,74 +0,0 @@ -DefaultFiles = function(defaults_directory) { - self = Files(defaults_directory - , reader_spec("defaults.csv", CSVReader) - , reader_spec("flows.csv", CSVReader) - , reader_spec("derivations.json", JSONReader) - , reader_spec("dimensions.csv", CSVReader) - ) - - self$defaults = function() self$get("defaults") - self$flows = function() self$get("flows") - self$derivations = function() self$get("derivations") - self$dimensions = function() self$get("dimensions") - - return_object(self, "DefaultFiles") -} - -Defaults = function(default_files) { - self = Base() - self$def = default_files - self$numeric = NumericPartition( - self$def$defaults()[!names(self$def$defaults()) %in% c("Value", "Notes")], - as.numeric(self$def$defaults()[["Value"]]) - ) - - self$.special = function() { - special_columns = c("Notes") - names(self$def$defaults()) %in% special_columns - } - self$.numeric = function() { - names(self$def$defaults()) %in% "Value" - } - self$.matrix = function() { - names(self$def$defaults()) %in% "Matrix" - } - self$.part_cols = function() { - !(self$.special() | self$.numeric() | self$.matrix()) - } - self$.neither_special_nor_numeric = function() { - !(self$.special() | self$.numeric()) - } - self$initialized_variables = function() { - Partition(self$def$defaults()[self$.part_cols()]) - } - self$matrix_names = function() unique(self$def$defaults()$Matrix) - self$initialized_matrix = function(name) { - i = self$def$dimensions()$Matrix == name - if (all(!i)) { - i = self$def$defaults()$Matrix == name - if (sum(i) == 1L) { - m = matrix(as.numeric(self$def$defaults()$Value[i]), 1, 1) - dimnames(m) = list("", "") - return(m) - } - } - row_part = self$def$dimensions()[i, "Row", drop = TRUE] - col_part = self$def$dimensions()[i, "Col", drop = TRUE] - self$numeric$matrix(name, row_part, col_part) - # macpan2:::NumericPartition( - # self$def$defaults()[i, self$.neither_special_nor_numeric(), drop = FALSE], - # as.numeric(self$def$defaults()[i, self$.numeric(), drop = TRUE]) - # ) - } - #self$initialized_matrix = function(name) { - # x = self$initialized_matrix_long(name) - #} - self$.make_matrix = function(name, row_part, col_part) { - i = self$def$defaults()$Matrix == name - vals = self$def$defaults()[i, "Value"] - row_part = StringDottedScalar(row_part) - col_part = StringDottedScalar(col_part) - self$initialized_variables()$select(row_part$undot())$filter() - } - return_object(self, "Defaults") -} diff --git a/R/derivation_expander.R b/R/derivation_expander.R deleted file mode 100644 index e69de29b..00000000 diff --git a/R/dsl_expr.R b/R/dsl_expr.R deleted file mode 100644 index 12afc0d6..00000000 --- a/R/dsl_expr.R +++ /dev/null @@ -1,16 +0,0 @@ -#' Compartmental Modelling Language -#' -#' -Clause = function() { - self = Base() - self$render = function(engine) {} - return_object(self, "Calculation") -} - -Transition = function(from, to, rate) { - self = Calculation() - self$from = from - self$to = to - self$rate = rate - return_object(self, "Transition") -} diff --git a/R/dynamic_model.R b/R/dynamic_model.R deleted file mode 100644 index de91e719..00000000 --- a/R/dynamic_model.R +++ /dev/null @@ -1,15 +0,0 @@ - -# DynamicTMBModel = function( -# before, -# during, -# after, -# initial_matrices, -# time_steps -# -# ) { -# self = Base() -# -# -# -# return_object(self, "DynamicTMBModel") -# } diff --git a/R/engine_defs.R b/R/engine_defs.R deleted file mode 100644 index ffed296c..00000000 --- a/R/engine_defs.R +++ /dev/null @@ -1,11 +0,0 @@ -Engine = function() { - self = Base() - self$null_simulator = function() stop("abstract method") - return_object(self, "Engine") -} - -TMB = function() { - self = Engine() - self$null_simulator = function() TMBSimulator(TMBModel()) - return_object(self, "TMB") -} diff --git a/R/model_def_run.R b/R/model_def_run.R index a53ca260..3f7b2759 100644 --- a/R/model_def_run.R +++ b/R/model_def_run.R @@ -1,24 +1,3 @@ -#' @export -mp_library = function(...) { - system.file("model_library", ..., package = "macpan2") |> Compartmental2() -} - -#' @export -mp_tmb_library = function(..., package = NULL) { - if (is.null(package)) { - model_directory = file.path(...) - } else { - model_directory = system.file(..., package = package) - } - def_env = new.env(parent = parent.frame()) - sys.source(file.path(model_directory, "tmb.R") - , envir = def_env - , chdir = TRUE - ) - def_env$spec -} - -#' @export Compartmental2 = function(model_directory) { self = Base() self$model_directory = model_directory @@ -59,121 +38,3 @@ Compartmental2 = function(model_directory) { return_object(self, "ModelDefRun") } - -VariablesScripts = function(model) { - self = Base() - self$model = model - self$state = function() self$model$index_data_type("state") - self$flow_rates = function() self$model$index_data_type("flow_rates") - self$influence_rates = function() self$model$index_data_type("influence_rates") - self$aggregated_states = function() self$model$index_data_type("aggregated_states") - self$normalized_state = function() self$model$index_data_type("normalized_state") - return_object(self, "VariablesScripts") -} - -LabelsScripts = function(model) { - self = Base() - self$model = model - self$variables = model$variables - - self$dynamic_model = model$dynamic_model - vs = self$dynamic_model$init_vecs - for (nm in names(vs)) self[[nm]] = LabelsGetter(self$dynamic_model, nm) - self$component_list = function() { - l = list() - vs = self$dynamic_model$init_vecs - for (nm in names(vs)) l[[nm]] = self[[nm]]() - l - } - - # self$state = function() self$variables$state()$labels() - # self$flow_rates = function() self$variables$flow_rates()$labels() - # self$influence_rates = function() self$variables$influence_rates()$labels() - # self$aggregated_states = function() self$variables$aggregated_states()$labels() - # self$normalized_state = function() self$variables$normalized_state()$labels() - # self$component_list = function() { - # - # } - return_object(self, "LabelsScripts") -} - -LabelsGetter = function(dynamic_model, vector_name) { - self = Base() - self$dynamic_model = dynamic_model - self$vector_name = vector_name - self$get = function() self$dynamic_model$init_vecs[[self$vector_name]] |> names() - self$get -} - -LabelsDynamic = function(dynamic_model) { - self = Base() - self$dynamic_model = dynamic_model - vs = self$dynamic_model$init_vecs - for (nm in names(vs)) self[[nm]] = LabelsGetter(self$dynamic_model, nm) - self$component_list = function() { - l = list() - vs = self$dynamic_model$init_vecs - for (nm in names(vs)) l[[nm]] = self[[nm]]() - l - } - return_object(self, "LabelsDynamic") -} - - -#' @export -DynamicModel = function( - expr_list = ExprList() - , ledgers = list() - , init_vecs = list() - , unstruc_mats = list() - ) { - self = Base() - self$expr_list = expr_list - self$ledgers = ledgers - self$init_vecs = lapply(init_vecs, mp_vector) - self$unstruc_mats = unstruc_mats - self$int_vec_names = function() { - lapply(self$ledgers, getElement, "table_names") |> unlist(use.names = TRUE) |> unique() - } - self$derived_matrix_names = function() { - setdiff(self$expr_list$all_formula_vars() - , c( - names(self$init_vecs) - , self$int_vec_names() - , names(self$unstruc_mats) - ) - ) - } - self$labels = LabelsDynamic(self) - return_object(self, "DynamicModel") -} - - -#' Dynamic Model -#' -#' -#' -#' @export -mp_dynamic_model = DynamicModel - -#' @export -mp_test_tmb = function(..., ledgers, vectors, unstruc_mats) { - m = mp_dynamic_model( - expr_list = mp_expr_list(before = list(...)) - , ledgers = ledgers - , init_vecs = vectors - , unstruc_mats = unstruc_mats - ) - mp_tmb_simulator(m - , time_steps = 0L - , vectors = method_apply(vectors, "numbers") - , unstruc_mats = unstruc_mats - , mats_to_return = m$derived_matrix_names() - , mats_to_save = m$derived_matrix_names() - ) |> mp_report(phases = "before") -} - -#' @export -print.DynamicModel = function(x, ...) { - print(x$expr_list) -} diff --git a/R/mp.R b/R/mp.R index 300e6243..a7b62e49 100644 --- a/R/mp.R +++ b/R/mp.R @@ -12,663 +12,7 @@ mp = function(mp_func) { } -#' Cartesian Product of Index Tables -#' -#' Produce a new index table by taking all possible pairwise combinations -#' of the input tables. This is useful for producing product models -#' that expand model components through stratification. -#' -#' @param ... Index tables (see \code{\link{mp_index}}). -#' -#' @examples -#' mp_cartesian( -#' mp_index(Epi = c("S", "I")), -#' mp_index(Age = c("young", "old")) -#' ) -#' -#' si = mp_index(Epi = c("S", "I")) -#' age = mp_index(Age = c("young", "old")) -#' loc = mp_index(City = c("hamilton", "toronto")) -#' vax = mp_index(Vax = c("unvax", "vax")) -#' (si -#' |> mp_cartesian(age) -#' |> mp_cartesian(loc) -#' |> mp_cartesian(vax) -#' ) -#' -#' flow_rates = mp_index(Epi = c("infection", "recovery")) -#' mp_union( -#' mp_cartesian( -#' mp_subset(flow_rates, Epi = "infection"), -#' age -#' ), -#' mp_subset(flow_rates, Epi = "recovery") -#' ) -#' -#' @family indexes -#' @family products -#' @export -mp_cartesian = function(...) Reduce(mp_cartesian_binary, list(...)) - -mp_cartesian_binary = function(x, y) { - shared_columns = intersect(names(x), names(y)) - if (length(shared_columns) != 0) { - msg_break( - msg_colon( - msg( - "Cannot take the Cartesian product of two indexes that", - "share columns names. But the input indexes share the", - "following columns" - ), - msg_indent(shared_columns) - ), - msg("Perhaps mp_join is more suitable?") - ) |> stop() - } - labelling_column_names = union(x$labelling_column_names, y$labelling_column_names) - f = join_partitions(x$partition$frame(), y$partition$frame()) - Index(f, labelling_column_names = labelling_column_names) -} - -#' Self Cartesian Product -#' -#' @param suffixes Length-2 character vector giving suffixes that -#' disambiguate the column names in the output. -#' @inheritParams cartesian -#' @family products -#' @export -mp_square = function(x, suffixes = c("A", "B")) { - l1 = sprintf("%s%s", x$labelling_column_names, suffixes[1L]) - l2 = sprintf("%s%s", x$labelling_column_names, suffixes[2L]) - n1 = sprintf("%s%s", names(x), suffixes[1L]) - n2 = sprintf("%s%s", names(x), suffixes[2L]) - x = (x$partition$frame() - |> setNames(n1) - |> Index(labelling_column_names = l1) - ) - y = (x$partition$frame() - |> setNames(n2) - |> Index(labelling_column_names = l2) - ) - mp_cartesian(x, y) -} - -#' Self Cartesian Product Excluding One Off-Diagonal Side -#' -#' @inheritParams cartesian -#' @param y_labelling_column_names TODO -#' @param exclude_diag Should 'diagonal' commponents be excluded from the output. -#' @param lower_tri Should the lower triangular components be include from the -#' output. If \code{FALSE} the result is upper triangular. -#' -#' @family products -#' @export -mp_triangle = function(x, y_labelling_column_names, exclude_diag = TRUE, lower_tri = FALSE) { - f = x$partition$frame() - g = setNames(f, y_labelling_column_names) - n = nrow(f) - if (exclude_diag) { - k = 2:n - i = sequence(k - 1) - j = rep(k, k - 1) - } else if (!exclude_diag) { - k = seq_len(n) - i = sequence(k) - j = rep(k, k) - } - if (lower_tri) { - ii = i - i = j - j = ii - } - f = cbind( - f[i, , drop = FALSE], - g[j, , drop = FALSE] - ) - Index(f, labelling_column_names = names(f)) -} - -#' Symmetric Self Cartesian Product -#' -#' @inheritParams mp_triangle -#' @family products -#' @export -mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { - f = x$partition$frame() - g = setNames(f, y_labelling_column_names) - n = nrow(f) - k = seq_len(n) - - if (exclude_diag) { - i = rep( k , times = n - 1L) - j = rep(rev(k), each = n - 1L) - } else { - i = rep(k, times = n) - j = rep(k, each = n) - } - - f = cbind( - f[i, , drop = FALSE], - g[j, , drop = FALSE] - ) - Index(f, labelling_column_names = names(f)) -} - -#' Linear Chain Product -#' -#' TODO: what does this mean? -#' -#' @inheritParams mp_square -#' @family products -#' @export -mp_linear = function(x, y_labelling_column_names) { - f = x$partition$frame() - g = setNames(f, y_labelling_column_names) - n = nrow(f) - - k = c(1L, rep(2L, n - 2L), 1L) - i = rep(seq_len(n), k) - j = sequence(k, c(2, seq_len(n - 2L), n - 1L), by = 2) - - f = cbind( - f[i, , drop = FALSE], - g[j, , drop = FALSE] - ) - Index(f, labelling_column_names = names(f)) -} - -#' Subset of Indexes -#' -#' Take a subset of the rows of an index table (see \code{\link{mp_index}}) -#' to produce another index table. The `mp_subset` function gives rows that -#' match a certain criterion and `mp_setdiff` gives rows that do not match. -#' -#' @param x Model index. -#' @param ... Name-value pairs. The names are columns (or sets of columns -#' using dot-concatenation) in \code{x} and the values are character vectors -#' that refer to labels with respect to those columns. These values -#' determine the resulting subset. -#' -#' @family indexes -#' @export -mp_subset = function(x, ...) { - partition = mp_choose(x, "pick", ...)$partition - Index(partition - , labelling_column_names = x$labelling_column_names - , reference_index = x - ) -} - -#' @rdname mp_subset -#' @export -mp_setdiff = function(x, ...) { - partition = mp_choose_out(x, "pick", ...)$partition - Index(partition - , labelling_column_names = x$labelling_column_names - , reference_index = x - ) -} - -#' Aggregate an Index -#' -#' Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows -#' identifying instances of an aggregation. -#' -#' @family ledgers -#' @export -mp_aggregate = function(index, by = "Group", ledger_column = "group") { - index_columns = to_names(by) - if (length(index_columns) == 1L & !index_columns %in% names(index)) { - partition = index$partition$constant(by, "a") - } else { - partition = index$partition - } - index = Index(partition) |> mp_group(by) - - Ledger( - partition$frame(), - initial_column_map(names(partition), ledger_column), - initial_reference_index_list(index, ledger_column), - setNames(list(index_columns), ledger_column) - ) -} - -#' Union of Indexes -#' -#' @param ... Indexes. -#' -#' @family indexes -#' @export -mp_union = function(...) UseMethod("mp_union") - -#' @export -mp_union.Index = function(...) { - l = list(...) - partitions = lapply(l, getElement, "partition") - labelling_column_names = (l - |> lapply(getElement, "labelling_column_names") - |> unlist(recursive = FALSE, use.names = FALSE) - |> unique() - ) - Index(do.call(union_vars, partitions)$frame(), labelling_column_names = labelling_column_names) -} - -## not used anymore? -#' @export -mp_union.Ledger = function(...) { - l = list(...) - column_map = lapply(l, getElement, "column_map") |> unique() - if (length(column_map) != 1L) { - msg_colon( - msg( - "Union of inconsistent Ledger objects.", - "All Ledger objects must have the same", - "column_map, but the following distinct", - "maps were found:" - ), - msg_indent_break(lapply(column_map, unlist)) - ) |> stop() - } - ## TODO: should really be checking for reference_index_list - labelling_column_names_list = lapply(l, getElement, "labelling_column_names_list") |> unique() - if (length(labelling_column_names_list) != 1L) { - msg_colon( - msg( - "Union of inconsistent Ledger objects.", - "All Ledger objects must have the same", - "labelling_column_names_list, but the following", - "distinct maps were found:" - ), - msg_indent_break(lapply(labelling_column_names_list, unlist)) - ) |> stop() - } - frame = mp_rbind(...) - LedgerData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) -} - -## not used anymore? -mp_rbind = function(...) { - (list(...) - |> lapply(as.data.frame) - |> do.call(what = bind_rows) ## bind_rows is in frame_utils.R - ) -} - -#' @export -mp_choose = function(x, subset_name, ...) { - l = list(...) - if (length(l) != 0L) valid$named_list$check(l) - p = x$partition - for (cc in names(l)) { - vals = l[[cc]] - is_blank = nchar(vals) == 0L - vals = setdiff(vals, "") - if (any(is_blank)) { - p = union_vars(p$blank_on(cc), p$filter(vals, .wrt = cc)) - } else { - p = p$filter(vals, .wrt = cc) - } - } - init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) -} - -#' @export -mp_choose_out = function(x, subset_name, ...) { - l = list(...) - p = x$partition - for (cc in names(l)) { - vals = l[[cc]] - p = p$filter_out(vals, .wrt = cc) - } - init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) -} - -#' Join Indexes -#' -#' Join two or more index tables (see \code{\link{mp_index}}) to produce a -#' ledger (see \code{\link{LedgerDefinition}}). -#' -#' When two index tables are passed to `...`, `mp_join` behaves very much like -#' an ordinary [inner join](https://en.wikipedia.org/wiki/Join_(SQL)). -#' When more than two tables are passed to `...`, `mp_join` iteratively joins -#' pairs of tables to produce a final ledger. For example, if index tables `A` -#' `B`, and `C` are passed to `mp_join`, an inner join of `A` and `B` is -#' performed and the result is joined with `C`. In each of these successive -#' internal joins. The properties of inner -#' joins ensures that the order of tables does not affect the set of rows in -#' the final table (SW states without proof!). -#' -#' When two index tables are passed to `...`, the `by` argument is just a -#' character vector of column names on which to join (as in standard R functions -#' for joining data frames), or the dot-concatenation of these column names. -#' For example, -#' ```{r, echo = TRUE, eval = TRUE} -#' state = mp_index( -#' Epi = c("S", "I", "S", "I"), -#' Age = c("young", "young", "old", "old") -#' ) -#' mp_join( -#' from = mp_subset(state, Epi = "S"), -#' to = mp_subset(state, Epi = "I"), -#' by = "Age" -#' ) -#' ``` -#' If there are more than two tables then the `by` argument must be a named -#' list of character vectors, each describing how to join the columns of -#' a pair of tables in `...`. The names of this list are dot-concatenations -#' of the names of pairs of tables in `...`. For example, -#' ```{r, echo = TRUE, eval = TRUE} -#' rates = mp_index( -#' Epi = c("lambda", "lambda"), -#' Age = c("young", "old") -#' ) -#' mp_join( -#' from = mp_subset(state, Epi = "S"), -#' to = mp_subset(state, Epi = "I"), -#' rate = mp_subset(rates, Epi = "lambda"), -#' by = list( -#' from.to = "Age", -#' from.rate = "Age" -#' ) -#' ) -#' ``` -#' If the `by` columns have different names in two tables, then you can -#' specify these using formula notation where the left-hand-side -#' is a dot-concatenation of columns in the first table and the -#' right-hand-side is a dot-concatenation of the columns in the second -#' table. For example, -#' ```{r} -#' contact = mp_index( -#' AgeSusceptible = c("young", "young", "old", "old"), -#' AgeInfectious = c("young", "old", "young", "old") -#' ) -#' mp_join( -#' sus = mp_subset(state, Epi = "S"), -#' inf = mp_subset(state, Epi = "I"), -#' con = contact, -#' by = list( -#' sus.con = "Age" ~ "AgeSusceptible", -#' inf.con = "Age" ~ "AgeInfectious" -#' ) -#' ) -#' ``` -#' -#' @param ... Named arguments giving indexes created by -#' \code{\link{mp_index}} or another function that manipulates indexes. -#' Each argument will become a position vector used to subset -#' or expand numeric vectors in archetype formulas. -#' @param by What columns to use to join the indexes. See below on -#' how to specify this argument. -#' -#' @family ledgers -#' @export -mp_join = function(..., by = empty_named_list()) { - table_list = valid$named_list$assert(list(...)) - table_nms = names(table_list) - - if (length(table_nms) < 2L) stop("cannot join fewer than two index objects.") - if (is.character(by)) { - if (length(table_nms) != 2L) { - stop("joining more than one index requires a list-valued by argument.") - } - by = setNames(list(by), to_name(table_nms)) - } - - by_list = valid$named_list$assert(by) - if (length(by_list) > 1L) { - table_order = (by_list - |> names() - |> lapply(to_names) - |> unlist() - |> unique() - |> union(names(table_list)) - ) - } else { - table_order = names(table_list) - } - ordered_table_list = table_list[table_order] - - by_nms = names(by_list) |> strsplit(".", fixed = TRUE) - good_by_nms = (by_nms - |> lapply(`%in%`, table_nms) - |> vapply(all, logical(1L)) - ) - if (any(!good_by_nms)) { - msg_break( - msg_colon( - "Indices to join were given the following names for the join output", - table_nms - ), - msg_colon( - msg( - "But the names of arguments that specify what columns will be", - "The following arguments were supplied to", - "determine what columns to join on" - ), - msg_indent_break(by_nms[!good_by_nms]) - ), - ) |> stop() - } - if (!is.null(table_nms)) { - for (nm in names(ordered_table_list)) { - if (nm != "" & inherits(ordered_table_list[[nm]], "Index")) { - ordered_table_list[[nm]] = mp_choose(ordered_table_list[[nm]], nm) - } - } - } - orig_tab_nms = (ordered_table_list - |> method_apply("table_names") - |> unname() - |> unlist(recursive = FALSE) - ) - table_nm_diffs = (by_nms - |> lapply(factor, levels = orig_tab_nms) - |> lapply(as.integer) - |> vapply(diff, integer(1L)) - ) - - bad_by_args = table_nm_diffs < 1L ## table names in the wrong order - if (any(bad_by_args)) { - fixed_by_nms = (by_nms[bad_by_args] - |> lapply(rev) - |> lapply(paste0, collapse = ".") - ) - fixed_by_args = (by_list[bad_by_args] - |> lapply(swap_sides) - ) - by_list[bad_by_args] = fixed_by_args - names(by_list)[bad_by_args] = fixed_by_nms - } - - z = ordered_table_list[[1L]] - for (i in 2:length(ordered_table_list)) { - args = c( - list( - x = z, - y = ordered_table_list[[i]] - ), - by_list - ) - z = do.call(merge_generic_by_util, args) - } - z$reorder(names(table_list)) -} - -mp_aggregate_old = function(formula - , group_by - , index - , ... -) { - prototypes = list( - group_sums = macpan2:::MethodPrototype(y ~ group_sums(x), c("x", "y"), character()) - ) - consistent_agg_meths = (prototypes - |> method_apply("consistent", formula) - |> vapply(c, logical(1L), USE.NAMES = TRUE) - ) - if (!any(consistent_agg_meths)) { - f = method_apply(prototypes, "as_character") - msg_break( - msg_colon( - "The following aggregation formula", - msg_indent(formula_as_character(formula)) - ), - msg_colon( - msg( - "was not consistent with any of the", - "available aggregation prototypes" - ), - msg_indent_break(f) - ) - ) |> stop() - } - agg_meth = (consistent_agg_meths - |> which() - |> names() - |> getElement(1L) ## first match takes precedence - ) - agg = prototypes[[agg_meth]] - - strata = mp_select(x, stratify_by) - subset = mp_subset(x, ...) - grouping_indices = mp_indices( - mp_labels(subset, stratify_by), - mp_labels(strata) - ) - subset_indices = mp_indices( - mp_labels(subset), - mp_labels(x) - ) - -} - - -#' @export -mp_decompose = function(formula, index, decomp_name, ...) { - input_formula = formula - table_args = list(...) - output_name = lhs_char(formula) - by_args = table_args - names(by_args) = sprintf("%s.%s", output_name, names(table_args)) - for (nm in names(table_args)) { - table_args[[nm]] = mp_select(index, table_args[[nm]]) - } - linked_indices = do.call(mp_join, - c(setNames(list(index), output_name), table_args, by_args) - ) - int_vecs = setNames( - vector(mode = "list", length(table_args)), - sprintf("%s_%s", names(table_args), decomp_name) - ) - iv_nms = names(int_vecs) - tab_nms = names(table_args) - expand_iv_nms = sprintf("%s[%s]", tab_nms, iv_nms) - replacement_formulas = mapply(two_sided - , tab_nms, expand_iv_nms - , SIMPLIFY = FALSE, USE.NAMES = FALSE - ) - - for (i in seq_along(table_args)) { - int_vecs[[iv_nms[i]]] = mp_indices( - linked_indices$labels_for[[tab_nms[i]]](), - linked_indices$partition_for[[tab_nms[i]]]()$labels() - ) - } - formula = update_formula(formula, replacement_formulas) - nlist( - formula, - input_formula, - linked_indices, - int_vecs - ) -} - -#' @export -mp_reference = function(x, dimension_name) { - UseMethod("mp_reference") -} - -#' @export -mp_reference.Ledger = function(x, dimension_name) { - ii = x$reference_index_list[[dimension_name]] - ii$reset_reference_index() - ii -} - -#' @export -mp_reference.Index = function(x, dimension_name) { - x$reference_index() -} - -#' @export -mp_extract = function(x, dimension_name) { - UseMethod("mp_extract") -} - -#' @export -mp_extract.Ledger = function(x, dimension_name) { - ii = x$index_for[[dimension_name]]() - ii$reset_reference_index() - ii -} - -#' @export -mp_extract.DynamicModel = function(x, dimension_name) { - y = try(x$init_vecs[[dimension_name]]$index, silent = TRUE) - if (!inherits(y, "Index")) { - msg( - "Failed to find an index for", - dimension_name, "in this object" - ) |> stop() - } - y -} - -#' @export -mp_extract.ModelDefRun = function(x, dimension_name) { - mp_extract(x$dynamic_model, dimension_name) -} - -#' Rename Index Columns -#' -#' @param ... Name-value pairs. The name gives the new name and the value -#' is a character vector giving the old name. -#' -#' @family indexes -#' @export -mp_rename = function(x, ...) { - l = list(...) - new_nms = names(l) - old_nms = unlist(l, recursive = FALSE, use.names = FALSE) - f = x$partition$frame() - labs = x$labelling_column_names - i = match(old_nms, names(f)) - if (any(is.na(i))) { - msg_break( - msg_colon( - "Attempted to replace the following names that do not exist", - msg_indent(old_nms[is.na(i)]) - ), - msg_colon( - "These are the only names that are available", - msg_indent(names(f)) - ) - ) |> stop() - } - j = match(old_nms, labs) - names(f)[i] = new_nms - labs[j[!is.na(j)]] = new_nms[!is.na(j)] - Index(f, labelling_column_names = labs) -} - -#' @export -mp_group = function(index, by) { - frame = index$partition$select(to_names(by))$frame() - nms = names(frame)[names(frame) %in% index$labelling_column_names] - Index(frame, labelling_column_names = nms) -} - -#' @export +# FIXME: What's this?? mp_indicator = function(x, ...) { l = list(...) for (nm in names(l)) { @@ -676,243 +20,3 @@ mp_indicator = function(x, ...) { } Reduce(`&`, l) } - -#' @export -mp_indices = function(x, table) { - match(x, table) - 1L -} - -#' @export -mp_labels = function(x, labelling_column_names) { - UseMethod("mp_labels") -} - -#' @export -mp_labels.Index = function(x, labelling_column_names) { - if (missing(labelling_column_names)) return(x$labels()) - x$partial_labels(labelling_column_names) -} - -#' @export -mp_zero_vector = function(x, ...) { - UseMethod("mp_zero_vector") -} - -#' @export -mp_zero_vector.character = function(x, ...) { - (x - |> as.vector() - |> zero_vector() - ) -} - -#' @export -mp_zero_vector.Index = function(x, labelling_column_names, ...) { - (x - |> mp_subset(...) - |> mp_labels(labelling_column_names) - |> zero_vector() - ) -} - -#' @export -mp_labels.Ledger = function(x, labelling_column_names) { - x$labels_for[[labelling_column_names]]() -} - -#' @export -mp_expr_group_sum = function(x - , stratify_by - , output_name - , vector_name - , subset_name - , grouping_name - , length_name - , ... -) { - strata = mp_select(x, stratify_by) - subset = mp_subset(x, ...) - grouping_indices = mp_indices( - mp_labels(subset, stratify_by), - mp_labels(strata) - ) - subset_indices = mp_indices( - mp_labels(subset), - mp_labels(x) - ) - length_int = strata$labels() |> length() - e_lhs = output_name - e_rhs = sprintf("group_sums(%s[%s], %s, %s)" - , vector_name - , subset_name - , grouping_name - , length_name - ) - list( - formula = two_sided(e_lhs, e_rhs), - int_vecs = setNames( - list(grouping_indices, subset_indices, length_int), - c(grouping_name, subset_name, length_name) - ), - strata = strata, - subset = subset - ) -} - -#' @export -mp_expr_binop = function(x, y - , stratify_by - , output_name - , vector_name - , subset_name - , grouping_name - , length_name - , ... -) {} - - -#' Lookup -#' -#' Lookup a subset or factor index associated with a symbol, and return the -#' index associated with that symbol. -#' -#' @param index Index table (see \code{\link{mp_index}}). -#' @param symbol Character string that could possibly be associated with a -#' subset or factor of `index`. -#' -#' @export -mp_lookup = function(index, symbol) { - - ## check if we are referring to possibly multiple grouping factors - dim_names = to_names(symbol) - if (all(dim_names %in% names(index))) { - return(mp_group(index, symbol)) - } - all_dim_names = names(index) - ii = increasing_int_seq(length(all_dim_names), length(dim_names)) - for (i in ii) { - named_symbol = (symbol - |> list() - |> setNames(to_name(all_dim_names[i])) - ) - args = c(list(index), named_symbol) - guess = try(do.call(mp_subset, args), silent = TRUE) - if (!inherits(guess, "try-error")) return(guess) - } - for (i in ii) { - all_perms = apply_permutations(i) - for (j in seq_len(nrow(all_perms))) { - named_symbol = (symbol - |> list() - |> setNames(to_name(all_dim_names[all_perms[j,]])) - ) - args = c(list(index), named_symbol) - guess = try(do.call(mp_subset, args), silent = TRUE) - if (!inherits(guess, "try-error")) return(guess) - } - } - stop("failed to find symbol") -} - - -# @param l result of mp_slices or mp_factor or just a named list of indices -mp_unpack = function(l, unpack = c('no', 'maybe', 'yes'), env) { - unpack = match.arg(unpack) - if (unpack %in% c('maybe', 'yes')) { - for (nm in names(l)) { - already_there = exists(nm, envir = env) - if (already_there) { - if (unpack == 'maybe') { - stop('cannot unpack because slice names already exist') - } else if (unpack == 'yes') { - warning('masking or overwriting existing objects with slices') - } - } - } - for (nm in names(l)) assign(nm, l[[nm]], envir = env) - } -} - -#' @export -mp_factors = function(index, unpack = c('no', 'maybe', 'yes')) { - unpack = match.arg(unpack) - factors = list() - for (d in names(index)) factors[[d]] = mp_group(index, d) - pf = parent.frame() - force(pf) - mp_unpack(factors, unpack, pf) - factors -} - -mp_subset_list = function(index, ..., unpack = c('no', 'maybe', 'yes')) { - unpack = match.arg(unpack) - subsets = list(...) - - for (s in names(subsets)) { - args = c(list(index), subsets[[s]]) - subsets[[s]] = do.call(mp_subset, args) - } - - pf = parent.frame() - force(pf) - mp_unpack(subsets, unpack, pf) - subsets -} - -mp_custom_slices = function(index, ..., unpack = c('no', 'maybe', 'yes')) { - unpack = match.arg(unpack) - slice_refs = list(...) - nms = names(slice_refs) - for (i in seq_along(slice_refs)) { - r = slice_refs[[i]] - if (is.null(nms[i]) | nchar(nms[i]) == 0L) { - slice_refs[[i]] = (index - |> mp_group(r) - |> as.data.frame() - |> unlist(recursive = FALSE, use.names = FALSE) - ) - names(slice_refs)[[i]] = r - } - } - slices = list() - for (d in names(slice_refs)) { - for (s in slice_refs[[d]]) { - if (s %in% names(slices)) { - warning("duplicated slice") - } else { - args = c(list(index), setNames(list(s), d)) - slices[[s]] = do.call(mp_subset, args) - } - } - } - pf = parent.frame() - force(pf) - mp_unpack(slices, unpack, pf) - slices -} - -## TODO: list of indices where precedence is higher left and top -#' @export -mp_slices = function(index, unpack = c('no', 'maybe', 'yes')) { - unpack = match.arg(unpack) - possible_slices = (index - |> as.data.frame() - |> lapply(unique) - |> lapply(setdiff, "") - ) - slices = list() - for (d in names(possible_slices)) { - for (s in possible_slices[[d]]) { - if (s %in% names(slices)) { - warning("duplicated slice") - } else { - args = c(list(index), setNames(list(s), d)) - slices[[s]] = do.call(mp_subset, args) - } - } - } - pf = parent.frame() - force(pf) - mp_unpack(slices, unpack, pf) - slices -} diff --git a/R/mp_bulk_indices.R b/R/mp_bulk_indices.R new file mode 100644 index 00000000..77539a37 --- /dev/null +++ b/R/mp_bulk_indices.R @@ -0,0 +1,145 @@ +#' Lookup +#' +#' Lookup a subset or factor index associated with a symbol, and return the +#' index associated with that symbol. +#' +#' @param index Index table (see \code{\link{mp_index}}). +#' @param symbol Character string that could possibly be associated with a +#' subset or factor of `index`. +#' +#' @export +mp_lookup = function(index, symbol) { + + ## check if we are referring to possibly multiple grouping factors + dim_names = to_names(symbol) + if (all(dim_names %in% names(index))) { + return(mp_group(index, symbol)) + } + all_dim_names = names(index) + ii = increasing_int_seq(length(all_dim_names), length(dim_names)) + for (i in ii) { + named_symbol = (symbol + |> list() + |> setNames(to_name(all_dim_names[i])) + ) + args = c(list(index), named_symbol) + guess = try(do.call(mp_subset, args), silent = TRUE) + if (!inherits(guess, "try-error")) return(guess) + } + for (i in ii) { + all_perms = apply_permutations(i) + for (j in seq_len(nrow(all_perms))) { + named_symbol = (symbol + |> list() + |> setNames(to_name(all_dim_names[all_perms[j,]])) + ) + args = c(list(index), named_symbol) + guess = try(do.call(mp_subset, args), silent = TRUE) + if (!inherits(guess, "try-error")) return(guess) + } + } + stop("failed to find symbol") +} + + +# @param l result of mp_slices or mp_factor or just a named list of indices +mp_unpack = function(l, unpack = c('no', 'maybe', 'yes'), env) { + unpack = match.arg(unpack) + if (unpack %in% c('maybe', 'yes')) { + for (nm in names(l)) { + already_there = exists(nm, envir = env) + if (already_there) { + if (unpack == 'maybe') { + stop('cannot unpack because slice names already exist') + } else if (unpack == 'yes') { + warning('masking or overwriting existing objects with slices') + } + } + } + for (nm in names(l)) assign(nm, l[[nm]], envir = env) + } +} + +#' @export +mp_factors = function(index, unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + factors = list() + for (d in names(index)) factors[[d]] = mp_group(index, d) + pf = parent.frame() + force(pf) + mp_unpack(factors, unpack, pf) + factors +} + +mp_subset_list = function(index, ..., unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + subsets = list(...) + + for (s in names(subsets)) { + args = c(list(index), subsets[[s]]) + subsets[[s]] = do.call(mp_subset, args) + } + + pf = parent.frame() + force(pf) + mp_unpack(subsets, unpack, pf) + subsets +} + +mp_custom_slices = function(index, ..., unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + slice_refs = list(...) + nms = names(slice_refs) + for (i in seq_along(slice_refs)) { + r = slice_refs[[i]] + if (is.null(nms[i]) | nchar(nms[i]) == 0L) { + slice_refs[[i]] = (index + |> mp_group(r) + |> as.data.frame() + |> unlist(recursive = FALSE, use.names = FALSE) + ) + names(slice_refs)[[i]] = r + } + } + slices = list() + for (d in names(slice_refs)) { + for (s in slice_refs[[d]]) { + if (s %in% names(slices)) { + warning("duplicated slice") + } else { + args = c(list(index), setNames(list(s), d)) + slices[[s]] = do.call(mp_subset, args) + } + } + } + pf = parent.frame() + force(pf) + mp_unpack(slices, unpack, pf) + slices +} + +## TODO: list of indices where precedence is higher left and top +#' @export +mp_slices = function(index, unpack = c('no', 'maybe', 'yes')) { + unpack = match.arg(unpack) + possible_slices = (index + |> as.data.frame() + |> lapply(unique) + |> lapply(setdiff, "") + ) + slices = list() + for (d in names(possible_slices)) { + for (s in possible_slices[[d]]) { + if (s %in% names(slices)) { + warning("duplicated slice") + } else { + args = c(list(index), setNames(list(s), d)) + slices[[s]] = do.call(mp_subset, args) + } + } + } + pf = parent.frame() + force(pf) + mp_unpack(slices, unpack, pf) + slices +} diff --git a/R/mp_cartesian.R b/R/mp_cartesian.R new file mode 100644 index 00000000..c5e0e3eb --- /dev/null +++ b/R/mp_cartesian.R @@ -0,0 +1,164 @@ +#' Cartesian Product of Index Tables +#' +#' Produce a new index table by taking all possible pairwise combinations +#' of the input tables. This is useful for producing product models +#' that expand model components through stratification. +#' +#' @param ... Index tables (see \code{\link{mp_index}}). +#' +#' @examples +#' mp_cartesian( +#' mp_index(Epi = c("S", "I")), +#' mp_index(Age = c("young", "old")) +#' ) +#' +#' si = mp_index(Epi = c("S", "I")) +#' age = mp_index(Age = c("young", "old")) +#' loc = mp_index(City = c("hamilton", "toronto")) +#' vax = mp_index(Vax = c("unvax", "vax")) +#' (si +#' |> mp_cartesian(age) +#' |> mp_cartesian(loc) +#' |> mp_cartesian(vax) +#' ) +#' +#' flow_rates = mp_index(Epi = c("infection", "recovery")) +#' mp_union( +#' mp_cartesian( +#' mp_subset(flow_rates, Epi = "infection"), +#' age +#' ), +#' mp_subset(flow_rates, Epi = "recovery") +#' ) +#' +#' @family indexes +#' @family products +#' @export +mp_cartesian = function(...) Reduce(mp_cartesian_binary, list(...)) + +mp_cartesian_binary = function(x, y) { + shared_columns = intersect(names(x), names(y)) + if (length(shared_columns) != 0) { + msg_break( + msg_colon( + msg( + "Cannot take the Cartesian product of two indexes that", + "share columns names. But the input indexes share the", + "following columns" + ), + msg_indent(shared_columns) + ), + msg("Perhaps mp_join is more suitable?") + ) |> stop() + } + labelling_column_names = union(x$labelling_column_names, y$labelling_column_names) + f = join_partitions(x$partition$frame(), y$partition$frame()) + Index(f, labelling_column_names = labelling_column_names) +} + +#' Self Cartesian Product +#' +#' @param suffixes Length-2 character vector giving suffixes that +#' disambiguate the column names in the output. +#' @inheritParams cartesian +#' @family products +#' @export +mp_square = function(x, suffixes = c("A", "B")) { + l1 = sprintf("%s%s", x$labelling_column_names, suffixes[1L]) + l2 = sprintf("%s%s", x$labelling_column_names, suffixes[2L]) + n1 = sprintf("%s%s", names(x), suffixes[1L]) + n2 = sprintf("%s%s", names(x), suffixes[2L]) + x = (x$partition$frame() + |> setNames(n1) + |> Index(labelling_column_names = l1) + ) + y = (x$partition$frame() + |> setNames(n2) + |> Index(labelling_column_names = l2) + ) + mp_cartesian(x, y) +} + +#' Self Cartesian Product Excluding One Off-Diagonal Side +#' +#' @inheritParams cartesian +#' @param y_labelling_column_names TODO +#' @param exclude_diag Should 'diagonal' commponents be excluded from the output. +#' @param lower_tri Should the lower triangular components be include from the +#' output. If \code{FALSE} the result is upper triangular. +#' +#' @family products +#' @export +mp_triangle = function(x, y_labelling_column_names, exclude_diag = TRUE, lower_tri = FALSE) { + f = x$partition$frame() + g = setNames(f, y_labelling_column_names) + n = nrow(f) + if (exclude_diag) { + k = 2:n + i = sequence(k - 1) + j = rep(k, k - 1) + } else if (!exclude_diag) { + k = seq_len(n) + i = sequence(k) + j = rep(k, k) + } + if (lower_tri) { + ii = i + i = j + j = ii + } + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Index(f, labelling_column_names = names(f)) +} + +#' Symmetric Self Cartesian Product +#' +#' @inheritParams mp_triangle +#' @family products +#' @export +mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { + f = x$partition$frame() + g = setNames(f, y_labelling_column_names) + n = nrow(f) + k = seq_len(n) + + if (exclude_diag) { + i = rep( k , times = n - 1L) + j = rep(rev(k), each = n - 1L) + } else { + i = rep(k, times = n) + j = rep(k, each = n) + } + + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Index(f, labelling_column_names = names(f)) +} + +#' Linear Chain Product +#' +#' TODO: what does this mean? +#' +#' @inheritParams mp_square +#' @family products +#' @export +mp_linear = function(x, y_labelling_column_names) { + f = x$partition$frame() + g = setNames(f, y_labelling_column_names) + n = nrow(f) + + k = c(1L, rep(2L, n - 2L), 1L) + i = rep(seq_len(n), k) + j = sequence(k, c(2, seq_len(n - 2L), n - 1L), by = 2) + + f = cbind( + f[i, , drop = FALSE], + g[j, , drop = FALSE] + ) + Index(f, labelling_column_names = names(f)) +} diff --git a/R/mp_decompose.R b/R/mp_decompose.R new file mode 100644 index 00000000..ddd36701 --- /dev/null +++ b/R/mp_decompose.R @@ -0,0 +1,39 @@ +#' @export +mp_decompose = function(formula, index, decomp_name, ...) { + input_formula = formula + table_args = list(...) + output_name = lhs_char(formula) + by_args = table_args + names(by_args) = sprintf("%s.%s", output_name, names(table_args)) + for (nm in names(table_args)) { + table_args[[nm]] = mp_select(index, table_args[[nm]]) + } + linked_indices = do.call(mp_join, + c(setNames(list(index), output_name), table_args, by_args) + ) + int_vecs = setNames( + vector(mode = "list", length(table_args)), + sprintf("%s_%s", names(table_args), decomp_name) + ) + iv_nms = names(int_vecs) + tab_nms = names(table_args) + expand_iv_nms = sprintf("%s[%s]", tab_nms, iv_nms) + replacement_formulas = mapply(two_sided + , tab_nms, expand_iv_nms + , SIMPLIFY = FALSE, USE.NAMES = FALSE + ) + + for (i in seq_along(table_args)) { + int_vecs[[iv_nms[i]]] = mp_indices( + linked_indices$labels_for[[tab_nms[i]]](), + linked_indices$partition_for[[tab_nms[i]]]()$labels() + ) + } + formula = update_formula(formula, replacement_formulas) + nlist( + formula, + input_formula, + linked_indices, + int_vecs + ) +} diff --git a/R/mp_dynamic_model.R b/R/mp_dynamic_model.R new file mode 100644 index 00000000..496b6d6b --- /dev/null +++ b/R/mp_dynamic_model.R @@ -0,0 +1,117 @@ +VariablesScripts = function(model) { + self = Base() + self$model = model + self$state = function() self$model$index_data_type("state") + self$flow_rates = function() self$model$index_data_type("flow_rates") + self$influence_rates = function() self$model$index_data_type("influence_rates") + self$aggregated_states = function() self$model$index_data_type("aggregated_states") + self$normalized_state = function() self$model$index_data_type("normalized_state") + return_object(self, "VariablesScripts") +} + +LabelsScripts = function(model) { + self = Base() + self$model = model + self$variables = model$variables + + self$dynamic_model = model$dynamic_model + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) self[[nm]] = LabelsGetter(self$dynamic_model, nm) + self$component_list = function() { + l = list() + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) l[[nm]] = self[[nm]]() + l + } + + # self$state = function() self$variables$state()$labels() + # self$flow_rates = function() self$variables$flow_rates()$labels() + # self$influence_rates = function() self$variables$influence_rates()$labels() + # self$aggregated_states = function() self$variables$aggregated_states()$labels() + # self$normalized_state = function() self$variables$normalized_state()$labels() + # self$component_list = function() { + # + # } + return_object(self, "LabelsScripts") +} + +LabelsGetter = function(dynamic_model, vector_name) { + self = Base() + self$dynamic_model = dynamic_model + self$vector_name = vector_name + self$get = function() self$dynamic_model$init_vecs[[self$vector_name]] |> names() + self$get +} + +LabelsDynamic = function(dynamic_model) { + self = Base() + self$dynamic_model = dynamic_model + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) self[[nm]] = LabelsGetter(self$dynamic_model, nm) + self$component_list = function() { + l = list() + vs = self$dynamic_model$init_vecs + for (nm in names(vs)) l[[nm]] = self[[nm]]() + l + } + return_object(self, "LabelsDynamic") +} + + +#' @export +DynamicModel = function( + expr_list = ExprList() + , ledgers = list() + , init_vecs = list() + , unstruc_mats = list() + ) { + self = Base() + self$expr_list = expr_list + self$ledgers = ledgers + self$init_vecs = lapply(init_vecs, mp_vector) + self$unstruc_mats = unstruc_mats + self$int_vec_names = function() { + lapply(self$ledgers, getElement, "table_names") |> unlist(use.names = TRUE) |> unique() + } + self$derived_matrix_names = function() { + setdiff(self$expr_list$all_formula_vars() + , c( + names(self$init_vecs) + , self$int_vec_names() + , names(self$unstruc_mats) + ) + ) + } + self$labels = LabelsDynamic(self) + return_object(self, "DynamicModel") +} + + +#' Dynamic Model +#' +#' +#' +#' @export +mp_dynamic_model = DynamicModel + +#' @export +mp_test_tmb = function(..., ledgers, vectors, unstruc_mats) { + m = mp_dynamic_model( + expr_list = mp_expr_list(before = list(...)) + , ledgers = ledgers + , init_vecs = vectors + , unstruc_mats = unstruc_mats + ) + mp_tmb_simulator(m + , time_steps = 0L + , vectors = method_apply(vectors, "numbers") + , unstruc_mats = unstruc_mats + , mats_to_return = m$derived_matrix_names() + , mats_to_save = m$derived_matrix_names() + ) |> mp_report(phases = "before") +} + +#' @export +print.DynamicModel = function(x, ...) { + print(x$expr_list) +} diff --git a/R/mp_group.R b/R/mp_group.R new file mode 100644 index 00000000..72aa6367 --- /dev/null +++ b/R/mp_group.R @@ -0,0 +1,120 @@ +#' @export +mp_group = function(index, by) { + frame = index$partition$select(to_names(by))$frame() + nms = names(frame)[names(frame) %in% index$labelling_column_names] + Index(frame, labelling_column_names = nms) +} + + +#' Aggregate an Index +#' +#' Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows +#' identifying instances of an aggregation. +#' +#' @family ledgers +#' @export +mp_aggregate = function(index, by = "Group", ledger_column = "group") { + index_columns = to_names(by) + if (length(index_columns) == 1L & !index_columns %in% names(index)) { + partition = index$partition$constant(by, "a") + } else { + partition = index$partition + } + index = Index(partition) |> mp_group(by) + + Ledger( + partition$frame(), + initial_column_map(names(partition), ledger_column), + initial_reference_index_list(index, ledger_column), + setNames(list(index_columns), ledger_column) + ) +} + + +mp_aggregate_old = function(formula + , group_by + , index + , ... +) { + prototypes = list( + group_sums = macpan2:::MethodPrototype(y ~ group_sums(x), c("x", "y"), character()) + ) + consistent_agg_meths = (prototypes + |> method_apply("consistent", formula) + |> vapply(c, logical(1L), USE.NAMES = TRUE) + ) + if (!any(consistent_agg_meths)) { + f = method_apply(prototypes, "as_character") + msg_break( + msg_colon( + "The following aggregation formula", + msg_indent(formula_as_character(formula)) + ), + msg_colon( + msg( + "was not consistent with any of the", + "available aggregation prototypes" + ), + msg_indent_break(f) + ) + ) |> stop() + } + agg_meth = (consistent_agg_meths + |> which() + |> names() + |> getElement(1L) ## first match takes precedence + ) + agg = prototypes[[agg_meth]] + + strata = mp_select(x, stratify_by) + subset = mp_subset(x, ...) + grouping_indices = mp_indices( + mp_labels(subset, stratify_by), + mp_labels(strata) + ) + subset_indices = mp_indices( + mp_labels(subset), + mp_labels(x) + ) + +} + + +#' @export +mp_expr_group_sum = function(x + , stratify_by + , output_name + , vector_name + , subset_name + , grouping_name + , length_name + , ... +) { + strata = mp_select(x, stratify_by) + subset = mp_subset(x, ...) + grouping_indices = mp_indices( + mp_labels(subset, stratify_by), + mp_labels(strata) + ) + subset_indices = mp_indices( + mp_labels(subset), + mp_labels(x) + ) + length_int = strata$labels() |> length() + e_lhs = output_name + e_rhs = sprintf("group_sums(%s[%s], %s, %s)" + , vector_name + , subset_name + , grouping_name + , length_name + ) + list( + formula = two_sided(e_lhs, e_rhs), + int_vecs = setNames( + list(grouping_indices, subset_indices, length_int), + c(grouping_name, subset_name, length_name) + ), + strata = strata, + subset = subset + ) +} diff --git a/R/index.R b/R/mp_index.R similarity index 90% rename from R/index.R rename to R/mp_index.R index 0d1f2565..82ec3908 100644 --- a/R/index.R +++ b/R/mp_index.R @@ -282,3 +282,56 @@ info_curve = function(partition) { infer_labelling_columns = function(partition) { names(partition)[seq_len(which.max(info_curve(partition)))] } + + +#' @export +mp_reference = function(x, dimension_name) { + UseMethod("mp_reference") +} + +#' @export +mp_reference.Ledger = function(x, dimension_name) { + ii = x$reference_index_list[[dimension_name]] + ii$reset_reference_index() + ii +} + +#' @export +mp_reference.Index = function(x, dimension_name) { + x$reference_index() +} + +#' @export +mp_extract = function(x, dimension_name) { + UseMethod("mp_extract") +} + +#' @export +mp_extract.Ledger = function(x, dimension_name) { + ii = x$index_for[[dimension_name]]() + ii$reset_reference_index() + ii +} + +#' @export +mp_extract.DynamicModel = function(x, dimension_name) { + y = try(x$init_vecs[[dimension_name]]$index, silent = TRUE) + if (!inherits(y, "Index")) { + msg( + "Failed to find an index for", + dimension_name, "in this object" + ) |> stop() + } + y +} + +#' @export +mp_extract.ModelDefRun = function(x, dimension_name) { + mp_extract(x$dynamic_model, dimension_name) +} + + +#' @export +mp_indices = function(x, table) { + match(x, table) - 1L +} diff --git a/R/mp_join.R b/R/mp_join.R new file mode 100644 index 00000000..d78cf4ca --- /dev/null +++ b/R/mp_join.R @@ -0,0 +1,170 @@ +#' Join Indexes +#' +#' Join two or more index tables (see \code{\link{mp_index}}) to produce a +#' ledger (see \code{\link{LedgerDefinition}}). +#' +#' When two index tables are passed to `...`, `mp_join` behaves very much like +#' an ordinary [inner join](https://en.wikipedia.org/wiki/Join_(SQL)). +#' When more than two tables are passed to `...`, `mp_join` iteratively joins +#' pairs of tables to produce a final ledger. For example, if index tables `A` +#' `B`, and `C` are passed to `mp_join`, an inner join of `A` and `B` is +#' performed and the result is joined with `C`. In each of these successive +#' internal joins. The properties of inner +#' joins ensures that the order of tables does not affect the set of rows in +#' the final table (SW states without proof!). +#' +#' When two index tables are passed to `...`, the `by` argument is just a +#' character vector of column names on which to join (as in standard R functions +#' for joining data frames), or the dot-concatenation of these column names. +#' For example, +#' ```{r, echo = TRUE, eval = TRUE} +#' state = mp_index( +#' Epi = c("S", "I", "S", "I"), +#' Age = c("young", "young", "old", "old") +#' ) +#' mp_join( +#' from = mp_subset(state, Epi = "S"), +#' to = mp_subset(state, Epi = "I"), +#' by = "Age" +#' ) +#' ``` +#' If there are more than two tables then the `by` argument must be a named +#' list of character vectors, each describing how to join the columns of +#' a pair of tables in `...`. The names of this list are dot-concatenations +#' of the names of pairs of tables in `...`. For example, +#' ```{r, echo = TRUE, eval = TRUE} +#' rates = mp_index( +#' Epi = c("lambda", "lambda"), +#' Age = c("young", "old") +#' ) +#' mp_join( +#' from = mp_subset(state, Epi = "S"), +#' to = mp_subset(state, Epi = "I"), +#' rate = mp_subset(rates, Epi = "lambda"), +#' by = list( +#' from.to = "Age", +#' from.rate = "Age" +#' ) +#' ) +#' ``` +#' If the `by` columns have different names in two tables, then you can +#' specify these using formula notation where the left-hand-side +#' is a dot-concatenation of columns in the first table and the +#' right-hand-side is a dot-concatenation of the columns in the second +#' table. For example, +#' ```{r} +#' contact = mp_index( +#' AgeSusceptible = c("young", "young", "old", "old"), +#' AgeInfectious = c("young", "old", "young", "old") +#' ) +#' mp_join( +#' sus = mp_subset(state, Epi = "S"), +#' inf = mp_subset(state, Epi = "I"), +#' con = contact, +#' by = list( +#' sus.con = "Age" ~ "AgeSusceptible", +#' inf.con = "Age" ~ "AgeInfectious" +#' ) +#' ) +#' ``` +#' +#' @param ... Named arguments giving indexes created by +#' \code{\link{mp_index}} or another function that manipulates indexes. +#' Each argument will become a position vector used to subset +#' or expand numeric vectors in archetype formulas. +#' @param by What columns to use to join the indexes. See below on +#' how to specify this argument. +#' +#' @family ledgers +#' @export +mp_join = function(..., by = empty_named_list()) { + table_list = valid$named_list$assert(list(...)) + table_nms = names(table_list) + + if (length(table_nms) < 2L) stop("cannot join fewer than two index objects.") + if (is.character(by)) { + if (length(table_nms) != 2L) { + stop("joining more than one index requires a list-valued by argument.") + } + by = setNames(list(by), to_name(table_nms)) + } + + by_list = valid$named_list$assert(by) + if (length(by_list) > 1L) { + table_order = (by_list + |> names() + |> lapply(to_names) + |> unlist() + |> unique() + |> union(names(table_list)) + ) + } else { + table_order = names(table_list) + } + ordered_table_list = table_list[table_order] + + by_nms = names(by_list) |> strsplit(".", fixed = TRUE) + good_by_nms = (by_nms + |> lapply(`%in%`, table_nms) + |> vapply(all, logical(1L)) + ) + if (any(!good_by_nms)) { + msg_break( + msg_colon( + "Indices to join were given the following names for the join output", + table_nms + ), + msg_colon( + msg( + "But the names of arguments that specify what columns will be", + "The following arguments were supplied to", + "determine what columns to join on" + ), + msg_indent_break(by_nms[!good_by_nms]) + ), + ) |> stop() + } + if (!is.null(table_nms)) { + for (nm in names(ordered_table_list)) { + if (nm != "" & inherits(ordered_table_list[[nm]], "Index")) { + ordered_table_list[[nm]] = mp_choose(ordered_table_list[[nm]], nm) + } + } + } + orig_tab_nms = (ordered_table_list + |> method_apply("table_names") + |> unname() + |> unlist(recursive = FALSE) + ) + table_nm_diffs = (by_nms + |> lapply(factor, levels = orig_tab_nms) + |> lapply(as.integer) + |> vapply(diff, integer(1L)) + ) + + bad_by_args = table_nm_diffs < 1L ## table names in the wrong order + if (any(bad_by_args)) { + fixed_by_nms = (by_nms[bad_by_args] + |> lapply(rev) + |> lapply(paste0, collapse = ".") + ) + fixed_by_args = (by_list[bad_by_args] + |> lapply(swap_sides) + ) + by_list[bad_by_args] = fixed_by_args + names(by_list)[bad_by_args] = fixed_by_nms + } + + z = ordered_table_list[[1L]] + for (i in 2:length(ordered_table_list)) { + args = c( + list( + x = z, + y = ordered_table_list[[i]] + ), + by_list + ) + z = do.call(merge_generic_by_util, args) + } + z$reorder(names(table_list)) +} diff --git a/R/mp_labels.R b/R/mp_labels.R new file mode 100644 index 00000000..e1adf5b6 --- /dev/null +++ b/R/mp_labels.R @@ -0,0 +1,10 @@ +#' @export +mp_labels = function(x, labelling_column_names) { + UseMethod("mp_labels") +} + +#' @export +mp_labels.Index = function(x, labelling_column_names) { + if (missing(labelling_column_names)) return(x$labels()) + x$partial_labels(labelling_column_names) +} diff --git a/R/mp_library.R b/R/mp_library.R new file mode 100644 index 00000000..a4acfd04 --- /dev/null +++ b/R/mp_library.R @@ -0,0 +1,19 @@ +#' @export +mp_library = function(...) { + system.file("model_library", ..., package = "macpan2") |> Compartmental2() +} + +#' @export +mp_tmb_library = function(..., package = NULL) { + if (is.null(package)) { + model_directory = file.path(...) + } else { + model_directory = system.file(..., package = package) + } + def_env = new.env(parent = parent.frame()) + sys.source(file.path(model_directory, "tmb.R") + , envir = def_env + , chdir = TRUE + ) + def_env$spec +} diff --git a/R/mp_subset.R b/R/mp_subset.R new file mode 100644 index 00000000..96665186 --- /dev/null +++ b/R/mp_subset.R @@ -0,0 +1,60 @@ +#' Subset of Indexes +#' +#' Take a subset of the rows of an index table (see \code{\link{mp_index}}) +#' to produce another index table. The `mp_subset` function gives rows that +#' match a certain criterion and `mp_setdiff` gives rows that do not match. +#' +#' @param x Model index. +#' @param ... Name-value pairs. The names are columns (or sets of columns +#' using dot-concatenation) in \code{x} and the values are character vectors +#' that refer to labels with respect to those columns. These values +#' determine the resulting subset. +#' +#' @family indexes +#' @export +mp_subset = function(x, ...) { + partition = mp_choose(x, "pick", ...)$partition + Index(partition + , labelling_column_names = x$labelling_column_names + , reference_index = x + ) +} + +#' @rdname mp_subset +#' @export +mp_setdiff = function(x, ...) { + partition = mp_choose_out(x, "pick", ...)$partition + Index(partition + , labelling_column_names = x$labelling_column_names + , reference_index = x + ) +} + +#' @export +mp_choose = function(x, subset_name, ...) { + l = list(...) + if (length(l) != 0L) valid$named_list$check(l) + p = x$partition + for (cc in names(l)) { + vals = l[[cc]] + is_blank = nchar(vals) == 0L + vals = setdiff(vals, "") + if (any(is_blank)) { + p = union_vars(p$blank_on(cc), p$filter(vals, .wrt = cc)) + } else { + p = p$filter(vals, .wrt = cc) + } + } + init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) +} + +#' @export +mp_choose_out = function(x, subset_name, ...) { + l = list(...) + p = x$partition + for (cc in names(l)) { + vals = l[[cc]] + p = p$filter_out(vals, .wrt = cc) + } + init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) +} diff --git a/R/mp_union.R b/R/mp_union.R new file mode 100644 index 00000000..4f7f17a2 --- /dev/null +++ b/R/mp_union.R @@ -0,0 +1,60 @@ +#' Union of Indexes +#' +#' @param ... Indexes. +#' +#' @family indexes +#' @export +mp_union = function(...) UseMethod("mp_union") + +#' @export +mp_union.Index = function(...) { + l = list(...) + partitions = lapply(l, getElement, "partition") + labelling_column_names = (l + |> lapply(getElement, "labelling_column_names") + |> unlist(recursive = FALSE, use.names = FALSE) + |> unique() + ) + Index(do.call(union_vars, partitions)$frame(), labelling_column_names = labelling_column_names) +} + +## not used anymore? +#' @export +mp_union.Ledger = function(...) { + l = list(...) + column_map = lapply(l, getElement, "column_map") |> unique() + if (length(column_map) != 1L) { + msg_colon( + msg( + "Union of inconsistent Ledger objects.", + "All Ledger objects must have the same", + "column_map, but the following distinct", + "maps were found:" + ), + msg_indent_break(lapply(column_map, unlist)) + ) |> stop() + } + ## TODO: should really be checking for reference_index_list + labelling_column_names_list = lapply(l, getElement, "labelling_column_names_list") |> unique() + if (length(labelling_column_names_list) != 1L) { + msg_colon( + msg( + "Union of inconsistent Ledger objects.", + "All Ledger objects must have the same", + "labelling_column_names_list, but the following", + "distinct maps were found:" + ), + msg_indent_break(lapply(labelling_column_names_list, unlist)) + ) |> stop() + } + frame = mp_rbind(...) + LedgerData(frame, l[[1L]]$reference_index_list, l[[1L]]$labelling_column_names_list) +} + +## not used anymore? +mp_rbind = function(...) { + (list(...) + |> lapply(as.data.frame) + |> do.call(what = bind_rows) ## bind_rows is in frame_utils.R + ) +} diff --git a/R/tmb_model.R b/R/tmb_model.R index cb0afd47..b6b8ad5a 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -701,6 +701,7 @@ TMBSimulator = function(tmb_model self$add = TMBSimulatorAdder(self) self$replace = TMBSimulatorReplacer(self) self$update = TMBSimulatorUpdater(self) + self$reset = TMBSimulatorResetter(self) self$current = TMBCurrentParams(self) self$get = TMBSimulatorGetters(self) diff --git a/R/tmb_model_editors.R b/R/tmb_model_editors.R index 939861a4..24dc93dd 100644 --- a/R/tmb_model_editors.R +++ b/R/tmb_model_editors.R @@ -255,3 +255,18 @@ TMBSimulatorUpdater = function(simulator) { } return_object(self, "TMBSimulatorUpdater") } + + +TMBSimulatorResetter = function(simulator) { + self = TMBUpdater(simulator$tmb_model) + self$simulator = simulator + self$params = function() { + self$model$params = OptParamsList(0) + self$simulator$cache$invalidate() + } + self$random = function() { + self$model$params = OptParamsList() + self$simulator$cache$invalidate() + } + return_object(self, "TMBSimulatorResetter") +} diff --git a/R/trajectories.R b/R/trajectories.R deleted file mode 100644 index 5da91d3f..00000000 --- a/R/trajectories.R +++ /dev/null @@ -1,6 +0,0 @@ -TMBSimVar = function(simulator, variable_name) { - self$simulator = simulator - self$variable_name = variable_name - self$derivation - return_object(self, "TMBSimVar") -} diff --git a/R/vec_utils.R b/R/vec_utils.R new file mode 100644 index 00000000..b1d1fd80 --- /dev/null +++ b/R/vec_utils.R @@ -0,0 +1,27 @@ + +#' @export +mp_zero_vector = function(x, ...) { + UseMethod("mp_zero_vector") +} + +#' @export +mp_zero_vector.character = function(x, ...) { + (x + |> as.vector() + |> zero_vector() + ) +} + +#' @export +mp_zero_vector.Index = function(x, labelling_column_names, ...) { + (x + |> mp_subset(...) + |> mp_labels(labelling_column_names) + |> zero_vector() + ) +} + +#' @export +mp_labels.Ledger = function(x, labelling_column_names) { + x$labels_for[[labelling_column_names]]() +} diff --git a/man/Clause.Rd b/man/Clause.Rd deleted file mode 100644 index dba2de1e..00000000 --- a/man/Clause.Rd +++ /dev/null @@ -1,11 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/dsl_expr.R -\name{Clause} -\alias{Clause} -\title{Compartmental Modelling Language} -\usage{ -Clause() -} -\description{ -Compartmental Modelling Language -} diff --git a/man/DerivationExtractor.Rd b/man/DerivationExtractor.Rd deleted file mode 100644 index 54a29358..00000000 --- a/man/DerivationExtractor.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/files_to_models.R -\name{DerivationExtractor} -\alias{DerivationExtractor} -\title{DerivationExtractor} -\usage{ -DerivationExtractor(model) -} -\arguments{ -\item{model}{Object of class \code{\link{Model}}} -} -\value{ -An object of class \code{DerivationExtractor} with the -following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$expand_derivation(derivation)} -- Expand a single derivation by name. -\item \verb{$expand_derivations()} -- Example all derivations in the model. -} -} -} -\description{ -Construct an object for extracting the derivations within a -\code{\link{Model}}. -} diff --git a/man/Derivations2ExprList.Rd b/man/Derivations2ExprList.Rd deleted file mode 100644 index cae423ab..00000000 --- a/man/Derivations2ExprList.Rd +++ /dev/null @@ -1,35 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/files_to_models.R -\name{Derivations2ExprList} -\alias{Derivations2ExprList} -\title{Convert Derivation Lists to Expression Lists} -\usage{ -Derivations2ExprList(user_expr, standard_expr) -} -\arguments{ -\item{user_expr}{\code{\link{UserExpr}} object.} - -\item{standard_expr}{\code{\link{StandardExpr}} object.} -} -\value{ -Object of class \code{Derivations2ExprList} with the following -methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$expr_list()} -- An alternate constructor of \code{\link{ExprList}} -objects from a set of derivations. -\item \verb{$math_expr_list()} -- List of \code{\link{MathExpression}}s associated -with a set of derivations. -} -} - -\subsection{Arguments}{ -\itemize{ -\item \code{.simulate_exprs} -- See the argument of the same name in -\code{\link{ExprList}}. -} -} -} -\description{ -Convert Derivation Lists to Expression Lists -} diff --git a/man/MethodPrototype.Rd b/man/MethodPrototype.Rd new file mode 100644 index 00000000..9c3b8c25 --- /dev/null +++ b/man/MethodPrototype.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enum_methods.R +\name{MethodPrototype} +\alias{MethodPrototype} +\title{Method Prototype} +\usage{ +MethodPrototype(formula, mat_arg_nms, int_vec_arg_nms) +} +\arguments{ +\item{formula}{Formula for defining a method type using a prototype.} + +\item{mat_arg_nms}{Character vector naming the matrix-valued arguments.} + +\item{int_vec_arg_nms}{Character vector naming the integer-vector-valued +arguments.} +} +\description{ +Define a method type using a prototype. These prototypes can be compared +with methods defined in R to see if they are consistent with a method +type that has been defined in C++. All +arguments are automatically derived through comments in the C++ code where +the method types are defined. +} diff --git a/man/MethodTypeUtils.Rd b/man/MethodTypeUtils.Rd new file mode 100644 index 00000000..0e535e7f --- /dev/null +++ b/man/MethodTypeUtils.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enum_methods.R +\name{MethodTypeUtils} +\alias{MethodTypeUtils} +\title{Method Type Utilities} +\usage{ +MethodTypeUtils() +} +\description{ +This class is here so that \code{MethodTypes}, which is automatically generated +from the C++ code, can inherit from it. +} diff --git a/man/Scalar2Vector.Rd b/man/Scalar2Vector.Rd deleted file mode 100644 index 88517560..00000000 --- a/man/Scalar2Vector.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/files_to_models.R -\name{Scalar2Vector} -\alias{Scalar2Vector} -\title{Scalar2Vector} -\usage{ -Scalar2Vector(derivation_extractor) -} -\arguments{ -\item{derivation_extractor}{Object of type \code{\link{DerivationExtractor}} -\subsection{Methods}{ -\itemize{ -\item \verb{$vectorizer(expanded_derivation)} -\item \verb{$vectorize()} -} -}} -} -\description{ -Construct an object for replacing scalar names within a \code{\link{Model}} -model, with the equivalent vector name. -} diff --git a/man/SimulatorConstructor.Rd b/man/SimulatorConstructor.Rd deleted file mode 100644 index 93320297..00000000 --- a/man/SimulatorConstructor.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/alt_model_constructors.R -\name{SimulatorConstructor} -\alias{SimulatorConstructor} -\title{Simulator Constructor} -\usage{ -SimulatorConstructor(model_directory, integration_method = RK4, ...) -} -\arguments{ -\item{model_directory}{A string giving a path to a directory containing -model definition files.} - -\item{integration_method}{One of the functions described in -\link{integration_methods}, used to integrate the dynamical system.} - -\item{...}{Arguments to pass to the simulator constructor.} -} -\description{ -Simulator Constructor -} diff --git a/man/StandardExpr.Rd b/man/StandardExpr.Rd deleted file mode 100644 index 717066f6..00000000 --- a/man/StandardExpr.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/files_to_models.R -\name{StandardExpr} -\alias{StandardExpr} -\title{Standard Expressions} -\usage{ -StandardExpr(model) -} -\arguments{ -\item{model}{Object created by \code{\link{Model}}.} -} -\description{ -Evaluate standard model expressions. -} diff --git a/man/StandardExprHazard.Rd b/man/StandardExprHazard.Rd deleted file mode 100644 index e66cf692..00000000 --- a/man/StandardExprHazard.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/files_to_models.R -\name{StandardExprHazard} -\alias{StandardExprHazard} -\title{Standard Expressions for Hazard-Corrected Models} -\usage{ -StandardExprHazard(model) -} -\arguments{ -\item{model}{Object created by \code{\link{Model}}.} -} -\description{ -Evaluate standard expressions in hazard-corrected models. -} diff --git a/man/UserExpr.Rd b/man/UserExpr.Rd deleted file mode 100644 index ec5f4864..00000000 --- a/man/UserExpr.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/files_to_models.R -\name{UserExpr} -\alias{UserExpr} -\title{UserExpr} -\usage{ -UserExpr(model) -} -\arguments{ -\item{model}{Object created by \code{\link{Model}}.} -} -\description{ -Evaluate user input expressions -} diff --git a/man/dot-known_dist.Rd b/man/dot-known_dist.Rd deleted file mode 100644 index 4955b4a2..00000000 --- a/man/dot-known_dist.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/calibration.R -\docType{data} -\name{.known_dist} -\alias{.known_dist} -\title{Pick out obs/location pairs from terms involving probability distributions -find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) -find_obs_pairs(stuff ~ other_stuff + more_stuff)} -\format{ -An object of class \code{character} of length 5. -} -\usage{ -.known_dist -} -\description{ -Pick out obs/location pairs from terms involving probability distributions -find_obs_pairs(~ dnorm(a, b, c) + dpois(d, e)) -find_obs_pairs(stuff ~ other_stuff + more_stuff) -} -\keyword{datasets} diff --git a/man/mk_meth_cls.Rd b/man/mk_meth_cls.Rd new file mode 100644 index 00000000..a3d4e6d6 --- /dev/null +++ b/man/mk_meth_cls.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enum_methods.R +\name{mk_meth_cls} +\alias{mk_meth_cls} +\title{Make Method Class} +\usage{ +mk_meth_cls(cls_nm, meth_type_id) +} +\arguments{ +\item{cls_nm}{Character string giving the name of the class.} + +\item{meth_type_id}{Integer giving the associated ID of the method type.} +} +\description{ +Place a method object in the package namespace for a given method type +defined in the C++ code. +} diff --git a/man/mp_aggregate.Rd b/man/mp_aggregate.Rd index f48d7669..689cd3e3 100644 --- a/man/mp_aggregate.Rd +++ b/man/mp_aggregate.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_group.R \name{mp_aggregate} \alias{mp_aggregate} \title{Aggregate an Index} diff --git a/man/mp_cartesian.Rd b/man/mp_cartesian.Rd index b283d96e..2baa9e4e 100644 --- a/man/mp_cartesian.Rd +++ b/man/mp_cartesian.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_cartesian.R \name{mp_cartesian} \alias{mp_cartesian} \title{Cartesian Product of Index Tables} @@ -43,7 +43,6 @@ mp_union( \seealso{ Other functions that return index tables \code{\link{mp_index}()}, -\code{\link{mp_rename}()}, \code{\link{mp_subset}()}, \code{\link{mp_union}()} diff --git a/man/mp_dynamic_model.Rd b/man/mp_dynamic_model.Rd index d83b13d2..8215c26b 100644 --- a/man/mp_dynamic_model.Rd +++ b/man/mp_dynamic_model.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/model_def_run.R +% Please edit documentation in R/mp_dynamic_model.R \name{mp_dynamic_model} \alias{mp_dynamic_model} \title{Dynamic Model} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index 42a283ad..1e5c6b63 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/index.R +% Please edit documentation in R/mp_index.R \name{mp_index} \alias{mp_index} \alias{print.Index} @@ -155,7 +155,6 @@ mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) Other functions that return index tables \code{\link{mp_cartesian}()}, -\code{\link{mp_rename}()}, \code{\link{mp_subset}()}, \code{\link{mp_union}()} } diff --git a/man/mp_join.Rd b/man/mp_join.Rd index 4fed5260..67e2f666 100644 --- a/man/mp_join.Rd +++ b/man/mp_join.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_join.R \name{mp_join} \alias{mp_join} \title{Join Indexes} diff --git a/man/mp_linear.Rd b/man/mp_linear.Rd index af47f082..2a837d3a 100644 --- a/man/mp_linear.Rd +++ b/man/mp_linear.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_cartesian.R \name{mp_linear} \alias{mp_linear} \title{Linear Chain Product} diff --git a/man/mp_lookup.Rd b/man/mp_lookup.Rd index 10ca5803..98299c54 100644 --- a/man/mp_lookup.Rd +++ b/man/mp_lookup.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_bulk_indices.R \name{mp_lookup} \alias{mp_lookup} \title{Lookup} diff --git a/man/mp_rename.Rd b/man/mp_rename.Rd deleted file mode 100644 index f8afe281..00000000 --- a/man/mp_rename.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R -\name{mp_rename} -\alias{mp_rename} -\title{Rename Index Columns} -\usage{ -mp_rename(x, ...) -} -\arguments{ -\item{...}{Name-value pairs. The name gives the new name and the value -is a character vector giving the old name.} -} -\description{ -Rename Index Columns -} -\seealso{ -Other functions that return index tables -\code{\link{mp_cartesian}()}, -\code{\link{mp_index}()}, -\code{\link{mp_subset}()}, -\code{\link{mp_union}()} -} -\concept{indexes} diff --git a/man/mp_square.Rd b/man/mp_square.Rd index ed4c565c..4ecffb0b 100644 --- a/man/mp_square.Rd +++ b/man/mp_square.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_cartesian.R \name{mp_square} \alias{mp_square} \title{Self Cartesian Product} diff --git a/man/mp_subset.Rd b/man/mp_subset.Rd index ffe1881c..ec4e6b92 100644 --- a/man/mp_subset.Rd +++ b/man/mp_subset.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_subset.R \name{mp_subset} \alias{mp_subset} \alias{mp_setdiff} @@ -26,7 +26,6 @@ match a certain criterion and \code{mp_setdiff} gives rows that do not match. Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, -\code{\link{mp_rename}()}, \code{\link{mp_union}()} } \concept{indexes} diff --git a/man/mp_symmetric.Rd b/man/mp_symmetric.Rd index 0cb0fe9b..f887aac8 100644 --- a/man/mp_symmetric.Rd +++ b/man/mp_symmetric.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_cartesian.R \name{mp_symmetric} \alias{mp_symmetric} \title{Symmetric Self Cartesian Product} diff --git a/man/mp_triangle.Rd b/man/mp_triangle.Rd index af34c094..aedb6e88 100644 --- a/man/mp_triangle.Rd +++ b/man/mp_triangle.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_cartesian.R \name{mp_triangle} \alias{mp_triangle} \title{Self Cartesian Product Excluding One Off-Diagonal Side} diff --git a/man/mp_union.Rd b/man/mp_union.Rd index f8dfaf9d..9f934ea1 100644 --- a/man/mp_union.Rd +++ b/man/mp_union.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mp.R +% Please edit documentation in R/mp_union.R \name{mp_union} \alias{mp_union} \title{Union of Indexes} @@ -16,7 +16,6 @@ Union of Indexes Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, -\code{\link{mp_rename}()}, \code{\link{mp_subset}()} } \concept{indexes} diff --git a/R/alt_model_constructors.R b/misc/old-r-source/alt_model_constructors.R similarity index 100% rename from R/alt_model_constructors.R rename to misc/old-r-source/alt_model_constructors.R diff --git a/R/derivations.R b/misc/old-r-source/derivations.R similarity index 100% rename from R/derivations.R rename to misc/old-r-source/derivations.R diff --git a/R/files_to_models.R b/misc/old-r-source/files_to_models.R similarity index 100% rename from R/files_to_models.R rename to misc/old-r-source/files_to_models.R From 1741e8bb0e953ba4880bb96f221e7ed4e3006a86 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 13:58:07 -0500 Subject: [PATCH 317/332] library model example edits --- .../calibration_example.R | 4 ++-- inst/starter_models/si/example.R | 2 +- .../sir_waning/calibration_example.R | 21 ++++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R b/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R index d5c8d653..8c1d2d59 100644 --- a/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R +++ b/inst/starter_models/lotka_volterra_predator_prey/calibration_example.R @@ -1,4 +1,3 @@ -source("inst/starter_models/lotka_volterra_predator_prey/tmb.R") library(macpan2) library(ggplot2) library(dplyr) @@ -7,7 +6,8 @@ library(dplyr) ## get model spec from library ## ------------------------- -#spec = mp_tmb_library("starter_models","lotka_volterra_predator_prey",package="macpan2") +source(system.file("starter_models/lotka_volterra_predator_prey/tmb.R", package = "macpan2")) +#spec = mp_tmb_library("starter_models","lotka_volterra_predator_prey", package="macpan2") spec ## ------------------------- diff --git a/inst/starter_models/si/example.R b/inst/starter_models/si/example.R index b18b4a1e..6943bf70 100644 --- a/inst/starter_models/si/example.R +++ b/inst/starter_models/si/example.R @@ -1,4 +1,4 @@ -source("inst/starter_models/si/tmb.R") +source(system.file("starter_models/si/tmb.R", package = "macpan2")) library(ggplot2) library(dplyr) diff --git a/inst/starter_models/sir_waning/calibration_example.R b/inst/starter_models/sir_waning/calibration_example.R index 5eaa6add..bbb22eff 100644 --- a/inst/starter_models/sir_waning/calibration_example.R +++ b/inst/starter_models/sir_waning/calibration_example.R @@ -7,7 +7,7 @@ library(dplyr) ## get model spec from library ## ------------------------- -spec = mp_tmb_library("starter_models","sir_waning",package="macpan2") +spec = mp_tmb_library("starter_models", "sir_waning", package = "macpan2") spec ## ------------------------- @@ -164,3 +164,22 @@ if (interactive()) { if (interactive()) { plot(sir_waning$report() %>% filter(matrix=="waning_immunity") %>% select(time,value), type = "l", las = 1, ylab='Waning Immunity') } + +## ------------------------- +## exercise random effects a bit +## ------------------------- + +## In real life we would add an informative or partially informative +## prior for the random effect, but here we just use uniform for log phi. + +sir_waning$reset$params() +sir_waning$replace$params(log(spec$default$beta), "log_beta") +sir_waning$replace$random(log(spec$default$phi), "log_phi") +sir_waning$optimize$nlminb() + +## They both seem to be reasonably close to the true values +print(sir_waning$sdreport()) +print(exp(sir_waning$sdreport()$par.fixed)) +print(exp(sir_waning$sdreport()$par.random)) +print(sir_waning$current$params_frame()) +print(sir_waning$current$random_frame()) From 72433d6d1fab0f125cf8af72e9ca0668b436fd7d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 13:59:45 -0500 Subject: [PATCH 318/332] reprex not necessary as #121 was not a bug --- tests/mp_union-reprex.R | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 tests/mp_union-reprex.R diff --git a/tests/mp_union-reprex.R b/tests/mp_union-reprex.R deleted file mode 100644 index f50d9295..00000000 --- a/tests/mp_union-reprex.R +++ /dev/null @@ -1,21 +0,0 @@ -library(macpan2) - -state = mp_index(Epi = c("S", "I", "R")) -rate = mp_index(Epi = c("beta", "gamma", "lambda")) - -# infection ledger -infection = mp_join( - from_states = mp_subset(state, Epi = "S"), - to_states = mp_subset(state, Epi = "I"), - flow_rates = mp_subset(rate, Epi = "lambda") -) - -# recovery ledger -recovery = mp_join( - from_states = mp_subset(state, Epi = "I"), - to_states = mp_subset(state, Epi = "R"), - flow_rates = mp_subset(rate, Epi = "gamma") -) - -# unite into a single flows ledger -mp_union(infection, recovery) From eb16bfff2418f6e74e68e9f7ec310dac1cf01b2f Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 14:00:11 -0500 Subject: [PATCH 319/332] try turning on GH actions again --- .github/workflows/R-CMD-check.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 925e3eda..d83877ad 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -18,9 +18,9 @@ jobs: fail-fast: false matrix: config: - # - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} # - {os: macos-latest, r: 'release'} - - {os: windows-latest, r: 'release'} + # - {os: windows-latest, r: 'release'} # - {os: ubuntu-latest, r: 'release'} # - {os: ubuntu-latest, r: 'oldrel-1'} @@ -52,5 +52,3 @@ jobs: - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true - build_args: 'c("--no-manual", "--no-build-vignettes")' - error-on: '"never"' From e9fd0e6eba32e783f2f1968c6b3ac16a6bc7bfcb Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 15:03:35 -0500 Subject: [PATCH 320/332] coverage and reorg --- Makefile | 8 +- NAMESPACE | 6 - R/link.R | 521 ----------------- R/mp_join.R | 522 ++++++++++++++++++ R/mp_labels.R | 5 + R/obj_utils.R | 3 - R/test_utils.R | 1 - R/vec_utils.R | 5 - man/FlowExpander.Rd | 15 - man/LedgerDefinition.Rd | 2 +- man/Simulators.Rd | 55 -- man/integration_methods.Rd | 25 - {R => misc/old-r-source}/connection.R | 0 {R => misc/old-r-source}/flow_expander.R | 0 {R => misc/old-r-source}/model_products.R | 0 {R => misc/old-r-source}/simulation_methods.R | 0 {R => misc/old-r-source}/simulators.R | 0 17 files changed, 535 insertions(+), 633 deletions(-) delete mode 100644 R/link.R delete mode 100644 R/obj_utils.R delete mode 100644 R/test_utils.R delete mode 100644 man/FlowExpander.Rd delete mode 100644 man/Simulators.Rd delete mode 100644 man/integration_methods.Rd rename {R => misc/old-r-source}/connection.R (100%) rename {R => misc/old-r-source}/flow_expander.R (100%) rename {R => misc/old-r-source}/model_products.R (100%) rename {R => misc/old-r-source}/simulation_methods.R (100%) rename {R => misc/old-r-source}/simulators.R (100%) diff --git a/Makefile b/Makefile index 1adac661..524c998b 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,12 @@ quick-doc-install: R/*.R misc/dev/dev.cpp make quick-install +forced-quick-doc-install: + touch misc/old-r-source/*.R + make doc-update + make quick-install + + quick-test-all: make quick-doc-install make run-vignette-code @@ -135,7 +141,7 @@ R/engine_functions.R: src/macpan2.cpp echo "NULL" >> $@ -doc-update: R/*.R misc/dev/dev.cpp +doc-update: R/*.R misc/dev/dev.cpp misc/old-r-source/*.R echo "suppressWarnings(roxygen2::roxygenize(\".\",roclets = c(\"collate\", \"rd\", \"namespace\")))" | R --slave touch doc-update diff --git a/NAMESPACE b/NAMESPACE index 48ce53e5..1e2474ed 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -114,15 +114,11 @@ export(CSVReader) export(Collection) export(DynamicModel) export(EngineMethods) -export(Euler) export(ExprList) export(Files) -export(FlowExpander) -export(Flows) export(Formula) export(Index) export(IndexedExpressions) -export(Infection) export(IntVecs) export(JSONReader) export(LedgerList) @@ -146,7 +142,6 @@ export(Products) export(Quantities) export(RReader) export(Reader) -export(Simulators) export(StringDataFromDotted) export(StringDataFromFrame) export(StructuredVector) @@ -171,7 +166,6 @@ export(finalizer_index) export(init_merge) export(initial_valid_vars) export(join_partitions) -export(labelled_frame) export(labelled_zero_vector) export(labelling_column_names) export(make_expr_parser) diff --git a/R/link.R b/R/link.R deleted file mode 100644 index 87938a4f..00000000 --- a/R/link.R +++ /dev/null @@ -1,521 +0,0 @@ -#' Ledgers -#' -#' A ledger is a table with rows that identify specific instances of a -#' functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers -#' are most commonly -#' created using the \code{\link{mp_join}} function as in the following -#' example. -#' ```{r} -#' age = mp_index(Age = c("young", "old")) -#' state = mp_cartesian( -#' mp_index(Epi = c("S", "I", "R")), -#' age -#' ) -#' mp_join( -#' from = mp_subset(state, Epi = "S"), -#' to = mp_subset(state, Epi = "I"), -#' from.to = "Age" -#' ) -#' ``` -#' @name LedgerDefinition -NULL - - -Ledger = function(frame, column_map, reference_index_list, labelling_column_names_list) { - self = Base() - self$frame = frame - self$column_map = column_map - self$reference_index_list = reference_index_list - self$labelling_column_names_list = labelling_column_names_list - # function() { - # lapply(self$reference_index_list, getElement, "labelling_column_names") - # } - self$table_names = function() names(self$column_map) - self$labels_for = list() - self$labels_frame = function() { - l = list() - for (d in names(self$column_map)) l[[d]] = self$labels_for[[d]]() - l |> as.data.frame() - } - self$frame_for = list() - self$partition_for = list() - self$index_for = list() - self$labels_for = list() - self$reference_labels_for = list() - self$positions_for = list() - self$reference_positions_for = list() - for (d in names(self$column_map)) { - getter = FrameGetter(self, d) - self$frame_for[[d]] = getter$get_frame - self$labels_for[[d]] = getter$get_labels - self$partition_for[[d]] = getter$get_partition - self$index_for[[d]] = getter$get_index - self$reference_labels_for[[d]] = getter$get_reference_labels - self$positions_for[[d]] = getter$get_positions - self$reference_positions_for[[d]] = getter$get_reference_positions - } - self$column_by_dim = list() - for (d in names(self$column_map)) { - self$column_by_dim[[d]] = list() - for (c in names(self$column_map[[d]])) { - self$column_by_dim[[d]][[c]] = ColumnGetter(self, d, c) - } - } - self$partition_list = function() { - l = list() - for (dn in names(self$partition_for)) { - l[[dn]] = self$partition_for[[dn]]() - } - l - } - self$frame_list = function() method_apply(self$partition_list(), "frame") - self$filter = function(condition) { - condition = substitute(condition) - i = eval(condition, envir = c(self$column_by_dim, self$frame)) - Ledger(self$frame[i, , drop = FALSE] - , column_map = self$column_map - , reference_index_list = self$reference_index_list - , labelling_column_names_list = self$labelling_column_names_list - ) - } - self$expr = function(condition) { - substitute(condition) - eval(condition, envir = c(self$column_by_dim, self$frame)) - } - self$reorder = function(table_names_order) { - ## NB: in-place! - self$labelling_column_names_list = self$labelling_column_names_list[table_names_order] - self$column_map = self$column_map[table_names_order] - self$reference_index_list = self$reference_index_list[table_names_order] - self - } - - ## hack! should probably have a method and then change the partition - ## field in Index to a method as well - self$partition = self$partition_for[[1L]]() - return_object(self, "Ledger") -} - -ColumnGetter = function(link, dimension_name, column_name) { - self = Base() - self$link = link - self$dimension_name = dimension_name - self$column_name = column_name - self$get = function() { - lp = self$link - i = lp$column_map[[self$dimension_name]][[self$column_name]] - lp$frame[[i]] - } - self = return_object(self, "ColumnGetter") - self$get -} - -FrameGetter = function(link, dimension_name) { - self = Base() - self$link = link - self$dimension_name = dimension_name - self$get_frame = function() { - i = unlist( - self$link$column_map[[self$dimension_name]], - use.names = FALSE - ) - setNames( - self$link$frame[, i, drop = FALSE], - names(self$link$column_map[[self$dimension_name]]) - ) - } - self$get_partition = function() self$get_frame() |> Partition() - self$get_index = function() { - Index( - self$get_partition(), - labelling_column_names = self$link$labelling_column_names_list[[self$dimension_name]], - reference_index = self$link$reference_index_list[[self$dimension_name]] - ) - } - self$get_labels = function() { - i = self$link$labelling_column_names_list[[self$dimension_name]] - f = self$get_frame()[, i, drop = FALSE] - l = as.list(f) - paste_args = c(l, sep = ".") - do.call(paste, paste_args) - } - self$get_reference_labels = function() { - self$get_index()$reference_labels() - } - self$get_positions = function(zero_based = FALSE) { - i = match(self$get_labels(), self$get_reference_labels()) - if (zero_based) i = i - 1L - i - } - self$get_reference_positions = function(zero_based = FALSE) { - i = match(self$get_reference_labels(), self$get_labels()) - if (zero_based) i = i - 1L - i - } - return_object(self, "FrameGetter") -} - -initial_column_map = function(column_names, dimension_name) { - setNames( - list(setNames(as.list(column_names), column_names)), - dimension_name - ) -} - -initial_reference_index_list = function(index, dimension_name) { - setNames(list(index), dimension_name) -} - - -initial_labelling_column_names_list = function(labelling_column_names, dimension_name) { - setNames( - list(labelling_column_names), - dimension_name - ) -} - -## take two Merge objects and merge their frames -## and update the provenance-preserving column maps -merge_util = function(x, y, by.x, by.y) { - - ## ---- - ## resolve non-unique column names in the output, with - ## names of the original tables used to this point - ## ---- - suffixes = c( - paste0(sprintf(":%s", names(x$column_map)), collapse = ""), - paste0(sprintf(":%s", names(y$column_map)), collapse = "") - ) - - ## ---- - ## the merge itself - ## ---- - z = merge( - x$frame, y$frame, - by.x = by.x, by.y = by.y, - suffixes = suffixes, - sort = FALSE - ) - - if (nrow(z) == 0L) stop("Nothing matched in a join resulting in a ledger with zero rows, which is not allowed") - - ## ---- - ## update the column name map to preserve provenance - ## ---- - - # column names in inputs and output - x_cnms = names(x$frame) - y_cnms = names(y$frame) - z_cnms = names(z) - - # numbers of types of output columns - n_common = length(by.x) - n_x_only = length(x_cnms) - n_common - n_y_only = length(y_cnms) - n_common - - # column names in the output categorized - # by contributing input table(s) - z_common = z_cnms[seq_len(n_common)] - z_x_only = z_cnms[n_common + seq_len(n_x_only)] - z_y_only = z_cnms[n_common + n_x_only + seq_len(n_y_only)] - - # column names in the input tables, ordered - # as they are in the output table - x_z_order = c(by.x, x_cnms[!x_cnms %in% by.x]) - y_z_order = c(by.y, y_cnms[!y_cnms %in% by.y]) - - # these can be used to update the column map, - # by looping through the existing map and - # translating to the new column names with these - # x|y_map vectors. of course you will also need - # to concatenate the column maps first - x_map = setNames(c(z_common, z_x_only), x_z_order) - y_map = setNames(c(z_common, z_y_only), y_z_order) - - x_cmap = x$column_map - y_cmap = y$column_map - for (tnm in names(x_cmap)) { - for (cnm in names(x_cmap[[tnm]])) { - old_cnm = x_cmap[[tnm]][[cnm]] - x_cmap[[tnm]][[cnm]] = x_map[[old_cnm]] - } - } - for (tnm in names(y_cmap)) { - for (cnm in names(y_cmap[[tnm]])) { - old_cnm = y_cmap[[tnm]][[cnm]] - y_cmap[[tnm]][[cnm]] = y_map[[old_cnm]] - } - } - - z_column_map = c(x_cmap, y_cmap) - z_reference_index_list = c(x$reference_index_list, y$reference_index_list) - z_lab_names_list = c(x$labelling_column_names_list, y$labelling_column_names_list) - - ## ---- - ## wrap up the result with provenance-preserving column map - ## ---- - Ledger( - z, - z_column_map, - z_reference_index_list, - z_lab_names_list - ) -} - -filter_by_list = function(x_orig, y_orig, by_list) { - l = list() - nms = names(by_list) - for (i in seq_along(by_list)) { - nm_i = nms[[i]] - nms_i = to_names(nm_i) - if ((nms_i[[1L]] %in% x_orig) & (nms_i[[2L]] %in% y_orig)) { - l[[nm_i]] = by_list[[nm_i]] - } - } - l -} - -#' @export -init_merge = function(frame, dimension_name, reference_index, labelling_column_names) { - Ledger(frame - , initial_column_map(names(frame), dimension_name) - , initial_reference_index_list(reference_index, dimension_name) - , initial_labelling_column_names_list(labelling_column_names, dimension_name) - ) -} - -split_by = function(by, table_origins) UseMethod("split_by") - -#' @export -split_by.character = function(by, table_origins) { - by = to_names(by) - orig = to_names(table_origins) - list(by, by) |> setNames(orig) -} - -#' @export -split_by.formula = function(by, table_origins) { - orig = to_names(table_origins) - list( - to_names(lhs(by)[[2L]]), - to_names(rhs(by)[[2L]]) - ) |> setNames(orig) -} - - - - -apply_col_map = function(map, orig_table_nm, by) { - map[[orig_table_nm]][by] |> unlist(use.names = FALSE) -} - -## @param x Ledger object -## @param col_nm Name of a column to check for implicit provenance -is_provenance_implicit = function(x, col_nm) { - (x$column_map - |> lapply(getElement, col_nm) - |> Filter(f = Negate(is.null)) - |> vapply(`==`, logical(1L), col_nm) - ) -} - -## @param x Ledger object -explicit_provenance = function(x, col_nm) { - m = x$column_map - implicit = is_provenance_implicit(x, col_nm) - if (length(implicit) == 0L) { - msg_colon( - msg("Column", col_nm, "not found in any of the original tables"), - msg_indent(names(m)) - ) |> stop() - } - if (!any(implicit)) return(x) - - tabs_to_fix = (implicit - |> Filter(f = isTRUE) - |> names() - ) - - f = x$frame - l = x$labelling_column_names_list - ii = x$reference_index_list - - orig_col = f[[col_nm]] - for (tab_nm in tabs_to_fix) { - new_col_nm = sprintf("%s:%s", col_nm, tab_nm) - f[[new_col_nm]] = orig_col - m[[tab_nm]][[col_nm]] = new_col_nm - } - f[[col_nm]] = NULL - - ## frame, column_map, reference_index_list, labelling_column_names_list - Ledger(f, m, ii, l) -} - - -merge_generic_by_util = function(x, y, ...) { - by = filter_by_list( - names(x$column_map), - names(y$column_map), - list(...) - ) - by = mapply(split_by - , by - , names(by) - , SIMPLIFY = FALSE - , USE.NAMES = FALSE - ) |> setNames(names(by)) - by.x = character() - by.y = character() - for (nm in names(by)) { - orig = names(by[[nm]]) - by.x = append( - by.x, - apply_col_map(x$column_map, orig[[1L]], by[[nm]][[1L]]) - ) - by.y = append( - by.y, - apply_col_map(y$column_map, orig[[2L]], by[[nm]][[2L]]) - ) - } - by = data.frame(x = by.x, y = by.y) |> unique() - - dup.x = duplicated(by$x) - dup.y = duplicated(by$y) - if (any(dup.x) | any(dup.y)) { - if (any(dup.x)) { - cols_to_fix = by$x[dup.x] - for (col_nm in cols_to_fix) { - x = explicit_provenance(x, col_nm) - } - } - if (any(dup.y)) { - cols_to_fix = by$y[dup.y] - for (col_nm in cols_to_fix) { - y = explicit_provenance(y, col_nm) - } - } - merge_generic_by_util(x, y, ...) - } else { - merge_util(x, y, by$x, by$y) - } -} - -#' @export -as.data.frame.Ledger = function(x, row.names = NULL, optional = FALSE, ...) { - x$labels_frame() -} - -#' @export -summary.Ledger = function(object, ...) { - formats = c("name", "combined") - structure( - sapply(formats, link_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), - class = "summary.Ledger" - ) -} - -#' @export -print.summary.Ledger = function(x, ...) { - msg_hline() |> message() - msg( - "Ledger object from macpan2 describing", - "an aspect of model shape" - ) |> message() - msg_hline() |> message() - print(x$name) - print(x$combined, row.names = FALSE) -} - -#' @export -names.Ledger = function(x) names(x$frame) - -#' @export -labelling_column_names.Ledger = function(x) x$labelling_column_names_list - - -link_format_picker = function(x - , format = c("labels", "link", "combined", "separate") - ) { - switch(match.arg(format) - , labels = x$labels_frame() - , link = x$frame - , combined = cbind(x$labels_frame(), x$frame) - , separate = x$frame_list() - ) -} - -#' @export -print.Ledger = function(x - , format = c("labels", "link", "combined", "separate") - , ... - ) { - x = link_format_picker(x, format) - print(x, row.names = FALSE, ...) -} - -#' @export -head.Ledger = function(x - , n = 6L - , format = c("labels", "link", "combined", "separate") - , ... - ) { - x = link_format_picker(x, format) - if (format == "separate") { - return(lapply(x, head, n = n, ...)) - } else { - return(head(x, n = n, ...)) - } -} - -#' @export -tail.Ledger = function(x - , n = 6L - , format = c("labels", "link", "combined", "separate") - , ... - ) { - x = link_format_picker(x, format) - if (format == "separate") { - return(lapply(x, tail, n = n, ...)) - } else { - return(tail(x, n = n, ...)) - } -} - -#' @export -str.Ledger = function(x - , format = c("labels", "link", "combined", "separate") - , ... -) { - x = link_format_picker(x, format) - str(x, ...) -} - - -#' @export -LedgerList = function() { - self = Base() - self$list = list() |> setNames(as.character()) - self$add = function(new_link, ...) { - new_ledgers = list(...) - if (missing(new_link)) { - macpan2:::valid$named_list$check(new_ledgers) - } else if(length(new_ledgers) == 0L) { - new_nm = deparse1(substitute(new_link)) - new_ledgers = setNames(list(new_link), new_nm) - } else { - stop("If supplying more than one join result, please name them with {name} = {join_result}") - } - - for (nm in names(new_ledgers)) { - if (nm %in% names(self$list)) { - msg( - "Join result", nm, "is already in the list.", - "Overwriting the existing one." - ) |> message() - } - self$list[[nm]] = new_ledgers[[nm]] - } - } - return_object(self, "LedgerList") -} diff --git a/R/mp_join.R b/R/mp_join.R index d78cf4ca..fa2d250e 100644 --- a/R/mp_join.R +++ b/R/mp_join.R @@ -168,3 +168,525 @@ mp_join = function(..., by = empty_named_list()) { } z$reorder(names(table_list)) } + +#' Ledgers +#' +#' A ledger is a table with rows that identify specific instances of a +#' functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers +#' are most commonly +#' created using the \code{\link{mp_join}} function as in the following +#' example. +#' ```{r} +#' age = mp_index(Age = c("young", "old")) +#' state = mp_cartesian( +#' mp_index(Epi = c("S", "I", "R")), +#' age +#' ) +#' mp_join( +#' from = mp_subset(state, Epi = "S"), +#' to = mp_subset(state, Epi = "I"), +#' from.to = "Age" +#' ) +#' ``` +#' @name LedgerDefinition +NULL + + +Ledger = function(frame, column_map, reference_index_list, labelling_column_names_list) { + self = Base() + self$frame = frame + self$column_map = column_map + self$reference_index_list = reference_index_list + self$labelling_column_names_list = labelling_column_names_list + # function() { + # lapply(self$reference_index_list, getElement, "labelling_column_names") + # } + self$table_names = function() names(self$column_map) + self$labels_for = list() + self$labels_frame = function() { + l = list() + for (d in names(self$column_map)) l[[d]] = self$labels_for[[d]]() + l |> as.data.frame() + } + self$frame_for = list() + self$partition_for = list() + self$index_for = list() + self$labels_for = list() + self$reference_labels_for = list() + self$positions_for = list() + self$reference_positions_for = list() + for (d in names(self$column_map)) { + getter = FrameGetter(self, d) + self$frame_for[[d]] = getter$get_frame + self$labels_for[[d]] = getter$get_labels + self$partition_for[[d]] = getter$get_partition + self$index_for[[d]] = getter$get_index + self$reference_labels_for[[d]] = getter$get_reference_labels + self$positions_for[[d]] = getter$get_positions + self$reference_positions_for[[d]] = getter$get_reference_positions + } + self$column_by_dim = list() + for (d in names(self$column_map)) { + self$column_by_dim[[d]] = list() + for (c in names(self$column_map[[d]])) { + self$column_by_dim[[d]][[c]] = ColumnGetter(self, d, c) + } + } + self$partition_list = function() { + l = list() + for (dn in names(self$partition_for)) { + l[[dn]] = self$partition_for[[dn]]() + } + l + } + self$frame_list = function() method_apply(self$partition_list(), "frame") + self$filter = function(condition) { + condition = substitute(condition) + i = eval(condition, envir = c(self$column_by_dim, self$frame)) + Ledger(self$frame[i, , drop = FALSE] + , column_map = self$column_map + , reference_index_list = self$reference_index_list + , labelling_column_names_list = self$labelling_column_names_list + ) + } + self$expr = function(condition) { + substitute(condition) + eval(condition, envir = c(self$column_by_dim, self$frame)) + } + self$reorder = function(table_names_order) { + ## NB: in-place! + self$labelling_column_names_list = self$labelling_column_names_list[table_names_order] + self$column_map = self$column_map[table_names_order] + self$reference_index_list = self$reference_index_list[table_names_order] + self + } + + ## hack! should probably have a method and then change the partition + ## field in Index to a method as well + self$partition = self$partition_for[[1L]]() + return_object(self, "Ledger") +} + +ColumnGetter = function(link, dimension_name, column_name) { + self = Base() + self$link = link + self$dimension_name = dimension_name + self$column_name = column_name + self$get = function() { + lp = self$link + i = lp$column_map[[self$dimension_name]][[self$column_name]] + lp$frame[[i]] + } + self = return_object(self, "ColumnGetter") + self$get +} + +FrameGetter = function(link, dimension_name) { + self = Base() + self$link = link + self$dimension_name = dimension_name + self$get_frame = function() { + i = unlist( + self$link$column_map[[self$dimension_name]], + use.names = FALSE + ) + setNames( + self$link$frame[, i, drop = FALSE], + names(self$link$column_map[[self$dimension_name]]) + ) + } + self$get_partition = function() self$get_frame() |> Partition() + self$get_index = function() { + Index( + self$get_partition(), + labelling_column_names = self$link$labelling_column_names_list[[self$dimension_name]], + reference_index = self$link$reference_index_list[[self$dimension_name]] + ) + } + self$get_labels = function() { + i = self$link$labelling_column_names_list[[self$dimension_name]] + f = self$get_frame()[, i, drop = FALSE] + l = as.list(f) + paste_args = c(l, sep = ".") + do.call(paste, paste_args) + } + self$get_reference_labels = function() { + self$get_index()$reference_labels() + } + self$get_positions = function(zero_based = FALSE) { + i = match(self$get_labels(), self$get_reference_labels()) + if (zero_based) i = i - 1L + i + } + self$get_reference_positions = function(zero_based = FALSE) { + i = match(self$get_reference_labels(), self$get_labels()) + if (zero_based) i = i - 1L + i + } + return_object(self, "FrameGetter") +} + +initial_column_map = function(column_names, dimension_name) { + setNames( + list(setNames(as.list(column_names), column_names)), + dimension_name + ) +} + +initial_reference_index_list = function(index, dimension_name) { + setNames(list(index), dimension_name) +} + + +initial_labelling_column_names_list = function(labelling_column_names, dimension_name) { + setNames( + list(labelling_column_names), + dimension_name + ) +} + +## take two Merge objects and merge their frames +## and update the provenance-preserving column maps +merge_util = function(x, y, by.x, by.y) { + + ## ---- + ## resolve non-unique column names in the output, with + ## names of the original tables used to this point + ## ---- + suffixes = c( + paste0(sprintf(":%s", names(x$column_map)), collapse = ""), + paste0(sprintf(":%s", names(y$column_map)), collapse = "") + ) + + ## ---- + ## the merge itself + ## ---- + z = merge( + x$frame, y$frame, + by.x = by.x, by.y = by.y, + suffixes = suffixes, + sort = FALSE + ) + + if (nrow(z) == 0L) stop("Nothing matched in a join resulting in a ledger with zero rows, which is not allowed") + + ## ---- + ## update the column name map to preserve provenance + ## ---- + + # column names in inputs and output + x_cnms = names(x$frame) + y_cnms = names(y$frame) + z_cnms = names(z) + + # numbers of types of output columns + n_common = length(by.x) + n_x_only = length(x_cnms) - n_common + n_y_only = length(y_cnms) - n_common + + # column names in the output categorized + # by contributing input table(s) + z_common = z_cnms[seq_len(n_common)] + z_x_only = z_cnms[n_common + seq_len(n_x_only)] + z_y_only = z_cnms[n_common + n_x_only + seq_len(n_y_only)] + + # column names in the input tables, ordered + # as they are in the output table + x_z_order = c(by.x, x_cnms[!x_cnms %in% by.x]) + y_z_order = c(by.y, y_cnms[!y_cnms %in% by.y]) + + # these can be used to update the column map, + # by looping through the existing map and + # translating to the new column names with these + # x|y_map vectors. of course you will also need + # to concatenate the column maps first + x_map = setNames(c(z_common, z_x_only), x_z_order) + y_map = setNames(c(z_common, z_y_only), y_z_order) + + x_cmap = x$column_map + y_cmap = y$column_map + for (tnm in names(x_cmap)) { + for (cnm in names(x_cmap[[tnm]])) { + old_cnm = x_cmap[[tnm]][[cnm]] + x_cmap[[tnm]][[cnm]] = x_map[[old_cnm]] + } + } + for (tnm in names(y_cmap)) { + for (cnm in names(y_cmap[[tnm]])) { + old_cnm = y_cmap[[tnm]][[cnm]] + y_cmap[[tnm]][[cnm]] = y_map[[old_cnm]] + } + } + + z_column_map = c(x_cmap, y_cmap) + z_reference_index_list = c(x$reference_index_list, y$reference_index_list) + z_lab_names_list = c(x$labelling_column_names_list, y$labelling_column_names_list) + + ## ---- + ## wrap up the result with provenance-preserving column map + ## ---- + Ledger( + z, + z_column_map, + z_reference_index_list, + z_lab_names_list + ) +} + +filter_by_list = function(x_orig, y_orig, by_list) { + l = list() + nms = names(by_list) + for (i in seq_along(by_list)) { + nm_i = nms[[i]] + nms_i = to_names(nm_i) + if ((nms_i[[1L]] %in% x_orig) & (nms_i[[2L]] %in% y_orig)) { + l[[nm_i]] = by_list[[nm_i]] + } + } + l +} + +#' @export +init_merge = function(frame, dimension_name, reference_index, labelling_column_names) { + Ledger(frame + , initial_column_map(names(frame), dimension_name) + , initial_reference_index_list(reference_index, dimension_name) + , initial_labelling_column_names_list(labelling_column_names, dimension_name) + ) +} + +split_by = function(by, table_origins) UseMethod("split_by") + +#' @export +split_by.character = function(by, table_origins) { + by = to_names(by) + orig = to_names(table_origins) + list(by, by) |> setNames(orig) +} + +#' @export +split_by.formula = function(by, table_origins) { + orig = to_names(table_origins) + list( + to_names(lhs(by)[[2L]]), + to_names(rhs(by)[[2L]]) + ) |> setNames(orig) +} + + + + +apply_col_map = function(map, orig_table_nm, by) { + map[[orig_table_nm]][by] |> unlist(use.names = FALSE) +} + +## @param x Ledger object +## @param col_nm Name of a column to check for implicit provenance +is_provenance_implicit = function(x, col_nm) { + (x$column_map + |> lapply(getElement, col_nm) + |> Filter(f = Negate(is.null)) + |> vapply(`==`, logical(1L), col_nm) + ) +} + +## @param x Ledger object +explicit_provenance = function(x, col_nm) { + m = x$column_map + implicit = is_provenance_implicit(x, col_nm) + if (length(implicit) == 0L) { + msg_colon( + msg("Column", col_nm, "not found in any of the original tables"), + msg_indent(names(m)) + ) |> stop() + } + if (!any(implicit)) return(x) + + tabs_to_fix = (implicit + |> Filter(f = isTRUE) + |> names() + ) + + f = x$frame + l = x$labelling_column_names_list + ii = x$reference_index_list + + orig_col = f[[col_nm]] + for (tab_nm in tabs_to_fix) { + new_col_nm = sprintf("%s:%s", col_nm, tab_nm) + f[[new_col_nm]] = orig_col + m[[tab_nm]][[col_nm]] = new_col_nm + } + f[[col_nm]] = NULL + + ## frame, column_map, reference_index_list, labelling_column_names_list + Ledger(f, m, ii, l) +} + + +merge_generic_by_util = function(x, y, ...) { + by = filter_by_list( + names(x$column_map), + names(y$column_map), + list(...) + ) + by = mapply(split_by + , by + , names(by) + , SIMPLIFY = FALSE + , USE.NAMES = FALSE + ) |> setNames(names(by)) + by.x = character() + by.y = character() + for (nm in names(by)) { + orig = names(by[[nm]]) + by.x = append( + by.x, + apply_col_map(x$column_map, orig[[1L]], by[[nm]][[1L]]) + ) + by.y = append( + by.y, + apply_col_map(y$column_map, orig[[2L]], by[[nm]][[2L]]) + ) + } + by = data.frame(x = by.x, y = by.y) |> unique() + + dup.x = duplicated(by$x) + dup.y = duplicated(by$y) + if (any(dup.x) | any(dup.y)) { + if (any(dup.x)) { + cols_to_fix = by$x[dup.x] + for (col_nm in cols_to_fix) { + x = explicit_provenance(x, col_nm) + } + } + if (any(dup.y)) { + cols_to_fix = by$y[dup.y] + for (col_nm in cols_to_fix) { + y = explicit_provenance(y, col_nm) + } + } + merge_generic_by_util(x, y, ...) + } else { + merge_util(x, y, by$x, by$y) + } +} + +#' @export +as.data.frame.Ledger = function(x, row.names = NULL, optional = FALSE, ...) { + x$labels_frame() +} + +#' @export +summary.Ledger = function(object, ...) { + formats = c("name", "combined") + structure( + sapply(formats, link_format_picker, x = object, simplify = FALSE, USE.NAMES = TRUE), + class = "summary.Ledger" + ) +} + +#' @export +print.summary.Ledger = function(x, ...) { + msg_hline() |> message() + msg( + "Ledger object from macpan2 describing", + "an aspect of model shape" + ) |> message() + msg_hline() |> message() + print(x$name) + print(x$combined, row.names = FALSE) +} + +#' @export +names.Ledger = function(x) names(x$frame) + +#' @export +labelling_column_names.Ledger = function(x) x$labelling_column_names_list + + +link_format_picker = function(x + , format = c("labels", "link", "combined", "separate") + ) { + switch(match.arg(format) + , labels = x$labels_frame() + , link = x$frame + , combined = cbind(x$labels_frame(), x$frame) + , separate = x$frame_list() + ) +} + +#' @export +print.Ledger = function(x + , format = c("labels", "link", "combined", "separate") + , ... + ) { + x = link_format_picker(x, format) + print(x, row.names = FALSE, ...) +} + +#' @export +head.Ledger = function(x + , n = 6L + , format = c("labels", "link", "combined", "separate") + , ... + ) { + x = link_format_picker(x, format) + if (format == "separate") { + return(lapply(x, head, n = n, ...)) + } else { + return(head(x, n = n, ...)) + } +} + +#' @export +tail.Ledger = function(x + , n = 6L + , format = c("labels", "link", "combined", "separate") + , ... + ) { + x = link_format_picker(x, format) + if (format == "separate") { + return(lapply(x, tail, n = n, ...)) + } else { + return(tail(x, n = n, ...)) + } +} + +#' @export +str.Ledger = function(x + , format = c("labels", "link", "combined", "separate") + , ... +) { + x = link_format_picker(x, format) + str(x, ...) +} + + +#' @export +LedgerList = function() { + self = Base() + self$list = list() |> setNames(as.character()) + self$add = function(new_link, ...) { + new_ledgers = list(...) + if (missing(new_link)) { + macpan2:::valid$named_list$check(new_ledgers) + } else if(length(new_ledgers) == 0L) { + new_nm = deparse1(substitute(new_link)) + new_ledgers = setNames(list(new_link), new_nm) + } else { + stop("If supplying more than one join result, please name them with {name} = {join_result}") + } + + for (nm in names(new_ledgers)) { + if (nm %in% names(self$list)) { + msg( + "Join result", nm, "is already in the list.", + "Overwriting the existing one." + ) |> message() + } + self$list[[nm]] = new_ledgers[[nm]] + } + } + return_object(self, "LedgerList") +} diff --git a/R/mp_labels.R b/R/mp_labels.R index e1adf5b6..20da3709 100644 --- a/R/mp_labels.R +++ b/R/mp_labels.R @@ -8,3 +8,8 @@ mp_labels.Index = function(x, labelling_column_names) { if (missing(labelling_column_names)) return(x$labels()) x$partial_labels(labelling_column_names) } + +#' @export +mp_labels.Ledger = function(x, labelling_column_names) { + x$labels_for[[labelling_column_names]]() +} diff --git a/R/obj_utils.R b/R/obj_utils.R deleted file mode 100644 index bd1c09fb..00000000 --- a/R/obj_utils.R +++ /dev/null @@ -1,3 +0,0 @@ -method_names = function(x) { - names(which(unlist(eapply(x, is.function)))) -} diff --git a/R/test_utils.R b/R/test_utils.R deleted file mode 100644 index 7ed44f59..00000000 --- a/R/test_utils.R +++ /dev/null @@ -1 +0,0 @@ -constant_named_vector = function(const, nms) setNames(rep(const, length(nms)), nms) diff --git a/R/vec_utils.R b/R/vec_utils.R index b1d1fd80..e479be86 100644 --- a/R/vec_utils.R +++ b/R/vec_utils.R @@ -20,8 +20,3 @@ mp_zero_vector.Index = function(x, labelling_column_names, ...) { |> zero_vector() ) } - -#' @export -mp_labels.Ledger = function(x, labelling_column_names) { - x$labels_for[[labelling_column_names]]() -} diff --git a/man/FlowExpander.Rd b/man/FlowExpander.Rd deleted file mode 100644 index 3691f4d9..00000000 --- a/man/FlowExpander.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/flow_expander.R -\name{FlowExpander} -\alias{FlowExpander} -\title{Flow Expander} -\usage{ -FlowExpander(model) -} -\arguments{ -\item{model}{Object constructed with \code{\link{ModelFiles}} or -\code{\link{ModelCollection}}.} -} -\description{ -Flow Expander -} diff --git a/man/LedgerDefinition.Rd b/man/LedgerDefinition.Rd index 73f87631..f67dddfa 100644 --- a/man/LedgerDefinition.Rd +++ b/man/LedgerDefinition.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/link.R +% Please edit documentation in R/mp_join.R \name{LedgerDefinition} \alias{LedgerDefinition} \title{Ledgers} diff --git a/man/Simulators.Rd b/man/Simulators.Rd deleted file mode 100644 index c9d06cbd..00000000 --- a/man/Simulators.Rd +++ /dev/null @@ -1,55 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/simulators.R -\name{Simulators} -\alias{Simulators} -\title{Simulators} -\usage{ -Simulators(model) -} -\arguments{ -\item{model}{A \code{\link{Compartmental}} model.} -} -\value{ -An object with the following methods. -\subsection{Methods}{ - -\if{html}{\out{
      }}\preformatted{$tmb(time_steps - , state, rate - , ... - , .mats_to_save = .mats_to_return - , .mats_to_return = "state" - , .dimnames = list() -) -}\if{html}{\out{
      }} -} - -\subsection{Method Arguments}{ -\itemize{ -\item \code{time_steps}: The number of time steps run in each simulation. -\item \code{state}: Named numerical vector giving the default initial value of the -state vector. The names must be equal to \code{model$state_labels()}. -\item \code{flow}: Named numerical vector giving the default initial value of the -flow vector. The names must be equal to \code{model$flow_labels()}. -\item \code{...}: Named numerical objects that can be coerced to a matrix, giving -the matrices required by the model. -\item \code{.mats_to_save}: Character vector of names of matrices to save. Defaults -to \code{"state"}, which is the state vector. Other useful names include -\code{"total_inflow"} (the incidence associated with each state variable) -and \code{"total_outflow"} (the total leaving each state variable at each -time step). One may also add any variable in \code{model$other_labels()}. -See \code{\link{MatsList}} for details. -\item \code{.mats_to_return}: Character vector of names of matrices to return. Defaults -to \code{"state"}, which is the state vector. Other useful names include -\code{"total_inflow"} (the incidence associated with each state variable) -and \code{"total_outflow"} (the total leaving each state variable at each -time step). One may also add any variable in \code{model$other_labels()}. -See \code{\link{MatsList}} for details. -\item \code{.dimnames}: Advanced and often not necessary. See -\code{\link{MatsList}} for details. -} -} -} -\description{ -Adapt a \code{\link{Compartmental}} model to a simulation engine. Currently -TMB is the only simulation engine. -} diff --git a/man/integration_methods.Rd b/man/integration_methods.Rd deleted file mode 100644 index 44e6b317..00000000 --- a/man/integration_methods.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/simulation_methods.R -\name{integration_methods} -\alias{integration_methods} -\alias{Euler} -\alias{RK4} -\title{Integration Methods} -\usage{ -Euler(model_simulator) - -RK4(model_simulator) -} -\arguments{ -\item{model_simulator}{Object of class \code{\link{TMBSimulator}}.} -} -\description{ -Functions to pass as \code{integration_method}s to \code{\link{SimulatorConstructor}}. -} -\section{Functions}{ -\itemize{ -\item \code{Euler()}: Euler method. - -\item \code{RK4()}: Runge Kutta 4 method. - -}} diff --git a/R/connection.R b/misc/old-r-source/connection.R similarity index 100% rename from R/connection.R rename to misc/old-r-source/connection.R diff --git a/R/flow_expander.R b/misc/old-r-source/flow_expander.R similarity index 100% rename from R/flow_expander.R rename to misc/old-r-source/flow_expander.R diff --git a/R/model_products.R b/misc/old-r-source/model_products.R similarity index 100% rename from R/model_products.R rename to misc/old-r-source/model_products.R diff --git a/R/simulation_methods.R b/misc/old-r-source/simulation_methods.R similarity index 100% rename from R/simulation_methods.R rename to misc/old-r-source/simulation_methods.R diff --git a/R/simulators.R b/misc/old-r-source/simulators.R similarity index 100% rename from R/simulators.R rename to misc/old-r-source/simulators.R From 375c502ee1b2a80ac8246bbc1736d18060cca98b Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 22:02:11 -0500 Subject: [PATCH 321/332] #155 local coverage at 63% -- maybe good enough for now? --- NAMESPACE | 10 +- R/labelled_partitions.R | 58 --- R/mp_index.R | 33 ++ R/mp_library.R | 39 ++ R/names_and_labels.R | 13 +- R/variables.R | 147 +------ man/Model.Rd | 14 - man/ModelCollection.Rd | 36 -- man/ModelFiles.Rd | 39 -- man/Namer.Rd | 15 - man/StringData.Rd | 13 +- man/model_starter.Rd | 2 +- man/mp_cartesian.Rd | 1 + man/mp_index.Rd | 1 + man/mp_rename.Rd | 23 ++ man/mp_subset.Rd | 1 + man/mp_union.Rd | 1 + man/read_partition.Rd | 15 - misc/experiments/refactorcpp.R | 20 +- {R => misc/old-r-source}/Inputs.R | 0 {R => misc/old-r-source}/mapping.R | 0 .../old-r-source}/model_data_structure.R | 0 {R => misc/old-r-source}/model_files.R | 0 {R => misc/old-r-source}/namer.R | 0 tests/testthat/test-basis-functions.R | 20 + tests/testthat/test-by-running-library.R | 13 + tests/testthat/test-convolution.R | 15 + tests/testthat/test-diag.R | 13 + tests/testthat/test-dynamic-model.R | 373 ++++++++++++++++++ tests/testthat/test-kronecker.R | 8 + tests/testthat/test-model-structure.R | 38 ++ tests/testthat/test-opt-params.R | 23 ++ tests/testthat/test-random-effects.R | 0 .../test-simple-structured-model-specs.R | 46 +++ tests/testthat/test-time-step.R | 4 + tests/testthat/test-validity.R | 1 + 36 files changed, 683 insertions(+), 352 deletions(-) delete mode 100644 man/Model.Rd delete mode 100644 man/ModelCollection.Rd delete mode 100644 man/ModelFiles.Rd delete mode 100644 man/Namer.Rd create mode 100644 man/mp_rename.Rd delete mode 100644 man/read_partition.Rd rename {R => misc/old-r-source}/Inputs.R (100%) rename {R => misc/old-r-source}/mapping.R (100%) rename {R => misc/old-r-source}/model_data_structure.R (100%) rename {R => misc/old-r-source}/model_files.R (100%) rename {R => misc/old-r-source}/namer.R (100%) create mode 100644 tests/testthat/test-basis-functions.R create mode 100644 tests/testthat/test-by-running-library.R create mode 100644 tests/testthat/test-diag.R create mode 100644 tests/testthat/test-dynamic-model.R create mode 100644 tests/testthat/test-kronecker.R create mode 100644 tests/testthat/test-model-structure.R create mode 100644 tests/testthat/test-opt-params.R create mode 100644 tests/testthat/test-random-effects.R create mode 100644 tests/testthat/test-simple-structured-model-specs.R create mode 100644 tests/testthat/test-validity.R diff --git a/NAMESPACE b/NAMESPACE index 1e2474ed..012c256d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -130,11 +130,7 @@ export(MathExpressionFromStrings) export(MatsList) export(MethList) export(Method) -export(Model) -export(ModelCollection) -export(ModelFiles) export(NULLReader) -export(Namer) export(ObjectiveFunction) export(OptParamsList) export(Partition) @@ -155,7 +151,6 @@ export(VectorList) export(all_consistent) export(all_equal) export(all_not_equal) -export(atomic_partition) export(by_) export(cartesian) export(cartesian_self) @@ -199,6 +194,7 @@ export(mp_library) export(mp_linear) export(mp_lookup) export(mp_reference) +export(mp_rename) export(mp_report) export(mp_set_numbers) export(mp_setdiff) @@ -219,10 +215,7 @@ export(mp_zero_vector) export(nlist) export(not_all_equal) export(parse_expr_list) -export(partition) export(rbf) -export(read_frame) -export(read_partition) export(reader_spec) export(show_models) export(simple_sims) @@ -257,7 +250,6 @@ importFrom(oor,return_facade) importFrom(oor,return_object) importFrom(stats,as.formula) importFrom(stats,na.omit) -importFrom(stats,setNames) importFrom(tools,file_ext) importFrom(utils,head) importFrom(utils,read.table) diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 88ce96f0..3263a61c 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -134,60 +134,6 @@ Partition = memoise(Partition) #' @export names.Partition = function(x) x$names() -CompartmentalPartition = function(frame - , special_partitions = c(vec = "Vec", type = "Type") - ) { - special_partitions = c(vec, type) - if (!all(special_partitions %in% names(frame))) { - msg_break( - msg_colon( - msg_csv( - "The vec or type arguments", - special_partitions, - "are not in the names of the frame"), - msg_indent(names(frame)) - ) - ) - } - self = Base() - self$partition = Partition(frame) - self$vec = "Vec" - self$state = function() vec(self, "state", self$vec) - self$flow = function() vec(self, "flow", self$vec) - return_object(self, "CompartmentalPartition") -} - -#' Read Partition -#' -#' Read a CSV file in as a \code{\link{Partition}}. -#' -#' @param ... File path components to pass to \code{\link{CSVReader}}, and -#' subsequently to \code{\link{file.path}}. -#' -#' @export -read_partition = function(...) read_frame(...) |> Partition() - -#' @export -read_frame = function(...) CSVReader(...)$read() - -#' @export -partition = function(...) data.frame(...) |> Partition() - -#' @export -atomic_partition = function(state_variables, flow_rates, partition_name) { - l = list( - c(state_variables, flow_rates), - c( - rep("state", length(state_variables)), - rep("flow", length(flow_rates)) - ) - ) - (l - |> setNames(c(partition_name, "Vec")) - |> as.data.frame() - |> CompartmentalPartition() - ) -} vec = function(x, vec_name, vec_partition = "Vec") { x$filter(vec_name, .wrt = vec_partition)$select_out(vec_partition) @@ -280,10 +226,6 @@ union_vars = function(...) { y } -renew_part = function(x, vec_parts) { - if (length(vec_parts) == 1L) x = CompartmentalPartition(x, vec_parts) - x -} vec_part_names = function(...) { (list(...) diff --git a/R/mp_index.R b/R/mp_index.R index 82ec3908..55bc12f5 100644 --- a/R/mp_index.R +++ b/R/mp_index.R @@ -335,3 +335,36 @@ mp_extract.ModelDefRun = function(x, dimension_name) { mp_indices = function(x, table) { match(x, table) - 1L } + + +#' Rename Index Columns +#' +#' @param ... Name-value pairs. The name gives the new name and the value +#' is a character vector giving the old name. +#' +#' @family indexes +#' @export +mp_rename = function(x, ...) { + l = list(...) + new_nms = names(l) + old_nms = unlist(l, recursive = FALSE, use.names = FALSE) + f = x$partition$frame() + labs = x$labelling_column_names + i = match(old_nms, names(f)) + if (any(is.na(i))) { + msg_break( + msg_colon( + "Attempted to replace the following names that do not exist", + msg_indent(old_nms[is.na(i)]) + ), + msg_colon( + "These are the only names that are available", + msg_indent(names(f)) + ) + ) |> stop() + } + j = match(old_nms, labs) + names(f)[i] = new_nms + labs[j[!is.na(j)]] = new_nms[!is.na(j)] + Index(f, labelling_column_names = labs) +} diff --git a/R/mp_library.R b/R/mp_library.R index a4acfd04..3cfbd950 100644 --- a/R/mp_library.R +++ b/R/mp_library.R @@ -17,3 +17,42 @@ mp_tmb_library = function(..., package = NULL) { ) def_env$spec } + +## TODO: change model_starter to mp_model_starter + +#' Model Starter +#' +#' Create a directory with a template model definition. +#' +#' @param starter_name Currently can only be \code{sir}. +#' @param dir String giving the path to a directory for copying the +#' template model definition. +#' +#' @export +model_starter = function(starter_name, dir) { + starter_dir = system.file("starter_models" + , starter_name + , package = "macpan2" + ) + starter_files = list.files(starter_dir) + required_files = c( + tmb_engine_file = "tmb.R" + ) + if (!all(required_files %in% starter_files)) { + stop("Could not find a valid starter model by that name.") + } + + starter_paths = setNames( + file.path(starter_dir, required_files), + names(required_files) + ) + + if (dir.exists(dir)) stop("Directory for the model already exists.") + dir.create(dir, recursive = TRUE) + + file.copy(starter_paths, dir) + + ## TODO: handle the multi-engine case + ## TODO: implement proper file update monitoring (e.g. Files objects) + mp_tmb_library(dir) +} diff --git a/R/names_and_labels.R b/R/names_and_labels.R index 9a3d5fdb..d59fc512 100644 --- a/R/names_and_labels.R +++ b/R/names_and_labels.R @@ -470,17 +470,20 @@ StringUndottedData = function(labels, names) { #' String Data #' -#' Create objects for representing names and labels in a compartmental +#' Create objects for representing names and labels in a dynamical #' model. #' #' @examples -#' path = system.file("starter_models", "seir_symp_vax", package = "macpan2") -#' model = ModelFiles(path) -#' vars = StringDataFromFrame(model$variables()) +#' vars = (mp_cartesian( +#' mp_index(Epi = c("S", "I", "R")) +#' , mp_index(Age = c("young", "old")) +#' ) +#' |> as.data.frame() +#' |> StringDataFromFrame() +#' ) #' vars #' vars$dot() #' -#' #' @name StringData NULL diff --git a/R/variables.R b/R/variables.R index 8474d8b3..2216f25b 100644 --- a/R/variables.R +++ b/R/variables.R @@ -1,55 +1,4 @@ - -Variables = function(model) { - self = Base() - self$model = model - self$all = function() Partition(self$model$def$variables()) - self$.type = function(type) { - labels_this_type = self$model$settings$variable(type) - var_part = self$model$settings$var_part() - wrt = self$model$settings$name() - if (is.null(var_part)) { - vars = self$all()$filter_ordered(labels_this_type, .wrt = wrt) - } else { - vars = self$all()$filter(labels_this_type, .wrt = var_part) - } - vars - # if (length(var_nms) == 0L) { - # warning( - # "\nThere are no ", - # gsub("_", " ", type), - # " variables", - # "\nin this model." - # ) - # return(NULL) - # } - } - self$flow = function() self$.type("flow") - self$state = function() self$.type("state") - self$infectious_state = function() self$.type("infectious_state") - self$infected_state = function() self$.type("infected_state") - self$infection_flow = function() self$.type("infection_flow") - self$other = function() { - # TODO: A better way to handle the NULL case would make it possible - # to return a null Partition object. This is currently not possible - # for 'technical' reasons. - if (length(self$model$labels$other()) == 0L) { - warning( - "\nThere are no other variables in this model", - "\nexcept for state and flow variables." - ) - return(NULL) - } - s = self$model$def$settings() - self$all()$filter_out( - c(s$flow_variables, s$state_variables), - .wrt = s$required_partitions - ) - } - self$.all_types = function() setdiff(ls(self), c("cache", "model")) - initialize_cache(self, "all") - return_object(self, "Variables") -} - +## for back-compat NullLabels = function() { self = Base() self$variables = NULL ## TODO: should be NullVariables, which doesn't yet exist @@ -63,97 +12,3 @@ NullLabels = function() { initialize_cache(self, "all", "flow", "state", "infectious_state", "infection_flow", "other") return_object(self, "NullLabels") } - -VariableLabels = function(variables) { - self = Base() - self$variables = variables - self$.type = function(type) { - v = self$variables[[type]]() - if (is.null(v)) return(v) - v$labels() - } - self$component_list = function() { - list( - state = self$state(), - flow = self$flow() - ) - } - self$rp = function() self$variables$model$settings$names() - self$all = function() self$variables$all()$select(self$rp())$labels() - self$flow = function() self$variables$flow()$select(self$rp())$labels() - self$state = function() self$variables$state()$select(self$rp())$labels() - self$infectious_state = function() self$variables$infectious_state()$select(self$rp())$labels() - self$infected_state = function() self$variables$infected_state()$select(self$rp())$labels() - self$infection_flow = function() self$variables$infection_flow()$select(self$rp())$labels() - self$other = function() setdiff(self$all(), c(self$state(), self$flow())) - initialize_cache(self, "all", "flow", "state", "infectious_state", "infection_flow", "other", "rp") - return_object(self, "VariableLabels") -} - -VariableIndices = function(labels) { - self = Base() - self$flow = FlowTypeIndices(labels) - self$transmission = TransmissionIndices(labels) - return_object(self, "VariableIndices") -} - -IndexUtilities = function(labels) { - self = Base() - self$labels = labels - self$variables = labels$variables - self$model = labels$variables$model - self$match = function(...) match(...) - 1L # -1 for zero-based C++ indexing - self$.state = function() self$labels$state() - self$.flow = function() self$labels$flow() - self$.make_flow_method = function(flow_component, flow_type, vector_name) { - force(flow_type); force(flow_component); force(vector_name) - function() { - f = self$model$flows_expanded() - flow_labels = f[[flow_component]][f$type == flow_type] - #if (length(flow_labels) == 0L) return(integer(0L)) - self$match(flow_labels, self$labels[[vector_name]]()) - } - } - self$flow_types = c( - "per_capita", "per_capita_inflow", "per_capita_outflow", - "absolute", "absolute_inflow", "absolute_outflow" - ) - return_object(self, "IndexUtilities") -} - -FlowIndices = function(labels, type) { - self = IndexUtilities(labels) - self$type = type - self$from = self$.make_flow_method("from", type, "state") - self$to = self$.make_flow_method("to", type, "state") - self$flow = self$.make_flow_method("flow", type, "flow") - initialize_cache(self, "from", "to", "flow") - return_object(self, "FlowIndices") -} - -FlowTypeIndices = function(labels) { - self = IndexUtilities(labels) - for (type in self$flow_types) self[[type]] = FlowIndices(labels, type) - self$invalidate = function() { - for (type in self$flow_types) self[[type]]$cache$invalidate() - } - return_object(self, "FlowTypeIndices") -} - -TransmissionIndices = function(labels) { - self = IndexUtilities(labels) - self$infectious_state = function() { - self$match( - self$labels$infectious_state(), - self$.state() - ) - } - self$infection_flow = function() { - self$match( - self$labels$infection_flow(), - self$.flow() - ) - } - return_object(self, "TransmissionIndices") -} - diff --git a/man/Model.Rd b/man/Model.Rd deleted file mode 100644 index e262656f..00000000 --- a/man/Model.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/model_data_structure.R -\name{Model} -\alias{Model} -\title{Model} -\usage{ -Model(definition) -} -\arguments{ -\item{definition}{Output of \code{\link{ModelFiles}}.} -} -\description{ -Construct an object for representing a model structure. -} diff --git a/man/ModelCollection.Rd b/man/ModelCollection.Rd deleted file mode 100644 index 018f877d..00000000 --- a/man/ModelCollection.Rd +++ /dev/null @@ -1,36 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/model_files.R -\name{ModelCollection} -\alias{ModelCollection} -\title{Model Collection} -\usage{ -ModelCollection(variables, derivations, flows, settings) -} -\arguments{ -\item{variables}{Return value of the \code{variables} method in a -\code{\link{ModelFiles}} object.} - -\item{derivations}{Return value of the \code{derivations} method in a -\code{\link{ModelFiles}} object.} - -\item{flows}{Return value of the \code{flows} method in a -\code{\link{ModelFiles}} object.} - -\item{settings}{Return value of the \code{settings} method in a -\code{\link{ModelFiles}} object.} -} -\value{ -An object of class \code{ModelCollection} with the following -methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$variables$all()} -- -\item \verb{$derivations()} -\item \verb{$flows()} -\item \verb{$settings()} -} -} -} -\description{ -A model definition that is untied from a set of \code{\link{ModelFiles}}. -} diff --git a/man/ModelFiles.Rd b/man/ModelFiles.Rd deleted file mode 100644 index d0fd7aee..00000000 --- a/man/ModelFiles.Rd +++ /dev/null @@ -1,39 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/model_files.R -\name{ModelFiles} -\alias{ModelFiles} -\title{Model Files} -\usage{ -ModelFiles( - model_directory, - csv_reader = CSVReader, - json_reader = JSONReader, - txt_reader = TXTReader -) -} -\arguments{ -\item{model_directory}{String giving a path to a directory containing -the following files, \code{variables.csv}, \code{derivations.json}, \code{flows.csv}, -and \code{settings.json}, described by -\href{https://canmod.net/misc/model_definitions}{this spec}.} - -\item{csv_reader}{Class inheriting from \code{\link{Reader}} for reading -csv files.} - -\item{json_reader}{Class inheriting from \code{\link{Reader}} for reading -json files.} - -\item{txt_reader}{Class inheriting from \code{\link{Reader}} for reading -txt files.} -} -\description{ -Construct objects for accessing and caching model definition files. -} -\examples{ -d = system.file("starter_models", "seir_symp_vax", package = "macpan2") -m = ModelFiles(d) -m$flows() -expander = FlowExpander(m) -expander$expand_flows() - -} diff --git a/man/Namer.Rd b/man/Namer.Rd deleted file mode 100644 index b7881c64..00000000 --- a/man/Namer.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/namer.R -\name{Namer} -\alias{Namer} -\title{Namer} -\usage{ -Namer() -} -\value{ -An object of class \code{Namer} with two methods public, -\code{names} and \code{partitions}, which are inverses of each other. -} -\description{ -Name model variables and parse names into labelled partitions. -} diff --git a/man/StringData.Rd b/man/StringData.Rd index afbbedcb..184623ff 100644 --- a/man/StringData.Rd +++ b/man/StringData.Rd @@ -26,7 +26,7 @@ the elements of the columns.} \item{...}{Not used but present for S3 method consistency.} } \description{ -Create objects for representing names and labels in a compartmental +Create objects for representing names and labels in a dynamical model. } \section{Methods (by generic)}{ @@ -45,11 +45,14 @@ partition labels. }} \examples{ -path = system.file("starter_models", "seir_symp_vax", package = "macpan2") -model = ModelFiles(path) -vars = StringDataFromFrame(model$variables()) +vars = (mp_cartesian( + mp_index(Epi = c("S", "I", "R")) + , mp_index(Age = c("young", "old")) + ) + |> as.data.frame() + |> StringDataFromFrame() +) vars vars$dot() - } diff --git a/man/model_starter.Rd b/man/model_starter.Rd index 70108784..0833b76c 100644 --- a/man/model_starter.Rd +++ b/man/model_starter.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/model_data_structure.R +% Please edit documentation in R/mp_library.R \name{model_starter} \alias{model_starter} \title{Model Starter} diff --git a/man/mp_cartesian.Rd b/man/mp_cartesian.Rd index 2baa9e4e..ca0ab229 100644 --- a/man/mp_cartesian.Rd +++ b/man/mp_cartesian.Rd @@ -43,6 +43,7 @@ mp_union( \seealso{ Other functions that return index tables \code{\link{mp_index}()}, +\code{\link{mp_rename}()}, \code{\link{mp_subset}()}, \code{\link{mp_union}()} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index 1e5c6b63..621fde74 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -155,6 +155,7 @@ mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) Other functions that return index tables \code{\link{mp_cartesian}()}, +\code{\link{mp_rename}()}, \code{\link{mp_subset}()}, \code{\link{mp_union}()} } diff --git a/man/mp_rename.Rd b/man/mp_rename.Rd new file mode 100644 index 00000000..20dd037e --- /dev/null +++ b/man/mp_rename.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_index.R +\name{mp_rename} +\alias{mp_rename} +\title{Rename Index Columns} +\usage{ +mp_rename(x, ...) +} +\arguments{ +\item{...}{Name-value pairs. The name gives the new name and the value +is a character vector giving the old name.} +} +\description{ +Rename Index Columns +} +\seealso{ +Other functions that return index tables +\code{\link{mp_cartesian}()}, +\code{\link{mp_index}()}, +\code{\link{mp_subset}()}, +\code{\link{mp_union}()} +} +\concept{indexes} diff --git a/man/mp_subset.Rd b/man/mp_subset.Rd index ec4e6b92..5ec150ab 100644 --- a/man/mp_subset.Rd +++ b/man/mp_subset.Rd @@ -26,6 +26,7 @@ match a certain criterion and \code{mp_setdiff} gives rows that do not match. Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, +\code{\link{mp_rename}()}, \code{\link{mp_union}()} } \concept{indexes} diff --git a/man/mp_union.Rd b/man/mp_union.Rd index 9f934ea1..ed1f1cd3 100644 --- a/man/mp_union.Rd +++ b/man/mp_union.Rd @@ -16,6 +16,7 @@ Union of Indexes Other functions that return index tables \code{\link{mp_cartesian}()}, \code{\link{mp_index}()}, +\code{\link{mp_rename}()}, \code{\link{mp_subset}()} } \concept{indexes} diff --git a/man/read_partition.Rd b/man/read_partition.Rd deleted file mode 100644 index 7b39a8d8..00000000 --- a/man/read_partition.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/labelled_partitions.R -\name{read_partition} -\alias{read_partition} -\title{Read Partition} -\usage{ -read_partition(...) -} -\arguments{ -\item{...}{File path components to pass to \code{\link{CSVReader}}, and -subsequently to \code{\link{file.path}}.} -} -\description{ -Read a CSV file in as a \code{\link{Partition}}. -} diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 37a0b5ad..9497bdd9 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -209,7 +209,7 @@ mp_join( ## structured model indices ------------- - +state_sir = mp_index(Epi = c("S", "I", "R")) state = (state_sir |> mp_cartesian(cities) |> mp_cartesian(age) @@ -242,12 +242,12 @@ lambda = mp_subset(flow_rates, Epi = "lambda") ## create vectors for particular indexes ----------- -state_vector = Vector(state) +state_vector = mp_vector(state) state_vector$ set_numbers(Epi = c(I = 1))$ set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) -N = Vector(strata) -flow_rate_vector = Vector(flow_rates) +N = mp_vector(strata) +flow_rate_vector = mp_vector(flow_rates) flow_rate_vector$set_numbers(Epi = c(lambda = 0.15, gamma = 0.1, mu = 0.01)) ## links between entries in the indexes ----------- @@ -357,12 +357,14 @@ if (FALSE) { mp_index(EpiB = paste(letters[1:25], letters[2:26], sep = "")) ) |> mp_cartesian(cities) flow = mp_join( - from = mp_subset(state, EpiA = "S"), - to = mp_subset(state, EpiA = "I"), + from = mp_subset(state_sir, EpiA = "A"), + to = mp_subset(state_sir, EpiA = "B"), rate = mp_subset(flow_rates, EpiA = "gh"), - from.to = "Loc", - from.rate = "Loc", - to.rate = "Loc" + by = list( + from.to = "Loc", + from.rate = "Loc", + to.rate = "Loc" + ) ) flow } diff --git a/R/Inputs.R b/misc/old-r-source/Inputs.R similarity index 100% rename from R/Inputs.R rename to misc/old-r-source/Inputs.R diff --git a/R/mapping.R b/misc/old-r-source/mapping.R similarity index 100% rename from R/mapping.R rename to misc/old-r-source/mapping.R diff --git a/R/model_data_structure.R b/misc/old-r-source/model_data_structure.R similarity index 100% rename from R/model_data_structure.R rename to misc/old-r-source/model_data_structure.R diff --git a/R/model_files.R b/misc/old-r-source/model_files.R similarity index 100% rename from R/model_files.R rename to misc/old-r-source/model_files.R diff --git a/R/namer.R b/misc/old-r-source/namer.R similarity index 100% rename from R/namer.R rename to misc/old-r-source/namer.R diff --git a/tests/testthat/test-basis-functions.R b/tests/testthat/test-basis-functions.R new file mode 100644 index 00000000..f97389bb --- /dev/null +++ b/tests/testthat/test-basis-functions.R @@ -0,0 +1,20 @@ +test_that("radial basis functions are correctly computed", { + correct_rbf = structure( + c( + 1, 0.923116346386636, 0.726149037073691, 0.486752255959972, + 0.278037300453194, 0.135335283236613, 0.0561347628341337, + 0.0198410947443703, 0.00597602289500594, 0.00153381067932446, + 0.486752255959972, 0.726149037073691, 0.923116346386636, 1, + 0.923116346386636, 0.726149037073691, 0.486752255959972, + 0.278037300453194, 0.135335283236613, 0.0561347628341337, + 0.0561347628341337, 0.135335283236613, 0.278037300453194, + 0.486752255959972, 0.726149037073691, 0.923116346386636, 1, + 0.923116346386636, 0.726149037073691, 0.486752255959972, + 0.00153381067932446, 0.00597602289500594, 0.0198410947443703, + 0.0561347628341337, 0.135335283236613, 0.278037300453194, + 0.486752255959972, 0.726149037073691, 0.923116346386636, 1 + ) + , dim = c(10L, 4L) + ) + expect_equal(rbf(10, 4), correct_rbf) +}) diff --git a/tests/testthat/test-by-running-library.R b/tests/testthat/test-by-running-library.R new file mode 100644 index 00000000..5eb82b6c --- /dev/null +++ b/tests/testthat/test-by-running-library.R @@ -0,0 +1,13 @@ +library(macpan2) +model_names = show_models()$dir +for (m in model_names) { + print("========") + print(m) + calibration_file = system.file("starter_models" + , m + , "calibration_example.R" + , package = "macpan2" + ) + if (file.exists(calibration_file)) source(calibration_file) +} + \ No newline at end of file diff --git a/tests/testthat/test-convolution.R b/tests/testthat/test-convolution.R index e69de29b..aaf644ac 100644 --- a/tests/testthat/test-convolution.R +++ b/tests/testthat/test-convolution.R @@ -0,0 +1,15 @@ +## not done by any stretch + +kernel = c(0.5, 0.25, 0.25) +1 %*% kernel[1] +1:2 %*% kernel[1:2] +1:3 %*% kernel +2:4 %*% kernel +simple_sims( + list( + x ~ x + 1, + y ~ convolution(x, kernel) + ), + time_steps = 10, + mats = list(x = 0, y = empty_matrix, kernel = kernel) +) |> macpan2:::filter(time != 0, time != 11, matrix == "y") diff --git a/tests/testthat/test-diag.R b/tests/testthat/test-diag.R new file mode 100644 index 00000000..86d854a1 --- /dev/null +++ b/tests/testthat/test-diag.R @@ -0,0 +1,13 @@ +test_that("diagonals are properly set and extracted", { + A = matrix(1:9, 3, 3) + b = diag(A) + B = diag(b) + expect_equal( + engine_eval(~from_diag(A), A = A), + matrix(diag(A)) + ) + expect_equal( + engine_eval(~to_diag(from_diag(A)), A = A), + B + ) +}) diff --git a/tests/testthat/test-dynamic-model.R b/tests/testthat/test-dynamic-model.R new file mode 100644 index 00000000..6cdd4bf3 --- /dev/null +++ b/tests/testthat/test-dynamic-model.R @@ -0,0 +1,373 @@ +## FIXME: this is just a hack to get the coverage up so that +## i can better understand what needs to be retained and what +## is out-of-date. but for now we have just copied the code +## from the engine_agnostic_grammar vignette. + +## ----setup, echo = FALSE, message = FALSE, warning = FALSE-------------------- +library(macpan2) +library(ggplot2) +library(dplyr) + + +## ----SIR-starter, echo = FALSE------------------------------------------------ +## helper function to simplify the exposition in this vigette ----------- +SIR_starter <- function( + # index tables for model quantities + state, + rate, + # ledgers tabulating the use of different functional forms + flow, # list of individual ledgers + force_of_infection +){ + + ## Set up expressions list for each functional form -------------- + ## names refer to when the calculation gets performed relative to + ## the simulation time-step loop (before, during, ...) + expr_list <- mp_expr_list( + before = list( + ## aggregations + N ~ sum(state) + ), + during = list( + ## force of infections + rate[infection_flow_rates] ~ + state[infectious_states] * rate[transmission_rates] / N + + ## unsigned individual flows + , flow_per_time ~ state[from_states] * rate[flow_rates] + + ## state update + , total_inflow ~ group_sums(flow_per_time, to_states, state) + , total_outflow ~ group_sums(flow_per_time, from_states, state) + , state ~ state + total_inflow - total_outflow + ) + ) + + ## Ledgers for each specific calculation -------------- + ledgers <- list( + flow = mp_ledgers(flow), + force_of_infection = mp_ledgers(force_of_infection) + ) + + ## Initialize vectors from index tables (with all zeros for values) -------------- + # used as placeholders for user input + init_vecs <- list( + state = mp_vector(state), + rate = mp_vector(rate) + ) + + ## Initialize model object ----------------- + mp_dynamic_model( + expr_list = expr_list, + ledgers = ledgers, + init_vecs = init_vecs + ) +} + + +## ----sir-index-tables--------------------------------------------------------- +## index tables to label model quantities ------------------------- +state <- mp_index(Epi = c("S", "I", "R")) +rate <- mp_index(Epi = c("beta", "gamma", "lambda")) + + +## ----sir-state-and-rate------------------------------------------------------- +state +rate + + +## ----sir-infection-ledger----------------------------------------------------- +## infection ledger ------------------------- +infection <- mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") +) + + +## ----sir-infection-ledger-inputs---------------------------------------------- +mp_subset(state, Epi = "S") +mp_subset(state, Epi = "I") +mp_subset(rate, Epi = "lambda") + + +## ----sir-infection-ledger-2--------------------------------------------------- +infection + + +## ----sir-recovery-ledger------------------------------------------------------ +## recovery ledger ------------------------- +recovery <- mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) + +recovery + + +## ----sir-foi-ledger----------------------------------------------------------- +## force of infection ledger ------------------------- +# infection additionally involves the calculation of a force of infection +force_of_infection <- mp_join( + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta"), + infection_flow_rates = mp_subset(rate, Epi = "lambda") +) + + +## ----sir---------------------------------------------------------------------- +## SIR model object ------------------------- +sir <- SIR_starter( + # index tables + state = state, + rate = rate, + # ledgers + flow = list( + infection, + recovery + ), + force_of_infection = force_of_infection +) + + +## ----sir-simulator------------------------------------------------------------ +## SIR model simulator ------------------------- +sir_simulator <- mp_tmb_simulator( + dynamic_model = sir, + vectors = list( + state = c(S = 999, I = 1, R = 0), + rate = c(beta = 0.25, gamma = 0.1) + ), + time_steps = 100 +) + + +## ----sir-results-------------------------------------------------------------- +## SIR model simulation results ------------------------- +sir_results <- mp_report(sir_simulator) + + +## ----sir-results-head--------------------------------------------------------- +head(sir_results) + + +## ----sir-ggplot-example, fig.width = 6, fig.height = 4------------------------ +(sir_results + |> filter(matrix == "state") # keep just the state variables at each point in time + |> mutate(state = factor(row, levels = c("S", "I", "R"))) # to enforce logical state ordering in legend + |> ggplot(aes(time, value, colour = state)) + + geom_line() +) + + +## ----pivot_wider-------------------------------------------------------------- +sir_results_wide <- (sir_results + |> dplyr::filter(matrix == "state") # keep state variables at each point in time + ## drop unneeded columns before pivoting + |> dplyr::select(-c(matrix, col)) + |> tidyr::pivot_wider(id_cols = time, names_from = row) +) + +head(sir_results_wide, n = 3) + + +## ----sir-base-plot-ex, fig.width = 6------------------------------------------ +with(sir_results_wide, + plot(x = time, + y = I, + type = "l") +) + + +## ----sir-base-matplot-ex, fig.width = 6--------------------------------------- +par(las = 1) ## horizontal y-axis ticks +matplot(sir_results_wide[, 1], + sir_results_wide[,-1], + type = "l", + xlab = "time", ylab = "") +legend("left", col = 1:3, lty = 1:3, legend = state$labels()) + + +## ----strain-indices----------------------------------------------------------- +Strain_indices <- c("A", "B") + + +## ----strain-expand-I---------------------------------------------------------- +I_indices <- mp_cartesian( + mp_subset(state, Epi = "I"), + mp_index(Strain = Strain_indices) +) + +I_indices + + +## ----two-strain-state--------------------------------------------------------- +state <- mp_union( + mp_subset(state, Epi = "S"), + I_indices, + mp_subset(state, Epi = "R") +) + +state + + +## ----two-strain-rate---------------------------------------------------------- +rate <- + mp_union( + # stratify rates involved in the infection process by strain + mp_cartesian( + mp_subset(rate, Epi = c("beta", "lambda")), + mp_index(Strain = Strain_indices) + ), + # recovery rate will be the same across strains + mp_subset(rate, Epi = "gamma") +) + +rate + + +## ----two-strain-infection-default--------------------------------------------- +# infection ledger from before +mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda") +) + + +## ----two-strain-infection-ledger---------------------------------------------- +## new infection ledger ------------------------- +infection <- mp_join( + from_states = mp_subset(state, Epi = "S"), + to_states = mp_subset(state, Epi = "I"), + flow_rates = mp_subset(rate, Epi = "lambda"), + by = list( + to_states.flow_rates = "Strain" + ) +) + +infection + + +## ----two-strain-recovery-ledger----------------------------------------------- +recovery <- mp_join( + from_states = mp_subset(state, Epi = "I"), + to_states = mp_subset(state, Epi = "R"), + flow_rates = mp_subset(rate, Epi = "gamma") +) +recovery + + +## ----two-strain-foi-default--------------------------------------------------- +mp_join( + infection_flow_rates = mp_subset(rate, Epi = "lambda"), + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta") +) + + +## ----two-strain-foi-ledger---------------------------------------------------- +## new force of infection ledger ------------------------- +force_of_infection <- mp_join( + infection_flow_rates = mp_subset(rate, Epi = "lambda"), + infectious_states = mp_subset(state, Epi = "I"), + transmission_rates = mp_subset(rate, Epi = "beta"), + by = list( + infection_flow_rates.infectious_states = "Strain", # first pairwise join + infectious_states.transmission_rates = "Strain" # second pairwise join + ) +) + +force_of_infection + + +## ----two-strain-results, fig.width = 6, fig.height = 4------------------------ +two_strain_model <- SIR_starter( + # index tables + state = state, + rate = rate, + # ledgers + flow = list( + infection, + recovery + ), + force_of_infection = force_of_infection +) + +two_strain_simulator <- mp_tmb_simulator( + dynamic_model = two_strain_model, + vectors = list( + state = c(S = 998, I.A = 1, I.B = 1, R = 0), + rate = c(beta.A = 0.25, gamma = 0.1, beta.B = 0.2) + ), + time_steps = 100 +) + +two_strain_results <- (mp_report(two_strain_simulator) + |> filter(matrix == "state") +) + +levels <- unique(two_strain_results$row) # get state variables in the desired order + +(two_strain_results # keep state variables at each point in time + |> mutate(state = factor(row, levels = levels)) # to enforce logical state ordering in plot + |> ggplot(aes(time, value, colour = state)) + + geom_line() +) + + +## ----sir-starter-print-------------------------------------------------------- +## helper function to simplify the exposition in this vigette ----------- +SIR_starter <- function( + # index tables for model quantities + state, + rate, + # ledgers tabulating the use of different functional forms + flow, # list of individual ledgers + force_of_infection +){ + + ## Set up expressions list for each functional form -------------- + ## names refer to when the calculation gets performed relative to + ## the simulation time-step loop (before, during, ...) + expr_list <- mp_expr_list( + before = list( + ## aggregations + N ~ sum(state) + ), + during = list( + ## force of infections + rate[infection_flow_rates] ~ + state[infectious_states] * rate[transmission_rates] / N + + ## unsigned individual flows + , flow_per_time ~ state[from_states] * rate[flow_rates] + + ## state update + , total_inflow ~ group_sums(flow_per_time, to_states, state) + , total_outflow ~ group_sums(flow_per_time, from_states, state) + , state ~ state + total_inflow - total_outflow + ) + ) + + ## Ledgers for each specific calculation -------------- + ledgers <- list( + flow = mp_ledgers(flow), + force_of_infection = mp_ledgers(force_of_infection) + ) + + ## Initialize vectors from index tables (with all zeros for values) -------------- + # used as placeholders for user input + init_vecs <- list( + state = mp_vector(state), + rate = mp_vector(rate) + ) + + ## Initialize model object ----------------- + mp_dynamic_model( + expr_list = expr_list, + ledgers = ledgers, + init_vecs = init_vecs + ) +} diff --git a/tests/testthat/test-kronecker.R b/tests/testthat/test-kronecker.R new file mode 100644 index 00000000..389c3c7d --- /dev/null +++ b/tests/testthat/test-kronecker.R @@ -0,0 +1,8 @@ +test_that("kronecker product works", { + x = matrix(1:12, 3, 4) + y = matrix(1:8, 4, 2) + expect_equal( + engine_eval(~ x %x% y, x = x, y = y), + x %x% y + ) +}) diff --git a/tests/testthat/test-model-structure.R b/tests/testthat/test-model-structure.R new file mode 100644 index 00000000..c1047e78 --- /dev/null +++ b/tests/testthat/test-model-structure.R @@ -0,0 +1,38 @@ +library(macpan2) + +Epi = mp_index(Epi = c("S", "I", "R")) +Age = mp_index(Age = c("young", "old")) + +state = mp_cartesian(Epi, Age) +flow = mp_union( + mp_cartesian(mp_index(Epi = "foi"), Age) + , mp_index(Epi = "gamma") +) + +N = mp_aggregate(state, by = "Age") + +infection = mp_join( + from = mp_subset(state, Epi = "S") + , to = mp_subset(state, Epi = "I") + , rate = mp_subset(flow, Epi = "foi") + , by = list( + from.to = "Age" + , from.rate = "Age" + ) +) + +recovery = mp_join( + from = mp_subset(state, Epi = "I") + , to = mp_subset(state, Epi = "R") + , rate = mp_subset(flow, Epi = "gamma") + , by = list(from.to = "Age") +) + +A = mp_square(state) +weird_join_test = mp_join( + a = mp_subset(A, EpiA = "R") + , b = mp_subset(A, EpiA = "I") + , c = mp_subset(A, EpiA = "I") + , by = list(c.a = c("AgeA", "AgeB"), a.b = c("AgeA", "AgeB"), b.c = "AgeA") +) +weird_join_test diff --git a/tests/testthat/test-opt-params.R b/tests/testthat/test-opt-params.R new file mode 100644 index 00000000..0e2d93a9 --- /dev/null +++ b/tests/testthat/test-opt-params.R @@ -0,0 +1,23 @@ +test_that("parameters are not initially empty", { + params = OptParamsList(0 + , par_id = 0L + , mat = "x" + , row_id = 0L + , col_id = 0L + ) + init_mats = MatsList( + x = empty_matrix + , y = empty_matrix + , .mats_to_save = "y" + , .mats_to_return = "y" + ) + expr_list = ExprList(before = list(y ~ x + 1)) + expect_error( + TMBModel( + init_mats = init_mats, + expr_list = expr_list, + params = params + ), + regexp = "optimization parameters are not consistent with matrices" + ) +}) diff --git a/tests/testthat/test-random-effects.R b/tests/testthat/test-random-effects.R new file mode 100644 index 00000000..e69de29b diff --git a/tests/testthat/test-simple-structured-model-specs.R b/tests/testthat/test-simple-structured-model-specs.R new file mode 100644 index 00000000..1b3d32fd --- /dev/null +++ b/tests/testthat/test-simple-structured-model-specs.R @@ -0,0 +1,46 @@ +test_that("tmb model specs can vectorize state updates with indices", { + state_labels = c("S", "I", "R") + flow = data.frame( + rate = c("infection", "recovery") + , from = c("S" , "I" ) + , to = c("I" , "R" ) + ) + sir = mp_tmb_model_spec( + before = list( + init = state[S] ~ N - 1 + , state[I] ~ 1 + , state[R] ~ 0 + ) + , during = list( + flow_rate[infection] ~ beta * state[S] * state[I] / N + , flow_rate[recovery] ~ gamma * state[I] + , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) + ) + , default = list( + state = mp_zero_vector(state_labels) + , flow_rate = mp_zero_vector(flow$rate) + , N = 100 + , beta = 0.25 + , gamma = 0.1 + ) + , integers = list( + from = mp_indices(flow$from, state_labels) + , to = mp_indices(flow$to , state_labels) + ) + ) + print(sir) + #mp_extract_exprs(sir, "init") + + correct_trajectory = structure(list(matrix = c("state", "state", "state", "state", +"state", "state", "state", "state", "state", "state"), time = 1:10, + row = c("I", "I", "I", "I", "I", "I", "I", "I", "I", "I"), + col = c("", "", "", "", "", "", "", "", "", ""), value = c(1.1475, + 1.316046234375, 1.50841667298164, 1.72768475405867, 1.97722773451576, + 2.26072663861586, 2.58215441123499, 2.94574812507613, 3.35596031449616, + 3.81738379261492)), row.names = c(NA, -10L), class = "data.frame") + actual_trajectory = (sir + |> mp_simulator(time_steps = 10, outputs = "I") + |> mp_trajectory() + ) + expect_equal(actual_trajectory, correct_trajectory) +}) diff --git a/tests/testthat/test-time-step.R b/tests/testthat/test-time-step.R index a2b7360a..5781ac42 100644 --- a/tests/testthat/test-time-step.R +++ b/tests/testthat/test-time-step.R @@ -8,4 +8,8 @@ test_that("time_step function returns a valid time-step no matter what", { c(0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 6), m$ad_fun()$report()$values[,5] ) + expect_error( + simple_sims(list(x ~ time_step(-4)), 10, mats = list(x = 0)), + regexp = "Time lag needs to be non-negative" + ) }) diff --git a/tests/testthat/test-validity.R b/tests/testthat/test-validity.R new file mode 100644 index 00000000..d6c7e2eb --- /dev/null +++ b/tests/testthat/test-validity.R @@ -0,0 +1 @@ +valid = macpan2:::TMBAdaptorValidity() From df723c088237d777cfbb1b587fcf87d28df3cd32 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Wed, 17 Jan 2024 22:18:23 -0500 Subject: [PATCH 322/332] gh actions --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index beca668c..e12e6558 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,7 +38,8 @@ Suggests: numDeriv, parallel Remotes: - canmod/oor + canmod/oor, + canmod/macpan2helpers LinkingTo: TMB, RcppEigen, From f8b3c4e386022f229150b23f01ee7e85d5ee9643 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 04:26:54 -0500 Subject: [PATCH 323/332] gh actions --- .github/workflows/R-CMD-check.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index d83877ad..5f60bd2b 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -18,15 +18,16 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + # - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} # - {os: macos-latest, r: 'release'} # - {os: windows-latest, r: 'release'} - # - {os: ubuntu-latest, r: 'release'} # - {os: ubuntu-latest, r: 'oldrel-1'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} R_KEEP_PKG_SOURCE: yes + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true steps: - uses: actions/checkout@v3 From 8d5aafb90b2cc76452a336d9b81d813c7a65980e Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 04:46:39 -0500 Subject: [PATCH 324/332] gh actions --- .github/workflows/R-CMD-check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 5f60bd2b..930346a1 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -49,6 +49,7 @@ jobs: with: extra-packages: any::rcmdcheck needs: check + upgrade: true - uses: r-lib/actions/check-r-package@v2 with: From 77e5f7d1e52ebde12c64758a883032bae44f3eb7 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 04:48:52 -0500 Subject: [PATCH 325/332] gh actions --- .github/workflows/R-CMD-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 930346a1..e6392935 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -2,7 +2,7 @@ # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help on: push: - branches: [main, master, refactorcpp] + branches: [main, master] pull_request: branches: [main, master] @@ -49,7 +49,7 @@ jobs: with: extra-packages: any::rcmdcheck needs: check - upgrade: true + upgrade: 'TRUE' - uses: r-lib/actions/check-r-package@v2 with: From 0c54f8894868fe1be59e65bac5d883adb5681f47 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 09:34:49 -0500 Subject: [PATCH 326/332] #155 --- NAMESPACE | 21 +- R/calibration.R | 4 + R/{case_changes.R => case_utils.R} | 0 R/component_deps.R | 8 + R/const_int_vec.R | 2 +- R/engine_eval.R | 2 +- R/engine_methods.R | 2 +- R/enum_methods.R | 6 +- R/expr_list.R | 107 ++++--- R/formula.R | 129 ++++----- R/formula_data.R | 15 +- R/frame_formatter.R | 14 - R/frame_utils.R | 19 +- R/index_to_tmb.R | 2 +- R/labelled_partitions.R | 2 +- R/lists.R | 2 +- R/mats_list.R | 3 +- R/mp_dynamic_model.R | 2 +- R/mp_group.R | 2 +- R/mp_join.R | 12 +- R/mp_library.R | 2 +- R/mp_tmb_model_spec.R | 187 +++++++++++++ R/parse_expr.R | 15 +- R/tmb_model.R | 261 +++--------------- R/validity.R | 4 - R/vec_utils.R | 10 +- R/vector.R | 18 +- inst/model_library/sir/model_structure.R | 2 +- inst/model_library/sir_age/model_structure.R | 2 +- man/EngineMethods.Rd | 21 -- man/IndexedExpressions.Rd | 29 -- man/IntVecs.Rd | 14 - man/LedgerDefinition.Rd | 5 +- man/MatsList.Rd | 3 +- man/MethodPrototype.Rd | 23 -- man/MethodTypeUtils.Rd | 12 - man/TMBModel.Rd | 96 ------- man/TMBSimulator.Rd | 50 ---- man/empty_matrix.Rd | 15 +- man/mk_meth_cls.Rd | 17 -- man/mp_ledgers.Rd | 6 +- man/{ExprList.Rd => mp_tmb_expr_list.Rd} | 6 +- man/mp_tmb_model_spec.Rd | 49 ++++ man/mp_vector.Rd | 4 +- man/mp_zero_vector.Rd | 16 ++ misc/build/method_head.R | 6 +- misc/experiments/refactorcpp.R | 16 +- {R => misc/old-r-source}/model_def_run.R | 0 tests/testthat/test-c.R | 6 +- .../testthat/test-dimnames-and-shape-change.R | 12 +- tests/testthat/test-dynamic-model.R | 4 +- tests/testthat/test-error-row.R | 4 +- tests/testthat/test-expr-list.R | 14 +- tests/testthat/test-literals.R | 4 +- tests/testthat/test-opt-params.R | 4 +- tests/testthat/test-rbind-time-lag.R | 4 +- tests/testthat/test-rcbind.R | 8 +- .../test-simple-structured-model-specs.R | 12 +- tests/testthat/test-time-step.R | 4 +- tests/testthat/test-tmb-model-spec.R | 17 ++ tests/testthat/test-tmb-model.R | 6 +- tests/testthat/test-tmb.R | 4 +- vignettes/engine_agnostic_grammar.Rmd | 4 +- vignettes/tmb_model.Rmd | 6 +- 64 files changed, 589 insertions(+), 767 deletions(-) rename R/{case_changes.R => case_utils.R} (100%) delete mode 100644 R/frame_formatter.R create mode 100644 R/mp_tmb_model_spec.R delete mode 100644 man/EngineMethods.Rd delete mode 100644 man/IndexedExpressions.Rd delete mode 100644 man/IntVecs.Rd delete mode 100644 man/MethodPrototype.Rd delete mode 100644 man/MethodTypeUtils.Rd delete mode 100644 man/TMBModel.Rd delete mode 100644 man/TMBSimulator.Rd delete mode 100644 man/mk_meth_cls.Rd rename man/{ExprList.Rd => mp_tmb_expr_list.Rd} (97%) create mode 100644 man/mp_tmb_model_spec.Rd create mode 100644 man/mp_zero_vector.Rd rename {R => misc/old-r-source}/model_def_run.R (100%) create mode 100644 tests/testthat/test-tmb-model-spec.R diff --git a/NAMESPACE b/NAMESPACE index 012c256d..47e53fef 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,10 +3,6 @@ S3method(Index,Index) S3method(Index,Partition) S3method(Index,data.frame) -S3method(StructuredVector,Index) -S3method(StructuredVector,StructuredVector) -S3method(StructuredVector,data.frame) -S3method(StructuredVector,numeric) S3method(as.data.frame,Index) S3method(as.data.frame,Ledger) S3method(as.matrix,StructuredVector) @@ -19,13 +15,11 @@ S3method(col_labels,character) S3method(head,Ledger) S3method(labelling_column_names,Index) S3method(labelling_column_names,Ledger) -S3method(labels,Compartmental) S3method(labels,DynamicModel) S3method(labels,Index) S3method(labels,LabelsDynamic) S3method(labels,LabelsScripts) S3method(labels,ModelDefRun) -S3method(labels,TMBCompartmentalSimulator) S3method(labels,TMBDynamicSimulator) S3method(labels,VariableLabels) S3method(length,StructuredVector) @@ -113,15 +107,9 @@ export(BinaryOperator) export(CSVReader) export(Collection) export(DynamicModel) -export(EngineMethods) -export(ExprList) export(Files) -export(Formula) export(Index) -export(IndexedExpressions) -export(IntVecs) export(JSONReader) -export(LedgerList) export(Log) export(LogFile) export(Logit) @@ -140,10 +128,6 @@ export(RReader) export(Reader) export(StringDataFromDotted) export(StringDataFromFrame) -export(StructuredVector) -export(TMBModel) -export(TMBModelSpec) -export(TMBSimulator) export(TXTReader) export(Time) export(Transform) @@ -177,14 +161,12 @@ export(mp_decompose) export(mp_default) export(mp_dynamic_model) export(mp_expr_group_sum) -export(mp_expr_list) export(mp_extract) export(mp_extract_exprs) export(mp_factors) export(mp_final) export(mp_group) export(mp_index) -export(mp_indexed_exprs) export(mp_indices) export(mp_initial) export(mp_join) @@ -204,6 +186,7 @@ export(mp_square) export(mp_subset) export(mp_symmetric) export(mp_test_tmb) +export(mp_tmb_expr_list) export(mp_tmb_library) export(mp_tmb_model_spec) export(mp_tmb_simulator) @@ -250,7 +233,9 @@ importFrom(oor,return_facade) importFrom(oor,return_object) importFrom(stats,as.formula) importFrom(stats,na.omit) +importFrom(stats,setNames) importFrom(tools,file_ext) importFrom(utils,head) importFrom(utils,read.table) +importFrom(utils,tail) useDynLib(macpan2) diff --git a/R/calibration.R b/R/calibration.R index 60d47b9c..4177f43f 100644 --- a/R/calibration.R +++ b/R/calibration.R @@ -1,3 +1,7 @@ +## Internal classes that handle optimization. Objects of +## these classes are in the `optimize` and `optimization_history` +## fields of `TMBSimulator` objects. + TMBOptimizer = function(simulator) { self = Base() self$simulator = simulator diff --git a/R/case_changes.R b/R/case_utils.R similarity index 100% rename from R/case_changes.R rename to R/case_utils.R diff --git a/R/component_deps.R b/R/component_deps.R index 95bbc175..1780f208 100644 --- a/R/component_deps.R +++ b/R/component_deps.R @@ -1,3 +1,11 @@ +## Internal classes that handle dependency management. Complex +## objects (e.g. those of TMBModel class) contain a number of +## component objects. When one of these components becomes modified, +## this should signal that other components also get modified. +## These Dependencies and Refresher classes automate the process +## of finding dependencies and ensuring that appropriate refreshing +## happens. + Dependencies = function(container, ...) { self = Base() self$container = container diff --git a/R/const_int_vec.R b/R/const_int_vec.R index 788a9742..ca933c33 100644 --- a/R/const_int_vec.R +++ b/R/const_int_vec.R @@ -3,7 +3,7 @@ #' Make a list of integer vectors available for engine methods. #' #' @param ... Named arguments, each of which can be coerced to an integer vector. -#' @export +#' @noRd IntVecs = function(...) { self = Base() diff --git a/R/engine_eval.R b/R/engine_eval.R index 639ab926..58bc192b 100644 --- a/R/engine_eval.R +++ b/R/engine_eval.R @@ -33,7 +33,7 @@ #' , y = pi #' , .matrix_to_return = "x" #' ) -#' @importFrom stats as.formula +#' @importFrom stats as.formula setNames engine_eval = function(e, ..., .matrix_to_return, .tmb_cpp = getOption("macpan2_dll"), .structure_labels = NullLabels()) { dot_mats = list(...) diff --git a/R/engine_methods.R b/R/engine_methods.R index 111dc57f..2522dacb 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -10,7 +10,7 @@ #' @param int_vecs An \code{\link{IntVecs}} object containing integer vectors #' that can be used by the methods. #' -#' @export +#' @noRd EngineMethods = function(exprs = list(), int_vecs = IntVecs()) { self = Base() diff --git a/R/enum_methods.R b/R/enum_methods.R index 0cbd3497..aad4cfc6 100644 --- a/R/enum_methods.R +++ b/R/enum_methods.R @@ -13,7 +13,7 @@ #' @param int_vec_arg_nms Character vector naming the integer-vector-valued #' arguments. #' -#' @nord +#' @noRd MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { self = Base() self$formula = formula @@ -64,7 +64,7 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { #' @param cls_nm Character string giving the name of the class. #' @param meth_type_id Integer giving the associated ID of the method type. #' -#' @nord +#' @noRd mk_meth_cls = function(cls_nm, meth_type_id) { pf = parent.frame() force(pf) @@ -83,7 +83,7 @@ mk_meth_cls = function(cls_nm, meth_type_id) { #' This class is here so that `MethodTypes`, which is automatically generated #' from the C++ code, can inherit from it. #' -#' @nord +#' @noRd MethodTypeUtils = function() { self = Base() diff --git a/R/expr_list.R b/R/expr_list.R index 89d0936e..627988ac 100644 --- a/R/expr_list.R +++ b/R/expr_list.R @@ -79,59 +79,6 @@ ExprListUtils = function() { return_object(self, "ExprListUtils") } -#' Expression List -#' -#' Create a list of expressions for defining a compartmental model in TMB. -#' -#' @param before List of formulas to be evaluated in the order provided before -#' the simulation loop begins. Each \code{\link{formula}} must have a left hand -#' side that gives the name of the matrix being updated, and a right hand side -#' giving an expression containing only the names of matrices in the model, -#' functions defined in \code{macpan2.cpp}, and numerical literals (e.g. -#' \code{3.14}). The available functions are described in -#' \code{\link{engine_functions}}. Names can be provided for the components of -#' \code{before}, and these names do not have to be unique. These names are -#' used by the \code{.simulate_exprs} argument. -#' @param during List of formulas to be evaluated at every iteration of the -#' simulation loop, with the same rules as \code{before}. -#' @param after List of formulas to be evaluated after the simulation loop, -#' with the same rules as \code{before}. -#' @param .simulate_exprs Character vector of names of expressions to be -#' evaluated within TMB simulate blocks. This is useful when an expression -#' cannot be evaluated during the computation of the objective function and -#' its gradients (e.g. if the expression contains randomness or other -#' discontinuities that will break the automatic differentiation machinery -#' of TMB). -#' -#' @return Object of class \code{ExprList} with the following methods. -#' -#' ## Methods -#' -#' * `$data_arg(...)`: Return the following components of the data structure -#' to pass to C++. -#' * `expr_output_id` -- Indices into the list of matrices identifying the -#' matrix being produced. -#' * `expr_sim_block` -- Identified whether or not the expression should be -#' evaluated inside a simulate macro within TMB. -#' * `expr_num_p_table_rows` -- Number of rows associated with each -#' expression in the parse table (`p_table_*`) -#' * `eval_schedule` -- Vector giving the number of expressions to evaluate -#' in each phase (before, during, or after) of the simulation. -#' * `p_table_x` -- Parse table column giving an index for looking up either -#' function, matrix, or literal. -#' * `p_table_n` -- Parse table column giving the number of arguments in -#' functions. -#' * `p_table_i` -- Parse table column giving indices for looking up the -#' rows in the parse table corresponding with the first argument of the -#' function. -#' -#' ## Method Arguments -#' -#' * `...`: Character vector containing the names of the matrices in the model. -#' -#' -#' @importFrom oor method_apply -#' @export ExprList = function( before = list() , during = list() @@ -281,8 +228,60 @@ ExprList = function( return_object(self, "ExprList") } +#' Expression List +#' +#' Create a list of expressions for defining a compartmental model in TMB. +#' +#' @param before List of formulas to be evaluated in the order provided before +#' the simulation loop begins. Each \code{\link{formula}} must have a left hand +#' side that gives the name of the matrix being updated, and a right hand side +#' giving an expression containing only the names of matrices in the model, +#' functions defined in \code{macpan2.cpp}, and numerical literals (e.g. +#' \code{3.14}). The available functions are described in +#' \code{\link{engine_functions}}. Names can be provided for the components of +#' \code{before}, and these names do not have to be unique. These names are +#' used by the \code{.simulate_exprs} argument. +#' @param during List of formulas to be evaluated at every iteration of the +#' simulation loop, with the same rules as \code{before}. +#' @param after List of formulas to be evaluated after the simulation loop, +#' with the same rules as \code{before}. +#' @param .simulate_exprs Character vector of names of expressions to be +#' evaluated within TMB simulate blocks. This is useful when an expression +#' cannot be evaluated during the computation of the objective function and +#' its gradients (e.g. if the expression contains randomness or other +#' discontinuities that will break the automatic differentiation machinery +#' of TMB). +#' +#' @return Object of class \code{ExprList} with the following methods. +#' +#' ## Methods +#' +#' * `$data_arg(...)`: Return the following components of the data structure +#' to pass to C++. +#' * `expr_output_id` -- Indices into the list of matrices identifying the +#' matrix being produced. +#' * `expr_sim_block` -- Identified whether or not the expression should be +#' evaluated inside a simulate macro within TMB. +#' * `expr_num_p_table_rows` -- Number of rows associated with each +#' expression in the parse table (`p_table_*`) +#' * `eval_schedule` -- Vector giving the number of expressions to evaluate +#' in each phase (before, during, or after) of the simulation. +#' * `p_table_x` -- Parse table column giving an index for looking up either +#' function, matrix, or literal. +#' * `p_table_n` -- Parse table column giving the number of arguments in +#' functions. +#' * `p_table_i` -- Parse table column giving indices for looking up the +#' rows in the parse table corresponding with the first argument of the +#' function. +#' +#' ## Method Arguments +#' +#' * `...`: Character vector containing the names of the matrices in the model. +#' +#' +#' @importFrom oor method_apply #' @export -mp_expr_list = ExprList +mp_tmb_expr_list = ExprList #' @export print.ExprList = function(x, ...) x$print_exprs() diff --git a/R/formula.R b/R/formula.R index fd5b1b3a..4f161bd7 100644 --- a/R/formula.R +++ b/R/formula.R @@ -1,64 +1,65 @@ -#' @export -Formula = function(formula, name = "", mats_list = MatsList()) { - self = Base() - self$formula = formula - self$name = name - self$method_types = MethodTypes() - self$lhs = function() lhs(self$formula) - self$rhs = function() rhs(self$formula) - self$could_be_getter = function() { - self$method_types$could_make_method(self$rhs()) - } - self$could_be_setter = function() { - self$method_types$could_make_method(self$formula) - } - self$method_name = function() { - pt = method_parser(self$lhs()) - name = paste0(rev(pt$x[pt$n == 0]), collapse = "_") - if (self$could_be_setter()) { - return(sprintf("set_%s", name)) - } - sprintf("get_%s", name) - } - self$setter = function() { - try(self$method_types$make_method(self$formula), silent = TRUE) - } - self$getter = function() { - try(self$method_types$make_method(self$rhs()), silent = TRUE) - } - self$mat_args = function() { - meth = self$setter() - if (inherits(meth, "try-error")) meth = self$getter() - if (!inherits(meth, "try-error")) return(meth$mat_args()) - c(mat_vec_nms(self$rhs()), mat_vec_nms(self$lhs())) |> unique() - } - self$int_vec_args = function() { - meth = self$setter() - if (inherits(meth, "try-error")) meth = self$getter() - if (!inherits(meth, "try-error")) return(meth$int_vec_args()) - character(0L) - } - self$expr_list = function() { - if (self$could_be_setter()) { - f = two_sided("dummy", self$method_name()) - } else if (self$could_be_getter()) { - f = two_sided(lhs_char(self$formula), self$method_name()) - } else if (is_two_sided(self$formula)) { - f = to_assign(self$formula) - } else { - stop("Invalid formula") - } - setNames(list(f), self$name) - } - self$meth_list = function() { - if (self$could_be_setter()) { - f = self$formula - } else if (self$could_be_getter()) { - f = self$rhs() - } else { - return(list()) - } - setNames(list(f), self$method_name()) - } - return_object(self, "Formula") -} +# ## not used -- but don't quite want to remove from the R folder +# +# Formula = function(formula, name = "", mats_list = MatsList()) { +# self = Base() +# self$formula = formula +# self$name = name +# self$method_types = MethodTypes() +# self$lhs = function() lhs(self$formula) +# self$rhs = function() rhs(self$formula) +# self$could_be_getter = function() { +# self$method_types$could_make_method(self$rhs()) +# } +# self$could_be_setter = function() { +# self$method_types$could_make_method(self$formula) +# } +# self$method_name = function() { +# pt = method_parser(self$lhs()) +# name = paste0(rev(pt$x[pt$n == 0]), collapse = "_") +# if (self$could_be_setter()) { +# return(sprintf("set_%s", name)) +# } +# sprintf("get_%s", name) +# } +# self$setter = function() { +# try(self$method_types$make_method(self$formula), silent = TRUE) +# } +# self$getter = function() { +# try(self$method_types$make_method(self$rhs()), silent = TRUE) +# } +# self$mat_args = function() { +# meth = self$setter() +# if (inherits(meth, "try-error")) meth = self$getter() +# if (!inherits(meth, "try-error")) return(meth$mat_args()) +# c(mat_vec_nms(self$rhs()), mat_vec_nms(self$lhs())) |> unique() +# } +# self$int_vec_args = function() { +# meth = self$setter() +# if (inherits(meth, "try-error")) meth = self$getter() +# if (!inherits(meth, "try-error")) return(meth$int_vec_args()) +# character(0L) +# } +# self$expr_list = function() { +# if (self$could_be_setter()) { +# f = two_sided("dummy", self$method_name()) +# } else if (self$could_be_getter()) { +# f = two_sided(lhs_char(self$formula), self$method_name()) +# } else if (is_two_sided(self$formula)) { +# f = to_assign(self$formula) +# } else { +# stop("Invalid formula") +# } +# setNames(list(f), self$name) +# } +# self$meth_list = function() { +# if (self$could_be_setter()) { +# f = self$formula +# } else if (self$could_be_getter()) { +# f = self$rhs() +# } else { +# return(list()) +# } +# setNames(list(f), self$method_name()) +# } +# return_object(self, "Formula") +# } diff --git a/R/formula_data.R b/R/formula_data.R index 2aae6d4c..017f2c84 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -47,10 +47,10 @@ LedgerData = function(...) { return_object(self, "LedgerData") } -#' #' @export -#' print.LedgerData = function(x, ...) { -#' print(x$frame, row.names = FALSE) -#' } + +# print.LedgerData = function(x, ...) { +# print(x$frame, row.names = FALSE) +# } #' Bundle up Ledgers #' @@ -72,7 +72,7 @@ mp_ledgers = function(...) { } -#' Indexed Expressions +#' Indexed Expressions (not currently used -- experimental) #' #' @param ... Formula objects that reference the columns in the #' \code{index_data}, the vectors in \code{vector_list} and the matrices @@ -83,7 +83,7 @@ mp_ledgers = function(...) { #' @param unstructured_matrix_list Named list of objects that can be coerced #' to a matrix. #' -#' @export +#' @noRd IndexedExpressions = function(... , index_data , vector_list = list() @@ -99,7 +99,7 @@ IndexedExpressions = function(... } self$mats_list = function() { all_vars = (self$formulas - |> lapply(macpan2:::formula_components) + |> lapply(formula_components) |> lapply(getElement, "variables") |> unlist(use.names = FALSE, recursive = FALSE) |> unique() @@ -130,6 +130,5 @@ IndexedExpressions = function(... } -#' @export mp_indexed_exprs = IndexedExpressions diff --git a/R/frame_formatter.R b/R/frame_formatter.R deleted file mode 100644 index 3b6e129b..00000000 --- a/R/frame_formatter.R +++ /dev/null @@ -1,14 +0,0 @@ -frame_formatter = function(frame) { - make_underline = function(n) paste0(rep("-", times = n), collapse = "") - cnames = names(frame) - underlines = lapply(lapply(cnames, nchar), make_underline) - frame = rbind( - setNames(as.data.frame(as.list(cnames)), cnames), - setNames(as.data.frame(underlines), cnames), - frame - ) - l = lapply(frame, as.character) - widths = lapply(lapply(l, nchar), max) - fixed_width_list = mapply(format, l, width = widths, MoreArgs = list(justify = "left"), SIMPLIFY = FALSE, USE.NAMES = FALSE) - paste0(do.call(paste, c(fixed_width_list, list(sep = " "))), collapse = "\n") -} diff --git a/R/frame_utils.R b/R/frame_utils.R index 626e2d3d..55169b6b 100644 --- a/R/frame_utils.R +++ b/R/frame_utils.R @@ -1,4 +1,4 @@ -## from poorman +## frame utils from poorman to avoid hard dependency on dplyr dotdotdot <- function(..., .impute_names = FALSE) { dots <- eval(substitute(alist(...))) @@ -91,7 +91,24 @@ filter <- function(.data, ...) { } +## frame utils _not_ from poorman + reset_rownames = function(x) { rownames(x) = NULL x } + +frame_formatter = function(frame) { + make_underline = function(n) paste0(rep("-", times = n), collapse = "") + cnames = names(frame) + underlines = lapply(lapply(cnames, nchar), make_underline) + frame = rbind( + setNames(as.data.frame(as.list(cnames)), cnames), + setNames(as.data.frame(underlines), cnames), + frame + ) + l = lapply(frame, as.character) + widths = lapply(lapply(l, nchar), max) + fixed_width_list = mapply(format, l, width = widths, MoreArgs = list(justify = "left"), SIMPLIFY = FALSE, USE.NAMES = FALSE) + paste0(do.call(paste, c(fixed_width_list, list(sep = " "))), collapse = "\n") +} diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 845ee956..c4da5207 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -1,4 +1,4 @@ -#' TMB Simulator from Dynamic Model +#' TMB Simulator #' #' @param dynamic_model Object product by \code{\link{dynamic_model}}. #' @param vectors Named list of named vectors as initial values for the diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 3263a61c..399c84a7 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -192,7 +192,7 @@ empty_frame = function(...) { } enforce_schema = function(frame, ...) { - anchor = macpan2:::empty_frame(...) + anchor = empty_frame(...) if (is.null(frame)) return(anchor) anchor = as.list(anchor) for (c in names(anchor)) { diff --git a/R/lists.R b/R/lists.R index 1de849aa..22ec0260 100644 --- a/R/lists.R +++ b/R/lists.R @@ -54,7 +54,7 @@ melt_matrix = function(x) { melt_default_matrix_list = function(x) { f = (x |> lapply(melt_matrix) - |> macpan2:::bind_rows(.id = "matrix") + |> bind_rows(.id = "matrix") ) rownames(f) = NULL f diff --git a/R/mats_list.R b/R/mats_list.R index 7cfa275d..76204a28 100644 --- a/R/mats_list.R +++ b/R/mats_list.R @@ -18,8 +18,7 @@ #' For matrices that do not change their dimensions, set \code{\link{dimnames}} #' by adding \code{\link{dimnames}} to the matrices passed to \code{...}. #' @param .structure_labels An optional object for obtaining labels of -#' elements of special vectors and matrices. Such an object can be found in -#' the `$labels` field of a \code{\link{Compartmental}} model. Note that this +#' elements of special vectors and matrices. Note that this #' is an advanced technique. #' #' @return Object of class \code{MatsList} with the following methods. diff --git a/R/mp_dynamic_model.R b/R/mp_dynamic_model.R index 496b6d6b..f7bd5b5f 100644 --- a/R/mp_dynamic_model.R +++ b/R/mp_dynamic_model.R @@ -97,7 +97,7 @@ mp_dynamic_model = DynamicModel #' @export mp_test_tmb = function(..., ledgers, vectors, unstruc_mats) { m = mp_dynamic_model( - expr_list = mp_expr_list(before = list(...)) + expr_list = mp_tmb_expr_list(before = list(...)) , ledgers = ledgers , init_vecs = vectors , unstruc_mats = unstruc_mats diff --git a/R/mp_group.R b/R/mp_group.R index 72aa6367..9fe3edff 100644 --- a/R/mp_group.R +++ b/R/mp_group.R @@ -37,7 +37,7 @@ mp_aggregate_old = function(formula , ... ) { prototypes = list( - group_sums = macpan2:::MethodPrototype(y ~ group_sums(x), c("x", "y"), character()) + group_sums = MethodPrototype(y ~ group_sums(x), c("x", "y"), character()) ) consistent_agg_meths = (prototypes |> method_apply("consistent", formula) diff --git a/R/mp_join.R b/R/mp_join.R index fa2d250e..a35bbd06 100644 --- a/R/mp_join.R +++ b/R/mp_join.R @@ -173,9 +173,8 @@ mp_join = function(..., by = empty_named_list()) { #' #' A ledger is a table with rows that identify specific instances of a #' functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers -#' are most commonly -#' created using the \code{\link{mp_join}} function as in the following -#' example. +#' are most commonly created using the \code{\link{mp_join}} function as in the +#' following example. #' ```{r} #' age = mp_index(Age = c("young", "old")) #' state = mp_cartesian( @@ -625,6 +624,7 @@ print.Ledger = function(x print(x, row.names = FALSE, ...) } +#' @importFrom utils head #' @export head.Ledger = function(x , n = 6L @@ -639,6 +639,7 @@ head.Ledger = function(x } } +#' @importFrom utils tail #' @export tail.Ledger = function(x , n = 6L @@ -662,15 +663,14 @@ str.Ledger = function(x str(x, ...) } - -#' @export +## not used?? LedgerList = function() { self = Base() self$list = list() |> setNames(as.character()) self$add = function(new_link, ...) { new_ledgers = list(...) if (missing(new_link)) { - macpan2:::valid$named_list$check(new_ledgers) + valid$named_list$check(new_ledgers) } else if(length(new_ledgers) == 0L) { new_nm = deparse1(substitute(new_link)) new_ledgers = setNames(list(new_link), new_nm) diff --git a/R/mp_library.R b/R/mp_library.R index 3cfbd950..527a6685 100644 --- a/R/mp_library.R +++ b/R/mp_library.R @@ -1,6 +1,6 @@ #' @export mp_library = function(...) { - system.file("model_library", ..., package = "macpan2") |> Compartmental2() + stop("under construction") } #' @export diff --git a/R/mp_tmb_model_spec.R b/R/mp_tmb_model_spec.R new file mode 100644 index 00000000..ac6d794a --- /dev/null +++ b/R/mp_tmb_model_spec.R @@ -0,0 +1,187 @@ +TMBModelSpec = function( + before = list() + , during = list() + , after = list() + , default = list() + , integers = list() + , must_save = character() + , must_not_save = character() + , sim_exprs = character() + ) { + self = Base() + self$before = before + self$during = during + self$after = after + self$default = default + self$integers = integers + self$must_save = must_save + self$must_not_save = must_not_save + self$sim_exprs = sim_exprs + + self$expr_insert = function( + before_start = list() + , before_end = list() + , during_start = list() + , during_end = list() + , after_start = list() + , after_end = list() + , sim_exprs = character() + ) { + TMBModelSpec( + before = c(before_start, self$before, before_end) + , during = c(during_start, self$during, during_end) + , after = c(after_start, self$after, after_end) + , default = self$default + , integers = self$integers + , must_save = self$must_save + , must_not_save = self$must_not_save + , sim_exprs = unique(c(sim_exprs, self$sim_exprs)) + ) + } + + self$expr_list = function() ExprList(self$before, self$during, self$after) + + self$all_derived_vars = function() { + self$expr_list()$all_derived_vars() + } + self$all_default_vars = function() { + self$expr_list()$all_default_vars() + } + self$all_formula_vars = function() { + self$expr_list()$all_formula_vars() + } + + ## check for name ambiguity + self$check_names = function() { + tmb_model_names = c(names(self$all_integers()), names(self$all_matrices())) + ambiguous = duplicated(tmb_model_names) + if (any(ambiguous)) { + msg( + msg_hline() + , msg_colon( + msg( + "The following names were used for one or more purposes." + , "(either as names in the default or integers lists," + , "or as names of the elements of vectors in the default list)" + ) + , msg_indent_break(unique(tmb_model_names[ambiguous])) + ) + ) |> stop() + } + TRUE + } + ## convert each name of each named vector in the default list into + ## an 'implied' integer vector for subsetting vectors in before, during, + ## and after expressions by position name + self$all_integers = function() { + ## TODO: make smarter so that only used integer vectors + ## are produced and maybe even check if an integer vector + ## is being used in the wrong numeric vector + implied_integers = implied_position_vectors(self$default) + c(implied_integers, self$integers) + } + + self$empty_matrices = function() { + dv = setdiff(self$all_derived_vars(), names(self$default)) + rep(list(empty_matrix), length(dv)) |> setNames(dv) + } + self$all_matrices = function() c(self$default, self$empty_matrices()) + + self$simulator_fresh = function( + time_steps = 0 + , outputs = character() + , default = list() + ) { + self$check_names() + initial_mats = self$all_matrices() + initial_mats[names(default)] = default + initial_rownames = (initial_mats + |> lapply(as.matrix) + |> lapply(rownames) + |> unlist(use.names = FALSE, recursive = TRUE) + |> unique() + ) + matrix_outputs = intersect(outputs, names(initial_mats)) + row_outputs = (outputs + |> setdiff(matrix_outputs) + |> intersect(initial_rownames) + ) + mats_to_return = (initial_mats + |> lapply(names) + |> Filter(f = is.character) + |> Filter(f = \(x) any(x %in% row_outputs)) + |> names() + |> c(matrix_outputs) + |> unique() + ) + mats_to_save = (mats_to_return + |> union(self$must_save) + |> setdiff(self$must_not_save) + ) + s = TMBModel( + init_mats = do.call( + MatsList + , c( + initial_mats + , list( + .mats_to_return = mats_to_return + , .mats_to_save = mats_to_save + ) + ) + ) + , expr_list = ExprList( + before = self$before + , during = self$during + , after = self$after + , .simulate_exprs = self$sim_exprs + ) + , engine_methods = EngineMethods( + int_vecs = do.call(IntVecs, self$all_integers()) + ) + , time_steps = Time(as.integer(time_steps)) + )$simulator(outputs = outputs, initialize_ad_fun = FALSE) + s$ad_fun() + s + } + self$simulator_cached = memoise(self$simulator_fresh) + return_object(self, "TMBModelSpec") +} + +#' Specify a TMB Model +#' +#' Specify a model in the TMB engine. +#' +#' @param before List of formulas to be evaluated (in the order provided) before +#' the simulation loop begins. Each \code{\link{formula}} must have a left hand +#' side that gives the name of the matrix being updated, and a right hand side +#' giving an expression containing only the names of matrices in the model, +#' functions defined in the TMB engine, and numerical literals (e.g. +#' \code{3.14}). The available functions in the TMB engine can be described in +#' \code{\link{engine_functions}}. Names can be provided for the components of +#' \code{before}, and these names do not have to be unique. These names are +#' used by the \code{sim_exprs} argument. +#' @param during List of formulas to be evaluated at every iteration of the +#' simulation loop, with the same rules as \code{before}. +#' @param after List of formulas to be evaluated after the simulation loop, +#' with the same rules as \code{before}. +#' @param default Named list of objects that can be coerced into +#' \code{\link{numeric}} \code{\link{matrices}}. The names refer to +#' variables that appear in \code{before}, \code{during}, and \code{after}. +#' @param integers list +#' @param must_save character +#' @param must_not_save character +#' @param sim_exprs character +#' +#' @export +mp_tmb_model_spec = TMBModelSpec + +#' @export +print.TMBModelSpec = function(x, ...) { + e = ExprList(x$before, x$during, x$after) + cat("---------------------\n") + msg("Default values:\n") |> cat() + cat("---------------------\n") + print(melt_default_matrix_list(x$default), row.names = FALSE) + cat("\n") + print(e) +} diff --git a/R/parse_expr.R b/R/parse_expr.R index a137b7bd..4eadb63b 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -330,22 +330,15 @@ initial_valid_vars = function(valid_var_names) { #' #' Empty matrices are useful when defining matrices that do not need to be #' initialized because they will get computed before they are required by -#' other expressions. They are also a useful placeholder for matrices that -#' should only have a value after a certain phase in the simulation. +#' other expressions. They can also provide a useful placeholder for matrices +#' that should only have a value after a certain phase in the simulation. #' #' @name empty_matrix #' @format A \code{\link{numeric}} \code{\link[base]{matrix}} with zero rows #' and zero columns. #' @examples -#' s = TMBModel( -#' init_mats = MatsList(x = empty_matrix -#' , .mats_to_save = "x" -#' , .mats_to_return = "x" -#' ), -#' expr_list = ExprList(during = list(x ~ time_step(0))), -#' time_steps = Time(2) -#' ) -#' s$simulator()$report() +#' spec = mp_tmb_model_spec(during = list(x ~ time_step(0))) +#' identical(spec$empty_matrices()$x, empty_matrix) ## TRUE #' @export empty_matrix = matrix(numeric(0L), 0L, 0L) diff --git a/R/tmb_model.R b/R/tmb_model.R index b6b8ad5a..8cefbee6 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -1,3 +1,36 @@ +#' Simulator +#' +#' Construct a simulator from a model specification object. +#' +#' @param model A model specification object. +#' @param time_steps How many time steps should be simulated when simulations +#' are requested? +#' @param outputs Character vector of names of model quantities that will be +#' outputted when simulations are requested. +#' @param default Named list of numerical objects that will update the default +#' values defined in the model specification object. Any number of objects +#' can be updated or not. +#' +#' @export +mp_simulator = function(model + , time_steps + , outputs + , default = list() + ) { + UseMethod("mp_simulator") +} + +#' @export +mp_simulator.TMBModelSpec = function(model + , time_steps + , outputs + , default = list() + ) { + model$simulator_fresh(time_steps, outputs, default) +} + + + #' TMB Model #' #' Define a compartmental model in TMB. This model uses the spec @@ -67,7 +100,7 @@ #' #' @useDynLib macpan2 #' @importFrom TMB MakeADFun -#' @export +#' @noRd TMBModel = function( init_mats = MatsList() , expr_list = ExprList() @@ -95,7 +128,6 @@ TMBModel = function( ## Standard Methods self$data_arg = function() { - # sexisting_literals = self$expr_list$.literals() c( self$init_mats$data_arg(), self$expr_list$data_arg(), @@ -177,176 +209,6 @@ TMBModel = function( -#' @export -TMBModelSpec = function( - before = list() - , during = list() - , after = list() - , default = list() - , integers = list() - , must_save = character() - , must_not_save = character() - , sim_exprs = character() - ) { - self = Base() - self$before = before - self$during = during - self$after = after - self$default = default - self$integers = integers - self$must_save = must_save - self$must_not_save = must_not_save - self$sim_exprs = sim_exprs - - self$expr_insert = function( - before_start = list() - , before_end = list() - , during_start = list() - , during_end = list() - , after_start = list() - , after_end = list() - , sim_exprs = character() - ) { - TMBModelSpec( - before = c(before_start, self$before, before_end) - , during = c(during_start, self$during, during_end) - , after = c(after_start, self$after, after_end) - , default = self$default - , integers = self$integers - , must_save = self$must_save - , must_not_save = self$must_not_save - , sim_exprs = unique(c(sim_exprs, self$sim_exprs)) - ) - } - - ## check for name ambiguity - self$check_names = function() { - tmb_model_names = c(names(self$integers), names(self$default)) - ambiguous = duplicated(tmb_model_names) - if (any(ambiguous)) { - msg( - msg_hline() - , msg_colon( - msg( - "The following names were used more than once" - , "(either as names in the default or integers lists," - , "or as names of the elements of vectors in the default list)" - ) - , msg_indent_break(unique(tmb_model_names[ambiguous])) - ) - ) |> stop() - } - TRUE - } - ## convert each name of each named vector in the default list into - ## an 'implied' integer vector for subsetting vectors in before, during, - ## and after expressions by position name - self$all_integers = function() { - self$check_names() - - ## TODO: make smarter so that only used integer vectors - ## are produced and maybe even check if an integer vector - ## is being used in the wrong numeric vector - implied_integers = implied_position_vectors(self$default) - c(implied_integers, self$integers) - } - - self$empty_matrices = function() { - e = ExprList(self$before, self$during, self$after) - dv = setdiff(e$all_derived_vars(), names(self$default)) - rep(list(empty_matrix), length(dv)) |> setNames(dv) - } - self$matrices = function() c(self$default, self$empty_matrices()) - - self$simulator_fresh = function( - time_steps = 0 - , outputs = character() - , default = list() - ) { - initial_mats = self$matrices() - initial_mats[names(default)] = default - initial_rownames = (initial_mats - |> lapply(as.matrix) - |> lapply(rownames) - |> unlist(use.names = FALSE, recursive = TRUE) - |> unique() - ) - matrix_outputs = intersect(outputs, names(initial_mats)) - row_outputs = (outputs - |> setdiff(matrix_outputs) - |> intersect(initial_rownames) - ) - mats_to_return = (initial_mats - |> lapply(names) - |> Filter(f = is.character) - |> Filter(f = \(x) any(x %in% row_outputs)) - |> names() - |> c(matrix_outputs) - |> unique() - ) - mats_to_save = (mats_to_return - |> union(self$must_save) - |> setdiff(self$must_not_save) - ) - s = TMBModel( - init_mats = do.call( - MatsList - , c( - initial_mats - , list( - .mats_to_return = mats_to_return - , .mats_to_save = mats_to_save - ) - ) - ) - , expr_list = ExprList( - before = self$before - , during = self$during - , after = self$after - , .simulate_exprs = self$sim_exprs - ) - , engine_methods = EngineMethods( - int_vecs = do.call(IntVecs, self$all_integers()) - ) - , time_steps = Time(as.integer(time_steps)) - )$simulator(outputs = outputs, initialize_ad_fun = FALSE) - s$ad_fun() - s - } - self$simulator_cached = memoise(self$simulator_fresh) - return_object(self, "TMBModelSpec") -} - -#' @export -print.TMBModelSpec = function(x, ...) { - e = ExprList(x$before, x$during, x$after) - cat("---------------------\n") - msg("Default values:\n") |> cat() - cat("---------------------\n") - print(melt_default_matrix_list(x$default), row.names = FALSE) - cat("\n") - print(e) -} - -#' @export -mp_tmb_model_spec = function( - before = list() - , during = list() - , after = list() - , default = list() - , integers = list() - , must_save = character() - , must_not_save = character() - , sim_exprs = character() - ) { - - TMBModelSpec( - before, during, after - , default, integers - , must_save, must_not_save - , sim_exprs - ) -} mp_tmb_before = function(model, start = list(), end = list()) { TMBModelSpec( @@ -358,36 +220,6 @@ mp_tmb_before = function(model, start = list(), end = list()) { ) } -#' Simulator -#' -#' Construct a simulator from a model specification object. -#' -#' @param model A model specification object. -#' @param time_steps How many time steps should be simulated when simulations -#' are requested? -#' @param outputs Character vector of names of model quantities that will be -#' outputted when simulations are requested. -#' @param default Named list of numerical objects that will update the default -#' values defined in the model specification object. Any number of objects -#' can be updated or not. -#' -#' @export -mp_simulator = function(model - , time_steps - , outputs - , default = list() - ) { - UseMethod("mp_simulator") -} - -#' @export -mp_simulator.TMBModelSpec = function(model - , time_steps - , outputs - , default = list() - ) { - model$simulator_fresh(time_steps, outputs, default) -} #' #' @export @@ -452,11 +284,11 @@ mp_trajectory.TMBSimulator = function(model) { -TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { - self = tmb_simulator - self$compartmental_model = compartmental_model - return_object(self, "TMBCompartmentalSimulator") -} +# TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { +# self = tmb_simulator +# self$compartmental_model = compartmental_model +# return_object(self, "TMBCompartmentalSimulator") +# } TMBDynamicSimulator = function(tmb_simulator, dynamic_model) { self = tmb_simulator @@ -467,13 +299,7 @@ TMBDynamicSimulator = function(tmb_simulator, dynamic_model) { #' @export labels.VariableLabels = function(object, ...) object$component_list() -#' @export -labels.Compartmental = function(object, ...) labels(object$labels) -#' @export -labels.TMBCompartmentalSimulator = function(object, ...) { - labels(object$compartmental_model) -} #' @export labels.ModelDefRun = function(object, ...) { @@ -541,7 +367,7 @@ TMBSimulationUtils = function() { if (!"after" %in% .phases) { r = r[r$time != num_t + 1L,,drop = FALSE] } - r |> macpan2:::filter( + r |> filter( (matrix %in% self$matrix_outputs()) | (row %in% self$row_outputs()) ) @@ -596,6 +422,9 @@ TMBSimulationUtils = function() { #' `$tmb_model$make_ad_fun_arg()`) before passing it yourself to #' `TMB::MakeADFun`. This is particularly useful if you want to modify #' `tmb_cpp`. +#' @param outputs Character vector of matrix and/or row names to be +#' returned as output from simulation functions such as +#' \code{\link{mp_trajectory}}. #' #' @return Object of class \code{TMBSimulator} with the following methods. #' @@ -615,11 +444,11 @@ TMBSimulationUtils = function() { #' object. #' #' @importFrom MASS mvrnorm -#' @export +#' @noRd TMBSimulator = function(tmb_model , tmb_cpp = getOption("macpan2_dll") , initialize_ad_fun = TRUE - , outputs = NULL ## character vector of matrix and/or row names + , outputs = NULL ) { self = TMBSimulationUtils() diff --git a/R/validity.R b/R/validity.R index 46c133fd..45847c70 100644 --- a/R/validity.R +++ b/R/validity.R @@ -151,10 +151,6 @@ CustomTypeChecking = function() { Is("EdgeModel"), "not an edge model" ) - self$compartmental = ValidityMessager( - Is("Compartmental"), - "not a compartmental model" - ) self$name_or_num = ValidityMessager( Any(is.name, is.numeric), "neither name nor number" diff --git a/R/vec_utils.R b/R/vec_utils.R index e479be86..b77af339 100644 --- a/R/vec_utils.R +++ b/R/vec_utils.R @@ -1,4 +1,10 @@ - +#' Zero Vector +#' +#' Create a \code{\link{numeric}} vector of all zeros with names +#' given by \code{x} +#' +#' @param x Object representing the names of the output vector. Most +#' commonly this will be a \code{\link{character}} vector. #' @export mp_zero_vector = function(x, ...) { UseMethod("mp_zero_vector") @@ -20,3 +26,5 @@ mp_zero_vector.Index = function(x, labelling_column_names, ...) { |> zero_vector() ) } + +zero_vector = function(labels) setNames(rep(0, length(labels)), labels) diff --git a/R/vector.R b/R/vector.R index 70baf88a..80a1a298 100644 --- a/R/vector.R +++ b/R/vector.R @@ -1,7 +1,7 @@ -#' @export + StructuredVector = function(x, ...) UseMethod("StructuredVector") -#' @export + StructuredVector.data.frame = function(x, index = NULL, values_name = "values", ...) { nms = setdiff(names(x), values_name) if (is.null(index)) index = mp_index(x[, nms, drop = FALSE]) @@ -51,7 +51,7 @@ StructuredVector.data.frame = function(x, index = NULL, values_name = "values", v$set_all_numbers(values) } -#' @export + StructuredVector.numeric = function(x, index, ...) { v = StructuredVector(index) index_name = to_name(index$labelling_column_names) @@ -60,12 +60,12 @@ StructuredVector.numeric = function(x, index, ...) { do.call(v$set_numbers, args) } -#' @export + StructuredVector.StructuredVector = function(x, index, ...) { StructuredVector(x$numbers(), index, ...) } -#' @export + StructuredVector.Index = function(x, ...) { self = Base() self$index = x @@ -143,15 +143,13 @@ as.matrix.StructuredVector = function(x, ...) { x$numbers() |> as.matrix() } -zero_vector = function(labels) setNames(rep(0, length(labels)), labels) - -#' Stub +#' Structured Vectors #' #' This documentation was originally in [mp_index()] and should be cleaned up #' See issue #131 #' -#' #' These labels can be used to create 'multidimensional' names for the elements +#' These labels can be used to create 'multidimensional' names for the elements #' of vectors. Here is the above example expressed in vector form. #' ```{r, echo = FALSE} #' v = StructuredVector(prod) @@ -221,7 +219,7 @@ VectorList = function() { self$add = function(new_vec, ...) { new_vecs = list(...) if (missing(new_vec)) { - macpan2:::valid$named_list$check(new_vecs) + valid$named_list$check(new_vecs) } else if(length(new_vecs) == 0L) { new_nm = deparse1(substitute(new_vec)) new_vecs = setNames(list(new_vec), new_nm) diff --git a/inst/model_library/sir/model_structure.R b/inst/model_library/sir/model_structure.R index a663f3b4..76e9b18e 100644 --- a/inst/model_library/sir/model_structure.R +++ b/inst/model_library/sir/model_structure.R @@ -2,7 +2,7 @@ library(macpan2) ## dynamics --------------------------------------------- -expr_list = mp_expr_list( +expr_list = mp_tmb_expr_list( before = list( ## aggregations N ~ sum(state) diff --git a/inst/model_library/sir_age/model_structure.R b/inst/model_library/sir_age/model_structure.R index 1bd45a8a..f14d8320 100644 --- a/inst/model_library/sir_age/model_structure.R +++ b/inst/model_library/sir_age/model_structure.R @@ -2,7 +2,7 @@ library(macpan2) ## dynamics --------------------------------------------- -expr_list = mp_expr_list( +expr_list = mp_tmb_expr_list( before = list( sub_population_sizes = diff --git a/man/EngineMethods.Rd b/man/EngineMethods.Rd deleted file mode 100644 index 1aeb3a7c..00000000 --- a/man/EngineMethods.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/engine_methods.R -\name{EngineMethods} -\alias{EngineMethods} -\title{Engine Methods} -\usage{ -EngineMethods(exprs = list(), int_vecs = IntVecs()) -} -\arguments{ -\item{exprs}{A named \code{list} of formulas that can be matched to one of the -method prototypes (TODO: describe these).} - -\item{int_vecs}{An \code{\link{IntVecs}} object containing integer vectors -that can be used by the methods.} -} -\description{ -List of methods for pre-processing matrices before returning or -assigning them. One benefit of these methods is that they can make use -of user-supplied integer vectors that are stored in C++ as integers. Another -benefit can be readability but this is subjective. -} diff --git a/man/IndexedExpressions.Rd b/man/IndexedExpressions.Rd deleted file mode 100644 index 768b53f5..00000000 --- a/man/IndexedExpressions.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/formula_data.R -\name{IndexedExpressions} -\alias{IndexedExpressions} -\title{Indexed Expressions} -\usage{ -IndexedExpressions( - ..., - index_data, - vector_list = list(), - unstructured_matrix_list = list() -) -} -\arguments{ -\item{...}{Formula objects that reference the columns in the -\code{index_data}, the vectors in \code{vector_list} and the matrices -in \code{unstructured_matrix_list}.} - -\item{vector_list}{Named list of objected produced using -\code{\link{mp_vector}}.} - -\item{unstructured_matrix_list}{Named list of objects that can be coerced -to a matrix.} - -\item{ledgers}{An object produced using \code{\link{mp_ledgers}}.} -} -\description{ -Indexed Expressions -} diff --git a/man/IntVecs.Rd b/man/IntVecs.Rd deleted file mode 100644 index 90450862..00000000 --- a/man/IntVecs.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/const_int_vec.R -\name{IntVecs} -\alias{IntVecs} -\title{Constant Integer Vectors} -\usage{ -IntVecs(...) -} -\arguments{ -\item{...}{Named arguments, each of which can be coerced to an integer vector.} -} -\description{ -Make a list of integer vectors available for engine methods. -} diff --git a/man/LedgerDefinition.Rd b/man/LedgerDefinition.Rd index f67dddfa..cc31e20f 100644 --- a/man/LedgerDefinition.Rd +++ b/man/LedgerDefinition.Rd @@ -6,9 +6,8 @@ \description{ A ledger is a table with rows that identify specific instances of a functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers -are most commonly -created using the \code{\link{mp_join}} function as in the following -example. +are most commonly created using the \code{\link{mp_join}} function as in the +following example. \if{html}{\out{
      }}\preformatted{age = mp_index(Age = c("young", "old")) state = mp_cartesian( diff --git a/man/MatsList.Rd b/man/MatsList.Rd index 163723af..938cf920 100644 --- a/man/MatsList.Rd +++ b/man/MatsList.Rd @@ -33,8 +33,7 @@ For matrices that do not change their dimensions, set \code{\link{dimnames}} by adding \code{\link{dimnames}} to the matrices passed to \code{...}.} \item{.structure_labels}{An optional object for obtaining labels of -elements of special vectors and matrices. Such an object can be found in -the \verb{$labels} field of a \code{\link{Compartmental}} model. Note that this +elements of special vectors and matrices. Note that this is an advanced technique.} } \value{ diff --git a/man/MethodPrototype.Rd b/man/MethodPrototype.Rd deleted file mode 100644 index 9c3b8c25..00000000 --- a/man/MethodPrototype.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enum_methods.R -\name{MethodPrototype} -\alias{MethodPrototype} -\title{Method Prototype} -\usage{ -MethodPrototype(formula, mat_arg_nms, int_vec_arg_nms) -} -\arguments{ -\item{formula}{Formula for defining a method type using a prototype.} - -\item{mat_arg_nms}{Character vector naming the matrix-valued arguments.} - -\item{int_vec_arg_nms}{Character vector naming the integer-vector-valued -arguments.} -} -\description{ -Define a method type using a prototype. These prototypes can be compared -with methods defined in R to see if they are consistent with a method -type that has been defined in C++. All -arguments are automatically derived through comments in the C++ code where -the method types are defined. -} diff --git a/man/MethodTypeUtils.Rd b/man/MethodTypeUtils.Rd deleted file mode 100644 index 0e535e7f..00000000 --- a/man/MethodTypeUtils.Rd +++ /dev/null @@ -1,12 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enum_methods.R -\name{MethodTypeUtils} -\alias{MethodTypeUtils} -\title{Method Type Utilities} -\usage{ -MethodTypeUtils() -} -\description{ -This class is here so that \code{MethodTypes}, which is automatically generated -from the C++ code, can inherit from it. -} diff --git a/man/TMBModel.Rd b/man/TMBModel.Rd deleted file mode 100644 index 8c8123f0..00000000 --- a/man/TMBModel.Rd +++ /dev/null @@ -1,96 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R -\name{TMBModel} -\alias{TMBModel} -\title{TMB Model} -\usage{ -TMBModel( - init_mats = MatsList(), - expr_list = ExprList(), - params = OptParamsList(0), - random = OptParamsList(), - obj_fn = ObjectiveFunction(~0), - time_steps = Time(0L), - engine_methods = EngineMethods(), - log_file = LogFile(), - do_pred_sdreport = TRUE -) -} -\arguments{ -\item{init_mats}{An object of class \code{\link{MatsList}}.} - -\item{expr_list}{An object of class \code{\link{ExprList}}.} - -\item{params}{An object of class \code{\link{OptParamsList}}.} - -\item{random}{An object of class \code{\link{OptParamsList}}.} - -\item{obj_fn}{An object of class \code{\link{ObjectiveFunction}}.} - -\item{time_steps}{An object of class \code{\link{Time}}.} - -\item{engine_methods}{An object of class \code{\link{EngineMethods}}.} - -\item{log_file}{An object of class \code{\link{LogFile}}.} - -\item{do_pred_sdreport}{A logical flag (\code{FALSE}/\code{TRUE}, or any value evaluating to 1 for \code{TRUE}) indicating whether predicted values should be accessible via \code{TMB::sdreport()}} -} -\value{ -Object of class \code{TMBModel} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$data_arg()} -- Return all of the components of the data structure -to pass to C++. -\item \verb{$param_arg()} -- Return all of the components of the parameter list -to pass to C++. -\item \verb{$simulator()} -- Return an object of class \code{\link{TMBSimulator}}, -which can be used to simulate data from the model. -} -} -} -\description{ -Define a compartmental model in TMB. This model uses the spec -\url{https://canmod.net/misc/cpp_side}. -} -\examples{ -sir = TMBModel( - init_mats = MatsList( - state = c(1 - 1e-2, 1e-2, 0), - beta = 0.3, - gamma = 0.2, - N = 1, - foi = 0, - ratemat = matrix(0, 3, 3), - flowmat = matrix(0, 3, 3), - .mats_to_save = c("state", "N", "foi"), - .mats_to_return = c("state", "N", "foi") - ), - expr_list = ExprList( - before = list( - N ~ sum(state) - ), - during = list( - foi ~ beta * state[1, 0] / N, - ratemat ~ matrix(c( - 0, 0, 0, - foi, 0, 0, - 0, gamma, 0), 3, 3), - flowmat ~ ratemat * state, - state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) - ) - ), - params = OptParamsList(0.3 - , par_id = 0L - , mat = "beta" - , row_id = 0L - , col_id = 0L - ), - random = OptParamsList(), - obj_fn = ObjectiveFunction(~ foi + 1), - time_steps = Time(time_steps = 30L) -) -sir$data_arg() -sir$param_arg() -sir$simulator()$report() - -} diff --git a/man/TMBSimulator.Rd b/man/TMBSimulator.Rd deleted file mode 100644 index d0c6f172..00000000 --- a/man/TMBSimulator.Rd +++ /dev/null @@ -1,50 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tmb_model.R -\name{TMBSimulator} -\alias{TMBSimulator} -\title{TMB Simulator} -\usage{ -TMBSimulator( - tmb_model, - tmb_cpp = getOption("macpan2_dll"), - initialize_ad_fun = TRUE, - outputs = NULL -) -} -\arguments{ -\item{tmb_model}{An object of class \code{\link{TMBModel}}.} - -\item{tmb_cpp}{Name of a C++ program using TMB as the simulation engine.} - -\item{initialize_ad_fun}{Should the TMB AD function be intialized? This -should usually be set to \code{TRUE} unless you want to hack the data -structure passed to TMB (which can be acquired using -\verb{$tmb_model$make_ad_fun_arg()}) before passing it yourself to -\code{TMB::MakeADFun}. This is particularly useful if you want to modify -\code{tmb_cpp}.} -} -\value{ -Object of class \code{TMBSimulator} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$report()}: Runs simulations and returns a data frame with the following -columns. -\itemize{ -\item \code{matrix}: Name of the matrix with values returned. -\item \code{time}: Time step of the values. -\item \code{row}: Row in the \code{matrix} containing the \code{value}. -\item \code{col}: Column in the \code{matrix} containing the \code{value}. -\item \code{value}: Numerical value being reported. -} -\item \verb{$error_code()}: If the simulations result in an engine error then the -code associated with this error is returned, otherwise the code \code{0} is -returned. -\item \verb{$ad_fun()}: Return the underlying \href{https://github.com/kaskr/adcomp}{TMB} -object. -} -} -} -\description{ -Construct an object with methods for simulating from and optimizing a -compartmental model made using \code{\link{TMBModel}}. -} diff --git a/man/empty_matrix.Rd b/man/empty_matrix.Rd index e4ebdb3e..5545d22e 100644 --- a/man/empty_matrix.Rd +++ b/man/empty_matrix.Rd @@ -14,18 +14,11 @@ empty_matrix \description{ Empty matrices are useful when defining matrices that do not need to be initialized because they will get computed before they are required by -other expressions. They are also a useful placeholder for matrices that -should only have a value after a certain phase in the simulation. +other expressions. They can also provide a useful placeholder for matrices +that should only have a value after a certain phase in the simulation. } \examples{ -s = TMBModel( - init_mats = MatsList(x = empty_matrix - , .mats_to_save = "x" - , .mats_to_return = "x" - ), - expr_list = ExprList(during = list(x ~ time_step(0))), - time_steps = Time(2) -) -s$simulator()$report() +spec = mp_tmb_model_spec(during = list(x ~ time_step(0))) +identical(spec$empty_matrices()$x, empty_matrix) ## TRUE } \keyword{datasets} diff --git a/man/mk_meth_cls.Rd b/man/mk_meth_cls.Rd deleted file mode 100644 index a3d4e6d6..00000000 --- a/man/mk_meth_cls.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enum_methods.R -\name{mk_meth_cls} -\alias{mk_meth_cls} -\title{Make Method Class} -\usage{ -mk_meth_cls(cls_nm, meth_type_id) -} -\arguments{ -\item{cls_nm}{Character string giving the name of the class.} - -\item{meth_type_id}{Integer giving the associated ID of the method type.} -} -\description{ -Place a method object in the package namespace for a given method type -defined in the C++ code. -} diff --git a/man/mp_ledgers.Rd b/man/mp_ledgers.Rd index bdf78593..c42bdd96 100644 --- a/man/mp_ledgers.Rd +++ b/man/mp_ledgers.Rd @@ -2,11 +2,7 @@ % Please edit documentation in R/formula_data.R \name{mp_ledgers} \alias{mp_ledgers} -\title{#' @export -print.LedgerData = function(x, ...) { -print(x$frame, row.names = FALSE) -} -Bundle up Ledgers} +\title{Bundle up Ledgers} \usage{ mp_ledgers(...) } diff --git a/man/ExprList.Rd b/man/mp_tmb_expr_list.Rd similarity index 97% rename from man/ExprList.Rd rename to man/mp_tmb_expr_list.Rd index 5ee1d645..540e05a7 100644 --- a/man/ExprList.Rd +++ b/man/mp_tmb_expr_list.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/expr_list.R -\name{ExprList} -\alias{ExprList} +\name{mp_tmb_expr_list} +\alias{mp_tmb_expr_list} \title{Expression List} \usage{ -ExprList( +mp_tmb_expr_list( before = list(), during = list(), after = list(), diff --git a/man/mp_tmb_model_spec.Rd b/man/mp_tmb_model_spec.Rd new file mode 100644 index 00000000..a7fe1eea --- /dev/null +++ b/man/mp_tmb_model_spec.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_tmb_model_spec.R +\name{mp_tmb_model_spec} +\alias{mp_tmb_model_spec} +\title{Specify a TMB Model} +\usage{ +mp_tmb_model_spec( + before = list(), + during = list(), + after = list(), + default = list(), + integers = list(), + must_save = character(), + must_not_save = character(), + sim_exprs = character() +) +} +\arguments{ +\item{before}{List of formulas to be evaluated (in the order provided) before +the simulation loop begins. Each \code{\link{formula}} must have a left hand +side that gives the name of the matrix being updated, and a right hand side +giving an expression containing only the names of matrices in the model, +functions defined in the TMB engine, and numerical literals (e.g. +\code{3.14}). The available functions in the TMB engine can be described in +\code{\link{engine_functions}}. Names can be provided for the components of +\code{before}, and these names do not have to be unique. These names are +used by the \code{sim_exprs} argument.} + +\item{during}{List of formulas to be evaluated at every iteration of the +simulation loop, with the same rules as \code{before}.} + +\item{after}{List of formulas to be evaluated after the simulation loop, +with the same rules as \code{before}.} + +\item{default}{Named list of objects that can be coerced into +\code{\link{numeric}} \code{\link{matrices}}. The names refer to +variables that appear in \code{before}, \code{during}, and \code{after}.} + +\item{integers}{list} + +\item{must_save}{character} + +\item{must_not_save}{character} + +\item{sim_exprs}{character} +} +\description{ +Specify a model in the TMB engine. +} diff --git a/man/mp_vector.Rd b/man/mp_vector.Rd index 932b7665..595c4caf 100644 --- a/man/mp_vector.Rd +++ b/man/mp_vector.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/vector.R \name{mp_vector} \alias{mp_vector} -\title{Stub} +\title{Structured Vectors} \usage{ mp_vector(x, ...) } @@ -11,7 +11,7 @@ This documentation was originally in \code{\link[=mp_index]{mp_index()}} and sho See issue #131 } \details{ -#' These labels can be used to create 'multidimensional' names for the elements +These labels can be used to create 'multidimensional' names for the elements of vectors. Here is the above example expressed in vector form. \if{html}{\out{
      }}\preformatted{v = StructuredVector(prod) diff --git a/man/mp_zero_vector.Rd b/man/mp_zero_vector.Rd new file mode 100644 index 00000000..f5d2e3c6 --- /dev/null +++ b/man/mp_zero_vector.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vec_utils.R +\name{mp_zero_vector} +\alias{mp_zero_vector} +\title{Zero Vector} +\usage{ +mp_zero_vector(x, ...) +} +\arguments{ +\item{x}{Object representing the names of the output vector. Most +commonly this will be a \code{\link{character}} vector.} +} +\description{ +Create a \code{\link{numeric}} vector of all zeros with names +given by \code{x} +} diff --git a/misc/build/method_head.R b/misc/build/method_head.R index 5213fba0..fc174757 100644 --- a/misc/build/method_head.R +++ b/misc/build/method_head.R @@ -11,7 +11,7 @@ #' @param int_vec_arg_nms Character vector naming the integer-vector-valued #' arguments. #' -#' @nord +#' @noRd MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { self = Base() self$formula = formula @@ -62,7 +62,7 @@ MethodPrototype = function(formula, mat_arg_nms, int_vec_arg_nms) { #' @param cls_nm Character string giving the name of the class. #' @param meth_type_id Integer giving the associated ID of the method type. #' -#' @nord +#' @noRd mk_meth_cls = function(cls_nm, meth_type_id) { pf = parent.frame() force(pf) @@ -81,7 +81,7 @@ mk_meth_cls = function(cls_nm, meth_type_id) { #' This class is here so that `MethodTypes`, which is automatically generated #' from the C++ code, can inherit from it. #' -#' @nord +#' @noRd MethodTypeUtils = function() { self = Base() diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index 9497bdd9..fdbbaf5d 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -294,7 +294,7 @@ flows = mp_ledgers( , death_flows ) -vv = IndexedExpressions( +vv = macpan2:::IndexedExpressions( flows_per_time ~ state[from] * flow_rates[link] , inflow ~ group_sums(flows_per_time, to, state) , outflow ~ group_sums(flows_per_time, from, state) @@ -318,7 +318,7 @@ state_aggregation = mp_join( ## N ~ group_sums(state[alive], group_by, N) state_vector = Vector(state) state_vector$set_numbers(Epi = c(I = 1))$set_numbers(Epi.Loc.Age = c(S.cal.young = 1000)) -hh = IndexedExpressions( +hh = macpan2:::IndexedExpressions( N ~ group_sums(state[alive], group_by, N) , index_data = state_aggregation , vector_list = list( @@ -1015,7 +1015,7 @@ model$labels$flow() #undebug(oor::clean_method_environment) -iv = IntVecs( +iv = macpan2:::IntVecs( state_length = as.integer(length(model$labels$state())) , per_capita_from = indices$flow$per_capita$from() @@ -1081,7 +1081,7 @@ m = TMBModel( , state ~ state + total_inflow - total_outflow ) ), - engine_methods = EngineMethods(int_vecs = iv), + engine_methods = macpan2:::EngineMethods(int_vecs = iv), time_steps = Time(100L), params = OptParamsList(0.5, 0.2, 0.1 , par_id = 0:2 @@ -1352,7 +1352,7 @@ TMBModel( , state ~ state + total_inflow - total_outflow ) ), - engine_methods = EngineMethods( + engine_methods = macpan2:::EngineMethods( exprs = list( set_infection_flow = flow[infection] ~ per_capita_transmission %*% state[infectious] , get_per_capita = ~ state[per_capita_from] * flow[per_capita_flow] @@ -1370,7 +1370,7 @@ TMBModel( , get_per_capita_outflow_state_out = ~ group_sums(per_capita_outflow, per_capita_outflow_from, state_length) , get_absolute_outflow_state_out = ~ group_sums(absolute_outflow, absolute_outflow_from, state_length) ), - int_vecs = IntVecs( + int_vecs = macpan2:::IntVecs( state_length = length(model$labels$state()) , per_capita_from = indices$flow$per_capita$from() @@ -1437,7 +1437,7 @@ m = TMBModel( ) ), time = Time(150), - engine_methods = EngineMethods( + engine_methods = macpan2:::EngineMethods( exprs = list( from_states = ~ state[from_indices] , infectious_states = ~ state[infectious_indices] @@ -1447,7 +1447,7 @@ m = TMBModel( , outflow = ~ group_sums(per_capita, from_indices, state_length) , beta = ~ time_var(beta_ts, beta_cp, beta_n, beta_group) ), - int_vecs = IntVecs( + int_vecs = macpan2:::IntVecs( from_indices = 0:1 , to_indices = 1:2 , state_length = 3L diff --git a/R/model_def_run.R b/misc/old-r-source/model_def_run.R similarity index 100% rename from R/model_def_run.R rename to misc/old-r-source/model_def_run.R diff --git a/tests/testthat/test-c.R b/tests/testthat/test-c.R index f1b6b772..682417e5 100644 --- a/tests/testthat/test-c.R +++ b/tests/testthat/test-c.R @@ -3,7 +3,7 @@ test_that("concatenation works with many different shapes of input", { z = rnorm(4L) w = matrix(rnorm(12L), 3L, 4L) answer = c(pi, z, w) - m = TMBModel( + m = macpan2:::TMBModel( init_mats = MatsList( answer = empty_matrix , x = empty_matrix @@ -12,9 +12,9 @@ test_that("concatenation works with many different shapes of input", { , w = w , .mats_to_return = "answer" ), - expr_list = ExprList(before = list(answer ~ c(x, y, z, w))) + expr_list =mp_tmb_expr_list(before = list(answer ~ c(x, y, z, w))) ) - s = TMBSimulator(m) + s = macpan2:::TMBSimulator(m) expect_equal( s$matrix(matrix_name = "answer", time_step = 1L, .phases = "after"), matrix(answer) diff --git a/tests/testthat/test-dimnames-and-shape-change.R b/tests/testthat/test-dimnames-and-shape-change.R index 6a19f4d8..ef345358 100644 --- a/tests/testthat/test-dimnames-and-shape-change.R +++ b/tests/testthat/test-dimnames-and-shape-change.R @@ -2,12 +2,12 @@ library(macpan2) library(testthat) test_that("matrices with dimnames can change size", { - m = TMBModel( + m = macpan2:::TMBModel( init_mats = MatsList(x = c(a = 0), .mats_to_save = "x", .mats_to_return = "x"), - expr_list = ExprList(during = list(x ~ c(x, time_step(0)))), + expr_list =mp_tmb_expr_list(during = list(x ~ c(x, time_step(0)))), time_steps = Time(3) ) - s = TMBSimulator(m) + s = macpan2:::TMBSimulator(m) r = s$report(.phases = c("before", "during")) expect_identical( r$value, @@ -20,16 +20,16 @@ test_that("matrices with dimnames can change size", { }) test_that("initially empty matrices can be associated with dimnames for at least some elements", { - m = TMBModel( + m = macpan2:::TMBModel( init_mats = MatsList(x = empty_matrix , .mats_to_save = "x" , .mats_to_return = "x" , .dimnames = list(x = list(letters[1:2], "")) ), - expr_list = ExprList(during = list(x ~ c(x, 1))), + expr_list =mp_tmb_expr_list(during = list(x ~ c(x, 1))), time_steps = Time(3) ) - s = TMBSimulator(m) + s = macpan2:::TMBSimulator(m) r = s$report(.phases = c("before", "during")) expect_identical( r$value, diff --git a/tests/testthat/test-dynamic-model.R b/tests/testthat/test-dynamic-model.R index 6cdd4bf3..1222860c 100644 --- a/tests/testthat/test-dynamic-model.R +++ b/tests/testthat/test-dynamic-model.R @@ -23,7 +23,7 @@ SIR_starter <- function( ## Set up expressions list for each functional form -------------- ## names refer to when the calculation gets performed relative to ## the simulation time-step loop (before, during, ...) - expr_list <- mp_expr_list( + expr_list <- mp_tmb_expr_list( before = list( ## aggregations N ~ sum(state) @@ -331,7 +331,7 @@ SIR_starter <- function( ## Set up expressions list for each functional form -------------- ## names refer to when the calculation gets performed relative to ## the simulation time-step loop (before, during, ...) - expr_list <- mp_expr_list( + expr_list <- mp_tmb_expr_list( before = list( ## aggregations N ~ sum(state) diff --git a/tests/testthat/test-error-row.R b/tests/testthat/test-error-row.R index f9adf048..cf0c5adc 100644 --- a/tests/testthat/test-error-row.R +++ b/tests/testthat/test-error-row.R @@ -3,8 +3,8 @@ test_that("failing expression is reported in c++ side errors", { engine_eval(~ x[-1], x = 0), "output_x \\~ x\\[-1\\]" ) - m = TMBModel( - expr_list = ExprList( + m = macpan2:::TMBModel( + expr_list =mp_tmb_expr_list( during = list( z ~ 1 + x[1] + 1, y ~ rbind_time(z) diff --git a/tests/testthat/test-expr-list.R b/tests/testthat/test-expr-list.R index fc9549a7..8cd2626d 100644 --- a/tests/testthat/test-expr-list.R +++ b/tests/testthat/test-expr-list.R @@ -3,7 +3,7 @@ # }) test_that("expressions are inserted and printed", { - l = ExprList(during = list(a ~ 1, b ~ 2, c ~ 3)) + l =mp_tmb_expr_list(during = list(a ~ 1, b ~ 2, c ~ 3)) l = l$insert(X ~ 500, Y ~ 600, .phase = "before") l = l$insert(A ~ 0, .phase = "after") expect_snapshot(l$print_exprs()) @@ -12,25 +12,25 @@ test_that("expressions are inserted and printed", { test_that("formula validity is enforced", { # expect_error( - # TMBModel( + # macpan2:::TMBModel( # init_mats = MatsList(a = 1), - # expr_list = ExprList(before = list(a[0] ~ 1)) + # expr_list =mp_tmb_expr_list(before = list(a[0] ~ 1)) # ), # "without subsetting on the left-hand-side" # ) expect_error( - TMBModel( + macpan2:::TMBModel( init_mats = MatsList(a = 1), - expr_list = ExprList(before = list( ~ 1)) + expr_list =mp_tmb_expr_list(before = list( ~ 1)) ), "Model expressions must be two-sided" ) }) test_that("proper error message comes when output matrices are not initialized", { - m = TMBModel( + m = macpan2:::TMBModel( init_mats = MatsList(a = 1), - expr_list = ExprList(before = list(b ~ a)) + expr_list =mp_tmb_expr_list(before = list(b ~ a)) ) expect_error( m$data_arg(), diff --git a/tests/testthat/test-literals.R b/tests/testthat/test-literals.R index a47ac64e..585f1200 100644 --- a/tests/testthat/test-literals.R +++ b/tests/testthat/test-literals.R @@ -1,7 +1,7 @@ test_that("expression lists process literals", { - m = TMBModel( + m = macpan2:::TMBModel( init_mats = MatsList(a = 0), - expr_list = ExprList(before = list(a ~ 2020202)), + expr_list =mp_tmb_expr_list(before = list(a ~ 2020202)), params = OptParamsList(0, par_id = 0L, mat = "a", row_id = 0L, col_id = 0L), random = OptParamsList(), time_steps = Time(1L), diff --git a/tests/testthat/test-opt-params.R b/tests/testthat/test-opt-params.R index 0e2d93a9..6e3bf0a0 100644 --- a/tests/testthat/test-opt-params.R +++ b/tests/testthat/test-opt-params.R @@ -11,9 +11,9 @@ test_that("parameters are not initially empty", { , .mats_to_save = "y" , .mats_to_return = "y" ) - expr_list = ExprList(before = list(y ~ x + 1)) + expr_list =mp_tmb_expr_list(before = list(y ~ x + 1)) expect_error( - TMBModel( + macpan2:::TMBModel( init_mats = init_mats, expr_list = expr_list, params = params diff --git a/tests/testthat/test-rbind-time-lag.R b/tests/testthat/test-rbind-time-lag.R index 843e6303..8cadd618 100644 --- a/tests/testthat/test-rbind-time-lag.R +++ b/tests/testthat/test-rbind-time-lag.R @@ -2,7 +2,7 @@ test_that("a selection of the iterations in the simulation history of a matrix t steps = 10 x = matrix(1:12, 4, 3) y = empty_matrix - s = TMBModel( + s = macpan2:::TMBModel( init_mats = MatsList( x = x, y = y, @@ -11,7 +11,7 @@ test_that("a selection of the iterations in the simulation history of a matrix t .mats_to_save = "x", .mats_to_return = "y" ), - expr_list = ExprList( + expr_list =mp_tmb_expr_list( during = list(x ~ x * 0.9), after = list(y ~ rbind_time(x, t, 1)) ), diff --git a/tests/testthat/test-rcbind.R b/tests/testthat/test-rcbind.R index 07098911..4ff86844 100644 --- a/tests/testthat/test-rcbind.R +++ b/tests/testthat/test-rcbind.R @@ -6,10 +6,10 @@ test_that("concatenation works with many different shapes of input", { , w = matrix(rnorm(12L), 3L, 4L) , .mats_to_return = c("answer", "z", "w") ) - expr_good = ExprList(before = list(answer ~ cbind(z, t(w)))) - expr_bad = ExprList(before = list(answer ~ cbind(z, w))) - good = TMBSimulator(TMBModel(mats, expr_good)) - bad = TMBSimulator(TMBModel(mats, expr_bad)) + expr_good =mp_tmb_expr_list(before = list(answer ~ cbind(z, t(w)))) + expr_bad =mp_tmb_expr_list(before = list(answer ~ cbind(z, w))) + good = macpan2:::TMBSimulator(macpan2:::TMBModel(mats, expr_good)) + bad = macpan2:::TMBSimulator(macpan2:::TMBModel(mats, expr_bad)) answer = good$matrix(matrix_name = "answer", time_step = 1L, .phases = c("before", "during", "after")) z = good$matrix(matrix_name = "z", time_step = 1L, .phases = c("before", "during", "after")) diff --git a/tests/testthat/test-simple-structured-model-specs.R b/tests/testthat/test-simple-structured-model-specs.R index 1b3d32fd..4d1258a6 100644 --- a/tests/testthat/test-simple-structured-model-specs.R +++ b/tests/testthat/test-simple-structured-model-specs.R @@ -12,13 +12,13 @@ test_that("tmb model specs can vectorize state updates with indices", { , state[R] ~ 0 ) , during = list( - flow_rate[infection] ~ beta * state[S] * state[I] / N - , flow_rate[recovery] ~ gamma * state[I] - , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) + flow_rates[infection] ~ beta * state[S] * state[I] / N + , flow_rates[recovery] ~ gamma * state[I] + , state ~ state + group_sums(flow_rates, to, state) - group_sums(flow_rates, from, state) ) , default = list( state = mp_zero_vector(state_labels) - , flow_rate = mp_zero_vector(flow$rate) + , flow_rates = mp_zero_vector(flow$rate) , N = 100 , beta = 0.25 , gamma = 0.1 @@ -29,6 +29,10 @@ test_that("tmb model specs can vectorize state updates with indices", { ) ) print(sir) + # sir$all_default_vars() + # sir$all_derived_vars() + # sir$all_formula_vars() + #mp_extract_exprs(sir, "init") correct_trajectory = structure(list(matrix = c("state", "state", "state", "state", diff --git a/tests/testthat/test-time-step.R b/tests/testthat/test-time-step.R index 5781ac42..3487bd59 100644 --- a/tests/testthat/test-time-step.R +++ b/tests/testthat/test-time-step.R @@ -1,7 +1,7 @@ test_that("time_step function returns a valid time-step no matter what", { - m = TMBModel( + m = macpan2:::TMBModel( init_mats = MatsList(x = 0, .mats_to_save = "x", .mats_to_return = "x"), - expr_list = ExprList(during = list(x ~ time_step(4))), + expr_list =mp_tmb_expr_list(during = list(x ~ time_step(4))), time_steps = Time(10) ) expect_identical( diff --git a/tests/testthat/test-tmb-model-spec.R b/tests/testthat/test-tmb-model-spec.R new file mode 100644 index 00000000..f0d92f1f --- /dev/null +++ b/tests/testthat/test-tmb-model-spec.R @@ -0,0 +1,17 @@ +library(macpan2) +si = mp_tmb_model_spec( + before = list( + I ~ 1 + , S ~ N - I + ) + , during = list( + infection_rate ~ beta * S * I / N + , S ~ S - infection_rate + , I ~ I + infection_rate + ) + , default = list(N = 100) +) +# si$all_default_vars() +# si$all_derived_vars() +# si$all_formula_vars() +mp_simulator(si, 10, "I", default = list(beta = 0.25)) diff --git a/tests/testthat/test-tmb-model.R b/tests/testthat/test-tmb-model.R index 71d1530a..d74d6fe9 100644 --- a/tests/testthat/test-tmb-model.R +++ b/tests/testthat/test-tmb-model.R @@ -1,8 +1,8 @@ test_that("simulator method works", { expect_identical( - TMBModel( + macpan2:::TMBModel( init_mats = MatsList(x = empty_matrix), - expr_list = ExprList(before = list(x ~ 1)), + expr_list =mp_tmb_expr_list(before = list(x ~ 1)), )$simulator()$report(), structure( list( @@ -16,6 +16,6 @@ test_that("simulator method works", { ) ) - null_model = TMBModel() + null_model = macpan2:::TMBModel() null_model$init_mats$.names() }) diff --git a/tests/testthat/test-tmb.R b/tests/testthat/test-tmb.R index f5f5da4a..8c1c8faf 100644 --- a/tests/testthat/test-tmb.R +++ b/tests/testthat/test-tmb.R @@ -1,8 +1,8 @@ library(macpan2) -m = TMBModel( +m = macpan2:::TMBModel( MatsList(x = 0, y = 0, .mats_to_save = c("x", "y"), .mats_to_return = c("x", "y")), - ExprList(during = list(x ~ x + 1)), + mp_tmb_expr_list(during = list(x ~ x + 1)), OptParamsList(0, par_id = 0, mat = "x", row_id = 0, col_id = 0), OptParamsList(), ObjectiveFunction(~x), diff --git a/vignettes/engine_agnostic_grammar.Rmd b/vignettes/engine_agnostic_grammar.Rmd index 5da2853e..990a69bf 100644 --- a/vignettes/engine_agnostic_grammar.Rmd +++ b/vignettes/engine_agnostic_grammar.Rmd @@ -113,7 +113,9 @@ SIR_starter <- function( ## Set up expressions list for each functional form -------------- ## names refer to when the calculation gets performed relative to ## the simulation time-step loop (before, during, ...) - expr_list <- mp_expr_list( + ## FIXME: we should not be referring to a specific engine in + ## a vignette about an 'engine-agnostic grammar' + expr_list <- mp_tmb_expr_list( before = list( ## aggregations N ~ sum(state) diff --git a/vignettes/tmb_model.Rmd b/vignettes/tmb_model.Rmd index f75cfb55..615d5302 100644 --- a/vignettes/tmb_model.Rmd +++ b/vignettes/tmb_model.Rmd @@ -29,8 +29,8 @@ from_states = c("S", "I") to_states = c("I", "R") flow_params = c(beta = 0.3, N = sum(state_vector)) -sir = TMBModel( - init_mats = MatsList( +sir = macpan2:::TMBModel( + init_mats = macpan2:::MatsList( state = state_vector , rates = flow_vector , params = flow_params @@ -55,7 +55,7 @@ sir = TMBModel( , .mats_to_return = "noisy_state" , .dimnames = list(noisy_state = list(names(state_vector), "")) ), - expr_list = ExprList( + expr_list = mp_tmb_expr_list( during = list( dummy ~ assign(rates, foi, 0, params[beta] * state[I] / params[N]) , flow ~ state[from] * rates From 61c3f6168cdb0722865a5a5820a98cdb99567f3f Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 10:10:10 -0500 Subject: [PATCH 327/332] #155 --- NAMESPACE | 20 +---- R/index_to_tmb.R | 51 ++++++----- R/index_utils.R | 8 +- R/labelled_partitions.R | 8 +- R/lists.R | 89 +++++++++---------- R/mp_dynamic_model.R | 31 +++---- README.Rmd | 6 +- README.md | 2 +- inst/model_library/sir/README.Rmd | 4 +- inst/model_library/sir/README.md | 2 +- inst/model_library/sir_age/experimentation.R | 2 +- inst/model_library/sir_age/simulator.R | 4 +- man/Partition.Rd | 57 ------------ man/mp_dynamic_model.Rd | 15 +++- ...b_simulator.Rd => mp_dynamic_simulator.Rd} | 12 +-- man/mp_extract_exprs.Rd | 20 ----- man/mp_report.Rd | 16 ---- man/union_vars.Rd | 15 ---- {R => misc/old-r-source}/indices.R | 0 tests/testthat/test-dynamic-model.R | 8 +- tests/testthat/test-labelled-partitions.R | 2 +- tests/testthat/test-partitions.R | 4 +- vignettes/engine_agnostic_grammar.Rmd | 12 +-- 23 files changed, 137 insertions(+), 251 deletions(-) delete mode 100644 man/Partition.Rd rename man/{mp_tmb_simulator.Rd => mp_dynamic_simulator.Rd} (57%) delete mode 100644 man/mp_extract_exprs.Rd delete mode 100644 man/mp_report.Rd delete mode 100644 man/union_vars.Rd rename {R => misc/old-r-source}/indices.R (100%) diff --git a/NAMESPACE b/NAMESPACE index 47e53fef..27f8d87d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,7 +11,6 @@ S3method(by_,character) S3method(by_,formula) S3method(c,String) S3method(c,StringData) -S3method(col_labels,character) S3method(head,Ledger) S3method(labelling_column_names,Index) S3method(labelling_column_names,Ledger) @@ -23,13 +22,11 @@ S3method(labels,ModelDefRun) S3method(labels,TMBDynamicSimulator) S3method(labels,VariableLabels) S3method(length,StructuredVector) -S3method(mp_combine_exprs,list) +S3method(mp_dynamic_simulator,DynamicModel) +S3method(mp_dynamic_simulator,ModelDefRun) S3method(mp_extract,DynamicModel) S3method(mp_extract,Ledger) S3method(mp_extract,ModelDefRun) -S3method(mp_extract_exprs,DynamicModel) -S3method(mp_extract_exprs,ExprList) -S3method(mp_extract_exprs,list) S3method(mp_final,TMBSimulator) S3method(mp_index,character) S3method(mp_index,data.frame) @@ -39,8 +36,6 @@ S3method(mp_labels,Ledger) S3method(mp_reference,Index) S3method(mp_reference,Ledger) S3method(mp_simulator,TMBModelSpec) -S3method(mp_tmb_simulator,DynamicModel) -S3method(mp_tmb_simulator,ModelDefRun) S3method(mp_trajectory,TMBSimulator) S3method(mp_union,Index) S3method(mp_union,Ledger) @@ -72,7 +67,6 @@ S3method(print,StructuredVector) S3method(print,TMBModelSpec) S3method(print,TMBSimulator) S3method(print,summary.Ledger) -S3method(row_labels,character) S3method(split_by,character) S3method(split_by,formula) S3method(str,Ledger) @@ -121,7 +115,6 @@ export(Method) export(NULLReader) export(ObjectiveFunction) export(OptParamsList) -export(Partition) export(Products) export(Quantities) export(RReader) @@ -135,7 +128,6 @@ export(VectorList) export(all_consistent) export(all_equal) export(all_not_equal) -export(by_) export(cartesian) export(cartesian_self) export(empty_matrix) @@ -144,7 +136,6 @@ export(finalizer_char) export(finalizer_index) export(init_merge) export(initial_valid_vars) -export(join_partitions) export(labelled_zero_vector) export(labelling_column_names) export(make_expr_parser) @@ -156,13 +147,12 @@ export(mp_cartesian) export(mp_catalogue) export(mp_choose) export(mp_choose_out) -export(mp_combine_exprs) export(mp_decompose) export(mp_default) export(mp_dynamic_model) +export(mp_dynamic_simulator) export(mp_expr_group_sum) export(mp_extract) -export(mp_extract_exprs) export(mp_factors) export(mp_final) export(mp_group) @@ -177,7 +167,6 @@ export(mp_linear) export(mp_lookup) export(mp_reference) export(mp_rename) -export(mp_report) export(mp_set_numbers) export(mp_setdiff) export(mp_simulator) @@ -185,11 +174,9 @@ export(mp_slices) export(mp_square) export(mp_subset) export(mp_symmetric) -export(mp_test_tmb) export(mp_tmb_expr_list) export(mp_tmb_library) export(mp_tmb_model_spec) -export(mp_tmb_simulator) export(mp_trajectory) export(mp_triangle) export(mp_union) @@ -206,7 +193,6 @@ export(to_labels) export(to_name) export(to_names) export(to_positions) -export(union_vars) importFrom(MASS,mvrnorm) importFrom(TMB,MakeADFun) importFrom(jsonlite,fromJSON) diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index c4da5207..3ee537ab 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -1,6 +1,10 @@ -#' TMB Simulator +#' TMB Simulator from Dynamic Model +#' +#' This is an 'old' function that was tested out at a workshop. +#' Currently it still drives the engine-agnostic-grammar vignette, +#' but we plan to replace this function with \code{\link{mp_simulator}}. #' -#' @param dynamic_model Object product by \code{\link{dynamic_model}}. +#' @param dynamic_model Object product by \code{\link{mp_dynamic_model}}. #' @param vectors Named list of named vectors as initial values for the #' simulations that are referenced in the expression list in the dynamic model. #' @param unstruc_mats = Named list of objects that can be coerced to @@ -10,13 +14,13 @@ #' #' @importFrom oor method_apply #' @export -mp_tmb_simulator = function(...) { - UseMethod("mp_tmb_simulator") +mp_dynamic_simulator = function(...) { + UseMethod("mp_dynamic_simulator") } #' @export -mp_tmb_simulator.DynamicModel = function(dynamic_model +mp_dynamic_simulator.DynamicModel = function(dynamic_model , time_steps = 0L , vectors = NULL , unstruc_mats = NULL @@ -102,7 +106,7 @@ mp_tmb_simulator.DynamicModel = function(dynamic_model } #' @export -mp_tmb_simulator.ModelDefRun = function(dynamic_model +mp_dynamic_simulator.ModelDefRun = function(dynamic_model , time_steps = 0L , vectors = NULL , unstruc_mats = NULL @@ -119,22 +123,23 @@ mp_tmb_simulator.ModelDefRun = function(dynamic_model ) { args = c(as.list(environment()), list(...)) args$dynamic_model = dynamic_model$dynamic_model - do.call(mp_tmb_simulator, args) + do.call(mp_dynamic_simulator, args) } -#' reporting function -#' @param parameter_vector Numeric vector equal in length to (?) -#' @param simulator Object produced by \code{\link{tmb_simulator}}. -#' @export -mp_report = function(simulator, parameter_vector = numeric(), phases = "during") { - if (length(parameter_vector) == 0L) parameter_vector = 0 - r = do.call( - simulator$report, - c( - as.list(parameter_vector), - list(.phases = phases) - ) - ) - rownames(r) = NULL - r -} +# reporting function -- probably will delete soon in favour +# of mp_trajectory +# @param parameter_vector Numeric vector equal in length to (?) +# @param simulator Object produced by \code{\link{tmb_simulator}}. + +# mp_report = function(simulator, parameter_vector = numeric(), phases = "during") { +# if (length(parameter_vector) == 0L) parameter_vector = 0 +# r = do.call( +# simulator$report, +# c( +# as.list(parameter_vector), +# list(.phases = phases) +# ) +# ) +# rownames(r) = NULL +# r +# } diff --git a/R/index_utils.R b/R/index_utils.R index 52e3d13a..346d4d1f 100644 --- a/R/index_utils.R +++ b/R/index_utils.R @@ -122,14 +122,14 @@ increasing_int_seq = memoise(increasing_int_seq) ## https://stackoverflow.com/questions/11095992/generating-all-distinct-permutations-of-a-list-in-r permutations <- function(n) { - if (n==1){ + if (n == 1) { return(matrix(1)) } else { - sp <- permutations(n-1) + sp <- permutations(n - 1) p <- nrow(sp) A <- matrix(nrow = n * p, ncol = n) - for(i in 1:n){ - A[(i-1)*p+1:p,] <- cbind(i,sp+(sp>=i)) + for (i in 1:n) { + A[(i - 1) * p + 1:p,] <- cbind(i,sp + (sp >= i)) } return(A) } diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 399c84a7..5ade48b0 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -39,7 +39,7 @@ #' #' * `products` #' -#' @export +#' @noRd Partition = function(frame) { self = Base() self$.partition = frame_to_part(frame) @@ -212,7 +212,7 @@ enforce_schema = function(frame, ...) { #' #' @param ... \code{\link{Partition}} objects to combine. #' -#' @export +#' @noRd union_vars = function(...) { not_null = function(x) !is.null(x) l = Filter(not_null, list(...)) @@ -292,7 +292,7 @@ NumericPartition = function(frame, numeric_vector) { return_object(self, "NumericPartition") } -#' @export + join_partitions = function(x, y, by = "") { by = by_(by) merge(x, y @@ -302,7 +302,7 @@ join_partitions = function(x, y, by = "") { ) } -#' @export + by_ = function(by) UseMethod("by_") #' @export diff --git a/R/lists.R b/R/lists.R index 22ec0260..6e52197e 100644 --- a/R/lists.R +++ b/R/lists.R @@ -75,52 +75,47 @@ assert_named_list = function(l) { self_named_vector = function(...) c(...) |> setNames(c(...)) -#' Extract Expressions by Name -#' -#' @param x Object containing named expressions. -#' @param ... Character vectors containing names of expressions. -#' -#' @returns An object the same type as \code{x} but only with -#' those expressions identified by the names in \code{...}. -#' -#' @export -mp_extract_exprs = function(x, ...) UseMethod("mp_extract_exprs") - -#' @export -mp_extract_exprs.list = function(x, ...) { - nms = (list(...) - |> lapply(as.character) - |> unlist(use.names = FALSE) - |> unique() - ) - x[names(x) %in% nms] -} - -#' @export -mp_extract_exprs.ExprList = function(x, ...) { - nms = (list(...) - |> lapply(as.character) - |> unlist(use.names = FALSE) - |> unique() - ) - ExprList( - before = mp_extract_exprs(x$before, ...), - during = mp_extract_exprs(x$during, ...), - after = mp_extract_exprs(x$after, ...), - .simulate_exprs = intersect(x$.simulate_exprs, nms) - ) -} - -#' @export -mp_extract_exprs.DynamicModel = function(x, ...) { - mp_extract_exprs(x$expr_list, ...) -} +# Extract Expressions by Name -- Experimental +# +# @param x Object containing named expressions. +# @param ... Character vectors containing names of expressions. +# +# @returns An object the same type as \code{x} but only with +# those expressions identified by the names in \code{...}. +# +# mp_extract_exprs = function(x, ...) UseMethod("mp_extract_exprs") +# +# mp_extract_exprs.list = function(x, ...) { +# nms = (list(...) +# |> lapply(as.character) +# |> unlist(use.names = FALSE) +# |> unique() +# ) +# x[names(x) %in% nms] +# } +# +# mp_extract_exprs.ExprList = function(x, ...) { +# nms = (list(...) +# |> lapply(as.character) +# |> unlist(use.names = FALSE) +# |> unique() +# ) +# ExprList( +# before = mp_extract_exprs(x$before, ...), +# during = mp_extract_exprs(x$during, ...), +# after = mp_extract_exprs(x$after, ...), +# .simulate_exprs = intersect(x$.simulate_exprs, nms) +# ) +# } +# +# mp_extract_exprs.DynamicModel = function(x, ...) { +# mp_extract_exprs(x$expr_list, ...) +# } +# -#' @export -mp_combine_exprs = function(...) { - UseMethod("mp_combine_exprs") -} - -#' @export -mp_combine_exprs.list = function(...) c(...) +# mp_combine_exprs = function(...) { +# UseMethod("mp_combine_exprs") +# } +# +# mp_combine_exprs.list = function(...) c(...) diff --git a/R/mp_dynamic_model.R b/R/mp_dynamic_model.R index f7bd5b5f..a037c544 100644 --- a/R/mp_dynamic_model.R +++ b/R/mp_dynamic_model.R @@ -88,28 +88,21 @@ DynamicModel = function( #' Dynamic Model -#' -#' -#' +#' +#' This is an 'old' model specification function that was tested out +#' at a workshop. Currently it still drives the engine-agnostic-grammar +#' vignette, but we plan to replace this function with +#' \code{\link{mp_tmb_model_spec}} and other model specification +#' functions. +#' +#' @param expr_list Expression list. +#' @param ledgers Ledgers. +#' @param init_vecs Initial structured vectors. +#' @param unstruc_mats Initial unstructured matrices. +#' #' @export mp_dynamic_model = DynamicModel -#' @export -mp_test_tmb = function(..., ledgers, vectors, unstruc_mats) { - m = mp_dynamic_model( - expr_list = mp_tmb_expr_list(before = list(...)) - , ledgers = ledgers - , init_vecs = vectors - , unstruc_mats = unstruc_mats - ) - mp_tmb_simulator(m - , time_steps = 0L - , vectors = method_apply(vectors, "numbers") - , unstruc_mats = unstruc_mats - , mats_to_return = m$derived_matrix_names() - , mats_to_save = m$derived_matrix_names() - ) |> mp_report(phases = "before") -} #' @export print.DynamicModel = function(x, ...) { diff --git a/README.Rmd b/README.Rmd index 5f31b62c..a138d83a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -180,7 +180,7 @@ A big question with calibration is do we want there to be an engine-agnostic DSL A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this data frame take? -One option is the same format as the output of `mp_report`. This would have several benefits. +One option is the same format as the output of `mp_trajectory`. This would have several benefits. * Consistency with input and output formats, making it a little easier to learn. * Easy to manipulate output into input for testing calibration functionality. @@ -431,8 +431,8 @@ si = mp_dynamic_model( unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) ) (si - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "reports") - |> mp_report() + |> mp_dynamic_simulator(time_steps = 10, mats_to_return = "reports") + |> mp_trajectory() ) ``` diff --git a/README.md b/README.md index 0bed9895..6ee92821 100644 --- a/README.md +++ b/README.md @@ -752,7 +752,7 @@ si = mp_dynamic_model( unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) ) (si - |> mp_tmb_simulator(time_steps = 10, mats_to_return = "reports") + |> mp_dynamic_simulator(time_steps = 10, mats_to_return = "reports") |> mp_report() ) ``` diff --git a/inst/model_library/sir/README.Rmd b/inst/model_library/sir/README.Rmd index 95281489..d7ab3fd2 100644 --- a/inst/model_library/sir/README.Rmd +++ b/inst/model_library/sir/README.Rmd @@ -11,7 +11,7 @@ library(dplyr) library(ggplot2) sir_dir = system.file("model_library", "sir", package = "macpan2") sir_mod = Compartmental2(sir_dir) -sir_sim = mp_tmb_simulator(sir_mod +sir_sim = mp_dynamic_simulator(sir_mod , vectors = list( state = c(S = 999, I = 1, R = 0), flow_rates = c(lambda = NA, gamma = 0.1), @@ -35,7 +35,7 @@ sir_mod$influences() ```{r plot_model} (sir_sim - |> mp_report() + |> mp_trajectory() |> filter(matrix == "state") |> mutate(state = factor(row, levels = c("S", "I", "R"))) |> ggplot() diff --git a/inst/model_library/sir/README.md b/inst/model_library/sir/README.md index 886a5321..113753b7 100644 --- a/inst/model_library/sir/README.md +++ b/inst/model_library/sir/README.md @@ -11,7 +11,7 @@ implemented as an example. library(ggplot2) sir_dir = system.file("model_library", "sir", package = "macpan2") sir_mod = Compartmental2(sir_dir) - sir_sim = mp_tmb_simulator(sir_mod + sir_sim = mp_dynamic_simulator(sir_mod , vectors = list( state = c(S = 999, I = 1, R = 0), flow_rates = c(lambda = NA, gamma = 0.1), diff --git a/inst/model_library/sir_age/experimentation.R b/inst/model_library/sir_age/experimentation.R index 11f90f4c..52401e3a 100644 --- a/inst/model_library/sir_age/experimentation.R +++ b/inst/model_library/sir_age/experimentation.R @@ -3,7 +3,7 @@ library(ggplot2) library(tidyr) library(stringr) -(mp_report(sir_sim) +(mp_trajectory(sir_sim) |> filter(matrix == "state") |> separate(row, c("epi", "age")) |> mutate(epi = factor(epi, levels = c("S", "I", "R"))) diff --git a/inst/model_library/sir_age/simulator.R b/inst/model_library/sir_age/simulator.R index 4c354023..dbcaa558 100644 --- a/inst/model_library/sir_age/simulator.R +++ b/inst/model_library/sir_age/simulator.R @@ -1,7 +1,7 @@ ## collect information into a simulator ----------------------- -sir_sim = mp_tmb_simulator(dynamic_model +sir_sim = mp_dynamic_simulator(dynamic_model , vectors = init_vecs , time_steps = 100L ) -mp_report(sir_sim) +mp_trajectory(sir_sim) diff --git a/man/Partition.Rd b/man/Partition.Rd deleted file mode 100644 index 6f49ea3c..00000000 --- a/man/Partition.Rd +++ /dev/null @@ -1,57 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/labelled_partitions.R -\name{Partition} -\alias{Partition} -\title{Partition} -\usage{ -Partition(frame) -} -\arguments{ -\item{frame}{Data frame representing the partition. The column names must -consist only of letters, numbers, and start with a letter. The columns of -the data frame must be character vectors such that each value is composed -entirely of letters, numbers, underscore, and must start with a letter -unless it is a blank string. Missing values are not allowed, but blank -strings are. Each row must be unique.} -} -\value{ -Object of class \code{Partition} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$frame()} -- The \code{Partition} object as a data frame. -\item \verb{$dotted()} -- The \code{Partition} object as a data frame with one united column. -\item \verb{$names()} -- The names of the \code{Partition} (i.e. the column names). -\item \verb{$name()} -- The name of the \code{Partition} (i.e. the dot-concatenated column names). -\item \verb{$labels()} -- The labels of the \code{Partition} (i.e. the row-wise dot-concatenated columns). -\item \verb{$partial_labels()} -- TODO -\item \verb{$filter(..., .wrt, .comparison_function)} -- Filter by keeping only a subset of labels. -\item \verb{$filter_out(..., .wrt, .comparison_function)} -- Filter by removing a subset of labels. -\item \verb{$filter_ordered(..., .wrt, .comparison_function = all_equal)} -- Filter and order by labels. -\itemize{ -\item \code{...} -- Labels to filter -\item \code{.wrt} -- The filtering labels are with respect to a particular \code{Partition}, and -\code{.wrt} is the name of this \code{Partition}. -\item \code{.comparison_function} -- Boolean function to decide if each filtering label is -equal to each label in the \code{Partition}. -} -\item \verb{$select(...)} -- Create a new \code{Partition} with a subset of names. The rows of the new -\code{Partition} are de-duplicated. -\item \verb{$select_out(...)} -- Create a new \code{Partition} without a subset of names. -\itemize{ -\item \code{...} -- Names to keep in the resulting \code{Partition}. -} -\item \verb{$expand(name)} -- TODO -\item \verb{$union(other)} -- TODO -} -} - -\subsection{Fields}{ -\itemize{ -\item \code{products} -} -} -} -\description{ -Create object for manipulating partitions, which are sets of -labels for representing and naming model entities. -} diff --git a/man/mp_dynamic_model.Rd b/man/mp_dynamic_model.Rd index 8215c26b..3817925c 100644 --- a/man/mp_dynamic_model.Rd +++ b/man/mp_dynamic_model.Rd @@ -11,6 +11,19 @@ mp_dynamic_model( unstruc_mats = list() ) } +\arguments{ +\item{expr_list}{Expression list.} + +\item{ledgers}{Ledgers.} + +\item{init_vecs}{Initial structured vectors.} + +\item{unstruc_mats}{Initial unstructured matrices.} +} \description{ -Dynamic Model +This is an 'old' model specification function that was tested out +at a workshop. Currently it still drives the engine-agnostic-grammar +vignette, but we plan to replace this function with +\code{\link{mp_tmb_model_spec}} and other model specification +functions. } diff --git a/man/mp_tmb_simulator.Rd b/man/mp_dynamic_simulator.Rd similarity index 57% rename from man/mp_tmb_simulator.Rd rename to man/mp_dynamic_simulator.Rd index 3ca2f152..686a3d7d 100644 --- a/man/mp_tmb_simulator.Rd +++ b/man/mp_dynamic_simulator.Rd @@ -1,13 +1,13 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/index_to_tmb.R -\name{mp_tmb_simulator} -\alias{mp_tmb_simulator} +\name{mp_dynamic_simulator} +\alias{mp_dynamic_simulator} \title{TMB Simulator from Dynamic Model} \usage{ -mp_tmb_simulator(...) +mp_dynamic_simulator(...) } \arguments{ -\item{dynamic_model}{Object product by \code{\link{dynamic_model}}.} +\item{dynamic_model}{Object product by \code{\link{mp_dynamic_model}}.} \item{vectors}{Named list of named vectors as initial values for the simulations that are referenced in the expression list in the dynamic model.} @@ -17,5 +17,7 @@ numerical matrices that are used in the expression list of the dynamic model.} } \description{ -TMB Simulator from Dynamic Model +This is an 'old' function that was tested out at a workshop. +Currently it still drives the engine-agnostic-grammar vignette, +but we plan to replace this function with \code{\link{mp_simulator}}. } diff --git a/man/mp_extract_exprs.Rd b/man/mp_extract_exprs.Rd deleted file mode 100644 index dd8b3ae4..00000000 --- a/man/mp_extract_exprs.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/lists.R -\name{mp_extract_exprs} -\alias{mp_extract_exprs} -\title{Extract Expressions by Name} -\usage{ -mp_extract_exprs(x, ...) -} -\arguments{ -\item{x}{Object containing named expressions.} - -\item{...}{Character vectors containing names of expressions.} -} -\value{ -An object the same type as \code{x} but only with -those expressions identified by the names in \code{...}. -} -\description{ -Extract Expressions by Name -} diff --git a/man/mp_report.Rd b/man/mp_report.Rd deleted file mode 100644 index 569838f7..00000000 --- a/man/mp_report.Rd +++ /dev/null @@ -1,16 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/index_to_tmb.R -\name{mp_report} -\alias{mp_report} -\title{reporting function} -\usage{ -mp_report(simulator, parameter_vector = numeric(), phases = "during") -} -\arguments{ -\item{simulator}{Object produced by \code{\link{tmb_simulator}}.} - -\item{parameter_vector}{Numeric vector equal in length to (?)} -} -\description{ -reporting function -} diff --git a/man/union_vars.Rd b/man/union_vars.Rd deleted file mode 100644 index 51c0ba77..00000000 --- a/man/union_vars.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/labelled_partitions.R -\name{union_vars} -\alias{union_vars} -\title{Union of Variables} -\usage{ -union_vars(...) -} -\arguments{ -\item{...}{\code{\link{Partition}} objects to combine.} -} -\description{ -Take the union of a set of variable lists, each of which is represented -by a \code{\link{Partition}} object. -} diff --git a/R/indices.R b/misc/old-r-source/indices.R similarity index 100% rename from R/indices.R rename to misc/old-r-source/indices.R diff --git a/tests/testthat/test-dynamic-model.R b/tests/testthat/test-dynamic-model.R index 1222860c..6c5ed90a 100644 --- a/tests/testthat/test-dynamic-model.R +++ b/tests/testthat/test-dynamic-model.R @@ -133,7 +133,7 @@ sir <- SIR_starter( ## ----sir-simulator------------------------------------------------------------ ## SIR model simulator ------------------------- -sir_simulator <- mp_tmb_simulator( +sir_simulator <- mp_dynamic_simulator( dynamic_model = sir, vectors = list( state = c(S = 999, I = 1, R = 0), @@ -145,7 +145,7 @@ sir_simulator <- mp_tmb_simulator( ## ----sir-results-------------------------------------------------------------- ## SIR model simulation results ------------------------- -sir_results <- mp_report(sir_simulator) +sir_results <- mp_trajectory(sir_simulator) ## ----sir-results-head--------------------------------------------------------- @@ -295,7 +295,7 @@ two_strain_model <- SIR_starter( force_of_infection = force_of_infection ) -two_strain_simulator <- mp_tmb_simulator( +two_strain_simulator <- mp_dynamic_simulator( dynamic_model = two_strain_model, vectors = list( state = c(S = 998, I.A = 1, I.B = 1, R = 0), @@ -304,7 +304,7 @@ two_strain_simulator <- mp_tmb_simulator( time_steps = 100 ) -two_strain_results <- (mp_report(two_strain_simulator) +two_strain_results <- (mp_trajectory(two_strain_simulator) |> filter(matrix == "state") ) diff --git a/tests/testthat/test-labelled-partitions.R b/tests/testthat/test-labelled-partitions.R index d85eb1f3..c9b1f0ae 100644 --- a/tests/testthat/test-labelled-partitions.R +++ b/tests/testthat/test-labelled-partitions.R @@ -43,7 +43,7 @@ test_that("model files can be read in and used", { test_that("labels, name, and names conversion is correct", { - p = Partition(data.frame(A = letters[1:4], B = letters[26:23])) + p = macpan2:::Partition(data.frame(A = letters[1:4], B = letters[26:23])) print(p) dotted_scalar = macpan2:::StringDottedScalar("a.z") dotted_vector = macpan2:::StringDottedVector("a.z", "b.y") diff --git a/tests/testthat/test-partitions.R b/tests/testthat/test-partitions.R index 25cf33ac..2aae2df2 100644 --- a/tests/testthat/test-partitions.R +++ b/tests/testthat/test-partitions.R @@ -1,7 +1,7 @@ library(oor) library(macpan2) r = CSVReader(system.file("starter_models", "sir_vax", "variables.csv", package = "macpan2")) -x = Partition(r$read()) +x = macpan2:::Partition(r$read()) x$name() process_partition_names = function(...) { @@ -65,7 +65,7 @@ PartitionAlt = function(frame) { } print.PartitionAlt = function(x, ...) print(x$frame) xx = PartitionAlt(r$read()) -x = Partition(r$read()) +x = macpan2:::Partition(r$read()) identical(xx$names(), x$names()) identical(xx$name(), x$name()) identical(xx$labels(), x$labels()) diff --git a/vignettes/engine_agnostic_grammar.Rmd b/vignettes/engine_agnostic_grammar.Rmd index 990a69bf..4a2010d3 100644 --- a/vignettes/engine_agnostic_grammar.Rmd +++ b/vignettes/engine_agnostic_grammar.Rmd @@ -264,11 +264,11 @@ sir <- SIR_starter( ) ``` -We can create a model simulator using `mp_tmb_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for each index (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): +We can create a model simulator using `mp_dynamic_simulator()`^[`tmb` stands for "template model builder", the underlying simulation engine provided by the [TMB package](https://kaskr.github.io/adcomp/Introduction.html)], giving it the model object (`model`), initial values for each index (`vectors`), as well as the number of total time steps in the simulation (`time_steps`): ```{r sir-simulator} ## SIR model simulator ------------------------- -sir_simulator <- mp_tmb_simulator( +sir_simulator <- mp_dynamic_simulator( dynamic_model = sir, vectors = list( state = c(S = 999, I = 1, R = 0), @@ -280,11 +280,11 @@ sir_simulator <- mp_tmb_simulator( Note that we've specified `NA` for `lambda` as it will be calculated for us using the force of infection functional form. -Then we can actually simulate the model by passing our model simulator to `mp_report()`: +Then we can actually simulate the model by passing our model simulator to `mp_trajectory()`: ```{r sir-results} ## SIR model simulation results ------------------------- -sir_results <- mp_report(sir_simulator) +sir_results <- mp_trajectory(sir_simulator) ``` The output of the simulation is a [long data frame](https://r4ds.had.co.nz/tidy-data.html#longer): @@ -514,7 +514,7 @@ two_strain_model <- SIR_starter( force_of_infection = force_of_infection ) -two_strain_simulator <- mp_tmb_simulator( +two_strain_simulator <- mp_dynamic_simulator( dynamic_model = two_strain_model, vectors = list( state = c(S = 998, I.A = 1, I.B = 1, R = 0), @@ -523,7 +523,7 @@ two_strain_simulator <- mp_tmb_simulator( time_steps = 100 ) -two_strain_results <- (mp_report(two_strain_simulator) +two_strain_results <- (mp_trajectory(two_strain_simulator) |> filter(matrix == "state") ) From 49b4391d4e7ae1200f26f2e90071d4ab2931f51d Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 11:54:10 -0500 Subject: [PATCH 328/332] #155 --- NAMESPACE | 50 ++------ R/engine_eval.R | 2 +- R/engine_methods.R | 4 +- R/formula_data.R | 4 +- R/index_to_tmb.R | 17 ++- R/labelled_partitions.R | 2 +- R/log_files.R | 2 +- R/mats_list.R | 2 +- R/method_caching.R | 2 + R/mp_cartesian.R | 5 +- R/mp_decompose.R | 7 +- R/mp_dynamic_model.R | 3 +- R/mp_group.R | 24 +++- R/mp_index.R | 51 ++++++-- R/mp_join.R | 9 +- R/mp_labels.R | 10 ++ R/mp_library.R | 28 ++++- R/mp_subset.R | 4 +- R/mp_tmb_model_spec.R | 4 +- R/name_utils.R | 12 +- R/objective_function.R | 2 +- R/opt_params.R | 2 +- R/parameter_files.R | 0 R/parse_expr.R | 2 +- R/parse_table.R | 0 R/products.R | 119 +++++++++--------- R/simple_sims.R | 8 +- R/time.R | 2 +- R/tmb_model.R | 37 ++---- R/vec_utils.R | 2 + R/vector.R | 39 +++--- R/zzz.R | 2 + README.Rmd | 4 +- README.md | 4 +- inst/starter_models/sir_symp/tmb.R | 10 +- man/LedgerDefinition.Rd | 2 +- man/LogFile.Rd | 27 ---- man/MatsList.Rd | 62 --------- man/MethList.Rd | 14 --- man/Method.Rd | 32 ----- man/ObjectiveFunction.Rd | 45 ------- man/OptParamsList.Rd | 63 ---------- man/Products.Rd | 14 --- man/Time.Rd | 26 ---- man/cartesian.Rd | 16 --- man/cartesian_self.Rd | 23 ---- man/engine_eval.Rd | 2 +- man/mp_aggregate.Rd | 9 ++ man/mp_dynamic_simulator.Rd | 17 ++- man/mp_extract.Rd | 17 +++ man/mp_group.Rd | 17 +++ man/mp_index.Rd | 2 +- man/mp_labels.Rd | 19 +++ man/mp_ledgers.Rd | 3 + man/mp_linear.Rd | 4 +- man/{model_starter.Rd => mp_model_starter.Rd} | 6 +- man/mp_positions.Rd | 21 ++++ man/mp_reference.Rd | 18 +++ man/mp_rename.Rd | 2 + man/mp_square.Rd | 2 +- man/{mp_vector.Rd => mp_structured_vector.Rd} | 39 ++++-- man/mp_symmetric.Rd | 2 +- man/mp_tmb_library.Rd | 33 +++++ man/mp_tmb_model_spec.Rd | 4 +- man/mp_trajectory.Rd | 16 +++ man/mp_triangle.Rd | 2 +- man/mp_zero_vector.Rd | 2 + man/parse_expr_list.Rd | 38 ------ man/simple_sims.Rd | 9 +- man/to_positions.Rd | 19 +++ misc/experiments/bigmatmult/bigmatmult.R | 2 +- misc/experiments/refactorcpp.R | 28 ++--- .../sir_from_model_def.R | 6 +- {R => misc/old-r-source}/mat_expr_bundlers.R | 0 misc/old-r-source/model_data_structure.R | 2 +- {R => misc/old-r-source}/quantities.R | 0 {R => misc/old-r-source}/settings.R | 0 tests/testthat/test-c.R | 2 +- .../testthat/test-dimnames-and-shape-change.R | 8 +- tests/testthat/test-dynamic-model.R | 8 +- tests/testthat/test-error-row.R | 4 +- tests/testthat/test-expr-list.R | 6 +- tests/testthat/test-labelled-partitions.R | 2 +- tests/testthat/test-literals.R | 10 +- tests/testthat/test-model-structure.R | 1 + tests/testthat/test-opt-params.R | 4 +- tests/testthat/test-rbind-time-lag.R | 4 +- tests/testthat/test-rcbind.R | 2 +- .../test-simple-structured-model-specs.R | 4 +- tests/testthat/test-time-step.R | 4 +- tests/testthat/test-tmb-model.R | 2 +- tests/testthat/test-tmb.R | 10 +- tests/testthat/test-variable-products.R | 6 +- vignettes/engine_agnostic_grammar.Rmd | 4 +- vignettes/example_models.Rmd | 2 +- vignettes/tmb_model.Rmd | 2 +- 96 files changed, 568 insertions(+), 657 deletions(-) delete mode 100644 R/parameter_files.R delete mode 100644 R/parse_table.R delete mode 100644 man/LogFile.Rd delete mode 100644 man/MatsList.Rd delete mode 100644 man/MethList.Rd delete mode 100644 man/Method.Rd delete mode 100644 man/ObjectiveFunction.Rd delete mode 100644 man/OptParamsList.Rd delete mode 100644 man/Products.Rd delete mode 100644 man/Time.Rd delete mode 100644 man/cartesian.Rd delete mode 100644 man/cartesian_self.Rd create mode 100644 man/mp_extract.Rd create mode 100644 man/mp_group.Rd create mode 100644 man/mp_labels.Rd rename man/{model_starter.Rd => mp_model_starter.Rd} (80%) create mode 100644 man/mp_positions.Rd create mode 100644 man/mp_reference.Rd rename man/{mp_vector.Rd => mp_structured_vector.Rd} (52%) create mode 100644 man/mp_tmb_library.Rd create mode 100644 man/mp_trajectory.Rd delete mode 100644 man/parse_expr_list.Rd create mode 100644 man/to_positions.Rd rename {R => misc/old-r-source}/mat_expr_bundlers.R (100%) rename {R => misc/old-r-source}/quantities.R (100%) rename {R => misc/old-r-source}/settings.R (100%) diff --git a/NAMESPACE b/NAMESPACE index 27f8d87d..2f44016a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -27,24 +27,22 @@ S3method(mp_dynamic_simulator,ModelDefRun) S3method(mp_extract,DynamicModel) S3method(mp_extract,Ledger) S3method(mp_extract,ModelDefRun) -S3method(mp_final,TMBSimulator) S3method(mp_index,character) S3method(mp_index,data.frame) -S3method(mp_initial,TMBSimulator) S3method(mp_labels,Index) S3method(mp_labels,Ledger) S3method(mp_reference,Index) S3method(mp_reference,Ledger) S3method(mp_simulator,TMBModelSpec) +S3method(mp_structured_vector,Index) +S3method(mp_structured_vector,Link) +S3method(mp_structured_vector,StructuredVector) +S3method(mp_structured_vector,character) +S3method(mp_structured_vector,data.frame) +S3method(mp_structured_vector,numeric) S3method(mp_trajectory,TMBSimulator) S3method(mp_union,Index) S3method(mp_union,Ledger) -S3method(mp_vector,Index) -S3method(mp_vector,Link) -S3method(mp_vector,StructuredVector) -S3method(mp_vector,character) -S3method(mp_vector,data.frame) -S3method(mp_vector,numeric) S3method(mp_zero_vector,Index) S3method(mp_zero_vector,character) S3method(names,Index) @@ -60,7 +58,6 @@ S3method(print,Index) S3method(print,Ledger) S3method(print,MathExpression) S3method(print,Partition) -S3method(print,Quantities) S3method(print,String) S3method(print,StringData) S3method(print,StructuredVector) @@ -100,71 +97,43 @@ S3method(to_positions,numeric) export(BinaryOperator) export(CSVReader) export(Collection) -export(DynamicModel) export(Files) -export(Index) export(JSONReader) export(Log) -export(LogFile) export(Logit) export(MathExpressionFromFunc) export(MathExpressionFromStrings) -export(MatsList) -export(MethList) -export(Method) export(NULLReader) -export(ObjectiveFunction) -export(OptParamsList) -export(Products) -export(Quantities) export(RReader) export(Reader) export(StringDataFromDotted) export(StringDataFromFrame) export(TXTReader) -export(Time) export(Transform) -export(VectorList) export(all_consistent) export(all_equal) export(all_not_equal) -export(cartesian) -export(cartesian_self) export(empty_matrix) export(engine_eval) export(finalizer_char) export(finalizer_index) -export(init_merge) export(initial_valid_vars) -export(labelled_zero_vector) -export(labelling_column_names) export(make_expr_parser) -export(model_starter) export(mp_aggregate) -export(mp_calibrator) -export(mp_calibrator.TMBModelSpec) export(mp_cartesian) -export(mp_catalogue) -export(mp_choose) -export(mp_choose_out) -export(mp_decompose) -export(mp_default) export(mp_dynamic_model) export(mp_dynamic_simulator) -export(mp_expr_group_sum) export(mp_extract) export(mp_factors) -export(mp_final) export(mp_group) export(mp_index) -export(mp_indices) -export(mp_initial) export(mp_join) export(mp_labels) export(mp_ledgers) -export(mp_library) export(mp_linear) export(mp_lookup) +export(mp_model_starter) +export(mp_positions) export(mp_reference) export(mp_rename) export(mp_set_numbers) @@ -172,6 +141,7 @@ export(mp_setdiff) export(mp_simulator) export(mp_slices) export(mp_square) +export(mp_structured_vector) export(mp_subset) export(mp_symmetric) export(mp_tmb_expr_list) @@ -180,11 +150,9 @@ export(mp_tmb_model_spec) export(mp_trajectory) export(mp_triangle) export(mp_union) -export(mp_vector) export(mp_zero_vector) export(nlist) export(not_all_equal) -export(parse_expr_list) export(rbf) export(reader_spec) export(show_models) diff --git a/R/engine_eval.R b/R/engine_eval.R index 58bc192b..7ef7e3a9 100644 --- a/R/engine_eval.R +++ b/R/engine_eval.R @@ -17,7 +17,7 @@ #' are extending the #' [engine](https://canmod.github.io/macpan2/articles/cpp_side.html) #' yourself. -#' @param .structure_labels See \code{\link{MatsList}}. +#' @param .structure_labels Deprecated. #' #' @return Matrix being produced on the right-hand-side or matrix given in #' \code{.matrix_to_return} if it is provided. diff --git a/R/engine_methods.R b/R/engine_methods.R index 2522dacb..829d3334 100644 --- a/R/engine_methods.R +++ b/R/engine_methods.R @@ -54,7 +54,7 @@ MethListFromEngineMethods = function(engine_methods) { #' Method List #' #' @param ... List of \code{\link{Method}} objects. -#' @export +#' @noRd MethList = function(...) { self = Base() @@ -103,7 +103,7 @@ names.MethList = function(x) { #' by the method to produce an output matrix. #' @param init_mats Object of class \code{\link{MatsList}}. #' @param int_vecs = Object of class \code{\link{IntVecs}}. -#' @export +#' @noRd Method = function(name, mat_args, int_vec_args, init_mats = MatsList(), int_vecs = IntVecs()) { self = Base() diff --git a/R/formula_data.R b/R/formula_data.R index 017f2c84..ce8a14d0 100644 --- a/R/formula_data.R +++ b/R/formula_data.R @@ -57,6 +57,8 @@ LedgerData = function(...) { #' Bundle up several ledgers (see \code{\link{LedgerDefinition}}) to pass #' to \code{\link{mp_dynamic_model}}. #' +#' @param ... Ledgers to bundle up. +#' #' @export mp_ledgers = function(...) { wrap_ledgers_in_one_element_lists = function(x) { @@ -79,7 +81,7 @@ mp_ledgers = function(...) { #' in \code{unstructured_matrix_list}. #' @param ledgers An object produced using \code{\link{mp_ledgers}}. #' @param vector_list Named list of objected produced using -#' \code{\link{mp_vector}}. +#' \code{\link{mp_structured_vector}}. #' @param unstructured_matrix_list Named list of objects that can be coerced #' to a matrix. #' diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 3ee537ab..9309f706 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -10,11 +10,26 @@ #' @param unstruc_mats = Named list of objects that can be coerced to #' numerical matrices that are used in the expression list of the #' dynamic model. +#' @inheritParams mp_tmb_model_spec #' @inheritParams TMBModel +#' @inheritParams TMBSimulator #' #' @importFrom oor method_apply #' @export -mp_dynamic_simulator = function(...) { +mp_dynamic_simulator = function(dynamic_model + , time_steps = 0L + , vectors = NULL + , unstruc_mats = NULL + , mats_to_save = NULL + , mats_to_return = NULL + , params = OptParamsList(0) + , random = OptParamsList() + , obj_fn = ObjectiveFunction(~0) + , log_file = LogFile() + , do_pred_sdreport = TRUE + , tmb_cpp = "macpan2" + , initialize_ad_fun = TRUE + , ...) { UseMethod("mp_dynamic_simulator") } diff --git a/R/labelled_partitions.R b/R/labelled_partitions.R index 5ade48b0..205cb47e 100644 --- a/R/labelled_partitions.R +++ b/R/labelled_partitions.R @@ -43,7 +43,7 @@ Partition = function(frame) { self = Base() self$.partition = frame_to_part(frame) - self$products = Products(self) + #self$products = Products(self) ## TODO: remove if no issues come up self$frame = function() self$.partition$frame() self$dotted = function() self$.partition$dot()$frame() self$names = function() names(self$frame()) diff --git a/R/log_files.R b/R/log_files.R index 5c56f214..5741742b 100644 --- a/R/log_files.R +++ b/R/log_files.R @@ -13,7 +13,7 @@ #' * `$err_msg()` -- Return the current error message in the log file, if any. #' * Other methods inherited from \code{\link{Files}} #' -#' @export +#' @noRd LogFile = function(directory = tempdir()) { self = Files(fix_dir(directory), reader_spec("log.txt", TXTReader)) self$log = function() self$get("log") diff --git a/R/mats_list.R b/R/mats_list.R index 76204a28..8805fb43 100644 --- a/R/mats_list.R +++ b/R/mats_list.R @@ -37,7 +37,7 @@ #' * `$add_mats(...)`: Add matrices to the list and return a new #' regenerated \code{MatsList} object. #' -#' @export +#' @noRd MatsList = function(... , .mats_to_save = character(0L) , .mats_to_return = character(0L) diff --git a/R/method_caching.R b/R/method_caching.R index 41d5d06a..c4631a60 100644 --- a/R/method_caching.R +++ b/R/method_caching.R @@ -1,3 +1,5 @@ +## Internal classes for caching the return value of a no-op method. + CachedMethod = function(object, method, method_name) { self = Base() self$object = object diff --git a/R/mp_cartesian.R b/R/mp_cartesian.R index c5e0e3eb..9e32846e 100644 --- a/R/mp_cartesian.R +++ b/R/mp_cartesian.R @@ -58,6 +58,7 @@ mp_cartesian_binary = function(x, y) { #' Self Cartesian Product #' +#' @param x An index. #' @param suffixes Length-2 character vector giving suffixes that #' disambiguate the column names in the output. #' @inheritParams cartesian @@ -81,7 +82,7 @@ mp_square = function(x, suffixes = c("A", "B")) { #' Self Cartesian Product Excluding One Off-Diagonal Side #' -#' @inheritParams cartesian +#' @inheritParams mp_square #' @param y_labelling_column_names TODO #' @param exclude_diag Should 'diagonal' commponents be excluded from the output. #' @param lower_tri Should the lower triangular components be include from the @@ -144,7 +145,7 @@ mp_symmetric = function(x, y_labelling_column_names, exclude_diag = TRUE) { #' #' TODO: what does this mean? #' -#' @inheritParams mp_square +#' @inheritParams mp_triangle #' @family products #' @export mp_linear = function(x, y_labelling_column_names) { diff --git a/R/mp_decompose.R b/R/mp_decompose.R index ddd36701..8efc7e3b 100644 --- a/R/mp_decompose.R +++ b/R/mp_decompose.R @@ -1,4 +1,7 @@ -#' @export +## Experimental -- for use cases like a decomposition of +## transmission rates into susceptibility and infectivity +## in a structured model. + mp_decompose = function(formula, index, decomp_name, ...) { input_formula = formula table_args = list(...) @@ -24,7 +27,7 @@ mp_decompose = function(formula, index, decomp_name, ...) { ) for (i in seq_along(table_args)) { - int_vecs[[iv_nms[i]]] = mp_indices( + int_vecs[[iv_nms[i]]] = mp_positions( linked_indices$labels_for[[tab_nms[i]]](), linked_indices$partition_for[[tab_nms[i]]]()$labels() ) diff --git a/R/mp_dynamic_model.R b/R/mp_dynamic_model.R index a037c544..4959a4ed 100644 --- a/R/mp_dynamic_model.R +++ b/R/mp_dynamic_model.R @@ -58,7 +58,6 @@ LabelsDynamic = function(dynamic_model) { } -#' @export DynamicModel = function( expr_list = ExprList() , ledgers = list() @@ -68,7 +67,7 @@ DynamicModel = function( self = Base() self$expr_list = expr_list self$ledgers = ledgers - self$init_vecs = lapply(init_vecs, mp_vector) + self$init_vecs = lapply(init_vecs, mp_structured_vector) self$unstruc_mats = unstruc_mats self$int_vec_names = function() { lapply(self$ledgers, getElement, "table_names") |> unlist(use.names = TRUE) |> unique() diff --git a/R/mp_group.R b/R/mp_group.R index 9fe3edff..8d14c9e0 100644 --- a/R/mp_group.R +++ b/R/mp_group.R @@ -1,3 +1,11 @@ +#' Group an Index +#' +#' Create a new index with fewer columns to create names for +#' an aggregated vector that is labelled by the input index. +#' +#' @param index Index to group rows. +#' @param by Column set label to group by. +#' #' @export mp_group = function(index, by) { frame = index$partition$select(to_names(by))$frame() @@ -11,6 +19,12 @@ mp_group = function(index, by) { #' Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows #' identifying instances of an aggregation. #' +#' @param index An index to aggregate over. +#' @param by A column set label to group by. By default a dummy +#' and constant \code{"Group"} column is created. +#' @param ledger_column Name of the column in the output ledger +#' that describes the groups. +#' #' @family ledgers #' @export mp_aggregate = function(index, by = "Group", ledger_column = "group") { @@ -68,11 +82,11 @@ mp_aggregate_old = function(formula strata = mp_select(x, stratify_by) subset = mp_subset(x, ...) - grouping_indices = mp_indices( + grouping_indices = mp_positions( mp_labels(subset, stratify_by), mp_labels(strata) ) - subset_indices = mp_indices( + subset_indices = mp_positions( mp_labels(subset), mp_labels(x) ) @@ -80,7 +94,7 @@ mp_aggregate_old = function(formula } -#' @export +## experimental mp_expr_group_sum = function(x , stratify_by , output_name @@ -92,11 +106,11 @@ mp_expr_group_sum = function(x ) { strata = mp_select(x, stratify_by) subset = mp_subset(x, ...) - grouping_indices = mp_indices( + grouping_indices = mp_positions( mp_labels(subset, stratify_by), mp_labels(strata) ) - subset_indices = mp_indices( + subset_indices = mp_positions( mp_labels(subset), mp_labels(x) ) diff --git a/R/mp_index.R b/R/mp_index.R index 55bc12f5..bc707222 100644 --- a/R/mp_index.R +++ b/R/mp_index.R @@ -8,7 +8,7 @@ #' #' For example, the following index table describes the state variables of the #' model: -#' ```{r} +#' ```{r index} #' sir = mp_index(Epi = c("S", "I", "R")) #' print(sir) #' ``` @@ -19,7 +19,7 @@ #' However, in more complicated models, it is good to think carefully about #' choosing descriptive category names. For example, in an age-structured SIR #' model, we could add an `Age` column to generate an index table as follows: -#' ```{r} +#' ```{r product} #' sir_age = mp_index( #' Epi = rep(c("S", "I", "R"), 2), #' Age = rep(c("young", "old"), each = 3) @@ -34,7 +34,7 @@ #' This index table could also be generated by first specifying individual index #' tables for the `Epi` and `Age` columns, and then using a `macpan2` product #' function that combines the tables into a single index table: -#' ```{r} +#' ```{r other_product} #' sir = mp_index(Epi = c("S", "I", "R")) #' age = mp_index(Age = c("young", "old")) #' prod = mp_cartesian(sir, age) @@ -47,7 +47,7 @@ #' We can produce the full labels of model quantities, which are simply #' dot-concatenated indices, one for each entry in the index table, using the #' `labels()` function: -#' ```{r, echo = FALSE} +#' ```{r labels, echo = FALSE} #' labels(prod) #' ``` #' @@ -84,7 +84,7 @@ #' #' @family indexes #' -#' @seealso [mp_vector()] +#' @seealso [mp_structured_vector()] #' @seealso [mp_set_numbers()] #' #' @export @@ -106,13 +106,10 @@ mp_index = function(..., labelling_column_names) UseMethod("mp_index") #' number of #' @param reference_index (Advanced) An optional partition to use when #' computing subset indices. -#' @param x \code{Index} object. #' @param ... For consistency with existing S3 methods. #' #' @seealso [mp_index()] #' @noRd -#' @keywords internal -#' @export Index = function(partition , vector_name = NULL , labelling_column_names = NULL @@ -214,7 +211,6 @@ as.data.frame.Index = function(x, row.names = NULL, optional = FALSE, ...) { x$partition$frame() } -#' @export labelling_column_names = function(x) UseMethod("labelling_column_names") #' @describeIn mp_index Retrieve the \code{labelling_column_names} of @@ -251,7 +247,7 @@ mp_index.data.frame = function(..., labelling_column_names) { } -#' @export +# experimental mp_catalogue = function(name, ..., labelling_column_names) { l = list(...) for (i in seq_along(l)) { @@ -284,6 +280,15 @@ infer_labelling_columns = function(partition) { } +#' Reference Index +#' +#' Extract the index used as a reference for generating position +#' vectors. +#' +#' @param x Object +#' @param dimension_name Name of a dimension used in a ledger if +#' applicable. +#' #' @export mp_reference = function(x, dimension_name) { UseMethod("mp_reference") @@ -301,6 +306,14 @@ mp_reference.Index = function(x, dimension_name) { x$reference_index() } +#' Extract Index +#' +#' Extract the index for a particular dimension in a ledger +#' from a ledger or from an object containing one or more ledgers. +#' +#' @param x Object +#' @param dimension_name Name of a dimension used in a ledger. +#' #' @export mp_extract = function(x, dimension_name) { UseMethod("mp_extract") @@ -331,14 +344,28 @@ mp_extract.ModelDefRun = function(x, dimension_name) { } +#' Position Vectors +#' +#' Return an integer vector of positions of \code{x} in +#' \code{table}. Currently this is a simple wrapper around +#' \code{\link{match}}. +#' +#' @param x Character vector +#' @param table Character vector +#' @param zero_based Use zero-based indexing? Defaults to \code{TRUE}, +#' otherwise standard R one-based indexing is used. +#' #' @export -mp_indices = function(x, table) { - match(x, table) - 1L +mp_positions = function(x, table, zero_based = TRUE) { + p = match(x, table) + if (zero_based) p = p - 1L + p } #' Rename Index Columns #' +#' @param x An index with columns to be renamed. #' @param ... Name-value pairs. The name gives the new name and the value #' is a character vector giving the old name. #' diff --git a/R/mp_join.R b/R/mp_join.R index a35bbd06..7c0bea43 100644 --- a/R/mp_join.R +++ b/R/mp_join.R @@ -17,7 +17,7 @@ #' character vector of column names on which to join (as in standard R functions #' for joining data frames), or the dot-concatenation of these column names. #' For example, -#' ```{r, echo = TRUE, eval = TRUE} +#' ```{r join, echo = TRUE, eval = TRUE} #' state = mp_index( #' Epi = c("S", "I", "S", "I"), #' Age = c("young", "young", "old", "old") @@ -32,7 +32,7 @@ #' list of character vectors, each describing how to join the columns of #' a pair of tables in `...`. The names of this list are dot-concatenations #' of the names of pairs of tables in `...`. For example, -#' ```{r, echo = TRUE, eval = TRUE} +#' ```{r rates, echo = TRUE, eval = TRUE} #' rates = mp_index( #' Epi = c("lambda", "lambda"), #' Age = c("young", "old") @@ -52,7 +52,7 @@ #' is a dot-concatenation of columns in the first table and the #' right-hand-side is a dot-concatenation of the columns in the second #' table. For example, -#' ```{r} +#' ```{r contact_join} #' contact = mp_index( #' AgeSusceptible = c("young", "young", "old", "old"), #' AgeInfectious = c("young", "old", "young", "old") @@ -175,7 +175,7 @@ mp_join = function(..., by = empty_named_list()) { #' functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers #' are most commonly created using the \code{\link{mp_join}} function as in the #' following example. -#' ```{r} +#' ```{r ledger_join} #' age = mp_index(Age = c("young", "old")) #' state = mp_cartesian( #' mp_index(Epi = c("S", "I", "R")), @@ -445,7 +445,6 @@ filter_by_list = function(x_orig, y_orig, by_list) { l } -#' @export init_merge = function(frame, dimension_name, reference_index, labelling_column_names) { Ledger(frame , initial_column_map(names(frame), dimension_name) diff --git a/R/mp_labels.R b/R/mp_labels.R index 20da3709..49a1469d 100644 --- a/R/mp_labels.R +++ b/R/mp_labels.R @@ -1,3 +1,13 @@ +#' Index Labels +#' +#' Return a character vector of labels for each row of an index +#' (or a ledger?? FIXME: what does this mean for ledgers??). +#' +#' @param x Object +#' @param labelling_column_names What index columns should be used +#' for generating the labels. If missing then defaults will be used. +#' (FIXME: clarify how the defaults are used.) +#' #' @export mp_labels = function(x, labelling_column_names) { UseMethod("mp_labels") diff --git a/R/mp_library.R b/R/mp_library.R index 527a6685..a3b28ac5 100644 --- a/R/mp_library.R +++ b/R/mp_library.R @@ -1,8 +1,30 @@ -#' @export + mp_library = function(...) { stop("under construction") } +#' TMB Library +#' +#' Get a TMB model specification from a model library. +#' +#' @param ... File path components pointing to a directory that +#' contains an R script that creates an object called `spec`, which +#' is produced by \code{\link{mp_tmb_model_spec}}. +#' @param package If \code{NULL}, \code{\link{file.path}} is used +#' to put together the \code{...} components but if \code{package} +#' is the name of a package (as a character string) then +#' \code{\link{system.file}} is used to put together the \code{...} +#' components. +#' +#' @seealso [show_models()] +#' +#' @examples +#' mp_tmb_library( +#' "starter_models" +#' , "si" +#' , package = "macpan2" +#' ) +#' #' @export mp_tmb_library = function(..., package = NULL) { if (is.null(package)) { @@ -18,8 +40,6 @@ mp_tmb_library = function(..., package = NULL) { def_env$spec } -## TODO: change model_starter to mp_model_starter - #' Model Starter #' #' Create a directory with a template model definition. @@ -29,7 +49,7 @@ mp_tmb_library = function(..., package = NULL) { #' template model definition. #' #' @export -model_starter = function(starter_name, dir) { +mp_model_starter = function(starter_name, dir) { starter_dir = system.file("starter_models" , starter_name , package = "macpan2" diff --git a/R/mp_subset.R b/R/mp_subset.R index 96665186..68aeef96 100644 --- a/R/mp_subset.R +++ b/R/mp_subset.R @@ -30,7 +30,8 @@ mp_setdiff = function(x, ...) { ) } -#' @export + +## TODO: clarify what mp_choose and mp_choose_out do specifically mp_choose = function(x, subset_name, ...) { l = list(...) if (length(l) != 0L) valid$named_list$check(l) @@ -48,7 +49,6 @@ mp_choose = function(x, subset_name, ...) { init_merge(p$frame(), subset_name, x$reference_index(), x$labelling_column_names) } -#' @export mp_choose_out = function(x, subset_name, ...) { l = list(...) p = x$partition diff --git a/R/mp_tmb_model_spec.R b/R/mp_tmb_model_spec.R index ac6d794a..1bf8d2cf 100644 --- a/R/mp_tmb_model_spec.R +++ b/R/mp_tmb_model_spec.R @@ -164,8 +164,8 @@ TMBModelSpec = function( #' simulation loop, with the same rules as \code{before}. #' @param after List of formulas to be evaluated after the simulation loop, #' with the same rules as \code{before}. -#' @param default Named list of objects that can be coerced into -#' \code{\link{numeric}} \code{\link{matrices}}. The names refer to +#' @param default Named list of objects, each of which can be coerced into +#' a \code{\link{numeric}} \code{\link{matrix}}. The names refer to #' variables that appear in \code{before}, \code{during}, and \code{after}. #' @param integers list #' @param must_save character diff --git a/R/name_utils.R b/R/name_utils.R index 1abe29a0..c0399046 100644 --- a/R/name_utils.R +++ b/R/name_utils.R @@ -110,7 +110,16 @@ to_name.Scalar = function(x) x$dot()$value() #' @export to_name.Names = function(x) x$dot()$value() - +#' To Positions +#' +#' Return position vector of indices corresponding to the +#' input object. +#' +#' @seealso [mp_positions()] +#' +#' @param x An object of a class that can be converted to a +#' position vector. +#' #' @export to_positions = function(x) UseMethod("to_positions") @@ -171,7 +180,6 @@ to_matrix_with_rownames = function(x, nms) { x[nms, , drop = FALSE] } -#' @export labelled_zero_vector = function(labels) { (rep(0, length(labels)) |> setNames(labels) diff --git a/R/objective_function.R b/R/objective_function.R index a2a140db..86a50c0b 100644 --- a/R/objective_function.R +++ b/R/objective_function.R @@ -29,7 +29,7 @@ #' * `.existing_literals`: Numeric vector giving the literals used in the #' model expressions produced before the objective function. #' -#' @export +#' @noRd ObjectiveFunction = function(obj_fn_expr) { ## Inherit Private Methods diff --git a/R/opt_params.R b/R/opt_params.R index 43895e6c..1149f7b5 100644 --- a/R/opt_params.R +++ b/R/opt_params.R @@ -37,7 +37,7 @@ #' is to be used to represent fixed parameters to be optimized or random #' parameters to be integrated out using the Laplace transform. #' -#' @export +#' @noRd OptParamsList = function(... , par_id = integer(0L) , mat = character(0L) diff --git a/R/parameter_files.R b/R/parameter_files.R deleted file mode 100644 index e69de29b..00000000 diff --git a/R/parse_expr.R b/R/parse_expr.R index 4eadb63b..ef0466c1 100644 --- a/R/parse_expr.R +++ b/R/parse_expr.R @@ -362,7 +362,7 @@ empty_matrix = matrix(numeric(0L), 0L, 0L) #' @param valid_methods \code{\link{MethList}} object. #' @param valid_int_vecs \code{\link{IntVecs}} object. #' -#' @export +#' @noRd parse_expr_list = function(expr_list , valid_vars , valid_literals = numeric(0L) diff --git a/R/parse_table.R b/R/parse_table.R deleted file mode 100644 index e69de29b..00000000 diff --git a/R/products.R b/R/products.R index d2a4911e..0e18c23d 100644 --- a/R/products.R +++ b/R/products.R @@ -1,63 +1,66 @@ -#' Products -#' -#' @param x Object with a `$frame` method that returns a data frame. -#' -#' @export -Products = function(x) { - self = Base() - self$x = x - self$cartesian = function(y) { - x = self$x$frame() - y = y$frame() - matches = intersect(names(x), names(y)) - if (length(matches) != 0L) { - matched_names = names(y)[names(y) %in% matches] - maybe_nums = sub("^[^0-9]+([0-9]+)$", "\\1", matched_names) - which_nums = grepl("^[0-9]+$", maybe_nums) - matched_names = matched_names - nums = as.integer(matched_names[which_nums]) - matched_names - } - Partition(merge(x, y)) - } - return_object(self, "Products") -} +# Products +# +# @param x Object with a `$frame` method that returns a data frame. +# +# @export +# + +## probably going to remove in favour of mp_cartesian etc ... +# Products = function(x) { +# self = Base() +# self$x = x +# self$cartesian = function(y) { +# x = self$x$frame() +# y = y$frame() +# matches = intersect(names(x), names(y)) +# if (length(matches) != 0L) { +# matched_names = names(y)[names(y) %in% matches] +# maybe_nums = sub("^[^0-9]+([0-9]+)$", "\\1", matched_names) +# which_nums = grepl("^[0-9]+$", maybe_nums) +# matched_names = matched_names +# nums = as.integer(matched_names[which_nums]) +# matched_names +# } +# Partition(merge(x, y)) +# } +# return_object(self, "Products") +# } # methods for products. currently these are just for Partition # objects and so they are not yet done as S3 or S4. -#' Cartesian Product -#' -#' @param x A \code{\link{Partition}} object. -#' @param y A \code{\link{Partition}} object. -#' -#' @export -cartesian = function(x, y) x$products$cartesian(y) - -#' Self-Cartesian Product -#' -#' @param x A \code{\link{Partition}} object. -#' @param left_filter Character string giving the name of the left filter. -#' @param right_filter Character string giving the name of the right filter. -#' @param .wrt Character string giving the name of a column in `x`. -#' @param .rm Character string giving the dot-concatenated names of columns -#' to remove from `x`. -#' -#' @importFrom utils head -#' @export -cartesian_self = function(x, left_filter, right_filter, .wrt, .rm = .wrt) { - left_prefix = var_case_to_cls_case(left_filter) - right_prefix = var_case_to_cls_case(right_filter) - l = cartesian( - x$filter(left_filter, .wrt = .wrt)$select_out(to_names(.rm))$prefix(left_prefix), - x$filter(right_filter, .wrt = .wrt)$select_out(to_names(.rm))$prefix(right_prefix) - ) - nms = l$names() - nms_left = head(nms, length(nms) / 2L) - nms_right = tail(nms, length(nms) / 2L) - list( - l$partial_labels(nms_left), - l$partial_labels(nms_right) - ) |> setNames(c(left_filter, right_filter)) -} +# Cartesian Product +# +# @param x A \code{\link{Partition}} object. +# @param y A \code{\link{Partition}} object. +# +# @export +#cartesian = function(x, y) x$products$cartesian(y) +# Self-Cartesian Product +# +# @param x A \code{\link{Partition}} object. +# @param left_filter Character string giving the name of the left filter. +# @param right_filter Character string giving the name of the right filter. +# @param .wrt Character string giving the name of a column in `x`. +# @param .rm Character string giving the dot-concatenated names of columns +# to remove from `x`. +# +# @importFrom utils head +# @export +# cartesian_self = function(x, left_filter, right_filter, .wrt, .rm = .wrt) { +# left_prefix = var_case_to_cls_case(left_filter) +# right_prefix = var_case_to_cls_case(right_filter) +# l = cartesian( +# x$filter(left_filter, .wrt = .wrt)$select_out(to_names(.rm))$prefix(left_prefix), +# x$filter(right_filter, .wrt = .wrt)$select_out(to_names(.rm))$prefix(right_prefix) +# ) +# nms = l$names() +# nms_left = head(nms, length(nms) / 2L) +# nms_right = tail(nms, length(nms) / 2L) +# list( +# l$partial_labels(nms_left), +# l$partial_labels(nms_right) +# ) |> setNames(c(left_filter, right_filter)) +# } +# diff --git a/R/simple_sims.R b/R/simple_sims.R index 2af54e86..75ed729b 100644 --- a/R/simple_sims.R +++ b/R/simple_sims.R @@ -4,12 +4,10 @@ #' [engine](https://canmod.github.io/macpan2/articles/cpp_side). The expressions #' are only allowed to use valid \code{\link{engine_functions}}. #' @param time_steps Number of time steps to iterate. -#' @param ... Named list \code{\link{numeric}} objects that can be coerced to -#' \code{\link{numeric}} \code{\link{matrix}} objects. These matrices can be -#' referred to by name in the expressions in `iteration_exprs`. +#' @param int_vecs Named list of integer vectors. +#' @param mats Named list of matrices. #' -#' @return A data frame with the simulation results returned by the `$report()` -#' method in \code{\link{TMBSimulator}}. +#' @return A data frame with the simulation results. #' #' @export simple_sims = function(iteration_exprs, time_steps, int_vecs = list(), mats = list()) { diff --git a/R/time.R b/R/time.R index f4c8915c..a0412d77 100644 --- a/R/time.R +++ b/R/time.R @@ -12,7 +12,7 @@ #' to pass to C++. #' * `time_steps` -- Number of time steps in the simulation loop. #' -#' @export +#' @noRd Time = function(time_steps) { self = Base() self$time_steps = time_steps diff --git a/R/tmb_model.R b/R/tmb_model.R index 8cefbee6..3a137e9c 100644 --- a/R/tmb_model.R +++ b/R/tmb_model.R @@ -221,29 +221,19 @@ mp_tmb_before = function(model, start = list(), end = list()) { } -#' -#' @export -mp_calibrator = function(model, data) { - -} - -#' @export -mp_calibrator.TMBModelSpec = function(model, data) { - -} -#' @export mp_default = function(model_simulator, ...) { + stop("under construction") UseMethod("mp_default") } -#' @export mp_initial = function(model_simulator, ...) { + stop("under construction") UseMethod("mp_initial") } -#' @export mp_initial.TMBSimulator = function(model_simulator, matrices, params = NULL) { + stop("under construction") (model_simulator $replace $time_steps(time_steps) @@ -254,14 +244,14 @@ mp_initial.TMBSimulator = function(model_simulator, matrices, params = NULL) { } -#' @export mp_final = function(model_simulator, ...) { + stop("under construction") UseMethod("mp_final") } -#' @export mp_final.TMBSimulator = function(model_simulator, time_steps, outputs, ...) { + stop("under construction") (model_simulator $replace $time_steps(time_steps) @@ -271,7 +261,14 @@ mp_final.TMBSimulator = function(model_simulator, time_steps, outputs, ...) { ) } - +#' Trajectory +#' +#' Return simulations of the trajectory of the output +#' variables of a dynamical model simulator. +#' +#' @param model A dynamical model simulator produced by +#' \code{\link{mp_simulator}}. +#' #' @export mp_trajectory = function(model) { UseMethod("mp_trajectory") @@ -282,14 +279,6 @@ mp_trajectory.TMBSimulator = function(model) { model$report() |> reset_rownames() } - - -# TMBCompartmentalSimulator = function(tmb_simulator, compartmental_model) { -# self = tmb_simulator -# self$compartmental_model = compartmental_model -# return_object(self, "TMBCompartmentalSimulator") -# } - TMBDynamicSimulator = function(tmb_simulator, dynamic_model) { self = tmb_simulator self$dynamic_model = dynamic_model diff --git a/R/vec_utils.R b/R/vec_utils.R index b77af339..90bff6df 100644 --- a/R/vec_utils.R +++ b/R/vec_utils.R @@ -5,6 +5,8 @@ #' #' @param x Object representing the names of the output vector. Most #' commonly this will be a \code{\link{character}} vector. +#' @param ... Passed on to S3 methods. +#' #' @export mp_zero_vector = function(x, ...) { UseMethod("mp_zero_vector") diff --git a/R/vector.R b/R/vector.R index 80a1a298..468bcaa1 100644 --- a/R/vector.R +++ b/R/vector.R @@ -147,18 +147,21 @@ as.matrix.StructuredVector = function(x, ...) { #' Structured Vectors #' #' This documentation was originally in [mp_index()] and should be cleaned up -#' See issue #131 +#' See issue #131. Also this is an experimental feature. +#' +#' @param x A index. +#' @param ... Passed on to S3 methods. #' #' These labels can be used to create 'multidimensional' names for the elements #' of vectors. Here is the above example expressed in vector form. -#' ```{r, echo = FALSE} +#' ```{r structured_vectors, echo = FALSE} #' v = StructuredVector(prod) #' v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") #' ``` #' This example vector could be stored as a 3-by-2 matrix. But other examples #' cannot, making this indexing approach more general. For example, consider the #' following index. -#' ```{r, echo = FALSE} +#' ```{r symptoms, echo = FALSE} # symp = mp_index( # Epi = c("S", "I", "I", "R"), # Symptoms = c("", "mild", "severe", "") @@ -167,8 +170,8 @@ as.matrix.StructuredVector = function(x, ...) { #' ``` #' This index has an associated indexed vector that cannot be expressed as a #' matrix. -#' ```{r, echo = FALSE} -#' mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") +#' ```{r set_numbers, echo = FALSE} +#' mp_structured_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") #' ``` #' #' @examples @@ -177,42 +180,42 @@ as.matrix.StructuredVector = function(x, ...) { #' Age = c("young", "young", "old", "old") #' ) #' state_vector = (state -#' |> mp_vector() +#' |> mp_structured_vector() #' |> mp_set_numbers(Epi = c(S = 1000)) #' |> mp_set_numbers(Epi = c(I = 1), Age = "old") #' ) #' print(state_vector) #' #' @export -mp_vector = function(x, ...) UseMethod("mp_vector") +mp_structured_vector = function(x, ...) UseMethod("mp_structured_vector") #' @export -mp_vector.StructuredVector = function(x, ...) x +mp_structured_vector.StructuredVector = function(x, ...) x #' @export -mp_vector.Index = StructuredVector.Index +mp_structured_vector.Index = StructuredVector.Index #' @export -mp_vector.data.frame = StructuredVector.data.frame +mp_structured_vector.data.frame = StructuredVector.data.frame #' @export -mp_vector.numeric = StructuredVector.numeric +mp_structured_vector.numeric = StructuredVector.numeric #' @export -mp_vector.character = function(x, ...) zero_vector(x) +mp_structured_vector.character = function(x, ...) zero_vector(x) #' @export -mp_vector.Link = function(x, dimension_name, ...) { - mp_vector(x$labels_for[[dimension_name]]()) +mp_structured_vector.Link = function(x, dimension_name, ...) { + mp_structured_vector(x$labels_for[[dimension_name]]()) } +#' @describeIn mp_structured_vector Update numerical values of a structured +#' vector. TODO: details on syntax. #' @export mp_set_numbers = function(vector, ...) vector$clone()$set_numbers(...) - - -#' @export +## FIXME: Not used?? VectorList = function() { self = Base() self$list = list() |> setNames(as.character()) @@ -235,7 +238,7 @@ VectorList = function() { ) |> message() } if (inherits(new_vecs[[nm]], "Index")) { - new_vecs[[nm]] = mp_vector(new_vecs[[nm]]) + new_vecs[[nm]] = mp_structured_vector(new_vecs[[nm]]) } self$list[[nm]] = new_vecs[[nm]] } diff --git a/R/zzz.R b/R/zzz.R index 694b72e3..d7faa997 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,6 +1,8 @@ .onLoad <- function(lib, pkg) { options( macpan2_dll = "macpan2" + + ## FIXME: macpan2_vec_by is old and not relevant i think , macpan2_vec_by = c("state", "flow_rates", "trans_rates") |> self_named_vector() #, macpan2_memoise = TRUE ) diff --git a/README.Rmd b/README.Rmd index a138d83a..b4dadfa6 100644 --- a/README.Rmd +++ b/README.Rmd @@ -267,8 +267,8 @@ sir = mp_tmb_model_spec( , gamma = 0.1 ) , integers = list( - from = mp_indices(flow$from, state_labels) - , to = mp_indices(flow$to , state_labels) + from = mp_positions(flow$from, state_labels) + , to = mp_positions(flow$to , state_labels) ) ) (sir diff --git a/README.md b/README.md index 6ee92821..a933f19d 100644 --- a/README.md +++ b/README.md @@ -476,8 +476,8 @@ sir = mp_tmb_model_spec( , gamma = 0.1 ) , integers = list( - from = mp_indices(flow$from, state_labels) - , to = mp_indices(flow$to , state_labels) + from = mp_positions(flow$from, state_labels) + , to = mp_positions(flow$to , state_labels) ) ) (sir diff --git a/inst/starter_models/sir_symp/tmb.R b/inst/starter_models/sir_symp/tmb.R index 6ae6bcb5..e4a8942e 100644 --- a/inst/starter_models/sir_symp/tmb.R +++ b/inst/starter_models/sir_symp/tmb.R @@ -69,11 +69,11 @@ spec = mp_tmb_model_spec( ) ) , integers = list( - from = mp_indices(flow$from , state_labels) - , to = mp_indices(flow$to , state_labels) - , rate = mp_indices(flow$rate , flow_rate_labels) - , I = mp_indices(I_labels , state_labels) - , infection = mp_indices(infection_labels , flow_rate_labels) + from = mp_positions(flow$from , state_labels) + , to = mp_positions(flow$to , state_labels) + , rate = mp_positions(flow$rate , flow_rate_labels) + , I = mp_positions(I_labels , state_labels) + , infection = mp_positions(infection_labels , flow_rate_labels) ) ) diff --git a/man/LedgerDefinition.Rd b/man/LedgerDefinition.Rd index cc31e20f..2e558a4c 100644 --- a/man/LedgerDefinition.Rd +++ b/man/LedgerDefinition.Rd @@ -9,7 +9,7 @@ functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers are most commonly created using the \code{\link{mp_join}} function as in the following example. -\if{html}{\out{
      }}\preformatted{age = mp_index(Age = c("young", "old")) +\if{html}{\out{
      }}\preformatted{age = mp_index(Age = c("young", "old")) state = mp_cartesian( mp_index(Epi = c("S", "I", "R")), age diff --git a/man/LogFile.Rd b/man/LogFile.Rd deleted file mode 100644 index 0570e97b..00000000 --- a/man/LogFile.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/log_files.R -\name{LogFile} -\alias{LogFile} -\title{Log File} -\usage{ -LogFile(directory = tempdir()) -} -\arguments{ -\item{directory}{Directory within which to try to store a log file for a -model object. If appropriate file access is not available, a temporary -file will be used instead.} -} -\value{ -Object of class \code{LogFile} with the following methods. -\itemize{ -\item \verb{$log()} -- Character vector containing the lines in the log file. -\item \verb{$data_arg()} -- List containing the components of the \code{TMB} data -structure related to log files. -\item \verb{$copy(...)} -- Make a copy of the log file at \code{file.path(...)}. -\item \verb{$err_msg()} -- Return the current error message in the log file, if any. -\item Other methods inherited from \code{\link{Files}} -} -} -\description{ -Log File -} diff --git a/man/MatsList.Rd b/man/MatsList.Rd deleted file mode 100644 index 938cf920..00000000 --- a/man/MatsList.Rd +++ /dev/null @@ -1,62 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mats_list.R -\name{MatsList} -\alias{MatsList} -\title{Matrix List} -\usage{ -MatsList( - ..., - .mats_to_save = character(0L), - .mats_to_return = character(0L), - .dimnames = list(), - .structure_labels = NullLabels(), - .dummy = "dummy" -) -} -\arguments{ -\item{...}{Named objects that can be coerced to numerical matrices.} - -\item{.mats_to_save}{Character vector naming matrices to be saved at each -set in the simulation so that some calculations can make use of past value -(e.g. delayed effects) and/or to be able to retrieved the simulation -history after the simulation is complete.} - -\item{.mats_to_return}{Character vector naming matrices to be returned -after the simulate is complete.} - -\item{.dimnames}{Named list of \code{\link{dimnames}} for matrices that change -their dimensions over the simulation steps. These names correspond to the -names of the matrices. The output of the simulations will try their best -to honor these names, but if the shape of the matrix is too inconsistent -with the \code{\link{dimnames}} then numerical indices will be used instead. -For matrices that do not change their dimensions, set \code{\link{dimnames}} -by adding \code{\link{dimnames}} to the matrices passed to \code{...}.} - -\item{.structure_labels}{An optional object for obtaining labels of -elements of special vectors and matrices. Note that this -is an advanced technique.} -} -\value{ -Object of class \code{MatsList} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$data_arg()}: Return the following components of the data structure -to pass to C++. -\itemize{ -\item \code{mats} -- Unnamed list of numeric matrices. -\item \code{mats_save_hist} -- Boolean vector identifying which matrices should -have their history saved. -\item \code{mats_return} -- Boolean vector identifying which matrices should be -returned after a simulation. -} -\item \verb{$mat_dims()}: Return a data frame giving the numbers of rows and columns -of each matrix in the list. -\item \verb{$add_mats(...)}: Add matrices to the list and return a new -regenerated \code{MatsList} object. -} -} -} -\description{ -Create a list of initial values for matrices used to define a compartmental -model in TMB. -} diff --git a/man/MethList.Rd b/man/MethList.Rd deleted file mode 100644 index 8fcd2f05..00000000 --- a/man/MethList.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/engine_methods.R -\name{MethList} -\alias{MethList} -\title{Method List} -\usage{ -MethList(...) -} -\arguments{ -\item{...}{List of \code{\link{Method}} objects.} -} -\description{ -Method List -} diff --git a/man/Method.Rd b/man/Method.Rd deleted file mode 100644 index 45106b60..00000000 --- a/man/Method.Rd +++ /dev/null @@ -1,32 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/engine_methods.R -\name{Method} -\alias{Method} -\title{Engine Method Class} -\usage{ -Method( - name, - mat_args, - int_vec_args, - init_mats = MatsList(), - int_vecs = IntVecs() -) -} -\arguments{ -\item{name}{Method name.} - -\item{mat_args}{Character vector of names of matrices that will be used by -the method to produce an output matrix.} - -\item{int_vec_args}{Character vector of names of constants that will be used -by the method to produce an output matrix.} - -\item{init_mats}{Object of class \code{\link{MatsList}}.} - -\item{int_vecs}{= Object of class \code{\link{IntVecs}}.} -} -\description{ -Engine methods allow users to interact with the engine -by specifying a unique \code{name}. This name is used to reference recipes -for computing matrices from other matrices and constants. -} diff --git a/man/ObjectiveFunction.Rd b/man/ObjectiveFunction.Rd deleted file mode 100644 index a1085108..00000000 --- a/man/ObjectiveFunction.Rd +++ /dev/null @@ -1,45 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/objective_function.R -\name{ObjectiveFunction} -\alias{ObjectiveFunction} -\title{Objective Function} -\usage{ -ObjectiveFunction(obj_fn_expr) -} -\arguments{ -\item{obj_fn_expr}{One sided \code{\link{formula}} giving the objective -function of a TMB model. The right hand side expression must contain only -the names of matrices in the model, functions defined in \code{macpan2.cpp}, -and numerical literals (e.g. \code{3.14}).} -} -\value{ -Object of class \code{ObjectiveFunction} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \code{data_arg(..., .existing_literals)} -- Return the following components of the data structure -to pass to C++. -\itemize{ -\item \code{o_table_x} -- Objective function parse table column giving an index for looking up either -function, matrix, or literal. -\item \code{o_table_n} -- Objective function parse table column giving the number of arguments in -functions. -\item \code{o_table_i} -- Objective function parse table column giving indices for looking up the -rows in the parse table corresponding with the first argument of the -function. -\item \code{literals} -- Numeric vector of literals that can were used in the -expressions of the model. -} -} -} - -\subsection{Method Arguments}{ -\itemize{ -\item \code{...}: Character vector containing the names of the matrices in the model. -\item \code{.existing_literals}: Numeric vector giving the literals used in the -model expressions produced before the objective function. -} -} -} -\description{ -Define the objective function of a compartmental model in TMB. -} diff --git a/man/OptParamsList.Rd b/man/OptParamsList.Rd deleted file mode 100644 index 771fbe84..00000000 --- a/man/OptParamsList.Rd +++ /dev/null @@ -1,63 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/opt_params.R -\name{OptParamsList} -\alias{OptParamsList} -\title{Optimization Parameters List} -\usage{ -OptParamsList( - ..., - par_id = integer(0L), - mat = character(0L), - row_id = integer(0L), - col_id = integer(0L) -) -} -\arguments{ -\item{...}{Objects that can be coerced to numeric vectors, which will be -concatenated to produce the default value of the parameter vector.} - -\item{par_id}{Integer vector identifying elements of the parameter vector -to be used to replace elements of the model matrices.} - -\item{mat}{Character vector the same length as \code{par_id} giving the names of -the matrices containing the elements to replace.} - -\item{row_id}{Integer vector the same length as \code{par_id} giving the row -indices of the matrix elements to replace with parameter values.} - -\item{col_id}{Integer vector the same length as \code{par_id} giving the column -indices of the matrix elements to replace with parameter values.} -} -\value{ -Object of class \code{OptParamsList} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$data_arg(..., .type_string = c("p", "r"))}: Return the following components of the data structure -to pass to C++. -\itemize{ -\item \verb{\{.type_string\}_par_id} -- Integers identifying the replacing parameter. -\item \verb{\{.type_string\}_mat_id} -- Integers identifying the matrix within which -an element is to be replaced. -\item \verb{\{.type_string\}_row_id} -- Integers identifying the rows within matrices -to replace. -\item \verb{\{.type_string\}_col_id} -- Integers identifying the columns within -matrices to replace. -} -\item \verb{$vector()}: Return the initial value of the numerical parameter vector. -\item \verb{$data_frame()}: Return a data frame with each row describing a parameter. -} -} - -\subsection{Method Arguments}{ -\itemize{ -\item \code{...}: Character vector containing the names of the matrices in the model. -\item \code{.type_string}: Either \code{"p"} or \code{"r"} indicating whether the object -is to be used to represent fixed parameters to be optimized or random -parameters to be integrated out using the Laplace transform. -} -} -} -\description{ -Create an object for specifying matrix elements to be optimized or integrated -out of the objective function using a Laplace transform. -} diff --git a/man/Products.Rd b/man/Products.Rd deleted file mode 100644 index b74eb1dd..00000000 --- a/man/Products.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/products.R -\name{Products} -\alias{Products} -\title{Products} -\usage{ -Products(x) -} -\arguments{ -\item{x}{Object with a \verb{$frame} method that returns a data frame.} -} -\description{ -Products -} diff --git a/man/Time.Rd b/man/Time.Rd deleted file mode 100644 index 09b3fd4a..00000000 --- a/man/Time.Rd +++ /dev/null @@ -1,26 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/time.R -\name{Time} -\alias{Time} -\title{Time} -\usage{ -Time(time_steps) -} -\arguments{ -\item{time_steps}{Number of time steps in the simulation loop.} -} -\value{ -Object of class \code{Time} with the following methods. -\subsection{Methods}{ -\itemize{ -\item \verb{$data_arg()} -- Return the following components of the data structure -to pass to C++. -\itemize{ -\item \code{time_steps} -- Number of time steps in the simulation loop. -} -} -} -} -\description{ -Define the number of time steps in a compartmental model in TMB. -} diff --git a/man/cartesian.Rd b/man/cartesian.Rd deleted file mode 100644 index 657affe9..00000000 --- a/man/cartesian.Rd +++ /dev/null @@ -1,16 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/products.R -\name{cartesian} -\alias{cartesian} -\title{Cartesian Product} -\usage{ -cartesian(x, y) -} -\arguments{ -\item{x}{A \code{\link{Partition}} object.} - -\item{y}{A \code{\link{Partition}} object.} -} -\description{ -Cartesian Product -} diff --git a/man/cartesian_self.Rd b/man/cartesian_self.Rd deleted file mode 100644 index ec504d11..00000000 --- a/man/cartesian_self.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/products.R -\name{cartesian_self} -\alias{cartesian_self} -\title{Self-Cartesian Product} -\usage{ -cartesian_self(x, left_filter, right_filter, .wrt, .rm = .wrt) -} -\arguments{ -\item{x}{A \code{\link{Partition}} object.} - -\item{left_filter}{Character string giving the name of the left filter.} - -\item{right_filter}{Character string giving the name of the right filter.} - -\item{.wrt}{Character string giving the name of a column in \code{x}.} - -\item{.rm}{Character string giving the dot-concatenated names of columns -to remove from \code{x}.} -} -\description{ -Self-Cartesian Product -} diff --git a/man/engine_eval.Rd b/man/engine_eval.Rd index c2ae2a2f..00490845 100644 --- a/man/engine_eval.Rd +++ b/man/engine_eval.Rd @@ -29,7 +29,7 @@ are extending the \href{https://canmod.github.io/macpan2/articles/cpp_side.html}{engine} yourself.} -\item{.structure_labels}{See \code{\link{MatsList}}.} +\item{.structure_labels}{Deprecated.} } \value{ Matrix being produced on the right-hand-side or matrix given in diff --git a/man/mp_aggregate.Rd b/man/mp_aggregate.Rd index 689cd3e3..a64c260e 100644 --- a/man/mp_aggregate.Rd +++ b/man/mp_aggregate.Rd @@ -6,6 +6,15 @@ \usage{ mp_aggregate(index, by = "Group", ledger_column = "group") } +\arguments{ +\item{index}{An index to aggregate over.} + +\item{by}{A column set label to group by. By default a dummy +and constant \code{"Group"} column is created.} + +\item{ledger_column}{Name of the column in the output ledger +that describes the groups.} +} \description{ Create a one-column ledger (see \code{\link{LedgerDefinition}}) with rows identifying instances of an aggregation. diff --git a/man/mp_dynamic_simulator.Rd b/man/mp_dynamic_simulator.Rd index 686a3d7d..bff0aa14 100644 --- a/man/mp_dynamic_simulator.Rd +++ b/man/mp_dynamic_simulator.Rd @@ -4,7 +4,22 @@ \alias{mp_dynamic_simulator} \title{TMB Simulator from Dynamic Model} \usage{ -mp_dynamic_simulator(...) +mp_dynamic_simulator( + dynamic_model, + time_steps = 0L, + vectors = NULL, + unstruc_mats = NULL, + mats_to_save = NULL, + mats_to_return = NULL, + params = OptParamsList(0), + random = OptParamsList(), + obj_fn = ObjectiveFunction(~0), + log_file = LogFile(), + do_pred_sdreport = TRUE, + tmb_cpp = "macpan2", + initialize_ad_fun = TRUE, + ... +) } \arguments{ \item{dynamic_model}{Object product by \code{\link{mp_dynamic_model}}.} diff --git a/man/mp_extract.Rd b/man/mp_extract.Rd new file mode 100644 index 00000000..9b22154e --- /dev/null +++ b/man/mp_extract.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_index.R +\name{mp_extract} +\alias{mp_extract} +\title{Extract Index} +\usage{ +mp_extract(x, dimension_name) +} +\arguments{ +\item{x}{Object} + +\item{dimension_name}{Name of a dimension used in a ledger.} +} +\description{ +Extract the index for a particular dimension in a ledger +from a ledger or from an object containing one or more ledgers. +} diff --git a/man/mp_group.Rd b/man/mp_group.Rd new file mode 100644 index 00000000..7992e0b7 --- /dev/null +++ b/man/mp_group.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_group.R +\name{mp_group} +\alias{mp_group} +\title{Group an Index} +\usage{ +mp_group(index, by) +} +\arguments{ +\item{index}{Index to group rows.} + +\item{by}{Column set label to group by.} +} +\description{ +Create a new index with fewer columns to create names for +an aggregated vector that is labelled by the input index. +} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index 621fde74..e92eebe7 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -149,7 +149,7 @@ mp_cartesian(state, mp_index(City = c("hamilton", "toronto"))) } \seealso{ -\code{\link[=mp_vector]{mp_vector()}} +\code{\link[=mp_structured_vector]{mp_structured_vector()}} \code{\link[=mp_set_numbers]{mp_set_numbers()}} diff --git a/man/mp_labels.Rd b/man/mp_labels.Rd new file mode 100644 index 00000000..2c34f09d --- /dev/null +++ b/man/mp_labels.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_labels.R +\name{mp_labels} +\alias{mp_labels} +\title{Index Labels} +\usage{ +mp_labels(x, labelling_column_names) +} +\arguments{ +\item{x}{Object} + +\item{labelling_column_names}{What index columns should be used +for generating the labels. If missing then defaults will be used. +(FIXME: clarify how the defaults are used.)} +} +\description{ +Return a character vector of labels for each row of an index +(or a ledger?? FIXME: what does this mean for ledgers??). +} diff --git a/man/mp_ledgers.Rd b/man/mp_ledgers.Rd index c42bdd96..b05e2c18 100644 --- a/man/mp_ledgers.Rd +++ b/man/mp_ledgers.Rd @@ -6,6 +6,9 @@ \usage{ mp_ledgers(...) } +\arguments{ +\item{...}{Ledgers to bundle up.} +} \description{ Bundle up several ledgers (see \code{\link{LedgerDefinition}}) to pass to \code{\link{mp_dynamic_model}}. diff --git a/man/mp_linear.Rd b/man/mp_linear.Rd index 2a837d3a..cdfd0c85 100644 --- a/man/mp_linear.Rd +++ b/man/mp_linear.Rd @@ -7,7 +7,9 @@ mp_linear(x, y_labelling_column_names) } \arguments{ -\item{x}{A \code{\link{Partition}} object.} +\item{x}{An index.} + +\item{y_labelling_column_names}{TODO} } \description{ TODO: what does this mean? diff --git a/man/model_starter.Rd b/man/mp_model_starter.Rd similarity index 80% rename from man/model_starter.Rd rename to man/mp_model_starter.Rd index 0833b76c..86b86854 100644 --- a/man/model_starter.Rd +++ b/man/mp_model_starter.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/mp_library.R -\name{model_starter} -\alias{model_starter} +\name{mp_model_starter} +\alias{mp_model_starter} \title{Model Starter} \usage{ -model_starter(starter_name, dir) +mp_model_starter(starter_name, dir) } \arguments{ \item{starter_name}{Currently can only be \code{sir}.} diff --git a/man/mp_positions.Rd b/man/mp_positions.Rd new file mode 100644 index 00000000..2b8e4a2d --- /dev/null +++ b/man/mp_positions.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_index.R +\name{mp_positions} +\alias{mp_positions} +\title{Position Vectors} +\usage{ +mp_positions(x, table, zero_based = TRUE) +} +\arguments{ +\item{x}{Character vector} + +\item{table}{Character vector} + +\item{zero_based}{Use zero-based indexing? Defaults to \code{TRUE}, +otherwise standard R one-based indexing is used.} +} +\description{ +Return an integer vector of positions of \code{x} in +\code{table}. Currently this is a simple wrapper around +\code{\link{match}}. +} diff --git a/man/mp_reference.Rd b/man/mp_reference.Rd new file mode 100644 index 00000000..e49c3c88 --- /dev/null +++ b/man/mp_reference.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_index.R +\name{mp_reference} +\alias{mp_reference} +\title{Reference Index} +\usage{ +mp_reference(x, dimension_name) +} +\arguments{ +\item{x}{Object} + +\item{dimension_name}{Name of a dimension used in a ledger if +applicable.} +} +\description{ +Extract the index used as a reference for generating position +vectors. +} diff --git a/man/mp_rename.Rd b/man/mp_rename.Rd index 20dd037e..62312c2a 100644 --- a/man/mp_rename.Rd +++ b/man/mp_rename.Rd @@ -7,6 +7,8 @@ mp_rename(x, ...) } \arguments{ +\item{x}{An index with columns to be renamed.} + \item{...}{Name-value pairs. The name gives the new name and the value is a character vector giving the old name.} } diff --git a/man/mp_square.Rd b/man/mp_square.Rd index 4ecffb0b..60c4ebc8 100644 --- a/man/mp_square.Rd +++ b/man/mp_square.Rd @@ -7,7 +7,7 @@ mp_square(x, suffixes = c("A", "B")) } \arguments{ -\item{x}{A \code{\link{Partition}} object.} +\item{x}{An index.} \item{suffixes}{Length-2 character vector giving suffixes that disambiguate the column names in the output.} diff --git a/man/mp_vector.Rd b/man/mp_structured_vector.Rd similarity index 52% rename from man/mp_vector.Rd rename to man/mp_structured_vector.Rd index 595c4caf..cff675c8 100644 --- a/man/mp_vector.Rd +++ b/man/mp_structured_vector.Rd @@ -1,20 +1,23 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/vector.R -\name{mp_vector} -\alias{mp_vector} +\name{mp_structured_vector} +\alias{mp_structured_vector} +\alias{mp_set_numbers} \title{Structured Vectors} \usage{ -mp_vector(x, ...) -} -\description{ -This documentation was originally in \code{\link[=mp_index]{mp_index()}} and should be cleaned up -See issue #131 +mp_structured_vector(x, ...) + +mp_set_numbers(vector, ...) } -\details{ +\arguments{ +\item{x}{A index.} + +\item{...}{Passed on to S3 methods. + These labels can be used to create 'multidimensional' names for the elements of vectors. Here is the above example expressed in vector form. -\if{html}{\out{
      }}\preformatted{v = StructuredVector(prod) +\if{html}{\out{
      }}\preformatted{v = StructuredVector(prod) v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") }\if{html}{\out{
      }} @@ -22,21 +25,31 @@ This example vector could be stored as a 3-by-2 matrix. But other examples cannot, making this indexing approach more general. For example, consider the following index. -\if{html}{\out{
      }}\preformatted{}\if{html}{\out{
      }} +\if{html}{\out{
      }}\preformatted{}\if{html}{\out{
      }} This index has an associated indexed vector that cannot be expressed as a matrix. -\if{html}{\out{
      }}\preformatted{mp_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") -}\if{html}{\out{
      }} +\if{html}{\out{
      }}\preformatted{mp_structured_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") +}\if{html}{\out{
      }}} } +\description{ +This documentation was originally in \code{\link[=mp_index]{mp_index()}} and should be cleaned up +See issue #131. Also this is an experimental feature. +} +\section{Functions}{ +\itemize{ +\item \code{mp_set_numbers()}: Update numerical values of a structured +vector. TODO: details on syntax. + +}} \examples{ state = mp_index( Epi = c("S", "I", "S", "I"), Age = c("young", "young", "old", "old") ) state_vector = (state - |> mp_vector() + |> mp_structured_vector() |> mp_set_numbers(Epi = c(S = 1000)) |> mp_set_numbers(Epi = c(I = 1), Age = "old") ) diff --git a/man/mp_symmetric.Rd b/man/mp_symmetric.Rd index f887aac8..9eaafb34 100644 --- a/man/mp_symmetric.Rd +++ b/man/mp_symmetric.Rd @@ -7,7 +7,7 @@ mp_symmetric(x, y_labelling_column_names, exclude_diag = TRUE) } \arguments{ -\item{x}{A \code{\link{Partition}} object.} +\item{x}{An index.} \item{y_labelling_column_names}{TODO} diff --git a/man/mp_tmb_library.Rd b/man/mp_tmb_library.Rd new file mode 100644 index 00000000..a07b88eb --- /dev/null +++ b/man/mp_tmb_library.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_library.R +\name{mp_tmb_library} +\alias{mp_tmb_library} +\title{TMB Library} +\usage{ +mp_tmb_library(..., package = NULL) +} +\arguments{ +\item{...}{File path components pointing to a directory that +contains an R script that creates an object called \code{spec}, which +is produced by \code{\link{mp_tmb_model_spec}}.} + +\item{package}{If \code{NULL}, \code{\link{file.path}} is used +to put together the \code{...} components but if \code{package} +is the name of a package (as a character string) then +\code{\link{system.file}} is used to put together the \code{...} +components.} +} +\description{ +Get a TMB model specification from a model library. +} +\examples{ +mp_tmb_library( + "starter_models" + , "si" + , package = "macpan2" +) + +} +\seealso{ +\code{\link[=show_models]{show_models()}} +} diff --git a/man/mp_tmb_model_spec.Rd b/man/mp_tmb_model_spec.Rd index a7fe1eea..46d344d8 100644 --- a/man/mp_tmb_model_spec.Rd +++ b/man/mp_tmb_model_spec.Rd @@ -32,8 +32,8 @@ simulation loop, with the same rules as \code{before}.} \item{after}{List of formulas to be evaluated after the simulation loop, with the same rules as \code{before}.} -\item{default}{Named list of objects that can be coerced into -\code{\link{numeric}} \code{\link{matrices}}. The names refer to +\item{default}{Named list of objects, each of which can be coerced into +a \code{\link{numeric}} \code{\link{matrix}}. The names refer to variables that appear in \code{before}, \code{during}, and \code{after}.} \item{integers}{list} diff --git a/man/mp_trajectory.Rd b/man/mp_trajectory.Rd new file mode 100644 index 00000000..534d5233 --- /dev/null +++ b/man/mp_trajectory.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tmb_model.R +\name{mp_trajectory} +\alias{mp_trajectory} +\title{Trajectory} +\usage{ +mp_trajectory(model) +} +\arguments{ +\item{model}{A dynamical model simulator produced by +\code{\link{mp_simulator}}.} +} +\description{ +Return simulations of the trajectory of the output +variables of a dynamical model simulator. +} diff --git a/man/mp_triangle.Rd b/man/mp_triangle.Rd index aedb6e88..763a122b 100644 --- a/man/mp_triangle.Rd +++ b/man/mp_triangle.Rd @@ -12,7 +12,7 @@ mp_triangle( ) } \arguments{ -\item{x}{A \code{\link{Partition}} object.} +\item{x}{An index.} \item{y_labelling_column_names}{TODO} diff --git a/man/mp_zero_vector.Rd b/man/mp_zero_vector.Rd index f5d2e3c6..59af6fb5 100644 --- a/man/mp_zero_vector.Rd +++ b/man/mp_zero_vector.Rd @@ -9,6 +9,8 @@ mp_zero_vector(x, ...) \arguments{ \item{x}{Object representing the names of the output vector. Most commonly this will be a \code{\link{character}} vector.} + +\item{...}{Passed on to S3 methods.} } \description{ Create a \code{\link{numeric}} vector of all zeros with names diff --git a/man/parse_expr_list.Rd b/man/parse_expr_list.Rd deleted file mode 100644 index b0a3f6d6..00000000 --- a/man/parse_expr_list.Rd +++ /dev/null @@ -1,38 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/parse_expr.R -\name{parse_expr_list} -\alias{parse_expr_list} -\title{Parse Expression List} -\usage{ -parse_expr_list( - expr_list, - valid_vars, - valid_literals = numeric(0L), - valid_methods = MethList(), - valid_int_vecs = IntVecs() -) -} -\arguments{ -\item{expr_list}{List of one-sided formulas.} - -\item{valid_vars}{Named list of numerical matrices that can -be referred to in the formulas.} - -\item{valid_literals}{An optional numeric vector of initial valid literals -from a related expression list. Additional literals in the expressions -themselves will be discovered and added to this list.} - -\item{valid_methods}{\code{\link{MethList}} object.} - -\item{valid_int_vecs}{\code{\link{IntVecs}} object.} -} -\description{ -Parse a list of one-sided formulas representing expressions -in a compartmental model. All parsed expressions in the -output list will share an environment. -this environment contains the same set of -functions, variables (aka matrices), -literals, methods, and offset. these components are key -to the definition of the model and therefore should be common for each -expression in the model. -} diff --git a/man/simple_sims.Rd b/man/simple_sims.Rd index 0d3e5f85..628bd489 100644 --- a/man/simple_sims.Rd +++ b/man/simple_sims.Rd @@ -13,13 +13,12 @@ are only allowed to use valid \code{\link{engine_functions}}.} \item{time_steps}{Number of time steps to iterate.} -\item{...}{Named list \code{\link{numeric}} objects that can be coerced to -\code{\link{numeric}} \code{\link{matrix}} objects. These matrices can be -referred to by name in the expressions in \code{iteration_exprs}.} +\item{int_vecs}{Named list of integer vectors.} + +\item{mats}{Named list of matrices.} } \value{ -A data frame with the simulation results returned by the \verb{$report()} -method in \code{\link{TMBSimulator}}. +A data frame with the simulation results. } \description{ Simple Iterated Simulation diff --git a/man/to_positions.Rd b/man/to_positions.Rd new file mode 100644 index 00000000..be4d3513 --- /dev/null +++ b/man/to_positions.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/name_utils.R +\name{to_positions} +\alias{to_positions} +\title{To Positions} +\usage{ +to_positions(x) +} +\arguments{ +\item{x}{An object of a class that can be converted to a +position vector.} +} +\description{ +Return position vector of indices corresponding to the +input object. +} +\seealso{ +\code{\link[=mp_positions]{mp_positions()}} +} diff --git a/misc/experiments/bigmatmult/bigmatmult.R b/misc/experiments/bigmatmult/bigmatmult.R index 77e85e76..ed99ada4 100644 --- a/misc/experiments/bigmatmult/bigmatmult.R +++ b/misc/experiments/bigmatmult/bigmatmult.R @@ -8,7 +8,7 @@ macpan2:::dev_compile() # X = rbf(1000, d) # plot(X %*% rnorm(d), X %*% rnorm(d), type = "l") -# model_starter("sir", "misc/experiments/bigmatmult/model") +# mp_model_starter("sir", "misc/experiments/bigmatmult/model") # obs_incidence = rbf(2661, 20) %*% runif(20, max = 5) # plot(obs_incidence, type = "l") diff --git a/misc/experiments/refactorcpp.R b/misc/experiments/refactorcpp.R index fdbbaf5d..6600cf70 100644 --- a/misc/experiments/refactorcpp.R +++ b/misc/experiments/refactorcpp.R @@ -415,7 +415,7 @@ influence = mp_join( # formula, index # ) -# mp_expr_group_sum(state +# macpan2:::mp_expr_group_sum(state # , stratify_by = "Loc.Age" # , output_name = "N" # , vector_name = "state" @@ -434,7 +434,7 @@ mp_aggregate(N ~ group_sums(state) -trans_decomp = mp_decompose( +trans_decomp = macpan2:::mp_decompose( beta ~ contact * infectivity * susceptibility , index = mp_subset(trans_rates, Epi = "beta") , decomp_name = "trans" @@ -473,12 +473,12 @@ trans_decomp$int_vecs trans_decomp$labels_for$beta() trans_decomp$labels_for$infectivity() -mp_indices( +mp_positions( trans_decomp$labels_for$susceptibility(), trans_decomp$partition_for$susceptibility()$labels() ) -N_expr = mp_expr_group_sum(state +N_expr = macpan2:::mp_expr_group_sum(state , "Loc.Age" , "N", "state", "alive", "alive_groups", "length_N" , Vital = "alive" @@ -495,11 +495,11 @@ xx$frame state_strata = mp_group(state, "Loc.Age") alive_states = mp_subset(state, Vital = "alive") -alive_groups = mp_indices( +alive_groups = mp_positions( mp_labels(alive_states, "Loc.Age"), mp_labels(state_strata) ) -alive = mp_indices( +alive = mp_positions( mp_labels(alive_states), mp_labels(state) ) @@ -583,8 +583,8 @@ vec$N = Vector(N) N_groups = mp_join(alive, N, alive.N = "Loc") -mp_indices(N_groups$labels_for$N(), N$labels_for$N()) -mp_indices(alive$labels_for$alive(), state$labels()) +mp_positions(N_groups$labels_for$N(), N$labels_for$N()) +mp_positions(alive$labels_for$alive(), state$labels()) @@ -1082,14 +1082,14 @@ m = TMBModel( ) ), engine_methods = macpan2:::EngineMethods(int_vecs = iv), - time_steps = Time(100L), - params = OptParamsList(0.5, 0.2, 0.1 + time_steps = macpan2:::Time(100L), + params = macpan2:::OptParamsList(0.5, 0.2, 0.1 , par_id = 0:2 , mat = c("trans", "flow", "flow") , row_id = 0:2 , col_id = rep(0L, 3L) ), - obj_fn = ObjectiveFunction(~ sum(state^2)) + obj_fn = macpan2:::ObjectiveFunction(~ sum(state^2)) ) s = m$simulator() s$optimize$optim()$value @@ -1275,7 +1275,7 @@ for (type in model$indices$flow$flow_types) { indices = model$indices model$expr_list() -m = TMBModel(time_steps = Time(100)) +m = TMBModel(time_steps = macpan2:::Time(100)) m$insert$expressions() f = flow[infection] ~ per_capita_transmission %*% state[infectious] xx = macpan2:::MethodTypes() @@ -1399,7 +1399,7 @@ TMBModel( ) ), - time_steps = Time(100L) + time_steps = macpan2:::Time(100L) ) s = m$simulator() r = s$report(.phases = "during") @@ -1436,7 +1436,7 @@ m = TMBModel( , state ~ state - outflow + inflow ) ), - time = Time(150), + time = macpan2:::Time(150), engine_methods = macpan2:::EngineMethods( exprs = list( from_states = ~ state[from_indices] diff --git a/misc/experiments/structured_expressions/sir_from_model_def.R b/misc/experiments/structured_expressions/sir_from_model_def.R index 82dccd46..00f8a990 100644 --- a/misc/experiments/structured_expressions/sir_from_model_def.R +++ b/misc/experiments/structured_expressions/sir_from_model_def.R @@ -67,14 +67,14 @@ sir = TMBModel( state ~ state - row_sums(flowmat) + t(col_sums(flowmat)) ) ), - params = OptParamsList(0.3 + params = macpan2:::OptParamsList(0.3 , par_id = 0L , mat = "beta" , row_id = 0L , col_id = 0L ), - random = OptParamsList(), - obj_fn = ObjectiveFunction(~ foi + 1), + random = macpan2:::OptParamsList(), + obj_fn = macpan2:::ObjectiveFunction(~ foi + 1), time_steps = Time(time_steps = 2L) ) sir$data_arg() diff --git a/R/mat_expr_bundlers.R b/misc/old-r-source/mat_expr_bundlers.R similarity index 100% rename from R/mat_expr_bundlers.R rename to misc/old-r-source/mat_expr_bundlers.R diff --git a/misc/old-r-source/model_data_structure.R b/misc/old-r-source/model_data_structure.R index 57cfd9f9..a5d4b803 100644 --- a/misc/old-r-source/model_data_structure.R +++ b/misc/old-r-source/model_data_structure.R @@ -115,7 +115,7 @@ assert_variables = function(model) { #' template model definition. #' #' @export -model_starter = function(starter_name, dir) { +mp_model_starter = function(starter_name, dir) { starter_dir = system.file("starter_models" , starter_name , package = "macpan2" diff --git a/R/quantities.R b/misc/old-r-source/quantities.R similarity index 100% rename from R/quantities.R rename to misc/old-r-source/quantities.R diff --git a/R/settings.R b/misc/old-r-source/settings.R similarity index 100% rename from R/settings.R rename to misc/old-r-source/settings.R diff --git a/tests/testthat/test-c.R b/tests/testthat/test-c.R index 682417e5..f7118962 100644 --- a/tests/testthat/test-c.R +++ b/tests/testthat/test-c.R @@ -4,7 +4,7 @@ test_that("concatenation works with many different shapes of input", { w = matrix(rnorm(12L), 3L, 4L) answer = c(pi, z, w) m = macpan2:::TMBModel( - init_mats = MatsList( + init_mats = macpan2:::MatsList( answer = empty_matrix , x = empty_matrix , y = pi diff --git a/tests/testthat/test-dimnames-and-shape-change.R b/tests/testthat/test-dimnames-and-shape-change.R index ef345358..fc111918 100644 --- a/tests/testthat/test-dimnames-and-shape-change.R +++ b/tests/testthat/test-dimnames-and-shape-change.R @@ -3,9 +3,9 @@ library(testthat) test_that("matrices with dimnames can change size", { m = macpan2:::TMBModel( - init_mats = MatsList(x = c(a = 0), .mats_to_save = "x", .mats_to_return = "x"), + init_mats = macpan2:::MatsList(x = c(a = 0), .mats_to_save = "x", .mats_to_return = "x"), expr_list =mp_tmb_expr_list(during = list(x ~ c(x, time_step(0)))), - time_steps = Time(3) + time_steps = macpan2:::Time(3) ) s = macpan2:::TMBSimulator(m) r = s$report(.phases = c("before", "during")) @@ -21,13 +21,13 @@ test_that("matrices with dimnames can change size", { test_that("initially empty matrices can be associated with dimnames for at least some elements", { m = macpan2:::TMBModel( - init_mats = MatsList(x = empty_matrix + init_mats = macpan2:::MatsList(x = empty_matrix , .mats_to_save = "x" , .mats_to_return = "x" , .dimnames = list(x = list(letters[1:2], "")) ), expr_list =mp_tmb_expr_list(during = list(x ~ c(x, 1))), - time_steps = Time(3) + time_steps = macpan2:::Time(3) ) s = macpan2:::TMBSimulator(m) r = s$report(.phases = c("before", "during")) diff --git a/tests/testthat/test-dynamic-model.R b/tests/testthat/test-dynamic-model.R index 6c5ed90a..579520be 100644 --- a/tests/testthat/test-dynamic-model.R +++ b/tests/testthat/test-dynamic-model.R @@ -52,8 +52,8 @@ SIR_starter <- function( ## Initialize vectors from index tables (with all zeros for values) -------------- # used as placeholders for user input init_vecs <- list( - state = mp_vector(state), - rate = mp_vector(rate) + state = mp_structured_vector(state), + rate = mp_structured_vector(rate) ) ## Initialize model object ----------------- @@ -360,8 +360,8 @@ SIR_starter <- function( ## Initialize vectors from index tables (with all zeros for values) -------------- # used as placeholders for user input init_vecs <- list( - state = mp_vector(state), - rate = mp_vector(rate) + state = mp_structured_vector(state), + rate = mp_structured_vector(rate) ) ## Initialize model object ----------------- diff --git a/tests/testthat/test-error-row.R b/tests/testthat/test-error-row.R index cf0c5adc..4b9bd106 100644 --- a/tests/testthat/test-error-row.R +++ b/tests/testthat/test-error-row.R @@ -10,14 +10,14 @@ test_that("failing expression is reported in c++ side errors", { y ~ rbind_time(z) ) ), - init_mats = MatsList( + init_mats = macpan2:::MatsList( x = 1:10, y = empty_matrix, z = empty_matrix, .mats_to_save = "y", .mats_to_return = "y" ), - time_steps = Time(10) + time_steps = macpan2:::Time(10) ) expect_error( m$simulator()$report(), diff --git a/tests/testthat/test-expr-list.R b/tests/testthat/test-expr-list.R index 8cd2626d..825fab50 100644 --- a/tests/testthat/test-expr-list.R +++ b/tests/testthat/test-expr-list.R @@ -13,14 +13,14 @@ test_that("expressions are inserted and printed", { test_that("formula validity is enforced", { # expect_error( # macpan2:::TMBModel( - # init_mats = MatsList(a = 1), + # init_mats = macpan2:::MatsList(a = 1), # expr_list =mp_tmb_expr_list(before = list(a[0] ~ 1)) # ), # "without subsetting on the left-hand-side" # ) expect_error( macpan2:::TMBModel( - init_mats = MatsList(a = 1), + init_mats = macpan2:::MatsList(a = 1), expr_list =mp_tmb_expr_list(before = list( ~ 1)) ), "Model expressions must be two-sided" @@ -29,7 +29,7 @@ test_that("formula validity is enforced", { test_that("proper error message comes when output matrices are not initialized", { m = macpan2:::TMBModel( - init_mats = MatsList(a = 1), + init_mats = macpan2:::MatsList(a = 1), expr_list =mp_tmb_expr_list(before = list(b ~ a)) ) expect_error( diff --git a/tests/testthat/test-labelled-partitions.R b/tests/testthat/test-labelled-partitions.R index c9b1f0ae..e488ab28 100644 --- a/tests/testthat/test-labelled-partitions.R +++ b/tests/testthat/test-labelled-partitions.R @@ -1,6 +1,6 @@ test_that("model files can be read in and used", { skip("Skipping because we are still reworking this test for new approach") - f = model_starter("seir_symp_vax" + f = mp_model_starter("seir_symp_vax" , file.path(tempdir(TRUE), paste0(sample(LETTERS, 50, TRUE), collapse = "")) ) f$freeze() diff --git a/tests/testthat/test-literals.R b/tests/testthat/test-literals.R index 585f1200..1e0eff25 100644 --- a/tests/testthat/test-literals.R +++ b/tests/testthat/test-literals.R @@ -1,11 +1,11 @@ test_that("expression lists process literals", { m = macpan2:::TMBModel( - init_mats = MatsList(a = 0), + init_mats = macpan2:::MatsList(a = 0), expr_list =mp_tmb_expr_list(before = list(a ~ 2020202)), - params = OptParamsList(0, par_id = 0L, mat = "a", row_id = 0L, col_id = 0L), - random = OptParamsList(), - time_steps = Time(1L), - obj_fn = ObjectiveFunction(~0) + params = macpan2:::OptParamsList(0, par_id = 0L, mat = "a", row_id = 0L, col_id = 0L), + random = macpan2:::OptParamsList(), + time_steps = macpan2:::Time(1L), + obj_fn = macpan2:::ObjectiveFunction(~0) ) testthat::expect_identical(m$data_arg()$literals, c(2020202, 0)) }) diff --git a/tests/testthat/test-model-structure.R b/tests/testthat/test-model-structure.R index c1047e78..d102dcde 100644 --- a/tests/testthat/test-model-structure.R +++ b/tests/testthat/test-model-structure.R @@ -10,6 +10,7 @@ flow = mp_union( ) N = mp_aggregate(state, by = "Age") +mp_group(state, by = "Age") ## probably this is out-dated in favour of mp_aggregate?? infection = mp_join( from = mp_subset(state, Epi = "S") diff --git a/tests/testthat/test-opt-params.R b/tests/testthat/test-opt-params.R index 6e3bf0a0..ab6dee13 100644 --- a/tests/testthat/test-opt-params.R +++ b/tests/testthat/test-opt-params.R @@ -1,11 +1,11 @@ test_that("parameters are not initially empty", { - params = OptParamsList(0 + params = macpan2:::OptParamsList(0 , par_id = 0L , mat = "x" , row_id = 0L , col_id = 0L ) - init_mats = MatsList( + init_mats = macpan2:::MatsList( x = empty_matrix , y = empty_matrix , .mats_to_save = "y" diff --git a/tests/testthat/test-rbind-time-lag.R b/tests/testthat/test-rbind-time-lag.R index 8cadd618..e1488c9b 100644 --- a/tests/testthat/test-rbind-time-lag.R +++ b/tests/testthat/test-rbind-time-lag.R @@ -3,7 +3,7 @@ test_that("a selection of the iterations in the simulation history of a matrix t x = matrix(1:12, 4, 3) y = empty_matrix s = macpan2:::TMBModel( - init_mats = MatsList( + init_mats = macpan2:::MatsList( x = x, y = y, t = seq(from = 1, to = 9, by = 2), @@ -15,7 +15,7 @@ test_that("a selection of the iterations in the simulation history of a matrix t during = list(x ~ x * 0.9), after = list(y ~ rbind_time(x, t, 1)) ), - time_steps = Time(steps) + time_steps = macpan2:::Time(steps) )$simulator() y_tmb = s$matrix(time_step = 11, matrix_name = "y", .phases = c("before", "during", "after")) diff --git a/tests/testthat/test-rcbind.R b/tests/testthat/test-rcbind.R index 4ff86844..109f7d32 100644 --- a/tests/testthat/test-rcbind.R +++ b/tests/testthat/test-rcbind.R @@ -1,6 +1,6 @@ test_that("concatenation works with many different shapes of input", { set.seed(1L) - mats = MatsList( + mats = macpan2:::MatsList( answer = empty_matrix , z = rnorm(4L) , w = matrix(rnorm(12L), 3L, 4L) diff --git a/tests/testthat/test-simple-structured-model-specs.R b/tests/testthat/test-simple-structured-model-specs.R index 4d1258a6..43190e3b 100644 --- a/tests/testthat/test-simple-structured-model-specs.R +++ b/tests/testthat/test-simple-structured-model-specs.R @@ -24,8 +24,8 @@ test_that("tmb model specs can vectorize state updates with indices", { , gamma = 0.1 ) , integers = list( - from = mp_indices(flow$from, state_labels) - , to = mp_indices(flow$to , state_labels) + from = mp_positions(flow$from, state_labels) + , to = mp_positions(flow$to , state_labels) ) ) print(sir) diff --git a/tests/testthat/test-time-step.R b/tests/testthat/test-time-step.R index 3487bd59..0a289e4a 100644 --- a/tests/testthat/test-time-step.R +++ b/tests/testthat/test-time-step.R @@ -1,8 +1,8 @@ test_that("time_step function returns a valid time-step no matter what", { m = macpan2:::TMBModel( - init_mats = MatsList(x = 0, .mats_to_save = "x", .mats_to_return = "x"), + init_mats = macpan2:::MatsList(x = 0, .mats_to_save = "x", .mats_to_return = "x"), expr_list =mp_tmb_expr_list(during = list(x ~ time_step(4))), - time_steps = Time(10) + time_steps = macpan2:::Time(10) ) expect_identical( c(0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 6), diff --git a/tests/testthat/test-tmb-model.R b/tests/testthat/test-tmb-model.R index d74d6fe9..740e31a5 100644 --- a/tests/testthat/test-tmb-model.R +++ b/tests/testthat/test-tmb-model.R @@ -1,7 +1,7 @@ test_that("simulator method works", { expect_identical( macpan2:::TMBModel( - init_mats = MatsList(x = empty_matrix), + init_mats = macpan2:::MatsList(x = empty_matrix), expr_list =mp_tmb_expr_list(before = list(x ~ 1)), )$simulator()$report(), structure( diff --git a/tests/testthat/test-tmb.R b/tests/testthat/test-tmb.R index 8c1c8faf..903ddef5 100644 --- a/tests/testthat/test-tmb.R +++ b/tests/testthat/test-tmb.R @@ -1,12 +1,12 @@ library(macpan2) m = macpan2:::TMBModel( - MatsList(x = 0, y = 0, .mats_to_save = c("x", "y"), .mats_to_return = c("x", "y")), + macpan2:::MatsList(x = 0, y = 0, .mats_to_save = c("x", "y"), .mats_to_return = c("x", "y")), mp_tmb_expr_list(during = list(x ~ x + 1)), - OptParamsList(0, par_id = 0, mat = "x", row_id = 0, col_id = 0), - OptParamsList(), - ObjectiveFunction(~x), - Time(10) + macpan2:::OptParamsList(0, par_id = 0, mat = "x", row_id = 0, col_id = 0), + macpan2:::OptParamsList(), + macpan2:::ObjectiveFunction(~x), + macpan2:::Time(10) ) #f = m$make_ad_fun() #f$report()$mats_returned diff --git a/tests/testthat/test-variable-products.R b/tests/testthat/test-variable-products.R index 572a384b..fc028e3e 100644 --- a/tests/testthat/test-variable-products.R +++ b/tests/testthat/test-variable-products.R @@ -5,15 +5,15 @@ test_that("state and flow variables products are correct", { expect_identical( union_vars( - cartesian( + macpan2:::cartesian( sir$variables$state(), vax$variables$state() ), - cartesian( + macpan2:::cartesian( sir$variables$flow(), vax$variables$state() ), - cartesian( + macpan2:::cartesian( sir$variables$state(), vax$variables$flow() ) diff --git a/vignettes/engine_agnostic_grammar.Rmd b/vignettes/engine_agnostic_grammar.Rmd index 4a2010d3..4087f4e8 100644 --- a/vignettes/engine_agnostic_grammar.Rmd +++ b/vignettes/engine_agnostic_grammar.Rmd @@ -144,8 +144,8 @@ SIR_starter <- function( ## Initialize vectors from index tables (with all zeros for values) -------------- # used as placeholders for user input init_vecs <- list( - state = mp_vector(state), - rate = mp_vector(rate) + state = mp_structured_vector(state), + rate = mp_structured_vector(rate) ) ## Initialize model object ----------------- diff --git a/vignettes/example_models.Rmd b/vignettes/example_models.Rmd index 70e5fc8f..8636fca3 100644 --- a/vignettes/example_models.Rmd +++ b/vignettes/example_models.Rmd @@ -52,7 +52,7 @@ To see how to actually generate simulations from this model see [this article](h To take `sir` as a jumping-off point for producing your own model one may use the following code. ```{r} my_sir_dir = file.path(tempdir(), "my_sir") -model_starter("sir", my_sir_dir) +mp_model_starter("sir", my_sir_dir) ``` After running this code you can go to the files in `my_sir_dir` and modify what you see there. Note that you typically want to chose a specific directory for your model instead of using `tempdir`. You still need to read your own model in the usual way. diff --git a/vignettes/tmb_model.Rmd b/vignettes/tmb_model.Rmd index 615d5302..372d5232 100644 --- a/vignettes/tmb_model.Rmd +++ b/vignettes/tmb_model.Rmd @@ -65,7 +65,7 @@ sir = macpan2:::TMBModel( , noisy_state ~ rnbinom(state, 10) ) ), - time_steps = Time(100) + time_steps = macpan2:::Time(100) ) sir ``` From 8a5fdc39acf91514ffaa545ec21eb727dc874a8f Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 12:07:21 -0500 Subject: [PATCH 329/332] #155 --- R/index_to_tmb.R | 14 +++++++++++--- R/mp_bulk_indices.R | 11 ++++++++++- R/mp_index.R | 3 +-- R/vector.R | 2 +- man/mp_dynamic_simulator.Rd | 2 ++ man/mp_factors.Rd | 16 ++++++++++++++++ man/mp_index.Rd | 2 +- man/mp_slices.Rd | 16 ++++++++++++++++ man/mp_structured_vector.Rd | 2 +- 9 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 man/mp_factors.Rd create mode 100644 man/mp_slices.Rd diff --git a/R/index_to_tmb.R b/R/index_to_tmb.R index 9309f706..cb9a189e 100644 --- a/R/index_to_tmb.R +++ b/R/index_to_tmb.R @@ -5,14 +5,22 @@ #' but we plan to replace this function with \code{\link{mp_simulator}}. #' #' @param dynamic_model Object product by \code{\link{mp_dynamic_model}}. +#' @param time_steps Number of time steps to simulate. #' @param vectors Named list of named vectors as initial values for the #' simulations that are referenced in the expression list in the dynamic model. #' @param unstruc_mats = Named list of objects that can be coerced to #' numerical matrices that are used in the expression list of the #' dynamic model. -#' @inheritParams mp_tmb_model_spec -#' @inheritParams TMBModel -#' @inheritParams TMBSimulator +#' @param mats_to_save TODO +#' @param mats_to_return TODO +#' @param params TODO +#' @param random TODO +#' @param obj_fn TODO +#' @param log_file TODO +#' @param do_pred_sdreport TODO +#' @param tmb_cpp TODO +#' @param initialize_ad_fun TODO +#' @param ... TODO #' #' @importFrom oor method_apply #' @export diff --git a/R/mp_bulk_indices.R b/R/mp_bulk_indices.R index 77539a37..4dc5c5ac 100644 --- a/R/mp_bulk_indices.R +++ b/R/mp_bulk_indices.R @@ -60,6 +60,11 @@ mp_unpack = function(l, unpack = c('no', 'maybe', 'yes'), env) { } } +#' Factor an Index +#' +#' @param index An index to be factored. +#' @param unpack Place factors in the global environment? +#' #' @export mp_factors = function(index, unpack = c('no', 'maybe', 'yes')) { unpack = match.arg(unpack) @@ -118,7 +123,11 @@ mp_custom_slices = function(index, ..., unpack = c('no', 'maybe', 'yes')) { slices } -## TODO: list of indices where precedence is higher left and top +#' Slice an index +#' +#' @param index Index to slice up. +#' +#' @inheritParams mp_factors #' @export mp_slices = function(index, unpack = c('no', 'maybe', 'yes')) { unpack = match.arg(unpack) diff --git a/R/mp_index.R b/R/mp_index.R index bc707222..cc776fb3 100644 --- a/R/mp_index.R +++ b/R/mp_index.R @@ -108,7 +108,6 @@ mp_index = function(..., labelling_column_names) UseMethod("mp_index") #' computing subset indices. #' @param ... For consistency with existing S3 methods. #' -#' @seealso [mp_index()] #' @noRd Index = function(partition , vector_name = NULL @@ -229,7 +228,7 @@ to_labels.Index = function(x) x$labels() #' a character vector giving labels associated with each model component #' (i.e. row) being described. #' @export -labels.Index = function(x, ...) x$labels() +labels.Index = function(object, ...) object$labels() #' @export diff --git a/R/vector.R b/R/vector.R index 468bcaa1..b379080a 100644 --- a/R/vector.R +++ b/R/vector.R @@ -149,7 +149,7 @@ as.matrix.StructuredVector = function(x, ...) { #' This documentation was originally in [mp_index()] and should be cleaned up #' See issue #131. Also this is an experimental feature. #' -#' @param x A index. +#' @param x An index. #' @param ... Passed on to S3 methods. #' #' These labels can be used to create 'multidimensional' names for the elements diff --git a/man/mp_dynamic_simulator.Rd b/man/mp_dynamic_simulator.Rd index bff0aa14..1aec9df2 100644 --- a/man/mp_dynamic_simulator.Rd +++ b/man/mp_dynamic_simulator.Rd @@ -24,6 +24,8 @@ mp_dynamic_simulator( \arguments{ \item{dynamic_model}{Object product by \code{\link{mp_dynamic_model}}.} +\item{time_steps}{Number of time steps to simulate.} + \item{vectors}{Named list of named vectors as initial values for the simulations that are referenced in the expression list in the dynamic model.} diff --git a/man/mp_factors.Rd b/man/mp_factors.Rd new file mode 100644 index 00000000..9081112c --- /dev/null +++ b/man/mp_factors.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_bulk_indices.R +\name{mp_factors} +\alias{mp_factors} +\title{Factor an Index} +\usage{ +mp_factors(index, unpack = c("no", "maybe", "yes")) +} +\arguments{ +\item{index}{An index to be factored.} + +\item{unpack}{Place factors in the global environment?} +} +\description{ +Factor an Index +} diff --git a/man/mp_index.Rd b/man/mp_index.Rd index e92eebe7..7920fecf 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -19,7 +19,7 @@ mp_index(..., labelling_column_names) \method{to_labels}{Index}(x) -\method{labels}{Index}(x, ...) +\method{labels}{Index}(object, ...) } \arguments{ \item{...}{Character vectors to combine to produce an index. Alternatively, diff --git a/man/mp_slices.Rd b/man/mp_slices.Rd new file mode 100644 index 00000000..028d3f98 --- /dev/null +++ b/man/mp_slices.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mp_bulk_indices.R +\name{mp_slices} +\alias{mp_slices} +\title{Slice an index} +\usage{ +mp_slices(index, unpack = c("no", "maybe", "yes")) +} +\arguments{ +\item{index}{Index to slice up.} + +\item{unpack}{Place factors in the global environment?} +} +\description{ +Slice an index +} diff --git a/man/mp_structured_vector.Rd b/man/mp_structured_vector.Rd index cff675c8..4c36ba8f 100644 --- a/man/mp_structured_vector.Rd +++ b/man/mp_structured_vector.Rd @@ -10,7 +10,7 @@ mp_structured_vector(x, ...) mp_set_numbers(vector, ...) } \arguments{ -\item{x}{A index.} +\item{x}{An index.} \item{...}{Passed on to S3 methods. From 20ac334f75f2ef971e197cefc5818b905134b170 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 12:16:42 -0500 Subject: [PATCH 330/332] #155 --- R/mp_index.R | 2 ++ R/mp_join.R | 2 +- R/vector.R | 23 +---------------------- README.Rmd | 4 +--- man/LedgerDefinition.Rd | 7 +++++-- man/mp_dynamic_simulator.Rd | 20 ++++++++++++++++++++ man/mp_index.Rd | 4 ++++ man/mp_structured_vector.Rd | 4 +++- 8 files changed, 37 insertions(+), 29 deletions(-) diff --git a/R/mp_index.R b/R/mp_index.R index cc776fb3..860917c7 100644 --- a/R/mp_index.R +++ b/R/mp_index.R @@ -197,6 +197,7 @@ Index.Index = function(partition ) } +#' @param x An index. #' @describeIn mp_index Print an index. #' @export print.Index = function(x, ...) print(x$partition) @@ -224,6 +225,7 @@ labelling_column_names.Index = function(x) x$labelling_column_names #' @export to_labels.Index = function(x) x$labels() +#' @param object An index. #' @describeIn mp_index Convert an index into #' a character vector giving labels associated with each model component #' (i.e. row) being described. diff --git a/R/mp_join.R b/R/mp_join.R index 7c0bea43..d0f1f162 100644 --- a/R/mp_join.R +++ b/R/mp_join.R @@ -184,7 +184,7 @@ mp_join = function(..., by = empty_named_list()) { #' mp_join( #' from = mp_subset(state, Epi = "S"), #' to = mp_subset(state, Epi = "I"), -#' from.to = "Age" +#' by = list(from.to = "Age") #' ) #' ``` #' @name LedgerDefinition diff --git a/R/vector.R b/R/vector.R index b379080a..191dd417 100644 --- a/R/vector.R +++ b/R/vector.R @@ -152,28 +152,6 @@ as.matrix.StructuredVector = function(x, ...) { #' @param x An index. #' @param ... Passed on to S3 methods. #' -#' These labels can be used to create 'multidimensional' names for the elements -#' of vectors. Here is the above example expressed in vector form. -#' ```{r structured_vectors, echo = FALSE} -#' v = StructuredVector(prod) -#' v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") -#' ``` -#' This example vector could be stored as a 3-by-2 matrix. But other examples -#' cannot, making this indexing approach more general. For example, consider the -#' following index. -#' ```{r symptoms, echo = FALSE} -# symp = mp_index( -# Epi = c("S", "I", "I", "R"), -# Symptoms = c("", "mild", "severe", "") -# ) -# symp -#' ``` -#' This index has an associated indexed vector that cannot be expressed as a -#' matrix. -#' ```{r set_numbers, echo = FALSE} -#' mp_structured_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") -#' ``` -#' #' @examples #' state = mp_index( #' Epi = c("S", "I", "S", "I"), @@ -209,6 +187,7 @@ mp_structured_vector.Link = function(x, dimension_name, ...) { mp_structured_vector(x$labels_for[[dimension_name]]()) } +#' @param vector An index. #' @describeIn mp_structured_vector Update numerical values of a structured #' vector. TODO: details on syntax. #' @export diff --git a/README.Rmd b/README.Rmd index b4dadfa6..f444f277 100644 --- a/README.Rmd +++ b/README.Rmd @@ -1,8 +1,6 @@ --- title: "macpan2" -output: - github_document: - toc: true +output: github_document --- ```{r opts, echo = FALSE} diff --git a/man/LedgerDefinition.Rd b/man/LedgerDefinition.Rd index 2e558a4c..d0a11f8d 100644 --- a/man/LedgerDefinition.Rd +++ b/man/LedgerDefinition.Rd @@ -9,7 +9,7 @@ functional form used to define a \code{\link{mp_dynamic_model}}. Ledgers are most commonly created using the \code{\link{mp_join}} function as in the following example. -\if{html}{\out{
      }}\preformatted{age = mp_index(Age = c("young", "old")) +\if{html}{\out{
      }}\preformatted{age = mp_index(Age = c("young", "old")) state = mp_cartesian( mp_index(Epi = c("S", "I", "R")), age @@ -17,7 +17,10 @@ state = mp_cartesian( mp_join( from = mp_subset(state, Epi = "S"), to = mp_subset(state, Epi = "I"), - from.to = "Age" + by = list(from.to = "Age") ) +#> from to +#> S.young I.young +#> S.old I.old }\if{html}{\out{
      }} } diff --git a/man/mp_dynamic_simulator.Rd b/man/mp_dynamic_simulator.Rd index 1aec9df2..20c7ee06 100644 --- a/man/mp_dynamic_simulator.Rd +++ b/man/mp_dynamic_simulator.Rd @@ -32,6 +32,26 @@ simulations that are referenced in the expression list in the dynamic model.} \item{unstruc_mats}{= Named list of objects that can be coerced to numerical matrices that are used in the expression list of the dynamic model.} + +\item{mats_to_save}{TODO} + +\item{mats_to_return}{TODO} + +\item{params}{TODO} + +\item{random}{TODO} + +\item{obj_fn}{TODO} + +\item{log_file}{TODO} + +\item{do_pred_sdreport}{TODO} + +\item{tmb_cpp}{TODO} + +\item{initialize_ad_fun}{TODO} + +\item{...}{TODO} } \description{ This is an 'old' function that was tested out at a workshop. diff --git a/man/mp_index.Rd b/man/mp_index.Rd index 7920fecf..acc7a1c9 100644 --- a/man/mp_index.Rd +++ b/man/mp_index.Rd @@ -34,6 +34,10 @@ and must contain at least one name. The index given by the \code{labelling_column_names} must uniquely identify each row. The default \code{NULL} gives the set of columns, in order starting with the first column, that are required to uniquely identify each row.} + +\item{x}{An index.} + +\item{object}{An index.} } \description{ Make an index table to enumerate model quantity labels by category. These diff --git a/man/mp_structured_vector.Rd b/man/mp_structured_vector.Rd index 4c36ba8f..8bc12fd3 100644 --- a/man/mp_structured_vector.Rd +++ b/man/mp_structured_vector.Rd @@ -17,7 +17,7 @@ mp_set_numbers(vector, ...) These labels can be used to create 'multidimensional' names for the elements of vectors. Here is the above example expressed in vector form. -\if{html}{\out{
      }}\preformatted{v = StructuredVector(prod) +\if{html}{\out{
      }}\preformatted{v = mp_structured_vector(prod) v$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Age = "old") }\if{html}{\out{
      }} @@ -32,6 +32,8 @@ matrix. \if{html}{\out{
      }}\preformatted{mp_structured_vector(symp)$set_numbers(Epi = c(S = 1000))$set_numbers(Epi = c(I = 1), Symptoms = "severe") }\if{html}{\out{
      }}} + +\item{vector}{An index.} } \description{ This documentation was originally in \code{\link[=mp_index]{mp_index()}} and should be cleaned up From 9bf7fc757ea54be599b42cd08ddd9ec2a124f491 Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 12:34:18 -0500 Subject: [PATCH 331/332] update readme --- README.Rmd | 350 +---------------------------- README.md | 632 +---------------------------------------------------- 2 files changed, 11 insertions(+), 971 deletions(-) diff --git a/README.Rmd b/README.Rmd index f444f277..6c742a39 100644 --- a/README.Rmd +++ b/README.Rmd @@ -27,7 +27,7 @@ theme_bw = function() ggplot2::theme_bw(base_size = 18) Impactful applied public health modelling requires many interdisciplinary steps along the path from epidemiological research teams to operational decision makers. Researchers must quickly tailor a model to an emerging public-health concern, validate and calibrate it to data, work with decision makers to define model outputs useful for stakeholders, configure models to generate those outputs, and package up those insights in an appropriate format for stakeholders. Unlike traditional modelling approaches, `macpan2` tackles this challenge from a software-engineering perspective, which allows us to systematically address bottlenecks along this path to impact in ways that will make future solutions easier to achieve. The goal is to enable researchers to focus on their core strengths and fill knowledge gaps efficiently and effectively. -Although `macpan2` is designed as a compartmental modelling tool that is agnostic about the underlying computational engine, it currently makes use of [template model builder](https://github.com/kaskr/adcomp). Template model builder (TMB) is an `R` modelling package based on a `C++` framework incorporating mature [automatic differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) and [matrix algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) libraries. +Although `macpan2` is designed as a compartmental modelling tool that is agnostic about the underlying computational engine, it currently uses [template model builder](https://github.com/kaskr/adcomp) as the sole engine. Template model builder (TMB) is an `R` modelling package based on a `C++` framework incorporating mature [automatic differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) and [matrix algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) libraries. The [Public Health Risk Sciences Division](https://github.com/phac-nml-phrsd) at the [Public Health Agency of Canada](https://www.canada.ca/en/public-health.html) uses `macpan2` (for example, [here](https://phac-nml-phrsd.github.io/EPACmodel/)). @@ -36,7 +36,7 @@ The [Public Health Risk Sciences Division](https://github.com/phac-nml-phrsd) at * [Package reference](https://canmod.github.io/macpan2/) * [Quick-start guide](https://canmod.github.io/macpan2/articles/quickstart) * [Representation of compartmental models](https://canmod.github.io/macpan2/articles/model_definitions) [specification document] -* [`C++` engine](https://canmod.github.io/macpan2/articles/cpp_side) [specification document] +* [`TMB` engine](https://canmod.github.io/macpan2/articles/cpp_side) [specification document] * [Project history and trajectory](https://canmod.net/misc/macpan2_presentation) [slides] ## Installation @@ -51,7 +51,7 @@ remotes::install_github("canmod/macpan2") For projects in production one should install a specific version, as in the following command. ``` -remotes::install_github("canmod/macpan2@v0.0.3") +remotes::install_github("canmod/macpan2@v1.0.0") ``` ## Hello World @@ -90,348 +90,6 @@ Simulating from this model requires choosing the number of time-steps to run and ``` -## Design Concepts - -```{r design-concepts, echo=FALSE} -knitr::include_graphics("misc/readme/design-concepts.svg") -``` - -### Information Processing - -Like other statistical modelling software, the high-level purpose of `macpan2` is to process data sources (top-left) into results (bottom-left). In the case of `macpan2`, this processing is done using compartmental modelling (right). The major steps of this information processing are numbered in the diagram, and we describe each of these. - -1. Information processing begins with accessing and preparing numerical information from various data sources, with the output being standard numerical R objects. Depending on the nature of the analysis to follow, this information could include default values for parameters (e.g. transmission rate), initial values for the state variables (e.g. initial number of infectious individuals), operational schedules (e.g. timing of lockdown events or vaccine roll-out schedules), and data for model fitting (e.g. time series of hospital utilization). This step could involve connecting to real-time surveillance platforms or reading in static data files. There is not any functionality within `macpan2` for conducting this step -- [`macpan2` does not try to reinvent the wheel in data access and preparation](#modularity). -2. The structure of a compartmental model is defined in one of three ways. In all cases the output is ultimately a [model simulator](#model-simulator). - * (2a) A model is chosen from a model library and read into `R`, optionally updating the model structure using an [engine-agnostic model specification language](#engine-agnostic-model-specification-language). - * (2b) A model is written from scratch using the [engine-agnostic model specification language](#engine-agnostic-model-specification-language). - * (2c) A model is written from scratch using one of the [engine-specific model specification languages](#engine-specific-model-specification-languages). -These three are alternatives, in that if 2a is chosen then 2b and 2c are automatically executed and if 2b is chosen that 2c is automatic. The choice here is just how close (2c) or far (2a) from the actual computation engine do you want to be when specifying models. There are several [considerations when choosing a model specification workflow](#engine-agnostic-versus-engine-specific) when deciding which alternative to use. No matter which of these approaches is taken, the output of step 2 is a model simulator that can be used to generate modelling outputs like simulated incidence time-series or reproduction numbers. -3. Although model simulators come with default initial values so that they can be used immediately, typically one would like to modify these values without needing to edit the model specifications from step 2. There are two main use-cases involving such numerical modifications to the model simulators: In order to formally calibrate model parameters by fitting the model to observed time-series data and/or modifying default parameter values to reflect a what-if scenario. In both use-cases, a model simulator is used as input and another model simulator is produced as output. -4. Once the model defining and numerical initialization steps have been completed, model outputs are produced in long-format data frames. -5. Finally these model outputs are incorporated into forecasts, plots, reports, and diagnostics using standard tools outside of `macpan2`. - -### Modularity - -Modularity is a key principle of `macpan2` design in a few ways. - -First, `macpan2` is meant to plug into standard R workflows for data pre-processing and simulation post-processing. There is very little functionality in `macpan2` for configuring how data are prepared as input and modelling outputs are processed. Instead, `macpan2` accepts standard data objects (data frames, matrices, vectors) and returns simulations as long-format data frames that can be processed using standard tools like `dplyr` and `ggplot2`. This design principle is illustrated in the architecture diagram above that has blue steps representing standard non-`macpan2` workflows and red steps representing workflows that depend on `macpan2` data structures and objects. The challenges of building the red steps is big enough that we prefer to avoid reinventing the wheel of pre- and post-processing. - -Second, `macpan2` uses an engine plug-in architecture. Models defined in the [engine-agnostic model specification language](#engine-agnostic model specification language) can be rendered in a particular computational engine so that multiple computational approaches can be used to generate modelling outputs for a single model definition. This can be useful if different model outputs are more efficient or convenient for different computational approaches. For example, engines such as TMB that are capable of automatic differentiation are great for fast optimization of parameters and for computing $\mathcal{R}_0$ in models with arbitrary complexity, whereas other engines such as Adaptive Tau are better at stochastic simulation techniques like the Gillespie algorithm. Sometimes an engine will be unable to generate a particular output at all or with sufficient difficulty on the part of the user so as to render the use-case practically impossible. For example, it is not possible to conveniently utilize differential equation solvers in the TMB engine, limiting it to Euler or simple RK4-type solvers. Being able to swap out the TMB engine for one based on `deSolve` (or other similar package) would allow for more convenient and accurate solutions to differential equations without having to leave `macpan2`. - -Third, TODO: describe how the model specification language can be used to build up models modularly (e.g. swap out alternative state-updaters as discussed above but also add in model structures like age-groups and spatial structure to a simple unstructured model) - - -### Engine-Agnostic Model Specification Language - -TODO - -### Engine-Specific Model Specification Languages - -TODO - - - - - -## Interface Specifications - -* `mp_model_spec` -* `mp_tmb_model_spec` -* `mp_simulator` -* `mp_trajectory` - - ## Product Management -The [project board](https://github.com/orgs/canmod/projects/2) tracks the details of bugs, tasks, and feature development. But the following narrative will provide context on product development themes, their current state, and plans for improvement and implementation. - -### General Dynamic Simulation with TMB - -One can [define a generic set of update steps](#hello-world) that are iterated to produce a dynamic simulation model in TMB, and that can be used to generate model simulations. - -This part of the package is general, stable, and flexible. It also meets many modellers where they are, which is with the ability to write down a set of transitions/state updates. - -But it is not convenient if you would just like to simulate from it, which is what the [model library](#model-library) is for. - -### Model Library - -```{r tmb-library} -("starter_models" - |> mp_tmb_library("sir", package = "macpan2") - |> mp_simulator(time_steps = 10, outputs = "I") - |> mp_trajectory() -) -``` - -TODO: - -- [ ] Reuse the tools for the older concept of starter models -- [ ] Establish a specification - -### Calibration - -We will build a function, `mp_calibrate`, which takes (1) an object for simulating model trajectories and (2) other information for calibrating certain quantities of this model. This second type of information is detailed in the following sections. The output of `mp_calibrate` should be another object for simulating model trajectories that contains new default parameter values given by fits and additional stochasticity resulting from parameter estimation uncertainty. - -A big question with calibration is do we want there to be an engine-agnostic DSL layer, or do we just want it to make sense for engines where it makes sense? I think the latter, because otherwise we are making things difficult. We can try to be wise making reusable calibration machinery across engines if it comes to that. - -#### Specifying Data to Fit - -A data frame (or data frames) containing observed (possibly uneven) time series to compare with model simulations. What form should this data frame take? - -One option is the same format as the output of `mp_trajectory`. This would have several benefits. - -* Consistency with input and output formats, making it a little easier to learn. -* Easy to manipulate output into input for testing calibration functionality. -* Possibly simpler argument list to `mp_calibrate` because we would just relate the observed data to simulated data with the same name, of course we would still need an interface for distributional assumptions. -* Naturally handles missing values - -The main disadvantage of this is that format could differ from the indexed vectors discussed below. But this disadvantage should be fixable by having a way to convert indexed vector lists to the 'long-matrices' format. Actually yes ... this should be totally fine. So the general design is to have a generic S3 method for producing a 'long-matrices' data frame. - - -#### Specifying Distributional Assumptions - -Probably should be a few ways to do this depending on how many different assumptions need to be made. At one extreme every observation gets the same distribution, which is easily specified in an argument to `mp_calibrate`. At the other extreme each observation gets its own distribution (including distributional parameters like spread and shape), which could be specified by adding additional columns to the data frame with observed values. Designs for interfaces for use cases that are somewhere between these two extremes seem less obvious. - -#### Specifying Parameters to Fit - -There are two kinds of parameters to fit. - -* Existing quantities to be fitted (e.g. `beta`, initial number of susceptible individuals `S`). -* Creating new quantities to be fitted (e.g. distributional scale parameters declared along with [distributional asumptions](#specifying-distributional-assumptions). - -The scale (e.g. log, logit) on which to fit these parameters must also be specified. - -The new distributional parameters should go into a new indexed vector called something like `distributional_parameters`. (TODO: more general name for new parameters that are part of the observation model, e.g. convolution kernel parameters). - - -### Alternative Trajectory Solvers - -Let $x$ be the state vector and $b$ be the vector of per-capita flow rates. Let $z$ and $y$ be the vectors of from and to states -- that is $z$ ($y$) is the vector the same length of $b$ containing the elements of $x$ associated with the from (to) state for each flow. Therefore, the $i$th flow is from $z[i]$ to $y[i]$ at per-capita rate $b[i]$. - -The other way to think about it is that for a single flow, $x_i$ is the from state, $x_j$ is the to state, and $b_k$ is the per-capita flow rate. - -* Euler - * Inflow: $x_i b_k$ - * Outflow: $x_i b_k$ -* Mean Euler-Multimomial - * Inflow - * Outflow: $$ - - -``` -outflow ~ group_sums(state * flow_rates, from, state) -``` - - - -#### Euler Step - -#### Mean Euler-Multinomial (a.k.a. Hazard Correction) - -#### Runge-Kutta 4 - - -### Time-Varying Parameters - -TODO - -### Vectors in the TMB Engine - -This is a TMB-engine-specific warm-up to [model structure](#model-structure-and-bookkeeping). - -```{r tmb-with-indices} -state_labels = c("S", "I", "R") -flow = data.frame( - rate = c("infection", "recovery") - , from = c("S" , "I" ) - , to = c("I" , "R" ) -) -sir = mp_tmb_model_spec( - before = list( - state[S] ~ N - 1 - , state[I] ~ 1 - , state[R] ~ 0 - ) - , during = list( - flow_rate[infection] ~ beta * state[S] * state[I] / N - , flow_rate[recovery] ~ gamma * state[I] - , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) - ) - , default = list( - state = mp_zero_vector(state_labels) - , flow_rate = mp_zero_vector(flow$rate) - , N = 100 - , beta = 0.25 - , gamma = 0.1 - ) - , integers = list( - from = mp_positions(flow$from, state_labels) - , to = mp_positions(flow$to , state_labels) - ) -) -(sir - |> mp_simulator(time_steps = 10, outputs = "I") - |> mp_trajectory() -) -``` - - -### Model Structure and Bookkeeping - -Structured models are combinations of simpler modular model components. For example one might combine an SIR model with an age-group contact model to produce an age structured model. The modular model components are called atomic models. - -#### Structure in Expressions - -Models are composed of expression lists. Each expression in an unstructured model can be converted into a structured expression to create a structured model. For example, the following unstructured expression defines the rate at which new infections emerge. - -```{r scalar-infection, eval = FALSE} -infection ~ beta * S * I / N -``` - -Each symbol in this expression has a certain type within a structured model, and this type determines how it gets translated into a structured expression. The simplest structured model is one that collects `S` and `I` into a `state` vector with elements `S` and `I`. With this interpretation of the `S` and `I` symbols, the structured infection expression gets translated internally to the following. - -```{r subset-scalar-infection, eval = FALSE} -infection ~ beta * state[S] * state[I] / N -``` - - -Here `S` and `I` become symbols for extracting subsets of the `state` vector. In this case the expression itself remains a scalar expression but two of the scalars are obtained by extracting subsets of the `state` vector. It won't take much imagination to think of examples where multiple paths to infection are required, and therefore the single scalar-valued infection expression will be insufficient. - -We will have a vector-valued expression, for example, in a model with an expanded state vector that tracks the geographic location of `S` and `I` individuals. For example, a two patch model with an `east` and `west` patch would involve a four-dimensional state vector with the following elements: `S.east`, `S.west`, `I.east`, and `I.west`. In this case we now have two scalar-valued infection expressions. - -```{r vector-infection, eval = FALSE} -infection[east] ~ beta * state[S.east] * state[I.east] / N -infection[west] ~ beta * state[S.west] * state[I.west] / N -``` - -With two patches it is fine to write out all scalar-valued infection expressions, but with more patches and with different types of structure (e.g. age groups, symptom status, hospitalization, immunity status, etc ...) it will become crucial to have software that handles the bookkeeping internally. - -To see how easy this can be, note that this two-patch infection expression can be powerfully and compactly expressed as our original unstructured expression, `infection ~ beta * S * I / N`, where `S = c(state[S.east], state[S.west])` and `I = c(state[I.east], state[I.west])`. - -Why is this powerful? Because it separates the math of the dynamic mechanism, `infection ~ beta * S * I / N`, from the bookkeeping required in structured models where the same mechanism is applied again and again to different model strata. This is often how modellers think. For example, I might have a location-structured SIR model that I need to expand to be both age- and location-structured. In this case, infection is still the same process, whereby a susceptible individual contacts an infectious individual to create a flow from susceptible individuals to infectious individuals. The same math applies to all strata of the model. The boring but necessary part is to connect the math to the bookkeeping associated with the model structure, and so software should focus on making these bookkeeping changes as easy as possible and with minimal changes required to the underlying mathematical expressions. - -Let's look at more examples of infection, and watch the bookkeeping get more annoying. In an age-stratified model with two age groups, we now get four scalar-valued infection expressions of the form `infection ~ beta * S * I / N`. -```{r age-structured-infection, eval = FALSE} -infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] -infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] -infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] -infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N[old] -``` - -Here the first expression is for a young individual infecting an old individual, the second is for an old individual infecting a young individual, etc ... Things get worse if we have two age groups in two patches. - -```{r age-location-structured-infection, eval = FALSE} -infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] -infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] -infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] -infection[old.old.east] ~ beta[old.old.east] * state[S.old.east] * state[I.old.east] / N[old.east] -infection[young.young.west] ~ beta[young.young.west] * state[S.young.west] * state[I.young.west] / N[young.west] -infection[young.old.west] ~ beta[young.old.west] * state[S.young.west] * state[I.old.west] / N[old.west] -infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * state[I.young.west] / N[young.west] -infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] -``` - -This still isn't so bad, as we just have the first four expressions for `east` and the last four for `west`. But now let's introduce two symptom status categories: `mild` and `severe`. - -```{r age-location-symptom-structured-infection, eval = FALSE} -infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] -infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] -infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] -infection[young.young.east.severe.severe] ~ beta[young.young.east.severe.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] -infection[young.old.east.mild.mild] ~ beta[young.old.east.mild.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] -infection[young.old.east.mild.severe] ~ beta[young.old.east.mild.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] -infection[young.old.east.severe.mild] ~ beta[young.old.east.severe.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] -infection[young.old.east.severe.severe] ~ beta[young.old.east.severe.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] -infection[old.young.east.mild.mild] ~ beta[old.young.east.mild.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] -infection[old.young.east.mild.severe] ~ beta[old.young.east.mild.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] -infection[old.young.east.severe.mild] ~ beta[old.young.east.severe.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] -infection[old.young.east.severe.severe] ~ beta[old.young.east.severe.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] -infection[old.old.east.mild.mild] ~ beta[old.old.east.mild.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] -infection[old.old.east.mild.severe] ~ beta[old.old.east.mild.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] -infection[old.old.east.severe.mild] ~ beta[old.old.east.severe.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] -infection[old.old.east.severe.severe] ~ beta[old.old.east.severe.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] -infection[young.young.west.mild.mild] ~ beta[young.young.west.mild.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] -infection[young.young.west.mild.severe] ~ beta[young.young.west.mild.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] -infection[young.young.west.severe.mild] ~ beta[young.young.west.severe.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] -infection[young.young.west.severe.severe] ~ beta[young.young.west.severe.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] -infection[young.old.west.mild.mild] ~ beta[young.old.west.mild.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] -infection[young.old.west.mild.severe] ~ beta[young.old.west.mild.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] -infection[young.old.west.severe.mild] ~ beta[young.old.west.severe.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] -infection[young.old.west.severe.severe] ~ beta[young.old.west.severe.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] -infection[old.young.west.mild.mild] ~ beta[old.young.west.mild.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] -infection[old.young.west.mild.severe] ~ beta[old.young.west.mild.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] -infection[old.young.west.severe.mild] ~ beta[old.young.west.severe.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] -infection[old.young.west.severe.severe] ~ beta[old.young.west.severe.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] -infection[old.old.west.mild.mild] ~ beta[old.old.west.mild.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] -infection[old.old.west.mild.severe] ~ beta[old.old.west.mild.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] -infection[old.old.west.severe.mild] ~ beta[old.old.west.severe.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] -infection[old.old.west.severe.severe] ~ beta[old.old.west.severe.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] -``` - -This is intense. The names in square brackets get much less clear in several ways as the model gets more structured. This lack of clarity makes it difficult to see a variety of model assumptions by looking at scalar-valued expressions. The `infection` and `beta` vectors depend on two age categories and two symptom statuses, but only one location. This is because young people can infect old people (and vice versa), because mildly infectious people can cause severe infection (and vice versa), and because infectious people in the east cannot infect people in the west (and vice versa). For labels associated with two ages, what does the first age mean, relative to the second age? To discover this you need to know to look at the ages associated with the `S` and `I` states, and once you do this you can see that the first age category is associated with the susceptible individual and the second with the infectious individual. There is a related issue with symptom status, but it is expressed differently because `S` individuals are not structured by symptom status. In this case we match the second symptom status associated with `infection` and `beta` to the symptom status of the `I` states, which means that the first symptom status implicitly refers to the status of the newly infected individuals and not the infectious individuals. Another way to look at this last issue is that `I` boxes play two different roles. The first role is as an individual that infects an `S` individual, and the second is as the individual that that `S` individual becomes after it is infected. None of this is obvious from the scalar-valued expressions above, and it is difficult to imagine a clearer way to explicitly write each expression. - -Our approach is to do the bookkeeping in a different way. In particular we believe that a constructive approach to structure provides a more comprehensible description, as we describe next. In brief, we believe that a grammar for specifying the steps associated with adding structure can be clearer than a description of the final structured model. - -#### Constructive Descriptions of Model Structure - -The first step to being more constructive is to have a better representation of the structured vectors. Above we used dot-concatenation to represent the model strata. For example, in the two-patch SI model we have both epidemiological status and geographic location in the state variable names: `S.east`, `S.west`, `I.east`, and `I.west`. But as the state vector gets more structured it becomes more convenient to describe its variables using an index table, the rows of which describe each state variable. - -```{r cartesian} -state = mp_cartesian( - mp_index(Epi = c("S", "I")), - mp_index(Loc = c("east", "west")) -) -state -``` - -```{r group} -beta = mp_group(state, "Epi") -``` - -With this representation we can get subsets of the state vector that represent each epidemiological status. - -```{r subset} -mp_subset(state, Epi = "S") -mp_subset(state, Epi = "I") -``` - - -#### Structured Vectors - -These are column vectors, the rows of which - - -### Structure Encourages Reparameterization - - - - -### Alternative Engines - -TODO - -### Combining Expression Lists - -Because expression lists are really just lists of expressions, they can be combined as lists would normally be combined. In this example we keep the dynamics of the si model separate from under-reporting and reporting delay corrections to the raw prevalence (TODO: should really use incidence). - -```{r combining-expression-lists} -library(macpan2) -si_dynamics = list( - transition_rate = infection ~ beta * S * I / N - , state_update = S ~ S - infection - , state_update = I ~ I + infection -) -reporting_correction = list( - post_processing = reports ~ convolution(I, c(0.5, 0.25, 0.25)) -) -si = mp_dynamic_model( - expr_list = ExprList(during = c(si_dynamics, reporting_correction)), - unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) -) -(si - |> mp_dynamic_simulator(time_steps = 10, mats_to_return = "reports") - |> mp_trajectory() -) -``` - - +The [project board](https://github.com/orgs/canmod/projects/2) tracks the details of bugs, tasks, and feature development. diff --git a/README.md b/README.md index a933f19d..a14fb1d5 100644 --- a/README.md +++ b/README.md @@ -3,44 +3,6 @@ macpan2 ================ --
      Documentation -- Installation -- Hello World -- Design Concepts - - Information Processing - - Modularity - - Engine-Agnostic - Model Specification Language - - Engine-Specific - Model Specification Languages -- Product - Management - - General Dynamic Simulation - with TMB - - Model Library - - Calibration - - Alternative Trajectory - Solvers - - Time-Varying Parameters - - Vectors in the TMB Engine - - Model Structure and - Bookkeeping - - Structure Encourages - Reparameterization - - Alternative - Engines - - Combining Expression Lists - [![R-CMD-check](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/canmod/macpan2/actions/workflows/R-CMD-check.yaml) @@ -77,10 +39,10 @@ focus on their core strengths and fill knowledge gaps efficiently and effectively. Although `macpan2` is designed as a compartmental modelling tool that is -agnostic about the underlying computational engine, it currently makes -use of [template model builder](https://github.com/kaskr/adcomp). -Template model builder (TMB) is an `R` modelling package based on a -`C++` framework incorporating mature [automatic +agnostic about the underlying computational engine, it currently uses +[template model builder](https://github.com/kaskr/adcomp) as the sole +engine. Template model builder (TMB) is an `R` modelling package based +on a `C++` framework incorporating mature [automatic differentiation](https://cppad.readthedocs.io/en/latest/user_guide.html) and [matrix algebra](http://eigen.tuxfamily.org/index.php?title=Main_Page) @@ -100,7 +62,7 @@ Agency of Canada](https://www.canada.ca/en/public-health.html) uses - [Representation of compartmental models](https://canmod.github.io/macpan2/articles/model_definitions) \[specification document\] -- [`C++` engine](https://canmod.github.io/macpan2/articles/cpp_side) +- [`TMB` engine](https://canmod.github.io/macpan2/articles/cpp_side) \[specification document\] - [Project history and trajectory](https://canmod.net/misc/macpan2_presentation) \[slides\] @@ -119,7 +81,7 @@ Then, install the `macpan2` package with the following R command. For projects in production one should install a specific version, as in the following command. - remotes::install_github("canmod/macpan2@v0.0.3") + remotes::install_github("canmod/macpan2@v1.0.0") ## Hello World @@ -184,587 +146,7 @@ tools in R](#modularity), as we demonstrate with the following code. ![](misc/readme/plot-tmb-si-1.svg) -## Design Concepts - -![](misc/readme/design-concepts.svg) - -### Information Processing - -Like other statistical modelling software, the high-level purpose of -`macpan2` is to process data sources (top-left) into results -(bottom-left). In the case of `macpan2`, this processing is done using -compartmental modelling (right). The major steps of this information -processing are numbered in the diagram, and we describe each of these. - -1. Information processing begins with accessing and preparing numerical - information from various data sources, with the output being - standard numerical R objects. Depending on the nature of the - analysis to follow, this information could include default values - for parameters (e.g. transmission rate), initial values for the - state variables (e.g. initial number of infectious individuals), - operational schedules (e.g. timing of lockdown events or vaccine - roll-out schedules), and data for model fitting (e.g. time series of - hospital utilization). This step could involve connecting to - real-time surveillance platforms or reading in static data files. - There is not any functionality within `macpan2` for conducting this - step – [`macpan2` does not try to reinvent the wheel in data access - and preparation](#modularity). -2. The structure of a compartmental model is defined in one of three - ways. In all cases the output is ultimately a [model - simulator](#model-simulator). - - (2a) A model is chosen from a model library and read into `R`, - optionally updating the model structure using an - [engine-agnostic model specification - language](#engine-agnostic-model-specification-language). - - (2b) A model is written from scratch using the [engine-agnostic - model specification - language](#engine-agnostic-model-specification-language). - - (2c) A model is written from scratch using one of the - [engine-specific model specification - languages](#engine-specific-model-specification-languages). - These three are alternatives, in that if 2a is chosen then 2b - and 2c are automatically executed and if 2b is chosen that 2c is - automatic. The choice here is just how close (2c) or far (2a) - from the actual computation engine do you want to be when - specifying models. There are several [considerations when - choosing a model specification - workflow](#engine-agnostic-versus-engine-specific) when deciding - which alternative to use. No matter which of these approaches is - taken, the output of step 2 is a model simulator that can be - used to generate modelling outputs like simulated incidence - time-series or reproduction numbers. -3. Although model simulators come with default initial values so that - they can be used immediately, typically one would like to modify - these values without needing to edit the model specifications from - step 2. There are two main use-cases involving such numerical - modifications to the model simulators: In order to formally - calibrate model parameters by fitting the model to observed - time-series data and/or modifying default parameter values to - reflect a what-if scenario. In both use-cases, a model simulator is - used as input and another model simulator is produced as output. -4. Once the model defining and numerical initialization steps have been - completed, model outputs are produced in long-format data frames. -5. Finally these model outputs are incorporated into forecasts, plots, - reports, and diagnostics using standard tools outside of `macpan2`. - -### Modularity - -Modularity is a key principle of `macpan2` design in a few ways. - -First, `macpan2` is meant to plug into standard R workflows for data -pre-processing and simulation post-processing. There is very little -functionality in `macpan2` for configuring how data are prepared as -input and modelling outputs are processed. Instead, `macpan2` accepts -standard data objects (data frames, matrices, vectors) and returns -simulations as long-format data frames that can be processed using -standard tools like `dplyr` and `ggplot2`. This design principle is -illustrated in the architecture diagram above that has blue steps -representing standard non-`macpan2` workflows and red steps representing -workflows that depend on `macpan2` data structures and objects. The -challenges of building the red steps is big enough that we prefer to -avoid reinventing the wheel of pre- and post-processing. - -Second, `macpan2` uses an engine plug-in architecture. Models defined in -the [engine-agnostic model specification -language](#engine-agnostic%20model%20specification%20language) can be -rendered in a particular computational engine so that multiple -computational approaches can be used to generate modelling outputs for a -single model definition. This can be useful if different model outputs -are more efficient or convenient for different computational approaches. -For example, engines such as TMB that are capable of automatic -differentiation are great for fast optimization of parameters and for -computing $\mathcal{R}_0$ in models with arbitrary complexity, whereas -other engines such as Adaptive Tau are better at stochastic simulation -techniques like the Gillespie algorithm. Sometimes an engine will be -unable to generate a particular output at all or with sufficient -difficulty on the part of the user so as to render the use-case -practically impossible. For example, it is not possible to conveniently -utilize differential equation solvers in the TMB engine, limiting it to -Euler or simple RK4-type solvers. Being able to swap out the TMB engine -for one based on `deSolve` (or other similar package) would allow for -more convenient and accurate solutions to differential equations without -having to leave `macpan2`. - -Third, TODO: describe how the model specification language can be used -to build up models modularly (e.g. swap out alternative state-updaters -as discussed above but also add in model structures like age-groups and -spatial structure to a simple unstructured model) - -### Engine-Agnostic Model Specification Language - -TODO - -### Engine-Specific Model Specification Languages - -TODO - ## Product Management The [project board](https://github.com/orgs/canmod/projects/2) tracks -the details of bugs, tasks, and feature development. But the following -narrative will provide context on product development themes, their -current state, and plans for improvement and implementation. - -### General Dynamic Simulation with TMB - -One can [define a generic set of update steps](#hello-world) that are -iterated to produce a dynamic simulation model in TMB, and that can be -used to generate model simulations. - -This part of the package is general, stable, and flexible. It also meets -many modellers where they are, which is with the ability to write down a -set of transitions/state updates. - -But it is not convenient if you would just like to simulate from it, -which is what the [model library](#model-library) is for. - -### Model Library - -``` r -("starter_models" - |> mp_tmb_library("sir", package = "macpan2") - |> mp_simulator(time_steps = 10, outputs = "I") - |> mp_trajectory() -) -``` - - ## matrix time row col value - ## 1 I 1 0 0 1.098000 - ## 2 I 2 0 0 1.205169 - ## 3 I 3 0 0 1.322276 - ## 4 I 4 0 0 1.450133 - ## 5 I 5 0 0 1.589599 - ## 6 I 6 0 0 1.741573 - ## 7 I 7 0 0 1.906995 - ## 8 I 8 0 0 2.086833 - ## 9 I 9 0 0 2.282085 - ## 10 I 10 0 0 2.493761 - -TODO: - -- [ ] Reuse the tools for the older concept of starter models -- [ ] Establish a specification - -### Calibration - -We will build a function, `mp_calibrate`, which takes (1) an object for -simulating model trajectories and (2) other information for calibrating -certain quantities of this model. This second type of information is -detailed in the following sections. The output of `mp_calibrate` should -be another object for simulating model trajectories that contains new -default parameter values given by fits and additional stochasticity -resulting from parameter estimation uncertainty. - -A big question with calibration is do we want there to be an -engine-agnostic DSL layer, or do we just want it to make sense for -engines where it makes sense? I think the latter, because otherwise we -are making things difficult. We can try to be wise making reusable -calibration machinery across engines if it comes to that. - -#### Specifying Data to Fit - -A data frame (or data frames) containing observed (possibly uneven) time -series to compare with model simulations. What form should this data -frame take? - -One option is the same format as the output of `mp_report`. This would -have several benefits. - -- Consistency with input and output formats, making it a little easier - to learn. -- Easy to manipulate output into input for testing calibration - functionality. -- Possibly simpler argument list to `mp_calibrate` because we would - just relate the observed data to simulated data with the same name, - of course we would still need an interface for distributional - assumptions. -- Naturally handles missing values - -The main disadvantage of this is that format could differ from the -indexed vectors discussed below. - -#### Specifying Distributional Assumptions - -Probably should be a few ways to do this depending on how many different -assumptions need to be made. At one extreme every observation gets the -same distribution, which is easily specified in an argument to -`mp_calibrate`. At the other extreme each observation gets its own -distribution (including distributional parameters like spread and -shape), which could be specified by adding additional columns to the -data frame with observed values. Designs for interfaces for use cases -that are somewhere between these two extremes seem less obvious. - -#### Specifying Parameters to Fit - -There are two kinds of parameters to fit. - -- Existing quantities to be fitted (e.g. `beta`, initial number of - susceptible individuals `S`). -- Creating new quantities to be fitted (e.g. distributional scale - parameters declared along with [distributional - asumptions](#specifying-distributional-assumptions). - -The scale (e.g. log, logit) on which to fit these parameters must also -be specified. - -The new distributional parameters should go into a new indexed vector -called something like `distributional_parameters`. (TODO: more general -name for new parameters that are part of the observation model, -e.g. convolution kernel parameters). - -### Alternative Trajectory Solvers - -Let $x$ be the state vector and $b$ be the vector of per-capita flow -rates. Let $z$ and $y$ be the vectors of from and to states – that is -$z$ ($y$) is the vector the same length of $b$ containing the elements -of $x$ associated with the from (to) state for each flow. Therefore, the -$i$th flow is from $z[i]$ to $y[i]$ at per-capita rate $b[i]$. - -The other way to think about it is that for a single flow, $x_i$ is the -from state, $x_j$ is the to state, and $b_k$ is the per-capita flow -rate. - -- Euler - - Inflow: $x_i b_k$ - - Outflow: $x_i b_k$ -- Mean Euler-Multimomial - - Inflow - - Outflow: \$\$ - - - - outflow ~ group_sums(state * flow_rates, from, state) - -#### Euler Step - -#### Mean Euler-Multinomial (a.k.a. Hazard Correction) - -#### Runge-Kutta 4 - -### Time-Varying Parameters - -TODO - -### Vectors in the TMB Engine - -This is a TMB-engine-specific warm-up to [model -structure](#model-structure-and-bookkeeping). - -``` r -state_labels = c("S", "I", "R") -flow = data.frame( - rate = c("infection", "recovery") - , from = c("S" , "I" ) - , to = c("I" , "R" ) -) -sir = mp_tmb_model_spec( - before = list( - state[S] ~ N - 1 - , state[I] ~ 1 - , state[R] ~ 0 - ) - , during = list( - flow_rate[infection] ~ beta * state[S] * state[I] / N - , flow_rate[recovery] ~ gamma * state[I] - , state ~ state + group_sums(flow_rate, to, state) - group_sums(flow_rate, from, state) - ) - , default = list( - state = mp_zero_vector(state_labels) - , flow_rate = mp_zero_vector(flow$rate) - , N = 100 - , beta = 0.25 - , gamma = 0.1 - ) - , integers = list( - from = mp_positions(flow$from, state_labels) - , to = mp_positions(flow$to , state_labels) - ) -) -(sir - |> mp_simulator(time_steps = 10, outputs = "I") - |> mp_trajectory() -) -``` - - ## matrix time row col value - ## 1 state 1 I 1.147500 - ## 2 state 2 I 1.316046 - ## 3 state 3 I 1.508417 - ## 4 state 4 I 1.727685 - ## 5 state 5 I 1.977228 - ## 6 state 6 I 2.260727 - ## 7 state 7 I 2.582154 - ## 8 state 8 I 2.945748 - ## 9 state 9 I 3.355960 - ## 10 state 10 I 3.817384 - -### Model Structure and Bookkeeping - -Structured models are combinations of simpler modular model components. -For example one might combine an SIR model with an age-group contact -model to produce an age structured model. The modular model components -are called atomic models. - -#### Structure in Expressions - -Models are composed of expression lists. Each expression in an -unstructured model can be converted into a structured expression to -create a structured model. For example, the following unstructured -expression defines the rate at which new infections emerge. - -``` r -infection ~ beta * S * I / N -``` - -Each symbol in this expression has a certain type within a structured -model, and this type determines how it gets translated into a structured -expression. The simplest structured model is one that collects `S` and -`I` into a `state` vector with elements `S` and `I`. With this -interpretation of the `S` and `I` symbols, the structured infection -expression gets translated internally to the following. - -``` r -infection ~ beta * state[S] * state[I] / N -``` - -Here `S` and `I` become symbols for extracting subsets of the `state` -vector. In this case the expression itself remains a scalar expression -but two of the scalars are obtained by extracting subsets of the `state` -vector. It won’t take much imagination to think of examples where -multiple paths to infection are required, and therefore the single -scalar-valued infection expression will be insufficient. - -We will have a vector-valued expression, for example, in a model with an -expanded state vector that tracks the geographic location of `S` and `I` -individuals. For example, a two patch model with an `east` and `west` -patch would involve a four-dimensional state vector with the following -elements: `S.east`, `S.west`, `I.east`, and `I.west`. In this case we -now have two scalar-valued infection expressions. - -``` r -infection[east] ~ beta * state[S.east] * state[I.east] / N -infection[west] ~ beta * state[S.west] * state[I.west] / N -``` - -With two patches it is fine to write out all scalar-valued infection -expressions, but with more patches and with different types of structure -(e.g. age groups, symptom status, hospitalization, immunity status, etc -…) it will become crucial to have software that handles the bookkeeping -internally. - -To see how easy this can be, note that this two-patch infection -expression can be powerfully and compactly expressed as our original -unstructured expression, `infection ~ beta * S * I / N`, where -`S = c(state[S.east], state[S.west])` and -`I = c(state[I.east], state[I.west])`. - -Why is this powerful? Because it separates the math of the dynamic -mechanism, `infection ~ beta * S * I / N`, from the bookkeeping required -in structured models where the same mechanism is applied again and again -to different model strata. This is often how modellers think. For -example, I might have a location-structured SIR model that I need to -expand to be both age- and location-structured. In this case, infection -is still the same process, whereby a susceptible individual contacts an -infectious individual to create a flow from susceptible individuals to -infectious individuals. The same math applies to all strata of the -model. The boring but necessary part is to connect the math to the -bookkeeping associated with the model structure, and so software should -focus on making these bookkeeping changes as easy as possible and with -minimal changes required to the underlying mathematical expressions. - -Let’s look at more examples of infection, and watch the bookkeeping get -more annoying. In an age-stratified model with two age groups, we now -get four scalar-valued infection expressions of the form -`infection ~ beta * S * I / N`. - -``` r -infection[young.young] ~ beta[young.young] * state[S.young] * state[I.young] / N[young] -infection[young.old] ~ beta[young.old] * state[S.young] * state[I.old] / N[old] -infection[old.young] ~ beta[old.young] * state[S.old] * state[I.young] / N[young] -infection[old.old] ~ beta[old.old] * state[S.old] * state[I.old] / N[old] -``` - -Here the first expression is for a young individual infecting an old -individual, the second is for an old individual infecting a young -individual, etc … Things get worse if we have two age groups in two -patches. - -``` r -infection[young.young.east] ~ beta[young.young.east] * state[S.young.east] * state[I.young.east] / N[young.east] -infection[young.old.east] ~ beta[young.old.east] * state[S.young.east] * state[I.old.east] / N[old.east] -infection[old.young.east] ~ beta[old.young.east] * state[S.old.east] * state[I.young.east] / N[young.east] -infection[old.old.east] ~ beta[old.old.east] * state[S.old.east] * state[I.old.east] / N[old.east] -infection[young.young.west] ~ beta[young.young.west] * state[S.young.west] * state[I.young.west] / N[young.west] -infection[young.old.west] ~ beta[young.old.west] * state[S.young.west] * state[I.old.west] / N[old.west] -infection[old.young.west] ~ beta[old.young.west] * state[S.old.west] * state[I.young.west] / N[young.west] -infection[old.old.west] ~ beta[old.old.west] * state[S.old.west] * state[I.old.west] / N[old.west] -``` - -This still isn’t so bad, as we just have the first four expressions for -`east` and the last four for `west`. But now let’s introduce two symptom -status categories: `mild` and `severe`. - -``` r -infection[young.young.east.mild.mild] ~ beta[young.young.east.mild.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] -infection[young.young.east.mild.severe] ~ beta[young.young.east.mild.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] -infection[young.young.east.severe.mild] ~ beta[young.young.east.severe.mild] * state[S.young.east] * state[I.young.east.mild] / N[young.east] -infection[young.young.east.severe.severe] ~ beta[young.young.east.severe.severe] * state[S.young.east] * state[I.young.east.severe] / N[young.east] -infection[young.old.east.mild.mild] ~ beta[young.old.east.mild.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] -infection[young.old.east.mild.severe] ~ beta[young.old.east.mild.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] -infection[young.old.east.severe.mild] ~ beta[young.old.east.severe.mild] * state[S.young.east] * state[I.old.east.mild] / N[old.east] -infection[young.old.east.severe.severe] ~ beta[young.old.east.severe.severe] * state[S.young.east] * state[I.old.east.severe] / N[old.east] -infection[old.young.east.mild.mild] ~ beta[old.young.east.mild.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] -infection[old.young.east.mild.severe] ~ beta[old.young.east.mild.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] -infection[old.young.east.severe.mild] ~ beta[old.young.east.severe.mild] * state[S.old.east] * state[I.young.east.mild] / N[young.east] -infection[old.young.east.severe.severe] ~ beta[old.young.east.severe.severe] * state[S.old.east] * state[I.young.east.severe] / N[young.east] -infection[old.old.east.mild.mild] ~ beta[old.old.east.mild.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] -infection[old.old.east.mild.severe] ~ beta[old.old.east.mild.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] -infection[old.old.east.severe.mild] ~ beta[old.old.east.severe.mild] * state[S.old.east] * state[I.old.east.mild] / N[old.east] -infection[old.old.east.severe.severe] ~ beta[old.old.east.severe.severe] * state[S.old.east] * state[I.old.east.severe] / N[old.east] -infection[young.young.west.mild.mild] ~ beta[young.young.west.mild.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] -infection[young.young.west.mild.severe] ~ beta[young.young.west.mild.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] -infection[young.young.west.severe.mild] ~ beta[young.young.west.severe.mild] * state[S.young.west] * state[I.young.west.mild] / N[young.west] -infection[young.young.west.severe.severe] ~ beta[young.young.west.severe.severe] * state[S.young.west] * state[I.young.west.severe] / N[young.west] -infection[young.old.west.mild.mild] ~ beta[young.old.west.mild.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] -infection[young.old.west.mild.severe] ~ beta[young.old.west.mild.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] -infection[young.old.west.severe.mild] ~ beta[young.old.west.severe.mild] * state[S.young.west] * state[I.old.west.mild] / N[old.west] -infection[young.old.west.severe.severe] ~ beta[young.old.west.severe.severe] * state[S.young.west] * state[I.old.west.severe] / N[old.west] -infection[old.young.west.mild.mild] ~ beta[old.young.west.mild.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] -infection[old.young.west.mild.severe] ~ beta[old.young.west.mild.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] -infection[old.young.west.severe.mild] ~ beta[old.young.west.severe.mild] * state[S.old.west] * state[I.young.west.mild] / N[young.west] -infection[old.young.west.severe.severe] ~ beta[old.young.west.severe.severe] * state[S.old.west] * state[I.young.west.severe] / N[young.west] -infection[old.old.west.mild.mild] ~ beta[old.old.west.mild.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] -infection[old.old.west.mild.severe] ~ beta[old.old.west.mild.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] -infection[old.old.west.severe.mild] ~ beta[old.old.west.severe.mild] * state[S.old.west] * state[I.old.west.mild] / N[old.west] -infection[old.old.west.severe.severe] ~ beta[old.old.west.severe.severe] * state[S.old.west] * state[I.old.west.severe] / N[old.west] -``` - -This is intense. The names in square brackets get much less clear in -several ways as the model gets more structured. This lack of clarity -makes it difficult to see a variety of model assumptions by looking at -scalar-valued expressions. The `infection` and `beta` vectors depend on -two age categories and two symptom statuses, but only one location. This -is because young people can infect old people (and vice versa), because -mildly infectious people can cause severe infection (and vice versa), -and because infectious people in the east cannot infect people in the -west (and vice versa). For labels associated with two ages, what does -the first age mean, relative to the second age? To discover this you -need to know to look at the ages associated with the `S` and `I` states, -and once you do this you can see that the first age category is -associated with the susceptible individual and the second with the -infectious individual. There is a related issue with symptom status, but -it is expressed differently because `S` individuals are not structured -by symptom status. In this case we match the second symptom status -associated with `infection` and `beta` to the symptom status of the `I` -states, which means that the first symptom status implicitly refers to -the status of the newly infected individuals and not the infectious -individuals. Another way to look at this last issue is that `I` boxes -play two different roles. The first role is as an individual that -infects an `S` individual, and the second is as the individual that that -`S` individual becomes after it is infected. None of this is obvious -from the scalar-valued expressions above, and it is difficult to imagine -a clearer way to explicitly write each expression. - -Our approach is to do the bookkeeping in a different way. In particular -we believe that a constructive approach to structure provides a more -comprehensible description, as we describe next. In brief, we believe -that a grammar for specifying the steps associated with adding structure -can be clearer than a description of the final structured model. - -#### Constructive Descriptions of Model Structure - -The first step to being more constructive is to have a better -representation of the structured vectors. Above we used -dot-concatenation to represent the model strata. For example, in the -two-patch SI model we have both epidemiological status and geographic -location in the state variable names: `S.east`, `S.west`, `I.east`, and -`I.west`. But as the state vector gets more structured it becomes more -convenient to describe its variables using an index table, the rows of -which describe each state variable. - -``` r -state = mp_cartesian( - mp_index(Epi = c("S", "I")), - mp_index(Loc = c("east", "west")) -) -state -``` - - ## Epi Loc - ## S east - ## I east - ## S west - ## I west - -``` r -beta = mp_group(state, "Epi") -``` - -With this representation we can get subsets of the state vector that -represent each epidemiological status. - -``` r -mp_subset(state, Epi = "S") -``` - - ## Epi Loc - ## S east - ## S west - -``` r -mp_subset(state, Epi = "I") -``` - - ## Epi Loc - ## I east - ## I west - -#### Structured Vectors - -These are column vectors, the rows of which - -### Structure Encourages Reparameterization - -### Alternative Engines - -TODO - -### Combining Expression Lists - -Because expression lists are really just lists of expressions, they can -be combined as lists would normally be combined. In this example we keep -the dynamics of the si model separate from under-reporting and reporting -delay corrections to the raw prevalence (TODO: should really use -incidence). - -``` r -library(macpan2) -si_dynamics = list( - transition_rate = infection ~ beta * S * I / N - , state_update = S ~ S - infection - , state_update = I ~ I + infection -) -reporting_correction = list( - post_processing = reports ~ convolution(I, c(0.5, 0.25, 0.25)) -) -si = mp_dynamic_model( - expr_list = ExprList(during = c(si_dynamics, reporting_correction)), - unstruc_mats = list(S = 99, I = 1, beta = 0.25, N = 100) -) -(si - |> mp_dynamic_simulator(time_steps = 10, mats_to_return = "reports") - |> mp_report() -) -``` - - ## matrix time row col value - ## 1 reports 1 0 0 0.6237500 - ## 2 reports 2 0 0 0.7777422 - ## 3 reports 3 0 0 0.9691533 - ## 4 reports 4 0 0 1.2067453 - ## 5 reports 5 0 0 1.5011505 - ## 6 reports 6 0 0 1.8651709 - ## 7 reports 7 0 0 2.3140693 - ## 8 reports 8 0 0 2.8658120 - ## 9 reports 9 0 0 3.5412006 - ## 10 reports 10 0 0 4.3638003 +the details of bugs, tasks, and feature development. From 68841454f1065e458437e7e4c819e54ee1e9d6cb Mon Sep 17 00:00:00 2001 From: stevencarlislewalker Date: Thu, 18 Jan 2024 12:36:30 -0500 Subject: [PATCH 332/332] version 1 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index e12e6558..87b52431 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: macpan2 Title: Fast and Flexible Compartmental Modelling -Version: 0.0.4 +Version: 1.0.0 Authors@R: c( person("Steve Walker", email="swalk@mcmaster.ca", role=c("cre", "aut")), person("Irena Papst", role="ctb"),

    ^i32RBQ`3j0RD1@}L=H5?NfB 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 z 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�xH$4cANq(Q)Ex9K%Vmln4YoQcg{dr`-U@-*x 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%0DMHhuu2r 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