diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3e7e58b3..7181d7f2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -28,6 +28,33 @@ } }, + { + "label": "JMP: Test (with cache + FULL)", + "type": "R", + "code": [ + "devtools::test()" + ], + "env": { + "JMPOST_CACHE_DIR": "${workspaceFolder}/local/test_cache", + "JMPOST_FULL_TEST": "TRUE" + }, + "problemMatcher": [ + "$testthat" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": true, + "clear": true + } + }, + { "label": "JMP: Test Current File (with cache)", diff --git a/DESCRIPTION b/DESCRIPTION index 9c44fd8a..00195bd7 100755 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -80,6 +80,7 @@ Collate: 'LongitudinalGSF.R' 'LongitudinalQuantities.R' 'LongitudinalRandomSlope.R' + 'LongitudinalSteinFojo.R' 'SurvivalExponential.R' 'SurvivalLoglogistic.R' 'SurvivalWeibullPH.R' @@ -92,5 +93,6 @@ Collate: 'simulations_gsf.R' 'simulations_os.R' 'simulations_rs.R' + 'simulations_sf.R' 'zzz.R' VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 3768a4a7..aa11ab67 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -54,6 +54,7 @@ S3method(compileStanModel,JointModel) S3method(dim,Quantities) S3method(enableLink,LongitudinalGSF) S3method(enableLink,LongitudinalRandomSlope) +S3method(enableLink,LongitudinalSteinFojo) S3method(extractVariableNames,DataSubject) S3method(extractVariableNames,DataSurvival) S3method(generateQuantities,JointModelSamples) @@ -72,11 +73,14 @@ S3method(length,Link) S3method(linkDSLD,LongitudinalGSF) S3method(linkDSLD,LongitudinalModel) S3method(linkDSLD,LongitudinalRandomSlope) +S3method(linkDSLD,LongitudinalSteinFojo) S3method(linkIdentity,LongitudinalGSF) S3method(linkIdentity,LongitudinalModel) S3method(linkIdentity,LongitudinalRandomSlope) +S3method(linkIdentity,LongitudinalSteinFojo) S3method(linkTTG,LongitudinalGSF) S3method(linkTTG,LongitudinalModel) +S3method(linkTTG,LongitudinalSteinFojo) S3method(names,Parameter) S3method(names,ParameterList) S3method(sampleStanModel,JointModel) @@ -99,6 +103,7 @@ export(LongitudinalGSF) export(LongitudinalModel) export(LongitudinalQuantities) export(LongitudinalRandomSlope) +export(LongitudinalSteinFojo) export(Parameter) export(ParameterList) export(Prior) @@ -141,9 +146,13 @@ export(prior_std_normal) export(prior_student_t) export(prior_uniform) export(sampleStanModel) +export(sf_dsld) +export(sf_sld) +export(sf_ttg) export(show) export(sim_lm_gsf) export(sim_lm_random_slope) +export(sim_lm_sf) export(sim_os_exponential) export(sim_os_loglogistic) export(sim_os_weibull) @@ -159,6 +168,7 @@ exportClasses(Link) exportClasses(LongitudinalGSF) exportClasses(LongitudinalModel) exportClasses(LongitudinalRandomSlope) +exportClasses(LongitudinalSteinFojo) exportClasses(Parameter) exportClasses(ParameterList) exportClasses(Prior) diff --git a/R/LongitudinalSteinFojo.R b/R/LongitudinalSteinFojo.R new file mode 100755 index 00000000..1d5004d4 --- /dev/null +++ b/R/LongitudinalSteinFojo.R @@ -0,0 +1,136 @@ +#' @include LongitudinalModel.R +#' @include StanModule.R +#' @include generics.R +#' @include ParameterList.R +#' @include Parameter.R +#' @include Link.R +NULL + + +#' `LongitudinalSteinFojo` +#' +#' This class extends the general [`LongitudinalModel`] class for using the +#' Stein-Fojo model for the longitudinal outcome. +#' +#' @exportClass LongitudinalSteinFojo +.LongitudinalSteinFojo <- setClass( + Class = "LongitudinalSteinFojo", + contains = "LongitudinalModel" +) + + +#' @rdname LongitudinalSteinFojo-class +#' +#' @param mu_bsld (`Prior`)\cr for the mean baseline value `mu_bsld`. +#' @param mu_ks (`Prior`)\cr for the mean shrinkage rate `mu_ks`. +#' @param mu_kg (`Prior`)\cr for the mean growth rate `mu_kg`. +#' +#' @param omega_bsld (`Prior`)\cr for the baseline value standard deviation `omega_bsld`. +#' @param omega_ks (`Prior`)\cr for the shrinkage rate standard deviation `omega_ks`. +#' @param omega_kg (`Prior`)\cr for the growth rate standard deviation `omega_kg`. +#' +#' @param sigma (`Prior`)\cr for the variance of the longitudinal values `sigma`. +#' +#' @param centred (`logical`)\cr whether to use the centred parameterization. +#' +#' @export +LongitudinalSteinFojo <- function( + + mu_bsld = prior_normal(log(60), 1), + mu_ks = prior_normal(log(0.5), 1), + mu_kg = prior_normal(log(0.3), 1), + + omega_bsld = prior_lognormal(log(0.2), 1), + omega_ks = prior_lognormal(log(0.2), 1), + omega_kg = prior_lognormal(log(0.2), 1), + + sigma = prior_lognormal(log(0.1), 1), + + centred = FALSE +) { + + sf_model <- StanModule(decorated_render( + .x = paste0(read_stan("lm-stein-fojo/model.stan"), collapse = "\n"), + centred = centred + )) + + parameters <- list( + Parameter(name = "lm_sf_mu_bsld", prior = mu_bsld, size = "n_studies"), + Parameter(name = "lm_sf_mu_ks", prior = mu_ks, size = "n_arms"), + Parameter(name = "lm_sf_mu_kg", prior = mu_kg, size = "n_arms"), + + Parameter(name = "lm_sf_omega_bsld", prior = omega_bsld, size = 1), + Parameter(name = "lm_sf_omega_ks", prior = omega_ks, size = 1), + Parameter(name = "lm_sf_omega_kg", prior = omega_kg, size = 1), + + Parameter(name = "lm_sf_sigma", prior = sigma, size = 1) + ) + + assert_flag(centred) + parameters_extra <- if (centred) { + list( + Parameter( + name = "lm_sf_psi_bsld", + prior = prior_init_only(prior_lognormal(mu_bsld@init, omega_bsld@init)), + size = "Nind" + ), + Parameter( + name = "lm_sf_psi_ks", + prior = prior_init_only(prior_lognormal(mu_ks@init, omega_ks@init)), + size = "Nind" + ), + Parameter( + name = "lm_sf_psi_kg", + prior = prior_init_only(prior_lognormal(mu_kg@init, omega_kg@init)), + size = "Nind" + ) + ) + } else { + list( + Parameter(name = "lm_sf_eta_tilde_bsld", prior = prior_std_normal(), size = "Nind"), + Parameter(name = "lm_sf_eta_tilde_ks", prior = prior_std_normal(), size = "Nind"), + Parameter(name = "lm_sf_eta_tilde_kg", prior = prior_std_normal(), size = "Nind") + ) + } + parameters <- append(parameters, parameters_extra) + + x <- LongitudinalModel( + name = "Stein-Fojo", + stan = merge( + sf_model, + StanModule("lm-stein-fojo/functions.stan") + ), + parameters = do.call(ParameterList, parameters) + ) + .LongitudinalSteinFojo(x) +} + + + +#' @rdname standard-link-methods +#' @export +enableLink.LongitudinalSteinFojo <- function(object, ...) { + object@stan <- merge( + object@stan, + StanModule("lm-stein-fojo/link.stan") + ) + object +} + +#' @rdname standard-link-methods +#' @export +linkDSLD.LongitudinalSteinFojo <- function(object, ...) { + StanModule("lm-stein-fojo/link_dsld.stan") +} + +#' @rdname standard-link-methods +#' @export +linkTTG.LongitudinalSteinFojo <- function(object, ...) { + StanModule("lm-stein-fojo/link_ttg.stan") +} + +#' @rdname standard-link-methods +#' @export +linkIdentity.LongitudinalSteinFojo <- function(object, ...) { + StanModule("lm-stein-fojo/link_identity.stan") +} diff --git a/R/simulations.R b/R/simulations.R index dece32f2..ead016f4 100755 --- a/R/simulations.R +++ b/R/simulations.R @@ -93,12 +93,18 @@ simulate_joint_data <- function( msg = "The longitudinal dataset must be sorted by pt, time" ) + os_dat_chaz <- time_dat_baseline |> dplyr::mutate(log_haz_link = lm_dat$log_haz_link) |> # only works if lm_dat is sorted pt, time dplyr::mutate(log_bl_haz = os_fun(.data$evalp)) |> # Fix to avoid issue with log(0) = NaN values dplyr::mutate(log_bl_haz = dplyr::if_else(.data$evalp == 0, -999, .data$log_bl_haz)) |> dplyr::mutate(hazard_instant = exp(.data$log_bl_haz + .data$log_haz_cov + .data$log_haz_link)) |> + # Reset Inf values to large number to avoid NaN issues downstream + # This is suitable as Hazard limits tend to be in the range of -10 to 10 so large numbers + # are essentially equivalent to infinity for simulation purposes + dplyr::mutate(hazard_instant = dplyr::if_else(.data$hazard_instant == Inf, 999, .data$hazard_instant)) |> + dplyr::mutate(hazard_instant = dplyr::if_else(.data$hazard_instant == -Inf, -999, .data$hazard_instant)) |> dplyr::mutate(hazard_interval = .data$hazard_instant * .data$width) |> dplyr::group_by(.data$pt) |> dplyr::mutate(chazard = cumsum(.data$hazard_interval)) |> diff --git a/R/simulations_sf.R b/R/simulations_sf.R new file mode 100644 index 00000000..6b5765fd --- /dev/null +++ b/R/simulations_sf.R @@ -0,0 +1,108 @@ + + + +#' Stein-Fojo Functionals +#' +#' @param time (`numeric`)\cr time grid. +#' @param b (`number`)\cr baseline. +#' @param s (`number`)\cr shrinkage. +#' @param g (`number`)\cr growth. +#' +#' @returns The function results. +#' @keywords internal +#' @export +#' @examples +#' sf_sld(1:10, 20, 0.3, 0.6) +sf_sld <- function(time, b, s, g) { + b * (exp(-s * time) + exp(g * time) - 1) +} + + +#' @rdname sf_sld +#' @export +#' @examples +#' sf_ttg(1:10, 20, 0.3, 0.6) +sf_ttg <- function(time, b, s, g) { + t1 <- (log(s) - log(g)) / (g + s) + t1[t1 <= 0] <- 0 + return(t1) +} + + +#' @rdname sf_sld +#' @export +#' @examples +#' sf_dsld(1:10, 20, 0.3, 0.6) +sf_dsld <- function(time, b, s, g) { + t1 <- g * exp(g * time) + t2 <- s * exp(-s * time) + return(b * (t1 - t2)) +} + + + +#' Construct a Simulation Function for Longitudinal Data from Stein-Fojo Model +#' +#' @param sigma (`number`)\cr the variance of the longitudinal values. +#' @param mu_s (`numeric`)\cr the mean shrinkage rates for the two treatment arms. +#' @param mu_g (`numeric`)\cr the mean growth rates for the two treatment arms. +#' @param mu_b (`numeric`)\cr the mean baseline values for the two treatment arms. +#' @param omega_b (`number`)\cr the baseline value standard deviation. +#' @param omega_s (`number`)\cr the shrinkage rate standard deviation. +#' @param omega_g (`number`)\cr the growth rate standard deviation. +#' @param link_dsld (`number`)\cr the link coefficient for the derivative contribution. +#' @param link_ttg (`number`)\cr the link coefficient for the time-to-growth contribution. +#' @param link_identity (`number`)\cr the link coefficient for the SLD Identity contribution. +#' +#' @returns A function with argument `lm_base` that can be used to simulate +#' longitudinal data from the corresponding Stein-Fojo model. +#' +#' @export +sim_lm_sf <- function( + sigma = 0.01, + mu_s = c(0.6, 0.4), + mu_g = c(0.25, 0.35), + mu_b = 60, + omega_b = 0.2, + omega_s = 0.2, + omega_g = 0.2, + link_dsld = 0, + link_ttg = 0, + link_identity = 0 +) { + function(lm_base) { + + assert_that( + length(unique(lm_base$study)) == length(mu_b), + length(mu_b) == 1, + length(sigma) == 1, + length(mu_s) == length(unique(lm_base$arm)), + length(mu_s) == length(mu_g), + length(c(omega_b, omega_s, omega_g)) == 3 + ) + + baseline_covs <- lm_base |> + dplyr::distinct(.data$pt, .data$arm, .data$study) |> + dplyr::mutate(study_idx = as.numeric(factor(as.character(.data$study)))) |> + dplyr::mutate(arm_idx = as.numeric(factor(as.character(.data$arm)))) |> + dplyr::mutate(psi_b = stats::rlnorm(dplyr::n(), log(mu_b[.data$study_idx]), omega_b)) |> + dplyr::mutate(psi_s = stats::rlnorm(dplyr::n(), log(mu_s[.data$arm_idx]), omega_s)) |> + dplyr::mutate(psi_g = stats::rlnorm(dplyr::n(), log(mu_g[.data$arm_idx]), omega_g)) + + + lm_dat <- lm_base |> + dplyr::select(!dplyr::all_of(c("study", "arm"))) |> + dplyr::left_join(baseline_covs, by = "pt") |> + dplyr::mutate(mu_sld = sf_sld(.data$time, .data$psi_b, .data$psi_s, .data$psi_g)) |> + dplyr::mutate(dsld = sf_dsld(.data$time, .data$psi_b, .data$psi_s, .data$psi_g)) |> + dplyr::mutate(ttg = sf_ttg(.data$time, .data$psi_b, .data$psi_s, .data$psi_g)) |> + dplyr::mutate(sld = stats::rnorm(dplyr::n(), .data$mu_sld, .data$mu_sld * sigma)) |> + dplyr::mutate( + log_haz_link = + (link_dsld * .data$dsld) + + (link_ttg * .data$ttg) + + (link_identity * .data$mu_sld) + ) + return(lm_dat) + } +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 059bdb8d..652d0af6 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -40,11 +40,11 @@ reference: contents: - simulate_joint_data - sim_lm_gsf + - sim_lm_sf - sim_lm_random_slope - sim_os_exponential - sim_os_loglogistic - sim_os_weibull - - title: Prior Distributions contents: - Prior @@ -65,6 +65,7 @@ reference: contents: - LongitudinalRandomSlope - LongitudinalGSF + - LongitudinalSteinFojo - LongitudinalModel - title: Survival Model Specification diff --git a/design/lm_random_slope.md b/design/lm_random_slope.md deleted file mode 100755 index 89901425..00000000 --- a/design/lm_random_slope.md +++ /dev/null @@ -1,34 +0,0 @@ - -# Longitudinal Model Specifications - - - -## Random Slope Model - -$$ -y_{ij} = \mu_0 + s_i t_{ij} + \epsilon_{ij} -$$ - -where: - -- $y_{ij}$ is the tumour size for subject $i$ at timepoint $j$ -- $\mu_0$ is the intercept -- $s_i$ is the random slope for subject $i$ with $s_i \sim N(\mu_{sk}, \sigma_s)$ -- $\mu_{sk}$ is the mean for the random slope per treatment arm $k$ -- $\sigma_s$ is the variance term for the slopes -- $\epsilon_{ij}$ is the random error and that $\epsilon_{ij} \sim N(0, \sigma)$ - -### Log-Hazard Contribution - -$$ -C_i(t \mid \phi, s_i) = \phi s_i -$$ - -Where: - -- $t$ is time -- $s_i$ is the subjects random slope coeficient -- $\phi$ is a global scaling coeficient - -That is to say a subjects hazard is constant where that constant is a multiple of their random slope - diff --git a/design/objects.png b/design/objects.png deleted file mode 100755 index c66e65bb..00000000 Binary files a/design/objects.png and /dev/null differ diff --git a/design/objects.png.drawio b/design/objects.png.drawio deleted file mode 100755 index 33a4e4d3..00000000 --- a/design/objects.png.drawio +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/design/renvs.png b/design/renvs.png deleted file mode 100644 index f6db6419..00000000 Binary files a/design/renvs.png and /dev/null differ diff --git a/design/renvs.png.drawio b/design/renvs.png.drawio deleted file mode 100644 index 08155eb9..00000000 --- a/design/renvs.png.drawio +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/design/tests/sf-no-link.R b/design/tests/sf-no-link.R new file mode 100644 index 00000000..06e4e618 --- /dev/null +++ b/design/tests/sf-no-link.R @@ -0,0 +1,229 @@ +library(dplyr) +library(ggplot2) +library(stringr) +library(tidyr) +library(cmdstanr) + +# devtools::install_git("https://github.com/stan-dev/cmdstanr") + +# devtools::document() +devtools::load_all(export_all = FALSE) + +options("jmpost.cache_dir" = file.path("local", "models")) + +#### Example 1 - Fully specified model - using the defaults for everything + + + + +## Generate Test data with known parameters +## (based on internal data, time in years) +# set.seed(7143) +# jlist <- simulate_joint_data( +# .debug = TRUE, +# n_arm = c(70, 90), +# times = seq(0, 3, by = (1/365)/2), +# lambda_cen = 1 / 9000, +# beta_cat = c( +# "A" = 0, +# "B" = -0.1, +# "C" = 0.5 +# ), +# beta_cont = 0.3, +# lm_fun = sim_lm_gsf( +# sigma = 0.01, +# mu_s = c(0.6), +# mu_g = c(0.3), +# mu_b = 60, +# omega_b = 0.2, +# omega_s = 0.2, +# omega_g = 0.2, +# a_phi = 6, +# b_phi = 8 +# ), +# os_fun = sim_os_exponential( +# lambda = 1 / (400 / 365) +# ) +# ) +set.seed(7143) +jlist <- simulate_joint_data( + .debug = TRUE, + n_arm = c(85, 100), + times = seq(0, 3, by = (1/365)/2), + lambda_cen = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = -0.1, + "C" = 0.5 + ), + beta_cont = 0.3, + lm_fun = sim_lm_sf(), + os_fun = sim_os_exponential(1 / (400 / 365)) +) + + +set.seed(333) +select_times <- sample(jlist$lm$time, 12) + + + + +## Extract data to individual datasets +dat_os <- jlist$os + + +dat_lm <- jlist$lm |> + dplyr::filter(time %in% select_times) |> + dplyr::arrange(time, pt) |> + dplyr::mutate(time = time) + +dat_lm |> + dplyr::group_by(arm) |> + dplyr::summarise(across(c("psi_b", "psi_g", "psi_s"), mean)) + + +# mean(dat_os$time) +# mean(dat_os$event) + + +pnam <- unique(dat_os$pt) |> sample(size = 10) + +ggplot(data = dat_lm |> dplyr::filter(pt %in% pnam)) + + geom_point(aes(x = time, y = sld, col =pt, group =pt)) + + geom_line(aes(x = time, y = sld, col =pt, group =pt)) + + theme_bw() + +param <- dat_lm |> + select(arm, psi_b, psi_s, psi_g) |> + gather("KEY", "VAR", -arm) + +ggplot(data = param, aes(x = VAR, group = arm, col = arm)) + + geom_density() + + theme_bw() + + facet_wrap(~KEY, scales = "free") + + +jm <- JointModel( + longitudinal = LongitudinalSteinFojo( + mu_bsld = prior_normal(log(60), 1), + mu_ks = prior_normal(log(0.6), 1), + mu_kg = prior_normal(log(0.3), 1), + omega_bsld = prior_lognormal(log(0.2), 1), + omega_ks = prior_lognormal(log(0.2), 1), + omega_kg = prior_lognormal(log(0.2), 1), + sigma = prior_lognormal(log(0.01), 1), + centred = TRUE + ), + survival = SurvivalExponential( + lambda = prior_lognormal(log(1 / (400 / 365)), 1) + ) +) + + + + + +# Create local file with stan code for debugging purposes ONLY +write_stan(jm, "local/debug.stan") + + + +## Prepare data for sampling +jdat <- DataJoint( + subject = DataSubject( + data = dat_os, + subject = "pt", + arm = "arm", + study = "study" + ), + survival = DataSurvival( + data = dat_os, + formula = Surv(time, event) ~ cov_cat + cov_cont + ), + longitudinal = DataLongitudinal( + data = dat_lm, + formula = sld ~ time, + threshold = -999 + ) +) + + +# jmpost:::initialValues(jm) + + +## Sample from JointModel + +mp <- sampleStanModel( + jm, + data = jdat, + iter_warmup = 600, + iter_sampling = 1000, + refresh = 200, + chains = 3, + parallel_chains = 3 +) + + +summary_post <- function(model, vars, exp = FALSE) { + dat <- model$summary( + vars, + mean = mean, + q01 = \(x) purrr::set_names(quantile(x, 0.01), ""), + q99 = \(x) purrr::set_names(quantile(x, 0.99), ""), + rhat = posterior::rhat, + ess_bulk = posterior::ess_bulk, + ess_tail = posterior::ess_tail + ) + if (exp) { + dat$q01 <- dat$q01 |> exp() + dat$q99 <- dat$q99 |> exp() + dat$mean <- dat$mean |> exp() + } + dat +} + +summary_post(mp@results, c("lm_sf_mu_bsld", "lm_sf_mu_kg", "lm_sf_mu_ks"), TRUE) +summary_post(mp@results, c("lm_sf_beta", "lm_sf_gamma")) + + + + +################################ +# +# General Diagnostic stuff +# +# + + + +library(bayesplot) +mcmc_trace(mp$draws("lm_gsf_mu_phi[1]")) +mcmc_trace(mp$draws("lm_gsf_mu_bsld[1]")) +mcmc_hist(mp$draws("lm_gsf_mu_phi[1]")) + +mcmc_pairs(mp$draws(), vars) + +### Extract parameters and calculate confidence intervals +draws_means <- mp$draws(format = "df") |> + gather() |> + group_by(key) |> + summarise( + mean = mean(value), + sd = sd(value), + lci = quantile(value, 0.025), + uci = quantile(value, 0.975) + ) + + +#### Get a list of all named parameters +# draws_means$key |> str_replace("\\[.*\\]", "[]") |> unique() + +draws_means |> filter(key == "log_lik[42]") + + +draws_means |> + filter(key %in% vars) |> + mutate(key = factor(key, levels = vars)) |> + arrange(key) + + diff --git a/design/tests/sf-with-link.R b/design/tests/sf-with-link.R new file mode 100644 index 00000000..c9734b5f --- /dev/null +++ b/design/tests/sf-with-link.R @@ -0,0 +1,171 @@ +library(dplyr) +library(ggplot2) +library(stringr) +library(tidyr) +library(cmdstanr) + +# devtools::install_git("https://github.com/stan-dev/cmdstanr") + +devtools::document() +devtools::load_all(export_all = TRUE) + +options("jmpost.cache_dir" = file.path("local", "models")) + + + +## Generate Test data with known parameters +jlist <- simulate_joint_data( + n_arm = c(110, 110), + times = seq(0, 4, by = (1/365)/2), + lambda_cen = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = -0.1, + "C" = 0.5 + ), + beta_cont = 0.3, + lm_fun = sim_lm_sf( + sigma = 0.005, + mu_s = c(0.2, 0.25), + mu_g = c(0.15, 0.2), + mu_b = 60, + omega_b = 0.1, + omega_s = 0.1, + omega_g = 0.1, + link_ttg = -0.2, + link_dsld = 0.2 + ), + os_fun = sim_os_weibull( + lambda = 365 * (1/400), + gamma = 1 + ) +) + + +## Extract data to individual datasets +dat_os <- jlist$os + +select_times <- c(1, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900) * (1 / 365) + +dat_lm <- jlist$lm |> + dplyr::filter(time %in% select_times) |> + dplyr::arrange(pt, time) + + + + +pnam <- unique(dat_os$pt) |> sample(size = 10) + +ggplot(data = dat_lm |> dplyr::filter(pt %in% pnam)) + + geom_point(aes(x = time, y = sld, col =pt, group =pt)) + + geom_line(aes(x = time, y = sld, col =pt, group =pt)) + + theme_bw() + + +jm <- JointModel( + longitudinal = LongitudinalSteinFojo( + + mu_bsld = prior_normal(log(60), 0.5), + mu_ks = prior_normal(log(0.2), 0.5), + mu_kg = prior_normal(log(0.2), 0.5), + + omega_bsld = prior_lognormal(log(0.1), 0.5), + omega_ks = prior_lognormal(log(0.1), 0.5), + omega_kg = prior_lognormal(log(0.1), 0.5), + + sigma = prior_lognormal(log(0.005), 0.5), + centred = TRUE + + ), + survival = SurvivalExponential( + lambda = prior_lognormal(log(365 * (1 / 400)), 0.5) + ), + link = Link( + link_ttg(prior_normal(-0.2, 0.5)), + link_dsld(prior_normal(0.2, 0.5)) + ) +) + + + + +# Create local file with stan code for debugging purposes ONLY +write_stan(jm, "local/debug.stan") + + + +## Prepare data for sampling +jdat <- DataJoint( + subject = DataSubject( + data = dat_os, + subject = "pt", + arm = "arm", + study = "study" + ), + survival = DataSurvival( + data = dat_os, + formula = Surv(time, event) ~ cov_cat + cov_cont + ), + longitudinal = DataLongitudinal( + data = dat_lm, + formula = sld ~ time, + threshold = 5 + ) +) + +## Sample from JointModel + +set.seed(1231) + +mp <- sampleStanModel( + jm, + data = jdat, + iter_sampling = 700, + iter_warmup = 1200, + chains = 2, + parallel_chains = 2 +) + + + + +summary_post <- function(model, vars, exp = FALSE) { + dat <- model$summary( + vars, + mean = mean, + q01 = \(x) purrr::set_names(quantile(x, 0.01), ""), + q99 = \(x) purrr::set_names(quantile(x, 0.99), ""), + rhat = posterior::rhat, + ess_bulk = posterior::ess_bulk, + ess_tail = posterior::ess_tail + ) + if (exp) { + dat$q01 <- dat$q01 |> exp() + dat$q99 <- dat$q99 |> exp() + dat$mean <- dat$mean |> exp() + } + dat +} + +summary_post(mp@results, c("lm_sf_mu_bsld", "lm_sf_mu_kg", "lm_sf_mu_ks"), TRUE) +summary_post(mp@results, c("link_dsld", "sm_exp_lambda")) +summary_post(mp@results, c("link_ttg", "sm_exp_lambda")) + + +################################ +# +# General Diagnostic stuff +# +# + + + +mp@results$aummary(vars) + + +library(bayesplot) +mcmc_trace(mp@results$draws("lm_gsf_mu_phi[1]")) +mcmc_trace(mp@results$draws("lm_gsf_mu_bsld[1]")) +mcmc_hist(mp@results$draws(c("link_ttg", "sm_exp_lambda"))) + +mcmc_pairs(mp@results$draws(), c("link_ttg", "sm_exp_lambda")) diff --git a/inst/stan/lm-stein-fojo/functions.stan b/inst/stan/lm-stein-fojo/functions.stan new file mode 100644 index 00000000..fe33fd04 --- /dev/null +++ b/inst/stan/lm-stein-fojo/functions.stan @@ -0,0 +1,28 @@ + + +functions { + // + // Source - lm-stein-fojo/functions.stan + // + vector sld( + vector time, + vector psi_bsld, + vector psi_ks, + vector psi_kg + ) { + int n = rows(time); + vector[n] is_post_baseline = ifelse( + is_negative(time), + zeros_vector(n), + rep_vector(1, n) + ); + vector[n] result = fmin( + 8000.0, + psi_bsld .* is_post_baseline .* ( + exp(- psi_ks .* time) + exp(psi_kg .* time) - rep_vector(1, n) + ) + ); + return result; + } +} + diff --git a/inst/stan/lm-stein-fojo/link.stan b/inst/stan/lm-stein-fojo/link.stan new file mode 100755 index 00000000..698edbe2 --- /dev/null +++ b/inst/stan/lm-stein-fojo/link.stan @@ -0,0 +1,15 @@ + + + +transformed parameters { + // + // Source - lm-stein-fojo/link.stan + // + matrix[Nind, 3] link_function_inputs; + link_function_inputs[,1] = lm_sf_psi_bsld; + link_function_inputs[,2] = lm_sf_psi_ks; + link_function_inputs[,3] = lm_sf_psi_kg; +} + + + diff --git a/inst/stan/lm-stein-fojo/link_dsld.stan b/inst/stan/lm-stein-fojo/link_dsld.stan new file mode 100644 index 00000000..593a7542 --- /dev/null +++ b/inst/stan/lm-stein-fojo/link_dsld.stan @@ -0,0 +1,34 @@ + +functions { + // + // Source - lm-stein-fojo/link_dsld.stan + // + + // Derivative of SLD + matrix link_dsld_contrib( + matrix time, + matrix link_function_inputs + ) { + int nrows = rows(link_function_inputs); + int ncols = cols(time); + vector[nrows] psi_bsld = link_function_inputs[,1]; + vector[nrows] psi_ks = link_function_inputs[,2]; + vector[nrows] psi_kg = link_function_inputs[,3]; + + // Here we assume that psi's are replicated along the rows of the time matrix. + matrix[nrows, ncols] psi_bsld_matrix = rep_matrix(psi_bsld, ncols); + matrix[nrows, ncols] psi_ks_matrix = rep_matrix(psi_ks, ncols); + matrix[nrows, ncols] psi_kg_matrix = rep_matrix(psi_kg, ncols); + + matrix[nrows, ncols] result = fmin( + 8000.0, + psi_bsld_matrix .* ( + psi_kg_matrix .* exp(psi_kg_matrix .* time) - + psi_ks_matrix .* exp(- psi_ks_matrix .* time) + ) + ); + return result; + } +} + + diff --git a/inst/stan/lm-stein-fojo/link_identity.stan b/inst/stan/lm-stein-fojo/link_identity.stan new file mode 100644 index 00000000..02c1d035 --- /dev/null +++ b/inst/stan/lm-stein-fojo/link_identity.stan @@ -0,0 +1,24 @@ + +functions { + // + // Source - lm-stein-fojo/link_identity.stan + // + matrix link_identity_contrib( + matrix time, + matrix link_function_inputs + ) { + int nrows = rows(link_function_inputs); + int ncols = cols(time); + vector[nrows] psi_bsld = link_function_inputs[,1]; + vector[nrows] psi_ks = link_function_inputs[,2]; + vector[nrows] psi_kg = link_function_inputs[,3]; + matrix[nrows, ncols] result; + for (i in 1:ncols) { + result[,i] = fmin( + sld(time[,i], psi_bsld, psi_ks, psi_kg), + 8000.0 + ); + } + return result; + } +} diff --git a/inst/stan/lm-stein-fojo/link_ttg.stan b/inst/stan/lm-stein-fojo/link_ttg.stan new file mode 100644 index 00000000..0d716a3b --- /dev/null +++ b/inst/stan/lm-stein-fojo/link_ttg.stan @@ -0,0 +1,20 @@ + +functions { + // + // Source - lm-stein-fojo/link_ttg.stan + // + matrix link_ttg_contrib( + matrix time, + matrix link_function_inputs + ) { + int nrows = rows(link_function_inputs); + int ncols = cols(time); + vector[nrows] psi_bsld = link_function_inputs[,1]; + vector[nrows] psi_ks = link_function_inputs[,2]; + vector[nrows] psi_kg = link_function_inputs[,3]; + vector[nrows] ttg_contribution = (log(psi_ks) - log(psi_kg)) ./ (psi_ks + psi_kg); + matrix[nrows, ncols] ttg_contribution_matrix = rep_matrix(ttg_contribution, ncols); + return ttg_contribution_matrix; + } +} + diff --git a/inst/stan/lm-stein-fojo/model.stan b/inst/stan/lm-stein-fojo/model.stan new file mode 100755 index 00000000..e725df7e --- /dev/null +++ b/inst/stan/lm-stein-fojo/model.stan @@ -0,0 +1,121 @@ + + + + +parameters{ + // + // Source - lm-stein-fojo/model.stan + // + + vector[n_studies] lm_sf_mu_bsld; + vector[n_arms] lm_sf_mu_ks; + vector[n_arms] lm_sf_mu_kg; + + real lm_sf_omega_bsld; + real lm_sf_omega_ks; + real lm_sf_omega_kg; + +{% if centred -%} + vector[Nind] lm_sf_psi_bsld; + vector[Nind] lm_sf_psi_ks; + vector[Nind] lm_sf_psi_kg; +{% else -%} + vector[Nind] lm_sf_eta_tilde_bsld; + vector[Nind] lm_sf_eta_tilde_ks; + vector[Nind] lm_sf_eta_tilde_kg; +{%- endif -%} + + // Standard deviation of the error term + real lm_sf_sigma; + +} + + + + + +transformed parameters{ + // + // Source - lm-stein-fojo/model.stan + // + +{% if not centred -%} + vector[Nind] lm_sf_psi_bsld = exp( + lm_sf_mu_bsld[pt_study_index] + (lm_sf_eta_tilde_bsld * lm_sf_omega_bsld) + ); + vector[Nind] lm_sf_psi_ks = exp( + lm_sf_mu_ks[pt_arm_index] + (lm_sf_eta_tilde_ks * lm_sf_omega_ks) + ); + vector[Nind] lm_sf_psi_kg = exp( + lm_sf_mu_kg[pt_arm_index] + (lm_sf_eta_tilde_kg * lm_sf_omega_kg) + ); +{%- endif -%} + + vector[Nta_total] Ypred; + + Ypred = sld( + Tobs, + lm_sf_psi_bsld[ind_index], + lm_sf_psi_ks[ind_index], + lm_sf_psi_kg[ind_index] + ); + + log_lik += csr_matrix_times_vector( + Nind, + Nta_obs_y, + w_mat_inds_obs_y, + v_mat_inds_obs_y, + u_mat_inds_obs_y, + vect_normal_log_dens( + Yobs[obs_y_index], + Ypred[obs_y_index], + Ypred[obs_y_index] * lm_sf_sigma + ) + ); + + if (Nta_cens_y > 0 ) { + log_lik += csr_matrix_times_vector( + Nind, + Nta_cens_y, + w_mat_inds_cens_y, + v_mat_inds_cens_y, + u_mat_inds_cens_y, + vect_normal_log_cum( + Ythreshold, + Ypred[cens_y_index], + Ypred[cens_y_index] * lm_sf_sigma + ) + ); + } +} + + +model { + // + // Source - lm-stein-fojo/model.stan + // +{% if centred %} + lm_sf_psi_bsld ~ lognormal(lm_sf_mu_bsld[pt_study_index], lm_sf_omega_bsld); + lm_sf_psi_ks ~ lognormal(lm_sf_mu_ks[pt_arm_index], lm_sf_omega_ks); + lm_sf_psi_kg ~ lognormal(lm_sf_mu_kg[pt_arm_index], lm_sf_omega_kg); +{%- endif -%} +} + + +generated quantities { + // + // Source - lm-stein-fojo/model.stan + // + matrix[n_pt_select_index, n_lm_time_grid] y_fit_at_time_grid; + if (n_lm_time_grid > 0) { + for (i in 1:n_pt_select_index) { + int current_pt_index = pt_select_index[i]; + y_fit_at_time_grid[i, ] = to_row_vector(sld( + lm_time_grid, + rep_vector(lm_sf_psi_bsld[current_pt_index], n_lm_time_grid), + rep_vector(lm_sf_psi_ks[current_pt_index], n_lm_time_grid), + rep_vector(lm_sf_psi_kg[current_pt_index], n_lm_time_grid) + )); + } + } +} diff --git a/man/LongitudinalSteinFojo-class.Rd b/man/LongitudinalSteinFojo-class.Rd new file mode 100644 index 00000000..073c3375 --- /dev/null +++ b/man/LongitudinalSteinFojo-class.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/LongitudinalSteinFojo.R +\docType{class} +\name{LongitudinalSteinFojo-class} +\alias{LongitudinalSteinFojo-class} +\alias{.LongitudinalSteinFojo} +\alias{LongitudinalSteinFojo} +\title{\code{LongitudinalSteinFojo}} +\usage{ +LongitudinalSteinFojo( + mu_bsld = prior_normal(log(60), 1), + mu_ks = prior_normal(log(0.5), 1), + mu_kg = prior_normal(log(0.3), 1), + omega_bsld = prior_lognormal(log(0.2), 1), + omega_ks = prior_lognormal(log(0.2), 1), + omega_kg = prior_lognormal(log(0.2), 1), + sigma = prior_lognormal(log(0.1), 1), + centred = FALSE +) +} +\arguments{ +\item{mu_bsld}{(\code{Prior})\cr for the mean baseline value \code{mu_bsld}.} + +\item{mu_ks}{(\code{Prior})\cr for the mean shrinkage rate \code{mu_ks}.} + +\item{mu_kg}{(\code{Prior})\cr for the mean growth rate \code{mu_kg}.} + +\item{omega_bsld}{(\code{Prior})\cr for the baseline value standard deviation \code{omega_bsld}.} + +\item{omega_ks}{(\code{Prior})\cr for the shrinkage rate standard deviation \code{omega_ks}.} + +\item{omega_kg}{(\code{Prior})\cr for the growth rate standard deviation \code{omega_kg}.} + +\item{sigma}{(\code{Prior})\cr for the variance of the longitudinal values \code{sigma}.} + +\item{centred}{(\code{logical})\cr whether to use the centred parameterization.} +} +\description{ +This class extends the general \code{\link{LongitudinalModel}} class for using the +Stein-Fojo model for the longitudinal outcome. +} diff --git a/man/sf_sld.Rd b/man/sf_sld.Rd new file mode 100644 index 00000000..f4cc32ef --- /dev/null +++ b/man/sf_sld.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulations_sf.R +\name{sf_sld} +\alias{sf_sld} +\alias{sf_ttg} +\alias{sf_dsld} +\title{Stein-Fojo Functionals} +\usage{ +sf_sld(time, b, s, g) + +sf_ttg(time, b, s, g) + +sf_dsld(time, b, s, g) +} +\arguments{ +\item{time}{(\code{numeric})\cr time grid.} + +\item{b}{(\code{number})\cr baseline.} + +\item{s}{(\code{number})\cr shrinkage.} + +\item{g}{(\code{number})\cr growth.} +} +\value{ +The function results. +} +\description{ +Stein-Fojo Functionals +} +\examples{ +sf_sld(1:10, 20, 0.3, 0.6) +sf_ttg(1:10, 20, 0.3, 0.6) +sf_dsld(1:10, 20, 0.3, 0.6) +} +\keyword{internal} diff --git a/man/sim_lm_sf.Rd b/man/sim_lm_sf.Rd new file mode 100644 index 00000000..c0642e69 --- /dev/null +++ b/man/sim_lm_sf.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulations_sf.R +\name{sim_lm_sf} +\alias{sim_lm_sf} +\title{Construct a Simulation Function for Longitudinal Data from Stein-Fojo Model} +\usage{ +sim_lm_sf( + sigma = 0.01, + mu_s = c(0.6, 0.4), + mu_g = c(0.25, 0.35), + mu_b = 60, + omega_b = 0.2, + omega_s = 0.2, + omega_g = 0.2, + link_dsld = 0, + link_ttg = 0, + link_identity = 0 +) +} +\arguments{ +\item{sigma}{(\code{number})\cr the variance of the longitudinal values.} + +\item{mu_s}{(\code{numeric})\cr the mean shrinkage rates for the two treatment arms.} + +\item{mu_g}{(\code{numeric})\cr the mean growth rates for the two treatment arms.} + +\item{mu_b}{(\code{numeric})\cr the mean baseline values for the two treatment arms.} + +\item{omega_b}{(\code{number})\cr the baseline value standard deviation.} + +\item{omega_s}{(\code{number})\cr the shrinkage rate standard deviation.} + +\item{omega_g}{(\code{number})\cr the growth rate standard deviation.} + +\item{link_dsld}{(\code{number})\cr the link coefficient for the derivative contribution.} + +\item{link_ttg}{(\code{number})\cr the link coefficient for the time-to-growth contribution.} + +\item{link_identity}{(\code{number})\cr the link coefficient for the SLD Identity contribution.} +} +\value{ +A function with argument \code{lm_base} that can be used to simulate +longitudinal data from the corresponding Stein-Fojo model. +} +\description{ +Construct a Simulation Function for Longitudinal Data from Stein-Fojo Model +} diff --git a/man/standard-link-methods.Rd b/man/standard-link-methods.Rd index a2c972f0..495b7103 100644 --- a/man/standard-link-methods.Rd +++ b/man/standard-link-methods.Rd @@ -1,6 +1,6 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/generics.R, R/LongitudinalGSF.R, -% R/LongitudinalRandomSlope.R +% R/LongitudinalRandomSlope.R, R/LongitudinalSteinFojo.R \name{standard-link-methods} \alias{standard-link-methods} \alias{enableLink} @@ -14,6 +14,10 @@ \alias{enableLink.LongitudinalRandomSlope} \alias{linkDSLD.LongitudinalRandomSlope} \alias{linkIdentity.LongitudinalRandomSlope} +\alias{enableLink.LongitudinalSteinFojo} +\alias{linkDSLD.LongitudinalSteinFojo} +\alias{linkTTG.LongitudinalSteinFojo} +\alias{linkIdentity.LongitudinalSteinFojo} \title{Standard Link Methods} \usage{ enableLink(object, ...) @@ -37,6 +41,14 @@ linkIdentity(object, ...) \method{linkDSLD}{LongitudinalRandomSlope}(object, ...) \method{linkIdentity}{LongitudinalRandomSlope}(object, ...) + +\method{enableLink}{LongitudinalSteinFojo}(object, ...) + +\method{linkDSLD}{LongitudinalSteinFojo}(object, ...) + +\method{linkTTG}{LongitudinalSteinFojo}(object, ...) + +\method{linkIdentity}{LongitudinalSteinFojo}(object, ...) } \arguments{ \item{object}{(\code{\link{StanModel}}) \cr A \code{\link{StanModel}} object.} diff --git a/tests/testthat/_snaps/LongitudinalSteinFojo.md b/tests/testthat/_snaps/LongitudinalSteinFojo.md new file mode 100644 index 00000000..1acd86a3 --- /dev/null +++ b/tests/testthat/_snaps/LongitudinalSteinFojo.md @@ -0,0 +1,40 @@ +# Print method for LongitudinalSteinFojo works as expected + + Code + x <- LongitudinalSteinFojo() + print(x) + Output + + Stein-Fojo Longitudinal Model with parameters: + lm_sf_mu_bsld ~ normal(mu = 4.09434, sigma = 1) + lm_sf_mu_ks ~ normal(mu = -0.69315, sigma = 1) + lm_sf_mu_kg ~ normal(mu = -1.20397, sigma = 1) + lm_sf_omega_bsld ~ lognormal(mu = -1.60944, sigma = 1) + lm_sf_omega_ks ~ lognormal(mu = -1.60944, sigma = 1) + lm_sf_omega_kg ~ lognormal(mu = -1.60944, sigma = 1) + lm_sf_sigma ~ lognormal(mu = -2.30259, sigma = 1) + lm_sf_eta_tilde_bsld ~ std_normal() + lm_sf_eta_tilde_ks ~ std_normal() + lm_sf_eta_tilde_kg ~ std_normal() + + +--- + + Code + x <- LongitudinalSteinFojo(sigma = prior_normal(0, 1), mu_kg = prior_gamma(2, 1)) + print(x) + Output + + Stein-Fojo Longitudinal Model with parameters: + lm_sf_mu_bsld ~ normal(mu = 4.09434, sigma = 1) + lm_sf_mu_ks ~ normal(mu = -0.69315, sigma = 1) + lm_sf_mu_kg ~ gamma(alpha = 2, beta = 1) + lm_sf_omega_bsld ~ lognormal(mu = -1.60944, sigma = 1) + lm_sf_omega_ks ~ lognormal(mu = -1.60944, sigma = 1) + lm_sf_omega_kg ~ lognormal(mu = -1.60944, sigma = 1) + lm_sf_sigma ~ normal(mu = 0, sigma = 1) + lm_sf_eta_tilde_bsld ~ std_normal() + lm_sf_eta_tilde_ks ~ std_normal() + lm_sf_eta_tilde_kg ~ std_normal() + + diff --git a/tests/testthat/test-LongitudinalGSF.R b/tests/testthat/test-LongitudinalGSF.R index 3d338841..5376bfe9 100644 --- a/tests/testthat/test-LongitudinalGSF.R +++ b/tests/testthat/test-LongitudinalGSF.R @@ -32,7 +32,9 @@ test_that("Centralised parameterisation compiles without issues", { expect_true(all( c("lm_gsf_psi_kg", "lm_gsf_psi_bsld") %in% names(jm@parameters) )) - compileStanModel(jm) + x <- as.StanModule(jm) + x@generated_quantities <- "" + expect_stan_syntax(as.character(x)) }) @@ -44,7 +46,9 @@ test_that("Non-Centralised parameterisation compiles without issues", { expect_false(any( c("lm_gsf_psi_kg", "lm_gsf_psi_bsld") %in% names(jm@parameters) )) - compileStanModel(jm) + x <- as.StanModule(jm) + x@generated_quantities <- "" + expect_stan_syntax(as.character(x)) }) @@ -174,7 +178,7 @@ test_that("Can recover known distributional parameters from a full GSF joint mod dat <- summary_post( mp@results, - c("lm_gsf_beta", "lm_gsf_gamma", "lm_gsf_a_phi", "lm_gsf_b_phi", "sm_exp_lambda") + c("link_dsld", "link_ttg", "lm_gsf_a_phi", "lm_gsf_b_phi", "sm_exp_lambda") ) true_values <- c(0.1, 0.2, 4, 8, 4, 8, 1 / (1 / (400 / 365))) diff --git a/tests/testthat/test-LongitudinalSteinFojo.R b/tests/testthat/test-LongitudinalSteinFojo.R new file mode 100644 index 00000000..943fa404 --- /dev/null +++ b/tests/testthat/test-LongitudinalSteinFojo.R @@ -0,0 +1,188 @@ + + +test_that("LongitudinalSteinFojo works as expected with default arguments", { + result <- expect_silent(LongitudinalSteinFojo()) + expect_s4_class(result, "LongitudinalSteinFojo") +}) + + + +test_that("Print method for LongitudinalSteinFojo works as expected", { + + expect_snapshot({ + x <- LongitudinalSteinFojo() + print(x) + }) + + expect_snapshot({ + x <- LongitudinalSteinFojo( + sigma = prior_normal(0, 1), + mu_kg = prior_gamma(2, 1) + ) + print(x) + }) +}) + + +test_that("Centralised parameterisation compiles without issues", { + jm <- JointModel(longitudinal = LongitudinalSteinFojo(centred = TRUE)) + expect_false(any( + c("lm_sf_eta_tilde_kg", "lm_sf_eta_tilde_bsld") %in% names(jm@parameters) + )) + expect_true(all( + c("lm_sf_psi_kg", "lm_sf_psi_bsld") %in% names(jm@parameters) + )) + x <- as.StanModule(jm) + x@generated_quantities <- "" + expect_stan_syntax(as.character(x)) +}) + + +test_that("Non-Centralised parameterisation compiles without issues", { + jm <- JointModel(longitudinal = LongitudinalSteinFojo(centred = FALSE)) + expect_true(all( + c("lm_sf_eta_tilde_kg", "lm_sf_eta_tilde_bsld") %in% names(jm@parameters) + )) + expect_false(any( + c("lm_sf_psi_kg", "lm_sf_psi_bsld") %in% names(jm@parameters) + )) + x <- as.StanModule(jm) + x@generated_quantities <- "" + expect_stan_syntax(as.character(x)) +}) + + +test_that("Can recover known distributional parameters from a SF joint model", { + + skip_if_not(is_full_test()) + + set.seed(9438) + ## Generate Test data with known parameters + jlist <- simulate_joint_data( + n_arm = c(120, 120), + times = seq(0, 4, by = (1 / 365) / 2), + lambda_cen = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = -0.1, + "C" = 0.5 + ), + beta_cont = 0.3, + lm_fun = sim_lm_sf( + sigma = 0.005, + mu_s = c(0.2, 0.25), + mu_g = c(0.15, 0.2), + mu_b = 60, + omega_b = 0.1, + omega_s = 0.1, + omega_g = 0.1, + link_ttg = -0.2, + link_dsld = 0.2 + ), + os_fun = sim_os_weibull( + lambda = 1, + gamma = 1 + ) + ) + + + dat_os <- jlist$os + select_times <- c(1, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900) * (1 / 365) + dat_lm <- jlist$lm |> + dplyr::filter(time %in% select_times) |> + dplyr::arrange(pt, time) + + + jm <- JointModel( + longitudinal = LongitudinalSteinFojo( + + mu_bsld = prior_normal(log(60), 0.5), + mu_ks = prior_normal(log(0.2), 0.5), + mu_kg = prior_normal(log(0.2), 0.5), + + omega_bsld = prior_lognormal(log(0.1), 0.5), + omega_ks = prior_lognormal(log(0.1), 0.5), + omega_kg = prior_lognormal(log(0.1), 0.5), + + sigma = prior_lognormal(log(0.005), 0.5), + centred = TRUE + + ), + survival = SurvivalExponential( + lambda = prior_lognormal(log(365 * (1 / 400)), 0.5) + ), + link = Link( + link_ttg(prior_normal(-0.2, 0.5)), + link_dsld(prior_normal(0.2, 0.5)) + ) + ) + + jdat <- DataJoint( + subject = DataSubject( + data = dat_os, + subject = "pt", + arm = "arm", + study = "study" + ), + survival = DataSurvival( + data = dat_os, + formula = Surv(time, event) ~ cov_cat + cov_cont + ), + longitudinal = DataLongitudinal( + data = dat_lm, + formula = sld ~ time, + threshold = 5 + ) + ) + + ## Sample from JointModel + + set.seed(2213) + + mp <- sampleStanModel( + jm, + data = jdat, + iter_sampling = 600, + iter_warmup = 1000, + chains = 2, + parallel_chains = 2 + ) + + summary_post <- function(model, vars, exp = FALSE) { + dat <- model$summary( + vars, + mean = mean, + q01 = \(x) purrr::set_names(quantile(x, 0.01), ""), + q99 = \(x) purrr::set_names(quantile(x, 0.99), ""), + rhat = posterior::rhat, + ess_bulk = posterior::ess_bulk, + ess_tail = posterior::ess_tail + ) + if (exp) { + dat$q01 <- dat$q01 |> exp() + dat$q99 <- dat$q99 |> exp() + dat$mean <- dat$mean |> exp() + } + dat + } + + dat <- summary_post( + mp@results, + c("lm_sf_mu_bsld", "lm_sf_mu_ks", "lm_sf_mu_kg"), + TRUE + ) + true_values <- c(60, 0.2, 0.25, 0.15, 0.2) + expect_true(all(dat$q01 <= true_values)) + expect_true(all(dat$q99 >= true_values)) + expect_true(all(dat$ess_bulk > 100)) + + dat <- summary_post( + mp@results, + c("link_dsld", "link_ttg", "sm_exp_lambda") + ) + + true_values <- c(0.2, -0.2, 1) + expect_true(all(dat$q01 <= true_values)) + expect_true(all(dat$q99 >= true_values)) + expect_true(all(dat$ess_bulk > 100)) +}) diff --git a/vignettes/statistical-specification.Rmd b/vignettes/statistical-specification.Rmd index 3da57fc5..d5750373 100644 --- a/vignettes/statistical-specification.Rmd +++ b/vignettes/statistical-specification.Rmd @@ -35,12 +35,146 @@ contain complete information for this package yet. # Longitudinal Model Specification +## Random-Slope Model + +$$ +y_{ij} = \mu_0 + s_i t_{ij} + \epsilon_{ij} +$$ + +where: + +- $y_{ij}$ is the tumour size for subject $i$ at timepoint $j$ +- $\mu_0$ is the intercept +- $s_i$ is the random slope for subject $i$ with $s_i \sim N(\mu_{sk}, \sigma_s)$ +- $\mu_{sk}$ is the mean for the random slope within treatment arm $k$ +- $\sigma_s$ is the variance term for the slopes +- $\epsilon_{ij}$ is the error term with $\epsilon_{ij} \sim N(0, \sigma)$ + +### Derivative of the SLD Trajectory Link + +$$ +G(t \mid \mu_0, s_i) = s_i +$$ + +Accessible via `link_dsld()` + + +### Identity Link + +$$ +\begin{align*} +G(t \mid \mu_0, s_i) = \mu_0 + s_i t +\end{align*} +$$ + +Accessible via `link_identity()` + + +## Stein-Fojo Model + + +$$\begin{align*} +y_{ij} &\sim \mathcal{N}(SLD_{ij},\ SLD_{ij}^2 \sigma^2) \\ +\\ +SLD_{ij} &=b_{i} +\left[ + e^{-s_{i}t_{ij}}+ + e^ {g_{i}t_{ij}} - 1 +\right]\\ +\\ +b_i &\sim \text{LogNormal}(\mu_{bl(i)}, \omega_b) \\ +s_i &\sim \text{LogNormal}(\mu_{sk(i)}, \omega_s) \\ +g_i &\sim \text{LogNormal}(\mu_{gk(i)}, \omega_g) \\ +\end{align*} +$$ + +Where: + +* $i$ is the subject index +* $j$ is the visit index +* $y_{ij}$ is the observed tumour measurements +* $SLD_{ij}$ is the expected sum of longest diameter for subject $i$ at time point $j$ +* $t_{ij}$ is the time since first treatment for subject $i$ at visit $j$ +* $b_i$ is the subject baseline SLD measurement +* $s_i$ is the subject kinetics shrinkage parameter +* $g_i$ is the subject kinetics tumour growth parameter +* $\phi_i$ is the subject proportion of cells affected by the treatment +* $k(i)$ is the treatment arm index for subject $i$ +* $l(i)$ is the study index for subject $i$ +* $\mu_{\theta k(i)}$ is the population mean for parameter $\theta$ in group $k(i)$ +* $\omega_{\theta}$ is the population variance for parameter $\theta$. + + +If using the non-centred parameterisation then the following alternative formulation is used: +$$ +\begin{align*} +b_i &= exp(\mu_{bl(i)} + \omega_b * \eta_{b i}) \\ +s_i &= exp(\mu_{sk(i)} + \omega_s * \eta_{s i}) \\ +g_i &= exp(\mu_{gk(i)} + \omega_g * \eta_{g i}) \\ +\\ +\eta_{b i} &\sim N(0, 1)\\ +\eta_{s i} &\sim N(0, 1) \\ +\eta_{g i} &\sim N(0, 1) \\ +\end{align*} +$$ + +Where: +* $\eta_{\theta i}$ is a random effects offset on parameter $\theta$ for subject $i$ + + + +### Derivative of the SLD Trajectory Link + + +$$ +G(t \mid b_i, s_i, g_i, ) = b_i +\left[ + g_i e^{g_i t} - + s_i e^{-s_i t} +\right] +$$ + +Accessible via `link_dsld()` + + + +### Time to Growth Link + + +$$ +G(t \mid b_i, s_i, g_i) = \frac{ + \text{log}(s_i) - \text{log}(g_i) +}{ + s_i + g_i +} +$$ + +Accessible via `link_ttg()` + + +### Identity Link + +$$ +\begin{align*} +G(t \mid b_i, s_i, g_i) &= SLD_{ij} \\ +& =b_{i} +\left[ + e^{-s_{i}t_{ij}}+ + e^ {g_{i}t_{ij}} - 1 +\right] +\end{align*} +$$ + +Accessible via `link_identity()` + + + ## Generalized Stein-Fojo (GSF) Model $$ \begin{align*} -y_{ij} &\sim \mathcal{N}(SLD_{ij}, SLD_{ij}^2 \sigma^2) \\ \\ +y_{ij} &\sim \mathcal{N}(SLD_{ij},\ SLD_{ij}^2 \sigma^2) \\ \\ SLD_{ij} &=b_{i} \left[ \phi_i e^{-s_{i}t_{ij}}+ @@ -67,10 +201,24 @@ Where: * $k(i)$ is the treatment arm index for subject $i$ * $l(i)$ is the study index for subject $i$ * $\mu_{\theta k(i)}$ is the population mean for parameter $\theta$ in group $k(i)$ -* $\omega_{\theta}$ is the population parameter for parameter $\theta$. -* $\eta_{\theta i}$ is a random effects offset on parameter $\theta$ for subject $i$ +* $\omega_{\theta}$ is the population variance for parameter $\theta$. +If using the non-centred parameterisation then the following alternative formulation is used: +$$ +\begin{align*} +b_i &= exp(\mu_{bl(i)} + \omega_b * \eta_{b i}) \\ +s_i &= exp(\mu_{sk(i)} + \omega_s * \eta_{s i}) \\ +g_i &= exp(\mu_{gk(i)} + \omega_g * \eta_{g i}) \\ +\\ +\eta_{b i} &\sim N(0, 1)\\ +\eta_{s i} &\sim N(0, 1) \\ +\eta_{g i} &\sim N(0, 1) \\ +\end{align*} +$$ + +Where: +* $\eta_{\theta i}$ is a random effects offset on parameter $\theta$ for subject $i$