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$