From bc6cb23fd7dac5f22dac3a5609fc2915a2e629ae Mon Sep 17 00:00:00 2001 From: gowerc Date: Wed, 13 Mar 2024 16:55:22 +0000 Subject: [PATCH] Finished Simulation Rework --- NAMESPACE | 7 - R/DataJoint.R | 2 +- R/SimJointData.R | 45 ++- R/SimLongitudinal.R | 11 +- R/SimLongitudinalGSF.R | 47 +-- R/SimLongitudinalRandomSlope.R | 21 +- R/SimLongitudinalSteinFojo.R | 45 +-- R/SimSurvival.R | 77 +++- R/generics.R | 41 ++- README.Rmd | 15 +- README.md | 29 +- _pkgdown.yml | 19 +- inst/stan/lm-stein-fojo/link_dsld.stan | 2 +- ...{SimJointData.Rd => SimJointData-class.Rd} | 34 +- man/SimLongitudinal-class.Rd | 25 ++ man/SimLongitudinalGSF-class.Rd | 71 +++- man/SimLongitudinalRandomSlope-class.Rd | 45 ++- man/SimLongitudinalSteinFojo-class.Rd | 57 ++- man/SimSurvival-Shared.Rd | 40 +++ man/SimSurvival-class.Rd | 72 ++++ man/SimSurvivalExponential.Rd | 39 +- man/SimSurvivalLogLogistic.Rd | 39 +- man/SimSurvivalWeibullPH.Rd | 39 +- man/as_stan_list.DataObject.Rd | 2 +- man/gsf_sld.Rd | 5 - man/hazardWindows.Rd | 16 + man/hazardWindows.SimSurvival.Rd | 2 +- man/sampleObservations.Rd | 42 +++ man/sampleSubjects.Rd | 42 +++ man/sf_sld.Rd | 5 - tests/testthat/_snaps/JointModelSamples.md | 14 +- .../testthat/_snaps/LongitudinalQuantiles.md | 4 +- tests/testthat/helper-example_data.R | 24 +- tests/testthat/helper-setup.R | 6 + tests/testthat/test-JointModelSamples.R | 5 +- tests/testthat/test-LongitudinalGSF.R | 72 ++-- tests/testthat/test-LongitudinalQuantiles.R | 17 +- tests/testthat/test-LongitudinalRandomSlope.R | 44 +-- tests/testthat/test-LongitudinalSteinFojo.R | 61 ++-- tests/testthat/test-SimGroup.R | 21 ++ tests/testthat/test-SimJointData.R | 335 ++++-------------- tests/testthat/test-SimLongitudinalGSF.R | 62 ++++ .../test-SimLongitudinalRandomSlope.R | 89 +++++ .../testthat/test-SimLongitudinalSteinFojo.R | 59 +++ tests/testthat/test-SimSurvival.R | 83 +++++ tests/testthat/test-SurvivalExponential.R | 20 +- tests/testthat/test-SurvivalLoglogistic.R | 22 +- tests/testthat/test-SurvivalQuantities.R | 16 +- tests/testthat/test-SurvivalWeibullPH.R | 20 +- tests/testthat/test-brierScore.R | 2 +- tests/testthat/test-extract_quantities.R | 10 +- tests/testthat/test-misc_models.R | 2 +- tests/testthat/test-model_multi_chain.R | 32 +- 53 files changed, 1320 insertions(+), 636 deletions(-) rename man/{SimJointData.Rd => SimJointData-class.Rd} (57%) create mode 100644 man/SimLongitudinal-class.Rd create mode 100644 man/SimSurvival-Shared.Rd create mode 100644 man/SimSurvival-class.Rd create mode 100644 man/hazardWindows.Rd create mode 100644 man/sampleObservations.Rd create mode 100644 man/sampleSubjects.Rd create mode 100644 tests/testthat/test-SimGroup.R create mode 100644 tests/testthat/test-SimLongitudinalGSF.R create mode 100644 tests/testthat/test-SimLongitudinalRandomSlope.R create mode 100644 tests/testthat/test-SimLongitudinalSteinFojo.R create mode 100644 tests/testthat/test-SimSurvival.R diff --git a/NAMESPACE b/NAMESPACE index cc4fd376..02a78f38 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -100,7 +100,6 @@ S3method(summary,Quantities) S3method(summary,SurvivalQuantities) S3method(write_stan,JointModel) export(.JointModelSamples) -export(.SimLongitudinalSteinFojo) export(DataJoint) export(DataLongitudinal) export(DataSubject) @@ -141,9 +140,6 @@ export(brierScore) export(compileStanModel) export(enableLink) export(generateQuantities) -export(gsf_dsld) -export(gsf_sld) -export(gsf_ttg) export(initialValues) export(linkDSLD) export(linkIdentity) @@ -167,9 +163,6 @@ export(prior_uniform) export(sampleObservations) export(sampleStanModel) export(sampleSubjects) -export(sf_dsld) -export(sf_sld) -export(sf_ttg) export(show) export(write_stan) exportClasses(DataJoint) diff --git a/R/DataJoint.R b/R/DataJoint.R index 198a9bc2..640c754c 100755 --- a/R/DataJoint.R +++ b/R/DataJoint.R @@ -132,7 +132,7 @@ setValidity( #' #' @description #' Coerces a data object into a `list` of data components required -#' for fitting a [`JointModel`]. See the vignette (TODO) for more details. +#' for fitting a [`JointModel`]. See the "Extending jmpost" vignette for more details. #' #' @name as_stan_list.DataObject #' @family as_stan_list diff --git a/R/SimJointData.R b/R/SimJointData.R index 02904a03..b99aae23 100755 --- a/R/SimJointData.R +++ b/R/SimJointData.R @@ -1,29 +1,12 @@ -# TODO - DOCS - -#' @exportClass SimJointData -.SimJointData <- setClass( - "SimJointData", - slots = list( - longitudinal = "data.frame", - survival = "data.frame" - ) -) - - - - #' Simulating Joint Longitudinal and Time-to-Event Data #' #' @param design (`list`)\cr a list of [`SimGroup`] objects. See details. -#' @param times (`numeric`)\cr time grid, e.g. specifying the days after randomization. -#' @param lambda_cen (`number`)\cr rate of the exponential censoring distribution. -#' @param beta_cont (`number`)\cr coefficient for the continuous covariate. -#' @param beta_cat (`numeric`)\cr coefficients for the categorical covariate levels. -#' @param longitudinal (`function`)\cr function of `lm_base` generating the longitudinal model outcomes. -#' @param survival (`function`)\cr function of `lm_base` generating the survival model outcomes. +#' @param longitudinal ([`SimLongitudinal`])\cr object specifying how to simulate the longitudinal data +#' @param survival ([`SimSurvival`])\cr object specifying how to simulate the survival data #' @param .silent (`flag`)\cr whether to suppress info messages -#' @param .debug (`flag`)\cr whether to enter debug mode such that the function -#' would only return a subset of columns. +#' +#' @slot longitudinal (`data.frame`)\cr the simulated longitudinal data. +#' @slot survival (`data.frame`)\cr the simulated survival data. #' #' @details #' @@ -38,7 +21,20 @@ #' ) #' ``` #' -#' @returns List with simulated `lm` (longitudinal) and `os` (survival) data sets. +#' @name SimJointData-class +#' @exportClass SimJointData +.SimJointData <- setClass( + "SimJointData", + slots = list( + longitudinal = "data.frame", + survival = "data.frame" + ) +) + + + + +#' @rdname SimJointData-class #' @export SimJointData <- function( design = list( @@ -47,8 +43,7 @@ SimJointData <- function( ), longitudinal, survival, - .silent = FALSE, - .debug = FALSE + .silent = FALSE ) { assert( diff --git a/R/SimLongitudinal.R b/R/SimLongitudinal.R index 1e9b7fc0..012a08ab 100644 --- a/R/SimLongitudinal.R +++ b/R/SimLongitudinal.R @@ -1,5 +1,13 @@ -# TODO - docs +#' Abstract Simulation Class for Longitudinal Data +#' +#' @param times (`numeric`) the times to generate observations at. +#' +#' @description +#' This class exists to be extended by other classes that simulate longitudinal data. +#' It is not intended to be used directly. +#' @name SimLongitudinal-class +#' @family SimLongitudinal #' @exportClass SimLongitudinal .SimLongitudinal <- setClass( "SimLongitudinal", @@ -8,6 +16,7 @@ ) ) +#' @rdname SimLongitudinal-class #' @export SimLongitudinal <- function(times = seq(0, 100, 50)) { .SimLongitudinal(times = times) diff --git a/R/SimLongitudinalGSF.R b/R/SimLongitudinalGSF.R index 6104b5b7..5d8eec89 100644 --- a/R/SimLongitudinalGSF.R +++ b/R/SimLongitudinalGSF.R @@ -1,9 +1,7 @@ - -# TODO - Docs - -#' Construct a Simulation Function for Longitudinal Data from GSF Model +#' Simulate Longitudinal Data from a GSF Model #' +#' @param times (`numeric`)\cr the times to generate observations at. #' @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. @@ -17,9 +15,20 @@ #' @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 GSF model. -#' +#' @slot sigma (`numeric`)\cr See arguments. +#' @slot mu_s (`numeric`)\cr See arguments. +#' @slot mu_g (`numeric`)\cr See arguments. +#' @slot mu_b (`numeric`)\cr See arguments. +#' @slot omega_b (`numeric`)\cr See arguments. +#' @slot omega_s (`numeric`)\cr See arguments. +#' @slot omega_g (`numeric`)\cr See arguments. +#' @slot a_phi (`numeric`)\cr See arguments. +#' @slot b_phi (`numeric`)\cr See arguments. +#' @slot link_dsld (`numeric`)\cr See arguments. +#' @slot link_ttg (`numeric`)\cr See arguments. +#' @slot link_identity (`numeric`)\cr See arguments. +#' @family SimLongitudinal +#' @name SimLongitudinalGSF-class #' @exportClass SimLongitudinalGSF .SimLongitudinalGSF <- setClass( "SimLongitudinalGSF", @@ -40,6 +49,7 @@ ) ) +#' @rdname SimLongitudinalGSF-class #' @export SimLongitudinalGSF <- function( times = c(-100, -50, 0, 50, 100, 150, 250, 350, 450, 550) / 365, @@ -100,6 +110,7 @@ setValidity( ) +#' @rdname sampleObservations #' @export sampleObservations.SimLongitudinalGSF <- function(object, times_df) { times_df |> @@ -116,16 +127,17 @@ sampleObservations.SimLongitudinalGSF <- function(object, times_df) { } +#' @rdname sampleSubjects #' @export sampleSubjects.SimLongitudinalGSF <- function(object, subjects_df) { assert_that( - length(subjects_df$study) == length(object@mu_b), - length(subjects_df$arm) == length(object@mu_s), is.factor(subjects_df$study), - is.factor(subjects_df$arm) + is.factor(subjects_df$arm), + length(levels(subjects_df$study)) == length(object@mu_b), + length(levels(subjects_df$arm)) == length(object@mu_s) ) - subjects_df |> + res <- subjects_df |> dplyr::distinct(.data$pt, .data$arm, .data$study) |> dplyr::mutate(study_idx = as.numeric(.data$study)) |> dplyr::mutate(arm_idx = as.numeric(.data$arm)) |> @@ -133,6 +145,8 @@ sampleSubjects.SimLongitudinalGSF <- function(object, subjects_df) { dplyr::mutate(psi_s = stats::rlnorm(dplyr::n(), log(object@mu_s[.data$arm_idx]), object@omega_s)) |> dplyr::mutate(psi_g = stats::rlnorm(dplyr::n(), log(object@mu_g[.data$arm_idx]), object@omega_g)) |> dplyr::mutate(psi_phi = stats::rbeta(dplyr::n(), object@a_phi[.data$arm_idx], object@b_phi[.data$arm_idx])) + + res[, c("pt", "arm", "study", "psi_b", "psi_s", "psi_g", "psi_phi")] } @@ -149,11 +163,8 @@ sampleSubjects.SimLongitudinalGSF <- function(object, subjects_df) { #' @param phi (`number`)\cr shrinkage proportion. #' #' @returns The function results. -#' @export -#' @keywords internal #' -#' @examples -#' gsf_sld(1:10, 20, 0.3, 0.6, 0.2) +#' @keywords internal gsf_sld <- function(time, b, s, g, phi) { phi <- dplyr::if_else(time >= 0, phi, 0) b * (phi * exp(-s * time) + (1 - phi) * exp(g * time)) @@ -161,9 +172,6 @@ gsf_sld <- function(time, b, s, g, phi) { #' @rdname gsf_sld -#' @export -#' @examples -#' gsf_ttg(1:10, 20, 0.3, 0.6, 0.2) gsf_ttg <- function(time, b, s, g, phi) { t1 <- (log(s * phi / (g * (1 - phi))) / (g + s)) t1[t1 <= 0] <- 0 @@ -172,9 +180,6 @@ gsf_ttg <- function(time, b, s, g, phi) { #' @rdname gsf_sld -#' @export -#' @examples -#' gsf_dsld(1:10, 20, 0.3, 0.6, 0.2) gsf_dsld <- function(time, b, s, g, phi) { phi <- dplyr::if_else(time >= 0, phi, 0) t1 <- (1 - phi) * g * exp(g * time) diff --git a/R/SimLongitudinalRandomSlope.R b/R/SimLongitudinalRandomSlope.R index b0114e31..6483dc1c 100644 --- a/R/SimLongitudinalRandomSlope.R +++ b/R/SimLongitudinalRandomSlope.R @@ -1,9 +1,7 @@ - -# TODO - Docs - -#' Construct a Simulation Function for Longitudinal Data from Random Slope Model +#' Simulate Longitudinal Data from a Random Slope Model #' +#' @param times (`numeric`)\cr the times to generate observations at. #' @param intercept (`number`)\cr the mean baseline value for each study. #' @param slope_mu (`numeric`)\cr the population slope for each treatment arm. #' @param slope_sigma (`number`)\cr the random slope standard deviation. @@ -11,8 +9,15 @@ #' @param link_dsld (`number`)\cr the link coefficient for the DSLD contribution. #' @param link_identity (`number`)\cr the link coefficient for the identity contribution. #' -#' @returns A function with argument `lm_base` that can be used to simulate -#' longitudinal data from the corresponding random slope model. +#' @slot intercept (`numeric`)\cr See arguments. +#' @slot slope_mu (`numeric`)\cr See arguments. +#' @slot slope_sigma (`numeric`)\cr See arguments. +#' @slot sigma (`numeric`)\cr See arguments. +#' @slot link_dsld (`numeric`)\cr See arguments. +#' @slot link_identity (`numeric`)\cr See arguments. +#' +#' @family SimLongitudinal +#' @name SimLongitudinalRandomSlope-class #' @exportClass SimLongitudinalRandomSlope .SimLongitudinalRandomSlope <- setClass( "SimLongitudinalRandomSlope", @@ -27,6 +32,7 @@ ) ) +#' @rdname SimLongitudinalRandomSlope-class #' @export SimLongitudinalRandomSlope <- function( times = c(-100, -50, 0, 50, 100, 150, 250, 350, 450, 550), @@ -48,6 +54,7 @@ SimLongitudinalRandomSlope <- function( ) } +#' @rdname sampleObservations #' @export sampleObservations.SimLongitudinalRandomSlope <- function(object, times_df) { times_df |> @@ -61,7 +68,7 @@ sampleObservations.SimLongitudinalRandomSlope <- function(object, times_df) { ) } - +#' @rdname sampleSubjects #' @export sampleSubjects.SimLongitudinalRandomSlope <- function(object, subjects_df) { assert_that( diff --git a/R/SimLongitudinalSteinFojo.R b/R/SimLongitudinalSteinFojo.R index 8f3b1015..07383e6c 100644 --- a/R/SimLongitudinalSteinFojo.R +++ b/R/SimLongitudinalSteinFojo.R @@ -1,9 +1,7 @@ - -# TODO - Docs - -#' Construct a Simulation Function for Longitudinal Data from Stein-Fojo Model +#' Simulate Longitudinal Data from a Stein-Fojo Model #' +#' @param times (`numeric`)\cr the times to generate observations at. #' @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. @@ -15,10 +13,20 @@ #' @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. +#' @slot sigma (`numeric`)\cr See arguments. +#' @slot mu_s (`numeric`)\cr See arguments. +#' @slot mu_g (`numeric`)\cr See arguments. +#' @slot mu_b (`numeric`)\cr See arguments. +#' @slot omega_b (`numeric`)\cr See arguments. +#' @slot omega_s (`numeric`)\cr See arguments. +#' @slot omega_g (`numeric`)\cr See arguments. +#' @slot link_dsld (`numeric`)\cr See arguments. +#' @slot link_ttg (`numeric`)\cr See arguments. +#' @slot link_identity (`numeric`)\cr See arguments. #' -#' @exportclass SimLongitudinalSteinFojo +#' @family SimLongitudinal +#' @name SimLongitudinalSteinFojo-class +#' @exportClass SimLongitudinalSteinFojo .SimLongitudinalSteinFojo <- setClass( "SimLongitudinalSteinFojo", contains = "SimLongitudinal", @@ -36,6 +44,7 @@ ) ) +#' @rdname SimLongitudinalSteinFojo-class #' @export SimLongitudinalSteinFojo <- function( times = c(-100, -50, 0, 50, 100, 150, 250, 350, 450, 550) / 365, @@ -89,7 +98,7 @@ setValidity( } ) - +#' @rdname sampleObservations #' @export sampleObservations.SimLongitudinalSteinFojo <- function(object, times_df) { times_df |> @@ -106,22 +115,25 @@ sampleObservations.SimLongitudinalSteinFojo <- function(object, times_df) { } +#' @rdname sampleSubjects #' @export sampleSubjects.SimLongitudinalSteinFojo <- function(object, subjects_df) { assert_that( - length(subjects_df$study) == length(object@mu_b), - length(subjects_df$arm) == length(object@mu_s), is.factor(subjects_df$study), - is.factor(subjects_df$arm) + is.factor(subjects_df$arm), + length(levels(subjects_df$study)) == length(object@mu_b), + length(levels(subjects_df$arm)) == length(object@mu_s) ) - subjects_df |> + res <- subjects_df |> dplyr::distinct(.data$pt, .data$arm, .data$study) |> dplyr::mutate(study_idx = as.numeric(.data$study)) |> dplyr::mutate(arm_idx = as.numeric(.data$arm)) |> dplyr::mutate(psi_b = stats::rlnorm(dplyr::n(), log(object@mu_b[.data$study_idx]), object@omega_b)) |> dplyr::mutate(psi_s = stats::rlnorm(dplyr::n(), log(object@mu_s[.data$arm_idx]), object@omega_s)) |> dplyr::mutate(psi_g = stats::rlnorm(dplyr::n(), log(object@mu_g[.data$arm_idx]), object@omega_g)) + + res[, c("pt", "arm", "study", "psi_b", "psi_s", "psi_g")] } @@ -134,9 +146,6 @@ sampleSubjects.SimLongitudinalSteinFojo <- function(object, subjects_df) { #' #' @returns The function results. #' @keywords internal -#' @export -#' @examples -#' sf_sld(1:10, 20, 0.3, 0.6) sf_sld <- function(time, b, s, g) { s <- dplyr::if_else(time >= 0, s, 0) b * (exp(-s * time) + exp(g * time) - 1) @@ -144,9 +153,6 @@ sf_sld <- function(time, b, s, g) { #' @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 @@ -155,9 +161,6 @@ sf_ttg <- function(time, b, s, g) { #' @rdname sf_sld -#' @export -#' @examples -#' sf_dsld(1:10, 20, 0.3, 0.6) sf_dsld <- function(time, b, s, g) { s <- dplyr::if_else(time >= 0, s, 0) t1 <- g * exp(g * time) diff --git a/R/SimSurvival.R b/R/SimSurvival.R index bc4eae02..eeab06ef 100644 --- a/R/SimSurvival.R +++ b/R/SimSurvival.R @@ -1,5 +1,50 @@ -# TODO - docs + +#' `SimSurvival` Function Arguments +#' +#' The documentation lists all the conventional arguments for [`SimSurvival`] +#' constructors. +#' +#' @param time_max (`number`)\cr the maximum time to simulate to. +#' @param time_step (`number`)\cr the time interval between evaluating the log-hazard function. +#' @param lambda_censor (`number`)\cr the censoring rate. +#' @param beta_cont (`number`)\cr the continuous covariate coefficient. +#' @param beta_cat (`numeric`)\cr the categorical covariate coefficients. +#' @param loghazard (`function`)\cr the log hazard function. +#' @param ... Not Used. +#' +#' @section Hazard Evaluation: +#' +#' Event times are simulated by sampling a cumulative hazard limit from a \eqn{U(0, 1)} distribution +#' for +#' each subject and then counting how much hazard they've been exposed to by evaluating the +#' log-hazard function at a set interval. The `time_max` argument sets the upper bound for the +#' number of time points to evaluate the log-hazard function at with subjects who have not had an +#' event being censored at `time_max`. The `time_step` argument sets the interval at which to +#' evaluate the log-hazard function. Setting smaller values for `time_step` will increase the +#' precision of the simulation at the cost of increased computation time. Likewise, setting large +#' values for `time_max` will minimize the number of censored subjects at the cost of +#' incread computation time. +#' +#' @name SimSurvival-Shared +#' @keywords internal +NULL + + +#' Abstract Simulation Class for Survival Data +#' +#' @inheritParams SimSurvival-Shared +#' @inheritSection SimSurvival-Shared Hazard Evaluation +#' +#' @slot time_max (`numeric`)\cr See arguments. +#' @slot time_step (`numeric`)\cr See arguments. +#' @slot lambda_censor (`numeric`)\cr See arguments. +#' @slot beta_cont (`numeric`)\cr See arguments. +#' @slot beta_cat (`numeric`)\cr See arguments. +#' @slot loghazard (`function`)\cr See arguments. +#' +#' @family SimSurvival #' @exportClass SimSurvival +#' @name SimSurvival-class .SimSurvival <- setClass( "SimSurvival", slots = c( @@ -12,6 +57,7 @@ ) ) +#' @rdname SimSurvival-class #' @export SimSurvival <- function( time_max = 2000, @@ -33,7 +79,7 @@ SimSurvival <- function( #' Construct Time Intervals #' -#' @param x (`numeric`)\cr grid of time points. +#' @param object (`SimSurvival`)\cr the survival simulation object to create evaluation points for. #' #' @return A `tibble` with `lower`, `upper`, `time`, `eval` and `width`. #' @keywords internal @@ -51,6 +97,7 @@ hazardWindows.SimSurvival <- function(object) { ) } +#' @rdname sampleSubjects #' @export sampleSubjects.SimSurvival <- function(object, subjects_df) { subjects_df |> @@ -65,6 +112,8 @@ sampleSubjects.SimSurvival <- function(object, subjects_df) { dplyr::mutate(time_cen = stats::rexp(dplyr::n(), object@lambda_censor)) } + +#' @rdname sampleObservations #' @export sampleObservations.SimSurvival <- function(object, times_df) { @@ -117,12 +166,16 @@ sampleObservations.SimSurvival <- function(object, times_df) { } -#' Construct a Log Hazard Function for the Weibull Model +#' Simulate Survival Data from a Weibull Proportional Hazard Model #' #' @param lambda (`number`)\cr the scale parameter. #' @param gamma (`number`)\cr the shape parameter. #' -#' @returns A function of `time` returning the log hazard. +#' @inheritParams SimSurvival-Shared +#' @inheritSection SimSurvival-Shared Hazard Evaluation +#' +#' @family SimSurvival +#' #' @export SimSurvivalWeibullPH <- function( lambda, @@ -146,12 +199,15 @@ SimSurvivalWeibullPH <- function( } -#' Construct a Log Hazard Function for the Log-Logistic Model +#' Simulate Survival Data from a Log-Logistic Proportional Hazard Model #' #' @param a (`number`)\cr the scale parameter. #' @param b (`number`)\cr the shape parameter. #' -#' @returns A function of `time` returning the log hazard. +#' @inheritParams SimSurvival-Shared +#' @inheritSection SimSurvival-Shared Hazard Evaluation +#' +#' @family SimSurvival #' @export SimSurvivalLogLogistic <- function( a, @@ -178,11 +234,15 @@ SimSurvivalLogLogistic <- function( -#' Construct a Log Hazard Function for the Exponential Model +#' Simulate Survival Data from a Exponential Proportional Hazard Model #' #' @param lambda (`number`)\cr the rate parameter. #' -#' @returns A function of `time` returning the log hazard. +#' @inheritParams SimSurvival-Shared +#' @inheritSection SimSurvival-Shared Hazard Evaluation +#' +#' @family SimSurvival +#' #' @export SimSurvivalExponential <- function( lambda, @@ -203,4 +263,3 @@ SimSurvivalExponential <- function( } ) } - diff --git a/R/generics.R b/R/generics.R index 786141ca..03701e50 100755 --- a/R/generics.R +++ b/R/generics.R @@ -329,17 +329,54 @@ linkIdentity <- function(object, ...) { } -# TODO - Docs +#' Generate Simulated Observations +#' +#' @param object (`SimLongitudinal` or `SimSurvival`) \cr object to generate observations from. +#' @param times_df (`data.frame`) \cr the times at which to generate observations. See details. +#' +#' @details +#' The `times_df` argument should be a `data.frame` as created by `sampleSubjects` but +#' replicated for each time point at which observations are to be generated. That is if you want +#' to generate observations for times `c(0, 1, 2, 3)` then `times_df` should be created as: +#' ``` +#' subject_dat <- sampleSubjects(object, ...) +#' times_df <- tidyr::expand_grid( +#' subject_dat, +#' time = c(0, 1, 2, 3) +#' ) +#' ``` +#' #' @export sampleObservations <- function(object, times_df) { UseMethod("sampleObservations") } + +#' Generate Simulated Subjects +#' +#' @param object (`SimLongitudinal` or `SimSurvival`) \cr object to generate subjects from. +#' @param subjects_df (`data.frame`) \cr the subjects to generate observations for. See details. +#' +#' @details +#' The `subjects_df` argument should be a `data.frame` with 1 row per desired subject to create +#' with the following columns: +#' - `study` (`factor`) the study identifier. +#' - `arm` (`factor`) the treatment arm identifier. +#' - `pt` (`character`) the subject identifier. +#' +#' This method takes care of generating all the individual subject data required for the +#' [`sampleObservations`] method to generate the observations. #' @export sampleSubjects <- function(object, subjects_df) { UseMethod("sampleSubjects") } -hazardWindows <- function(object) { + +#' Generate time windows for evaluating a hazard function +#' +#' @param object (`SurvivalModel`) \cr object to generate time windows for. +#' @param ... Not used. +#' +hazardWindows <- function(object, ...) { UseMethod("hazardWindows") } diff --git a/README.Rmd b/README.Rmd index 5b29647f..4d300569 100644 --- a/README.Rmd +++ b/README.Rmd @@ -68,8 +68,17 @@ of the input data and use `DataJoint()` to bring it into the right format. library(jmpost) set.seed(321) sim_data <- SimJointData( - longitudinal = SimLongitudinalRandomSlope(), - survival = SimSurvivalExponential(lambda = 1 / 100) + design = list( + SimGroup(50, "Arm-A", "Study-X"), + SimGroup(50, "Arm-B", "Study-X") + ), + longitudinal = SimLongitudinalRandomSlope( + times = c(1, 50, 100, 150, 200, 250, 300), + ), + survival = SimSurvivalWeibullPH( + lambda = 1 / 300, + gamma = 0.97 + ) ) joint_data <- DataJoint( @@ -84,7 +93,7 @@ joint_data <- DataJoint( formula = Surv(time, event) ~ cov_cat + cov_cont ), longitudinal = DataLongitudinal( - data = sim_dat@longitudinal, + data = sim_data@longitudinal, formula = sld ~ time, threshold = 5 ) diff --git a/README.md b/README.md index 803fcc01..5ccbcdb2 100755 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ More specifically, the model implemented in this package utilizes a modelling framework described previously **\[1-3\]** to link overall survival to tumour size data in oncology clinical trials. -**\[1\]** [Tardivon *et al.* Association between tumour size kinetics and -survival in patients with urothelial carcinoma treated with +**\[1\]** [Tardivon *et al.* Association between tumour size kinetics +and survival in patients with urothelial carcinoma treated with atezolizumab: Implications for patient follow-up. *Clin Pharm Ther*, 2019](https://doi.org/10.1002/cpt.1450). **\[2\]** [Kerioui *et al.* Bayesian inference using Hamiltonian @@ -80,27 +80,34 @@ library(jmpost) #> heightDetails.titleGrob ggplot2 #> widthDetails.titleGrob ggplot2 set.seed(321) -sim_data <- simulate_joint_data( - lm_fun = sim_lm_random_slope(), - os_fun = sim_os_exponential(lambda = 1 / 100) +sim_data <- SimJointData( + design = list( + SimGroup(50, "Arm-A", "Study-X"), + SimGroup(50, "Arm-B", "Study-X") + ), + longitudinal = SimLongitudinalRandomSlope( + times = c(1, 50, 100, 150, 200, 250, 300), + ), + survival = SimSurvivalWeibullPH( + lambda = 1 / 300, + gamma = 0.97 + ) ) -os_data <- sim_data$os -long_data <- sim_data$lm |> - dplyr::arrange(time, pt) +#> INFO: 1 patients did not die before max(times) joint_data <- DataJoint( subject = DataSubject( - data = os_data, + data = sim_data@survival, subject = "pt", arm = "arm", study = "study" ), survival = DataSurvival( - data = os_data, + data = sim_data@survival, formula = Surv(time, event) ~ cov_cat + cov_cont ), longitudinal = DataLongitudinal( - data = long_data, + data = sim_data@longitudinal, formula = sld ~ time, threshold = 5 ) diff --git a/_pkgdown.yml b/_pkgdown.yml index 8e0c626c..e4806991 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -38,13 +38,15 @@ reference: - title: Data Simulation contents: - - simulate_joint_data - - sim_lm_gsf - - sim_lm_sf - - sim_lm_random_slope - - sim_os_exponential - - sim_os_loglogistic - - sim_os_weibull + - SimJointData + - SimLongitudinal + - SimLongitudinalGSF + - SimLongitudinalSteinFojo + - SimLongitudinalRandomSlope + - SimSurvival + - SimSurvivalLogLogistic + - SimSurvivalExponential + - SimSurvivalWeibullPH - SimGroup - title: Prior Distributions contents: @@ -158,6 +160,9 @@ reference: - length.Link - subset.DataJoint - extractVariableNames.DataLongitudinal + - hazardWindows + - sampleObservations + - sampleSubjects - title: Miscellaneous contents: diff --git a/inst/stan/lm-stein-fojo/link_dsld.stan b/inst/stan/lm-stein-fojo/link_dsld.stan index 9aa234bf..5dd260db 100644 --- a/inst/stan/lm-stein-fojo/link_dsld.stan +++ b/inst/stan/lm-stein-fojo/link_dsld.stan @@ -20,7 +20,7 @@ functions { matrix[nrows, ncols] psi_ks_matrix = rep_matrix(psi_ks, ncols); matrix[nrows, ncols] psi_kg_matrix = rep_matrix(psi_kg, ncols); - vector[n] psi_ks_matrix_mod = if_lt0_else(time, psi_ks_matrix, 0); + matrix[nrows, ncols] psi_ks_matrix_mod = if_lt0_else(time, psi_ks_matrix, 0); matrix[nrows, ncols] result = fmin( 8000.0, diff --git a/man/SimJointData.Rd b/man/SimJointData-class.Rd similarity index 57% rename from man/SimJointData.Rd rename to man/SimJointData-class.Rd index ac42ea00..401061d4 100644 --- a/man/SimJointData.Rd +++ b/man/SimJointData-class.Rd @@ -1,6 +1,9 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/SimJointData.R -\name{SimJointData} +\docType{class} +\name{SimJointData-class} +\alias{SimJointData-class} +\alias{.SimJointData} \alias{SimJointData} \title{Simulating Joint Longitudinal and Time-to-Event Data} \usage{ @@ -9,32 +12,17 @@ SimJointData( study = "Study-1", arm = "Arm-B")), longitudinal, survival, - .silent = FALSE, - .debug = FALSE + .silent = FALSE ) } \arguments{ \item{design}{(\code{list})\cr a list of \code{\link{SimGroup}} objects. See details.} -\item{longitudinal}{(\code{function})\cr function of \code{lm_base} generating the longitudinal model outcomes.} +\item{longitudinal}{(\code{\link{SimLongitudinal}})\cr object specifying how to simulate the longitudinal data} -\item{survival}{(\code{function})\cr function of \code{lm_base} generating the survival model outcomes.} +\item{survival}{(\code{\link{SimSurvival}})\cr object specifying how to simulate the survival data} \item{.silent}{(\code{flag})\cr whether to suppress info messages} - -\item{.debug}{(\code{flag})\cr whether to enter debug mode such that the function -would only return a subset of columns.} - -\item{times}{(\code{numeric})\cr time grid, e.g. specifying the days after randomization.} - -\item{lambda_cen}{(\code{number})\cr rate of the exponential censoring distribution.} - -\item{beta_cont}{(\code{number})\cr coefficient for the continuous covariate.} - -\item{beta_cat}{(\code{numeric})\cr coefficients for the categorical covariate levels.} -} -\value{ -List with simulated \code{lm} (longitudinal) and \code{os} (survival) data sets. } \description{ Simulating Joint Longitudinal and Time-to-Event Data @@ -51,3 +39,11 @@ list of \code{\link{SimGroup}} objects e.g. ) }\if{html}{\out{}} } +\section{Slots}{ + +\describe{ +\item{\code{longitudinal}}{(\code{data.frame})\cr the simulated longitudinal data.} + +\item{\code{survival}}{(\code{data.frame})\cr the simulated survival data.} +}} + diff --git a/man/SimLongitudinal-class.Rd b/man/SimLongitudinal-class.Rd new file mode 100644 index 00000000..09662d16 --- /dev/null +++ b/man/SimLongitudinal-class.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/SimLongitudinal.R +\docType{class} +\name{SimLongitudinal-class} +\alias{SimLongitudinal-class} +\alias{.SimLongitudinal} +\alias{SimLongitudinal} +\title{Abstract Simulation Class for Longitudinal Data} +\usage{ +SimLongitudinal(times = seq(0, 100, 50)) +} +\arguments{ +\item{times}{(\code{numeric}) the times to generate observations at.} +} +\description{ +This class exists to be extended by other classes that simulate longitudinal data. +It is not intended to be used directly. +} +\seealso{ +Other SimLongitudinal: +\code{\link{SimLongitudinalGSF-class}}, +\code{\link{SimLongitudinalRandomSlope-class}}, +\code{\link{SimLongitudinalSteinFojo-class}} +} +\concept{SimLongitudinal} diff --git a/man/SimLongitudinalGSF-class.Rd b/man/SimLongitudinalGSF-class.Rd index e0b10798..449ff1f0 100644 --- a/man/SimLongitudinalGSF-class.Rd +++ b/man/SimLongitudinalGSF-class.Rd @@ -4,8 +4,28 @@ \name{SimLongitudinalGSF-class} \alias{SimLongitudinalGSF-class} \alias{.SimLongitudinalGSF} -\title{Construct a Simulation Function for Longitudinal Data from GSF Model} +\alias{SimLongitudinalGSF} +\title{Simulate Longitudinal Data from a GSF Model} +\usage{ +SimLongitudinalGSF( + times = c(-100, -50, 0, 50, 100, 150, 250, 350, 450, 550)/365, + sigma = 0.01, + mu_s = c(0.6, 0.4), + mu_g = c(0.25, 0.35), + mu_b = 60, + a_phi = c(4, 6), + b_phi = c(4, 6), + omega_b = 0.2, + omega_s = 0.2, + omega_g = 0.2, + link_dsld = 0, + link_ttg = 0, + link_identity = 0 +) +} \arguments{ +\item{times}{(\code{numeric})\cr the times to generate observations at.} + \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.} @@ -14,26 +34,57 @@ \item{mu_b}{(\code{numeric})\cr the mean baseline values for the two treatment arms.} +\item{a_phi}{(\code{number})\cr the alpha parameter for the fraction of cells that respond to treatment.} + +\item{b_phi}{(\code{number})\cr the beta parameter for the fraction of cells that respond to treatment.} + \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{a_phi}{(\code{number})\cr the alpha parameter for the fraction of cells that respond to treatment.} - -\item{b_phi}{(\code{number})\cr the beta parameter for the fraction of cells that respond to treatment.} - \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 GSF model. -} \description{ -Construct a Simulation Function for Longitudinal Data from GSF Model +Simulate Longitudinal Data from a GSF Model +} +\section{Slots}{ + +\describe{ +\item{\code{sigma}}{(\code{numeric})\cr See arguments.} + +\item{\code{mu_s}}{(\code{numeric})\cr See arguments.} + +\item{\code{mu_g}}{(\code{numeric})\cr See arguments.} + +\item{\code{mu_b}}{(\code{numeric})\cr See arguments.} + +\item{\code{omega_b}}{(\code{numeric})\cr See arguments.} + +\item{\code{omega_s}}{(\code{numeric})\cr See arguments.} + +\item{\code{omega_g}}{(\code{numeric})\cr See arguments.} + +\item{\code{a_phi}}{(\code{numeric})\cr See arguments.} + +\item{\code{b_phi}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_dsld}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_ttg}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_identity}}{(\code{numeric})\cr See arguments.} +}} + +\seealso{ +Other SimLongitudinal: +\code{\link{SimLongitudinal-class}}, +\code{\link{SimLongitudinalRandomSlope-class}}, +\code{\link{SimLongitudinalSteinFojo-class}} } +\concept{SimLongitudinal} diff --git a/man/SimLongitudinalRandomSlope-class.Rd b/man/SimLongitudinalRandomSlope-class.Rd index 3ccbc30c..e8f05d45 100644 --- a/man/SimLongitudinalRandomSlope-class.Rd +++ b/man/SimLongitudinalRandomSlope-class.Rd @@ -4,8 +4,22 @@ \name{SimLongitudinalRandomSlope-class} \alias{SimLongitudinalRandomSlope-class} \alias{.SimLongitudinalRandomSlope} -\title{Construct a Simulation Function for Longitudinal Data from Random Slope Model} +\alias{SimLongitudinalRandomSlope} +\title{Simulate Longitudinal Data from a Random Slope Model} +\usage{ +SimLongitudinalRandomSlope( + times = c(-100, -50, 0, 50, 100, 150, 250, 350, 450, 550), + intercept = 50, + slope_mu = c(0.01, 0.03), + slope_sigma = 0.5, + sigma = 2, + link_dsld = 0, + link_identity = 0 +) +} \arguments{ +\item{times}{(\code{numeric})\cr the times to generate observations at.} + \item{intercept}{(\code{number})\cr the mean baseline value for each study.} \item{slope_mu}{(\code{numeric})\cr the population slope for each treatment arm.} @@ -18,10 +32,29 @@ \item{link_identity}{(\code{number})\cr the link coefficient for the identity contribution.} } -\value{ -A function with argument \code{lm_base} that can be used to simulate -longitudinal data from the corresponding random slope model. -} \description{ -Construct a Simulation Function for Longitudinal Data from Random Slope Model +Simulate Longitudinal Data from a Random Slope Model +} +\section{Slots}{ + +\describe{ +\item{\code{intercept}}{(\code{numeric})\cr See arguments.} + +\item{\code{slope_mu}}{(\code{numeric})\cr See arguments.} + +\item{\code{slope_sigma}}{(\code{numeric})\cr See arguments.} + +\item{\code{sigma}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_dsld}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_identity}}{(\code{numeric})\cr See arguments.} +}} + +\seealso{ +Other SimLongitudinal: +\code{\link{SimLongitudinal-class}}, +\code{\link{SimLongitudinalGSF-class}}, +\code{\link{SimLongitudinalSteinFojo-class}} } +\concept{SimLongitudinal} diff --git a/man/SimLongitudinalSteinFojo-class.Rd b/man/SimLongitudinalSteinFojo-class.Rd index a85f7ee6..bb30e20e 100644 --- a/man/SimLongitudinalSteinFojo-class.Rd +++ b/man/SimLongitudinalSteinFojo-class.Rd @@ -4,8 +4,26 @@ \name{SimLongitudinalSteinFojo-class} \alias{SimLongitudinalSteinFojo-class} \alias{.SimLongitudinalSteinFojo} -\title{Construct a Simulation Function for Longitudinal Data from Stein-Fojo Model} +\alias{SimLongitudinalSteinFojo} +\title{Simulate Longitudinal Data from a Stein-Fojo Model} +\usage{ +SimLongitudinalSteinFojo( + times = c(-100, -50, 0, 50, 100, 150, 250, 350, 450, 550)/365, + 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{times}{(\code{numeric})\cr the times to generate observations at.} + \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.} @@ -26,10 +44,37 @@ \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 +Simulate Longitudinal Data from a Stein-Fojo Model +} +\section{Slots}{ + +\describe{ +\item{\code{sigma}}{(\code{numeric})\cr See arguments.} + +\item{\code{mu_s}}{(\code{numeric})\cr See arguments.} + +\item{\code{mu_g}}{(\code{numeric})\cr See arguments.} + +\item{\code{mu_b}}{(\code{numeric})\cr See arguments.} + +\item{\code{omega_b}}{(\code{numeric})\cr See arguments.} + +\item{\code{omega_s}}{(\code{numeric})\cr See arguments.} + +\item{\code{omega_g}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_dsld}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_ttg}}{(\code{numeric})\cr See arguments.} + +\item{\code{link_identity}}{(\code{numeric})\cr See arguments.} +}} + +\seealso{ +Other SimLongitudinal: +\code{\link{SimLongitudinal-class}}, +\code{\link{SimLongitudinalGSF-class}}, +\code{\link{SimLongitudinalRandomSlope-class}} } +\concept{SimLongitudinal} diff --git a/man/SimSurvival-Shared.Rd b/man/SimSurvival-Shared.Rd new file mode 100644 index 00000000..6e3302ea --- /dev/null +++ b/man/SimSurvival-Shared.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/SimSurvival.R +\name{SimSurvival-Shared} +\alias{SimSurvival-Shared} +\title{\code{SimSurvival} Function Arguments} +\arguments{ +\item{time_max}{(\code{number})\cr the maximum time to simulate to.} + +\item{time_step}{(\code{number})\cr the time interval between evaluating the log-hazard function.} + +\item{lambda_censor}{(\code{number})\cr the censoring rate.} + +\item{beta_cont}{(\code{number})\cr the continuous covariate coefficient.} + +\item{beta_cat}{(\code{numeric})\cr the categorical covariate coefficients.} + +\item{loghazard}{(\code{function})\cr the log hazard function.} + +\item{...}{Not Used.} +} +\description{ +The documentation lists all the conventional arguments for \code{\link{SimSurvival}} +constructors. +} +\section{Hazard Evaluation}{ + + +Event times are simulated by sampling a cumulative hazard limit from a \eqn{U(0, 1)} distribution +for +each subject and then counting how much hazard they've been exposed to by evaluating the +log-hazard function at a set interval. The \code{time_max} argument sets the upper bound for the +number of time points to evaluate the log-hazard function at with subjects who have not had an +event being censored at \code{time_max}. The \code{time_step} argument sets the interval at which to +evaluate the log-hazard function. Setting smaller values for \code{time_step} will increase the +precision of the simulation at the cost of increased computation time. Likewise, setting large +values for \code{time_max} will minimize the number of censored subjects at the cost of +incread computation time. +} + +\keyword{internal} diff --git a/man/SimSurvival-class.Rd b/man/SimSurvival-class.Rd new file mode 100644 index 00000000..f2b64f40 --- /dev/null +++ b/man/SimSurvival-class.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/SimSurvival.R +\docType{class} +\name{SimSurvival-class} +\alias{SimSurvival-class} +\alias{.SimSurvival} +\alias{SimSurvival} +\title{Abstract Simulation Class for Survival Data} +\usage{ +SimSurvival( + time_max = 2000, + time_step = 1, + lambda_censor = 1/3000, + beta_cont = 0.2, + beta_cat = c(A = 0, B = -0.4, C = 0.2), + loghazard +) +} +\arguments{ +\item{time_max}{(\code{number})\cr the maximum time to simulate to.} + +\item{time_step}{(\code{number})\cr the time interval between evaluating the log-hazard function.} + +\item{lambda_censor}{(\code{number})\cr the censoring rate.} + +\item{beta_cont}{(\code{number})\cr the continuous covariate coefficient.} + +\item{beta_cat}{(\code{numeric})\cr the categorical covariate coefficients.} + +\item{loghazard}{(\code{function})\cr the log hazard function.} +} +\description{ +Abstract Simulation Class for Survival Data +} +\section{Slots}{ + +\describe{ +\item{\code{time_max}}{(\code{numeric})\cr See arguments.} + +\item{\code{time_step}}{(\code{numeric})\cr See arguments.} + +\item{\code{lambda_censor}}{(\code{numeric})\cr See arguments.} + +\item{\code{beta_cont}}{(\code{numeric})\cr See arguments.} + +\item{\code{beta_cat}}{(\code{numeric})\cr See arguments.} + +\item{\code{loghazard}}{(\code{function})\cr See arguments.} +}} + +\section{Hazard Evaluation}{ + + +Event times are simulated by sampling a cumulative hazard limit from a \eqn{U(0, 1)} distribution +for +each subject and then counting how much hazard they've been exposed to by evaluating the +log-hazard function at a set interval. The \code{time_max} argument sets the upper bound for the +number of time points to evaluate the log-hazard function at with subjects who have not had an +event being censored at \code{time_max}. The \code{time_step} argument sets the interval at which to +evaluate the log-hazard function. Setting smaller values for \code{time_step} will increase the +precision of the simulation at the cost of increased computation time. Likewise, setting large +values for \code{time_max} will minimize the number of censored subjects at the cost of +incread computation time. +} + +\seealso{ +Other SimSurvival: +\code{\link{SimSurvivalExponential}()}, +\code{\link{SimSurvivalLogLogistic}()}, +\code{\link{SimSurvivalWeibullPH}()} +} +\concept{SimSurvival} diff --git a/man/SimSurvivalExponential.Rd b/man/SimSurvivalExponential.Rd index dfdcef6a..40701985 100644 --- a/man/SimSurvivalExponential.Rd +++ b/man/SimSurvivalExponential.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/SimSurvival.R \name{SimSurvivalExponential} \alias{SimSurvivalExponential} -\title{Construct a Log Hazard Function for the Exponential Model} +\title{Simulate Survival Data from a Exponential Proportional Hazard Model} \usage{ SimSurvivalExponential( lambda, @@ -15,10 +15,39 @@ SimSurvivalExponential( } \arguments{ \item{lambda}{(\code{number})\cr the rate parameter.} -} -\value{ -A function of \code{time} returning the log hazard. + +\item{time_max}{(\code{number})\cr the maximum time to simulate to.} + +\item{time_step}{(\code{number})\cr the time interval between evaluating the log-hazard function.} + +\item{lambda_censor}{(\code{number})\cr the censoring rate.} + +\item{beta_cont}{(\code{number})\cr the continuous covariate coefficient.} + +\item{beta_cat}{(\code{numeric})\cr the categorical covariate coefficients.} } \description{ -Construct a Log Hazard Function for the Exponential Model +Simulate Survival Data from a Exponential Proportional Hazard Model +} +\section{Hazard Evaluation}{ + + +Event times are simulated by sampling a cumulative hazard limit from a \eqn{U(0, 1)} distribution +for +each subject and then counting how much hazard they've been exposed to by evaluating the +log-hazard function at a set interval. The \code{time_max} argument sets the upper bound for the +number of time points to evaluate the log-hazard function at with subjects who have not had an +event being censored at \code{time_max}. The \code{time_step} argument sets the interval at which to +evaluate the log-hazard function. Setting smaller values for \code{time_step} will increase the +precision of the simulation at the cost of increased computation time. Likewise, setting large +values for \code{time_max} will minimize the number of censored subjects at the cost of +incread computation time. +} + +\seealso{ +Other SimSurvival: +\code{\link{SimSurvival-class}}, +\code{\link{SimSurvivalLogLogistic}()}, +\code{\link{SimSurvivalWeibullPH}()} } +\concept{SimSurvival} diff --git a/man/SimSurvivalLogLogistic.Rd b/man/SimSurvivalLogLogistic.Rd index 241885e2..e9d4026c 100644 --- a/man/SimSurvivalLogLogistic.Rd +++ b/man/SimSurvivalLogLogistic.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/SimSurvival.R \name{SimSurvivalLogLogistic} \alias{SimSurvivalLogLogistic} -\title{Construct a Log Hazard Function for the Log-Logistic Model} +\title{Simulate Survival Data from a Log-Logistic Proportional Hazard Model} \usage{ SimSurvivalLogLogistic( a, @@ -18,10 +18,39 @@ SimSurvivalLogLogistic( \item{a}{(\code{number})\cr the scale parameter.} \item{b}{(\code{number})\cr the shape parameter.} -} -\value{ -A function of \code{time} returning the log hazard. + +\item{time_max}{(\code{number})\cr the maximum time to simulate to.} + +\item{time_step}{(\code{number})\cr the time interval between evaluating the log-hazard function.} + +\item{lambda_censor}{(\code{number})\cr the censoring rate.} + +\item{beta_cont}{(\code{number})\cr the continuous covariate coefficient.} + +\item{beta_cat}{(\code{numeric})\cr the categorical covariate coefficients.} } \description{ -Construct a Log Hazard Function for the Log-Logistic Model +Simulate Survival Data from a Log-Logistic Proportional Hazard Model +} +\section{Hazard Evaluation}{ + + +Event times are simulated by sampling a cumulative hazard limit from a \eqn{U(0, 1)} distribution +for +each subject and then counting how much hazard they've been exposed to by evaluating the +log-hazard function at a set interval. The \code{time_max} argument sets the upper bound for the +number of time points to evaluate the log-hazard function at with subjects who have not had an +event being censored at \code{time_max}. The \code{time_step} argument sets the interval at which to +evaluate the log-hazard function. Setting smaller values for \code{time_step} will increase the +precision of the simulation at the cost of increased computation time. Likewise, setting large +values for \code{time_max} will minimize the number of censored subjects at the cost of +incread computation time. +} + +\seealso{ +Other SimSurvival: +\code{\link{SimSurvival-class}}, +\code{\link{SimSurvivalExponential}()}, +\code{\link{SimSurvivalWeibullPH}()} } +\concept{SimSurvival} diff --git a/man/SimSurvivalWeibullPH.Rd b/man/SimSurvivalWeibullPH.Rd index fa47a5dc..8821b54b 100644 --- a/man/SimSurvivalWeibullPH.Rd +++ b/man/SimSurvivalWeibullPH.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/SimSurvival.R \name{SimSurvivalWeibullPH} \alias{SimSurvivalWeibullPH} -\title{Construct a Log Hazard Function for the Weibull Model} +\title{Simulate Survival Data from a Weibull Proportional Hazard Model} \usage{ SimSurvivalWeibullPH( lambda, @@ -18,10 +18,39 @@ SimSurvivalWeibullPH( \item{lambda}{(\code{number})\cr the scale parameter.} \item{gamma}{(\code{number})\cr the shape parameter.} -} -\value{ -A function of \code{time} returning the log hazard. + +\item{time_max}{(\code{number})\cr the maximum time to simulate to.} + +\item{time_step}{(\code{number})\cr the time interval between evaluating the log-hazard function.} + +\item{lambda_censor}{(\code{number})\cr the censoring rate.} + +\item{beta_cont}{(\code{number})\cr the continuous covariate coefficient.} + +\item{beta_cat}{(\code{numeric})\cr the categorical covariate coefficients.} } \description{ -Construct a Log Hazard Function for the Weibull Model +Simulate Survival Data from a Weibull Proportional Hazard Model +} +\section{Hazard Evaluation}{ + + +Event times are simulated by sampling a cumulative hazard limit from a \eqn{U(0, 1)} distribution +for +each subject and then counting how much hazard they've been exposed to by evaluating the +log-hazard function at a set interval. The \code{time_max} argument sets the upper bound for the +number of time points to evaluate the log-hazard function at with subjects who have not had an +event being censored at \code{time_max}. The \code{time_step} argument sets the interval at which to +evaluate the log-hazard function. Setting smaller values for \code{time_step} will increase the +precision of the simulation at the cost of increased computation time. Likewise, setting large +values for \code{time_max} will minimize the number of censored subjects at the cost of +incread computation time. +} + +\seealso{ +Other SimSurvival: +\code{\link{SimSurvival-class}}, +\code{\link{SimSurvivalExponential}()}, +\code{\link{SimSurvivalLogLogistic}()} } +\concept{SimSurvival} diff --git a/man/as_stan_list.DataObject.Rd b/man/as_stan_list.DataObject.Rd index 828e37c9..aa4a73ec 100644 --- a/man/as_stan_list.DataObject.Rd +++ b/man/as_stan_list.DataObject.Rd @@ -43,7 +43,7 @@ containing the subject identifier.} } \description{ Coerces a data object into a \code{list} of data components required -for fitting a \code{\link{JointModel}}. See the vignette (TODO) for more details. +for fitting a \code{\link{JointModel}}. See the "Extending jmpost" vignette for more details. } \seealso{ Other DataSubject: diff --git a/man/gsf_sld.Rd b/man/gsf_sld.Rd index 531916ef..88322ce0 100644 --- a/man/gsf_sld.Rd +++ b/man/gsf_sld.Rd @@ -29,9 +29,4 @@ The function results. \description{ Generalized Stein-Fojo Functionals } -\examples{ -gsf_sld(1:10, 20, 0.3, 0.6, 0.2) -gsf_ttg(1:10, 20, 0.3, 0.6, 0.2) -gsf_dsld(1:10, 20, 0.3, 0.6, 0.2) -} \keyword{internal} diff --git a/man/hazardWindows.Rd b/man/hazardWindows.Rd new file mode 100644 index 00000000..8b9f7747 --- /dev/null +++ b/man/hazardWindows.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/generics.R +\name{hazardWindows} +\alias{hazardWindows} +\title{Generate time windows for evaluating a hazard function} +\usage{ +hazardWindows(object, ...) +} +\arguments{ +\item{object}{(\code{SurvivalModel}) \cr object to generate time windows for.} + +\item{...}{Not used.} +} +\description{ +Generate time windows for evaluating a hazard function +} diff --git a/man/hazardWindows.SimSurvival.Rd b/man/hazardWindows.SimSurvival.Rd index 0ec4aad5..c3efba56 100644 --- a/man/hazardWindows.SimSurvival.Rd +++ b/man/hazardWindows.SimSurvival.Rd @@ -7,7 +7,7 @@ \method{hazardWindows}{SimSurvival}(object) } \arguments{ -\item{x}{(\code{numeric})\cr grid of time points.} +\item{object}{(\code{SimSurvival})\cr the survival simulation object to create evaluation points for.} } \value{ A \code{tibble} with \code{lower}, \code{upper}, \code{time}, \code{eval} and \code{width}. diff --git a/man/sampleObservations.Rd b/man/sampleObservations.Rd new file mode 100644 index 00000000..699e9aa2 --- /dev/null +++ b/man/sampleObservations.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/generics.R, R/SimLongitudinalGSF.R, +% R/SimLongitudinalRandomSlope.R, R/SimLongitudinalSteinFojo.R, +% R/SimSurvival.R +\name{sampleObservations} +\alias{sampleObservations} +\alias{sampleObservations.SimLongitudinalGSF} +\alias{sampleObservations.SimLongitudinalRandomSlope} +\alias{sampleObservations.SimLongitudinalSteinFojo} +\alias{sampleObservations.SimSurvival} +\title{Generate Simulated Observations} +\usage{ +sampleObservations(object, times_df) + +\method{sampleObservations}{SimLongitudinalGSF}(object, times_df) + +\method{sampleObservations}{SimLongitudinalRandomSlope}(object, times_df) + +\method{sampleObservations}{SimLongitudinalSteinFojo}(object, times_df) + +\method{sampleObservations}{SimSurvival}(object, times_df) +} +\arguments{ +\item{object}{(\code{SimLongitudinal} or \code{SimSurvival}) \cr object to generate observations from.} + +\item{times_df}{(\code{data.frame}) \cr the times at which to generate observations. See details.} +} +\description{ +Generate Simulated Observations +} +\details{ +The \code{times_df} argument should be a \code{data.frame} as created by \code{sampleSubjects} but +replicated for each time point at which observations are to be generated. That is if you want +to generate observations for times \code{c(0, 1, 2, 3)} then \code{times_df} should be created as: + +\if{html}{\out{
}}\preformatted{subject_dat <- sampleSubjects(object, ...) +times_df <- tidyr::expand_grid( + subject_dat, + time = c(0, 1, 2, 3) +) +}\if{html}{\out{
}} +} diff --git a/man/sampleSubjects.Rd b/man/sampleSubjects.Rd new file mode 100644 index 00000000..2d8d15b2 --- /dev/null +++ b/man/sampleSubjects.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/generics.R, R/SimLongitudinalGSF.R, +% R/SimLongitudinalRandomSlope.R, R/SimLongitudinalSteinFojo.R, +% R/SimSurvival.R +\name{sampleSubjects} +\alias{sampleSubjects} +\alias{sampleSubjects.SimLongitudinalGSF} +\alias{sampleSubjects.SimLongitudinalRandomSlope} +\alias{sampleSubjects.SimLongitudinalSteinFojo} +\alias{sampleSubjects.SimSurvival} +\title{Generate Simulated Subjects} +\usage{ +sampleSubjects(object, subjects_df) + +\method{sampleSubjects}{SimLongitudinalGSF}(object, subjects_df) + +\method{sampleSubjects}{SimLongitudinalRandomSlope}(object, subjects_df) + +\method{sampleSubjects}{SimLongitudinalSteinFojo}(object, subjects_df) + +\method{sampleSubjects}{SimSurvival}(object, subjects_df) +} +\arguments{ +\item{object}{(\code{SimLongitudinal} or \code{SimSurvival}) \cr object to generate subjects from.} + +\item{subjects_df}{(\code{data.frame}) \cr the subjects to generate observations for. See details.} +} +\description{ +Generate Simulated Subjects +} +\details{ +The \code{subjects_df} argument should be a \code{data.frame} with 1 row per desired subject to create +with the following columns: +\itemize{ +\item \code{study} (\code{factor}) the study identifier. +\item \code{arm} (\code{factor}) the treatment arm identifier. +\item \code{pt} (\code{character}) the subject identifier. +} + +This method takes care of generating all the individual subject data required for the +\code{\link{sampleObservations}} method to generate the observations. +} diff --git a/man/sf_sld.Rd b/man/sf_sld.Rd index 4de1584f..a7e8c036 100644 --- a/man/sf_sld.Rd +++ b/man/sf_sld.Rd @@ -27,9 +27,4 @@ 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/tests/testthat/_snaps/JointModelSamples.md b/tests/testthat/_snaps/JointModelSamples.md index df44bcdb..68f6a9ec 100644 --- a/tests/testthat/_snaps/JointModelSamples.md +++ b/tests/testthat/_snaps/JointModelSamples.md @@ -1,13 +1,7 @@ -# smoke test for JointModelSamples +# print works as expected for JointModelSamples Code - devnull <- capture.output({ - suppressMessages({ - mpp <- sampleStanModel(jm, data = jdat, iter_sampling = 100, iter_warmup = 150, - chains = 1, refresh = 0, parallel_chains = 1) - }) - }) - print(mpp) + print(test_data_1$jsamples) Output JointModelSamples Object with: @@ -16,12 +10,12 @@ # of chains = 1 Variables: - Ypred[2400] + Ypred[2800] beta_os_cov[3] lm_rs_ind_intercept[400] lm_rs_ind_rnd_slope[400] lm_rs_intercept - lm_rs_rslope_ind[2400] + lm_rs_rslope_ind[2800] lm_rs_sigma lm_rs_slope_mu[2] lm_rs_slope_sigma diff --git a/tests/testthat/_snaps/LongitudinalQuantiles.md b/tests/testthat/_snaps/LongitudinalQuantiles.md index 83501711..9a0a5f10 100644 --- a/tests/testthat/_snaps/LongitudinalQuantiles.md +++ b/tests/testthat/_snaps/LongitudinalQuantiles.md @@ -1,7 +1,7 @@ # LongitudinalQuantities print method works as expected Code - ptgroups <- c("pt_00011", "pt_00061", "pt_00001", "pt_00002") + ptgroups <- c("pt_011", "pt_061", "pt_001", "pt_002") times <- seq(0, 100, by = 10) samps_p1 <- LongitudinalQuantities(test_data_1$jsamples, ptgroups, times) print(samps_p1) @@ -15,7 +15,7 @@ --- Code - ptgroups <- c("pt_00011", "pt_00061") + ptgroups <- c("pt_011", "pt_061") samps_p2 <- LongitudinalQuantities(test_data_1$jsamples, ptgroups) print(samps_p2) Output diff --git a/tests/testthat/helper-example_data.R b/tests/testthat/helper-example_data.R index 1daa5bc6..d5652e38 100644 --- a/tests/testthat/helper-example_data.R +++ b/tests/testthat/helper-example_data.R @@ -60,20 +60,20 @@ ensure_test_data_1 <- function() { threshold = 5 ) ) - devnull <- capture.output({ - suppressMessages({ - mp <- sampleStanModel( - jm, - data = jdat, - iter_sampling = 100, - iter_warmup = 150, - chains = 1, - refresh = 0, - parallel_chains = 1 - ) - }) + + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_sampling = 100, + iter_warmup = 150, + chains = 1, + refresh = 0, + parallel_chains = 1 + ) }) + test_data_1$dat_os <- dat_os test_data_1$dat_lm <- dat_lm test_data_1$jmodel <- jm diff --git a/tests/testthat/helper-setup.R b/tests/testthat/helper-setup.R index 14d84659..53a44766 100644 --- a/tests/testthat/helper-setup.R +++ b/tests/testthat/helper-setup.R @@ -34,3 +34,9 @@ load_with_base_stan <- function(...) { StanModule("base/functions.stan") ) } + + +run_quietly <- function(expr) { + suppressMessages(capture.output((x <- expr))) + return(x) +} diff --git a/tests/testthat/test-JointModelSamples.R b/tests/testthat/test-JointModelSamples.R index 41ef6514..ff183e41 100644 --- a/tests/testthat/test-JointModelSamples.R +++ b/tests/testthat/test-JointModelSamples.R @@ -3,9 +3,6 @@ test_that("print works as expected for JointModelSamples", { ensure_test_data_1() expect_snapshot({ - devnull <- capture.output({ - test_data_1$jdata - }) - print(mpp) + print(test_data_1$jsamples) }) }) diff --git a/tests/testthat/test-LongitudinalGSF.R b/tests/testthat/test-LongitudinalGSF.R index 27a35964..4a343d72 100644 --- a/tests/testthat/test-LongitudinalGSF.R +++ b/tests/testthat/test-LongitudinalGSF.R @@ -57,27 +57,31 @@ test_that("Can recover known distributional parameters from a full GSF joint mod skip_if_not(is_full_test()) set.seed(7143) - jlist <- simulate_joint_data( - .debug = TRUE, + jlist <- SimJointData( design = list( SimGroup(80, "Arm-A", "Study-X"), SimGroup(100, "Arm-B", "Study-X") ), - times = seq(0, 3, by = (1 / 365) / 2), - lambda_cen = 1 / 9000, - beta_cat = c( - "A" = 0, - "B" = -0.1, - "C" = 0.5 + survival = SimSurvivalExponential( + lambda = 1 / (400 / 365), + time_max = 3, + time_step = 1 / 365, + lambda_censor = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = -0.1, + "C" = 0.5 + ), + beta_cont = 0.3 ), - beta_cont = 0.3, - lm_fun = sim_lm_gsf( + longitudinal = SimLongitudinalGSF( + times = c(-100, -50, 0, 1, 10, 50, 100, 150, 250, 300, 400, 500, 600) / 365, sigma = 0.01, mu_s = c(0.6, 0.4), mu_g = c(0.25, 0.35), mu_b = 60, - a_phi = c(4, 6), - b_phi = c(4, 6), + a_phi = c(20, 15), + b_phi = c(15, 20), omega_b = 0.2, omega_s = 0.2, omega_g = 0.2, @@ -85,30 +89,22 @@ test_that("Can recover known distributional parameters from a full GSF joint mod link_ttg = 0.2, link_identity = 0 ), - os_fun = sim_os_exponential(1 / (400 / 365)) + .silent = TRUE ) - set.seed(333) - select_times <- sample(jlist$lm$time, 12) - - dat_os <- jlist$os - dat_lm <- jlist$lm |> - dplyr::filter(time %in% select_times) |> - dplyr::arrange(time, pt) - jdat <- DataJoint( subject = DataSubject( - data = dat_os, + data = jlist@survival, subject = "pt", arm = "arm", study = "study" ), survival = DataSurvival( - data = dat_os, + data = jlist@survival, formula = Surv(time, event) ~ cov_cat + cov_cont ), longitudinal = DataLongitudinal( - data = dat_lm, + data = jlist@longitudinal, formula = sld ~ time ) ) @@ -121,8 +117,8 @@ test_that("Can recover known distributional parameters from a full GSF joint mod omega_bsld = prior_lognormal(log(0.2), 1), omega_ks = prior_lognormal(log(0.2), 1), omega_kg = prior_lognormal(log(0.2), 1), - a_phi = prior_lognormal(log(6), 1), - b_phi = prior_lognormal(log(8), 1), + a_phi = prior_lognormal(log(18), 1), + b_phi = prior_lognormal(log(18), 1), sigma = prior_lognormal(log(0.01), 1), centred = TRUE ), @@ -135,15 +131,19 @@ test_that("Can recover known distributional parameters from a full GSF joint mod ) ) - mp <- suppressWarnings(sampleStanModel( - jm, - data = jdat, - iter_warmup = 400, - iter_sampling = 800, - chains = 2, - refresh = 0, - parallel_chains = 2 - )) + suppressWarnings({ + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_warmup = 400, + iter_sampling = 800, + chains = 2, + refresh = 0, + parallel_chains = 2 + ) + }) + }) summary_post <- function(model, vars, exp = FALSE) { no_name_quant <- \(...) { @@ -184,7 +184,7 @@ test_that("Can recover known distributional parameters from a full GSF joint mod 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))) + true_values <- c(0.1, 0.2, 20, 15, 15, 20, 1 / (1 / (400 / 365))) expect_true(all(dat$q01 <= true_values)) expect_true(all(dat$q99 >= true_values)) expect_true(all(dat$ess_bulk > 100)) diff --git a/tests/testthat/test-LongitudinalQuantiles.R b/tests/testthat/test-LongitudinalQuantiles.R index c3e1cc12..5c4553ab 100644 --- a/tests/testthat/test-LongitudinalQuantiles.R +++ b/tests/testthat/test-LongitudinalQuantiles.R @@ -7,7 +7,7 @@ test_that("Test that LongitudinalQuantities works as expected", { longsamps <- LongitudinalQuantities( test_data_1$jsamples, - c("pt_00001", "pt_00002"), + c("pt_001", "pt_002"), c(10, 20, 200, 300) ) preds <- summary(longsamps) @@ -24,7 +24,7 @@ test_that("Test that LongitudinalQuantities works as expected", { expect_equal(unique(preds$group), test_data_1$dat_os$pt) - longsamps <- LongitudinalQuantities(test_data_1$jsamples, c("pt_00001", "pt_00003")) + longsamps <- LongitudinalQuantities(test_data_1$jsamples, c("pt_001", "pt_003")) preds <- preds <- summary(longsamps) expect_equal(nrow(preds), 2 * 201) # 201 default time points for 2 subjects expect_equal(names(preds), expected_column_names) @@ -35,7 +35,7 @@ test_that("LongitudinalQuantities does not support group aggregation", { expect_error( LongitudinalQuantities( test_data_1$jsamples, - groups = list("a" = c("pt_00011", "pt_00012")) + groups = list("a" = c("pt_011", "pt_012")) ), regexp = "not 'list'" ) @@ -45,7 +45,7 @@ test_that("LongitudinalQuantities does not support group aggregation", { test_that("autoplot.LongitudinalQuantities works as expected", { - samps <- LongitudinalQuantities(test_data_1$jsamples, c("pt_00011", "pt_00061")) + samps <- LongitudinalQuantities(test_data_1$jsamples, c("pt_011", "pt_061")) p <- autoplot( samps, conf.level = FALSE @@ -62,7 +62,7 @@ test_that("autoplot.LongitudinalQuantities works as expected", { samps <- LongitudinalQuantities( test_data_1$jsamples, - groups = c("pt_00011", "pt_00061", "pt_00001", "pt_00002"), + groups = c("pt_011", "pt_061", "pt_001", "pt_002"), time_grid = c(10, 20, 50, 200) ) p <- autoplot(samps) @@ -83,13 +83,13 @@ test_that("autoplot.LongitudinalQuantities works as expected", { test_that("LongitudinalQuantities print method works as expected", { expect_snapshot({ - ptgroups <- c("pt_00011", "pt_00061", "pt_00001", "pt_00002") + ptgroups <- c("pt_011", "pt_061", "pt_001", "pt_002") times <- seq(0, 100, by = 10) samps_p1 <- LongitudinalQuantities(test_data_1$jsamples, ptgroups, times) print(samps_p1) }) expect_snapshot({ - ptgroups <- c("pt_00011", "pt_00061") + ptgroups <- c("pt_011", "pt_061") samps_p2 <- LongitudinalQuantities(test_data_1$jsamples, ptgroups) print(samps_p2) }) @@ -106,9 +106,10 @@ test_that("LongitudinalQuantities print method works as expected", { test_that("LongitudinalQuantities can recover known results", { set.seed(101) + ensure_test_data_1() longsamps <- LongitudinalQuantities( test_data_1$jsamples, - time_grid = c(1, 100, 200, 250, 300, 350) + time_grid = c(0, 1, 100, 200, 250, 300, 350) ) dat_sum <- dplyr::tibble(summary(longsamps)) |> diff --git a/tests/testthat/test-LongitudinalRandomSlope.R b/tests/testthat/test-LongitudinalRandomSlope.R index a4762883..214b7543 100644 --- a/tests/testthat/test-LongitudinalRandomSlope.R +++ b/tests/testthat/test-LongitudinalRandomSlope.R @@ -69,17 +69,19 @@ test_that("LongitudinalRandomSlope correctly generates an intercept per study", link = Link() ) - mp <- suppressWarnings( - sampleStanModel( - jm, - data = jdat, - iter_warmup = 600, - iter_sampling = 900, - chains = 2, - refresh = 0, - parallel_chains = 2 - ) - ) + suppressWarnings({ + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_warmup = 600, + iter_sampling = 900, + chains = 2, + refresh = 0, + parallel_chains = 2 + ) + }) + }) samples <- mp@results$draws( c("lm_rs_intercept", "lm_rs_slope_mu", "lm_rs_slope_sigma", "lm_rs_sigma"), @@ -141,15 +143,17 @@ test_that("Random Slope Model can recover known parameter values", { ) ) - mp <- sampleStanModel( - jm, - data = jdat, - iter_sampling = 200, - iter_warmup = 400, - chains = 1, - refresh = 0, - parallel_chains = 1 - ) + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_sampling = 200, + iter_warmup = 400, + chains = 1, + refresh = 0, + parallel_chains = 1 + ) + }) vars <- c( diff --git a/tests/testthat/test-LongitudinalSteinFojo.R b/tests/testthat/test-LongitudinalSteinFojo.R index 1602b468..6f221de8 100644 --- a/tests/testthat/test-LongitudinalSteinFojo.R +++ b/tests/testthat/test-LongitudinalSteinFojo.R @@ -58,20 +58,13 @@ test_that("Can recover known distributional parameters from a SF joint model", { set.seed(9438) ## Generate Test data with known parameters - jlist <- simulate_joint_data( + jlist <- SimJointData( design = list( SimGroup(120, "Arm-A", "Study-X"), SimGroup(120, "Arm-B", "Study-X") ), - 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( + longitudinal = SimLongitudinalSteinFojo( + times = c(-100, -50, -10, 1, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900) * (1 / 365), sigma = 0.005, mu_s = c(0.2, 0.25), mu_g = c(0.15, 0.2), @@ -82,20 +75,22 @@ test_that("Can recover known distributional parameters from a SF joint model", { link_ttg = -0.2, link_dsld = 0.2 ), - os_fun = sim_os_weibull( + survival = SimSurvivalWeibullPH( + time_max = 4, + time_step = 1 / 365, lambda = 1, - gamma = 1 - ) + gamma = 1, + lambda_cen = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = -0.1, + "C" = 0.5 + ), + beta_cont = 0.3 + ), + .silent = TRUE ) - - 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( @@ -122,17 +117,17 @@ test_that("Can recover known distributional parameters from a SF joint model", { jdat <- DataJoint( subject = DataSubject( - data = dat_os, + data = jlist@survival, subject = "pt", arm = "arm", study = "study" ), survival = DataSurvival( - data = dat_os, + data = jlist@survival, formula = Surv(time, event) ~ cov_cat + cov_cont ), longitudinal = DataLongitudinal( - data = dat_lm, + data = jlist@longitudinal, formula = sld ~ time, threshold = 5 ) @@ -142,14 +137,16 @@ test_that("Can recover known distributional parameters from a SF joint model", { set.seed(2213) - mp <- sampleStanModel( - jm, - data = jdat, - iter_sampling = 600, - iter_warmup = 1000, - chains = 2, - parallel_chains = 2 - ) + mp <- run_quietly({ + 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( diff --git a/tests/testthat/test-SimGroup.R b/tests/testthat/test-SimGroup.R new file mode 100644 index 00000000..308ab334 --- /dev/null +++ b/tests/testthat/test-SimGroup.R @@ -0,0 +1,21 @@ + +test_that("SimGroup() works as expected", { + sim_group <- SimGroup(100, "Arm-A", "Study-X") + expect_s4_class(sim_group, "SimGroup") + expect_equal(sim_group@n, 100) + expect_equal(sim_group@arm, "Arm-A") + expect_equal(sim_group@study, "Study-X") + + expect_error( + SimGroup(c(100, 200), "Arm-A", "Study-X"), + regexp = "`n` must be a length 1 integer" + ) + expect_error( + SimGroup(100, c("Arm-A", "Arm-b"), "Study-X"), + regexp = "`arm` must be a length 1 string" + ) + expect_error( + SimGroup(100, "Arm-A", c("Study-X", "Study-Z")), + regexp = "`study` must be a length 1 string" + ) +}) diff --git a/tests/testthat/test-SimJointData.R b/tests/testthat/test-SimJointData.R index 3bd59c6b..6c38c434 100644 --- a/tests/testthat/test-SimJointData.R +++ b/tests/testthat/test-SimJointData.R @@ -1,304 +1,91 @@ -# sim_lm_gsf ---- -library(survival) - -test_that("sim_lm_gsf works as expected", { - df <- data.frame( - pt = rep(c("1", "2", "3"), each = 5), - time = rep(1:5, 3), - arm = factor(rep(c("A", "B"), c(1, 2) * 5)), - study = factor("study1") - ) - result <- expect_silent(sim_lm_gsf( - sigma = 0.003, - 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, - a_phi = c(4, 6), - b_phi = c(4, 6), - link_dsld = 0.5, - link_ttg = 1 - )) - expect_true(is.function(result)) - set.seed(123) - fun_result <- result(df) - expect_s3_class(fun_result, "data.frame") - expect_true(all(c("pt", "time", "sld", "log_haz_link", "study", "arm") %in% names(fun_result))) - expect_identical(nrow(df), nrow(fun_result)) - expect_true(all(fun_result$sld > 0)) - expect_true(!any(duplicated(fun_result$sld))) - expect_true(all(fun_result$log_haz_link > 0)) - expect_true(!any(duplicated(fun_result$log_haz_link))) -}) +test_that("SimJointData works as expected", { + set.seed(5433) + result <- expect_silent({ + SimJointData( + design = list( + SimGroup(50, "Arm-A", "Study-X"), + SimGroup(80, "Arm-B", "Study-X") + ), + survival = SimSurvivalWeibullPH( + time_max = 3, + time_step = 1 / 365, + lambda_censor = 1 / 9000, + lambda = 1 / 200 * 365, + gamma = 0.95, + beta_cont = 0.3, + beta_cat = c("A" = 0, "B" = -0.1, "C" = 0.5) + ), + longitudinal = SimLongitudinalGSF( + sigma = 0.003, + 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, + a_phi = c(4, 6), + b_phi = c(4, 6), + link_dsld = 0, + link_ttg = 0 + ), + .silent = TRUE + ) + }) -# sim_lm_random_slope ---- + expect_class(result, "SimJointData") -test_that("sim_lm_random_slope works as expected", { - df <- data.frame( - pt = rep(c("1", "2", "3"), each = 5), - time = rep(1:5, 3), - arm = factor(rep(c("A", "B"), c(1, 2) * 5)), - study = factor("study1") + expect_s3_class(result@survival, "tbl_df") + expect_identical( + names(result@survival), + c("pt", "study", "arm", "time", "event", "cov_cont", "cov_cat") ) - result <- expect_silent(sim_lm_random_slope( - intercept = 50, - slope_mu = c(0.01, 0.03), - slope_sigma = 0.5, - sigma = 2, - link_dsld = 0.1 - )) - expect_true(is.function(result)) - set.seed(123) - fun_result <- result(df) - expect_s3_class(fun_result, "data.frame") - expect_true(all(c("pt", "time", "sld", "log_haz_link", "study", "arm") %in% names(fun_result))) - expect_identical(nrow(df), nrow(fun_result)) - expect_true(all(fun_result$sld > 0)) - expect_true(!any(duplicated(fun_result$sld))) - expect_true(all(fun_result$log_haz_link != 0)) - expect_true(any(duplicated(fun_result$log_haz_link))) -}) + expect_equal(nrow(result@survival), 50 + 80) -# simulate_joint_data ---- -test_that("simulate_joint_data works as expected", { - set.seed(5433) - times <- seq(0, 4, by = (1 / 365) / 2) - result <- expect_silent(simulate_joint_data( - design = list( - SimGroup(50, "Arm-A", "Study-X"), - SimGroup(80, "Arm-B", "Study-X") - ), - times = times, - 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.003, - 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, - a_phi = c(4, 6), - b_phi = c(4, 6), - link_dsld = 0, - link_ttg = 0 - ), - os_fun = sim_os_weibull( - lambda = 1 / 200 * 365, - gamma = 0.97 - ), - .silent = TRUE - )) - expect_type(result, "list") - expect_s3_class(result$os, "tbl_df") + expect_s3_class(result@longitudinal, "tbl_df") expect_identical( - names(result$os), - c("pt", "time", "event", "cov_cont", "cov_cat", "study", "arm") - ) - expect_equal(nrow(result$os), 50 + 80) - expect_s3_class(result$lm, "tbl_df") - expect_identical( - names(result$lm), - c("pt", "time", "sld", "study", "arm", "observed") - ) - expect_equal( - nrow(result$lm |> dplyr::filter(.data$observed)), - sum(sapply(result$os$time, function(t) sum(times < t) + 1)) + names(result@longitudinal), + c("pt", "arm", "study", "time", "sld", "observed") ) + + lm_observed_after_death <- result@longitudinal |> + dplyr::filter(.data$observed) |> + dplyr::select(pt, time) |> + dplyr::left_join( + dplyr::select(result@survival, pt, stime = time), + by = "pt" + ) |> + dplyr::filter(.data$time > .data$stime) + + expect_equal(nrow(lm_observed_after_death), 0) }) -test_that("simulate_joint_data leads to valid DataJoint with almost only default arguments", { + +test_that("SimJointData leads to valid DataJoint with almost only default arguments", { set.seed(321) - sim_data <- simulate_joint_data( - lm_fun = sim_lm_random_slope(), - os_fun = sim_os_exponential(lambda = 1 / 100), - lambda_cen = 1 / 200, + sim_data <- SimJointData( + longitudinal = SimLongitudinalGSF(), + survival = SimSurvivalExponential(time_max = 4, lambda = 365 / 100, time_step = 1 / 365), .silent = TRUE ) - os_data <- sim_data$os - long_data <- sim_data$lm |> - dplyr::arrange(time, pt) joint_data <- DataJoint( subject = DataSubject( - data = os_data, + data = sim_data@survival, subject = "pt", arm = "arm", study = "study" ), survival = DataSurvival( - data = os_data, + data = sim_data@survival, formula = Surv(time, event) ~ cov_cat + cov_cont ), longitudinal = DataLongitudinal( - data = long_data, + data = sim_data@longitudinal, formula = sld ~ time, threshold = 5 ) ) expect_true(validObject(joint_data)) }) - - -test_that("sim_os_exponential creates a dataset with the correct parameter", { - set.seed(321) - sim_data <- simulate_joint_data( - design = list( - SimGroup(200, "Arm-A", "Study-X"), - SimGroup(200, "Arm-B", "Study-X") - ), - times = seq(1, 1000), - beta_cont = 0, - beta_cat = c("A" = 0, "B" = 0, "C" = 0), - lm_fun = sim_lm_random_slope(link_dsld = 0), - os_fun = sim_os_exponential(lambda = 1 / 100) - ) - osdat <- sim_data$os - mod <- survreg(Surv(time, event) ~ 1, data = osdat, dist = "exponential") - - par_real <- -log(1 / 100) - par_obs <- coef(mod)[1] - par_obs_se <- sqrt(vcov(mod)[1, 1]) - z_score <- (par_obs - par_real) / par_obs_se - z_score - expect_true(abs(z_score) < qnorm(0.7)) -}) - - -test_that("sim_os_weibull creates a dataset with the correct parameter", { - set.seed(2434) - lambda_real <- 1 / 100 - gamma_real <- 0.9 - sim_data <- simulate_joint_data( - design = list( - SimGroup(200, "Arm-A", "Study-X"), - SimGroup(200, "Arm-B", "Study-X") - ), - times = seq(1, 1000), - beta_cont = 0.2, - beta_cat = c("A" = 0, "B" = -0.6, "C" = 0.6), - lm_fun = sim_lm_random_slope(link_dsld = 0), # No link - os_fun = sim_os_weibull(lambda = lambda_real, gamma = gamma_real), - .silent = TRUE - ) - osdat <- sim_data$os - mod <- survreg(Surv(time, event) ~ cov_cont + cov_cat, data = osdat, dist = "weibull") - scale <- mod$scale - - ## Check covariate coefficients - alpha_real <- -c(0.2, -0.6, 0.6) * scale # Convert from PH to scale-location formulation - alpha_obs <- coef(mod)[-1] - alpha_se <- sqrt(diag(vcov(mod)[2:4, 2:4])) - z_score <- (alpha_obs - alpha_real) / alpha_se - expect_true(all(abs(z_score) < qnorm(0.9))) - - ## Check Lambda parameter - int_real <- -log(lambda_real) * (scale) - int_obs <- coef(mod)[1] - int_obs_se <- sqrt(vcov(mod)[1, 1]) - z_score <- (int_real - int_obs) / int_obs_se - expect_true(abs(z_score) < qnorm(0.9)) - - ## Check Scale Parameter - log_scale_real <- log(1 / gamma_real) - log_scale_obs <- log(scale) - log_scale_se <- sqrt(vcov(mod)["Log(scale)", "Log(scale)"]) - z_score <- (log_scale_obs - log_scale_real) / log_scale_se - expect_true(abs(z_score) < qnorm(0.9)) -}) - - -test_that("simulate_joint_data correctly generates an intercept per study", { - set.seed(3521) - sim_data <- simulate_joint_data( - times = seq(1, 1000) / 365, - design = list( - SimGroup(200, "Arm-A", "Study-X"), - SimGroup(200, "Arm-A", "Study-Y") - ), - lm_fun = sim_lm_random_slope( - intercept = c(50, 70), - slope_mu = 10, - slope_sigma = 0.5, - sigma = 2, - link_dsld = 0, - link_identity = 0 - ), - os_fun = sim_os_exponential(lambda = 1 / 100), - lambda_cen = 1 / 200, - .silent = TRUE - ) - - dat_lm <- sim_data$lm |> - dplyr::filter(time %in% (c(10, 100, 200, 500, 700, 900) / 365)) - - mod <- lme4::lmer( - dat = dat_lm, - formula = sld ~ 0 + study + time + (0 + time | pt), - ) - - ests <- lme4::fixef(mod) - ests_se <- sqrt(diag(as.matrix(vcov(mod)))) - z_score <- (ests - c(50, 70, 10)) / ests_se - expect_true(all(abs(z_score) < qnorm(0.99))) -}) - - -test_that("SimGroup() works as expected", { - sim_group <- SimGroup(100, "Arm-A", "Study-X") - expect_s4_class(sim_group, "SimGroup") - expect_equal(sim_group@n, 100) - expect_equal(sim_group@arm, "Arm-A") - expect_equal(sim_group@study, "Study-X") - - expect_error( - SimGroup(c(100, 200), "Arm-A", "Study-X"), - regexp = "`n` must be a length 1 integer" - ) - expect_error( - SimGroup(100, c("Arm-A", "Arm-b"), "Study-X"), - regexp = "`arm` must be a length 1 string" - ) - expect_error( - SimGroup(100, "Arm-A", c("Study-X", "Study-Z")), - regexp = "`study` must be a length 1 string" - ) -}) - - -test_that("Can simulate data with negative times", { - - - set.seed(5433) - times <- seq(-2, 4, by = (1 / 365) / 2) - result <- expect_silent() - - - - -}) - - -SimJointData( - design = list( - SimGroup(50, "Arm-A", "Study-X"), - SimGroup(80, "Arm-B", "Study-X") - ), - survival = SimSurvivalExponential( - lambda = 1 / 400 - ), - longitudinal = SimLongitudinalRandomSlope( - times = c(-50, -20, 0, 150, 250, 400) - ) -) - diff --git a/tests/testthat/test-SimLongitudinalGSF.R b/tests/testthat/test-SimLongitudinalGSF.R new file mode 100644 index 00000000..89674f16 --- /dev/null +++ b/tests/testthat/test-SimLongitudinalGSF.R @@ -0,0 +1,62 @@ + +test_that("SimLongitudinalGSF works as expected", { + sim <- SimLongitudinalGSF( + times = c(-100, 0, 50), + sigma = 0.00000001, + mu_b = c(60, 90), + mu_s = c(0.6, 0.4), + mu_g = c(0.25, 0.35), + omega_b = 0.000000001, + omega_s = 0.000000001, + omega_g = 0.000000001, + a_phi = c(99999999, 11111111), + b_phi = c(11111111, 99999999), + link_dsld = 0, + link_ttg = 0, + link_identity = 0 + ) + expect_true(is(sim, "SimLongitudinalGSF")) + + subs <- dplyr::tibble( + pt = c("1", "2", "3"), + arm = factor(c("A", "B", "A")), + study = factor(c("study1", "study1", "study2")) + ) + res_subs <- sampleSubjects(sim, subs) + expect_equal(res_subs$pt, subs$pt) + expect_equal(res_subs$arm, subs$arm) + expect_equal(res_subs$study, subs$study) + expect_equal(res_subs$psi_b, c(60, 60, 90), tolerance = 0.00001) + expect_equal(res_subs$psi_s, c(0.6, 0.4, 0.6), tolerance = 0.00001) + expect_equal(res_subs$psi_g, c(0.25, 0.35, 0.25), tolerance = 0.0001) + expect_equal(res_subs$psi_phi, c(0.9, 0.1, 0.9), tolerance = 0.01) + expect_equal(nrow(res_subs), nrow(subs)) + expect_equal( + names(res_subs), + c("pt", "arm", "study", "psi_b", "psi_s", "psi_g", "psi_phi") + ) + + + tdat <- purrr::map( + sim@times, + \(time) { + res_subs$time <- time + res_subs + } + ) |> + dplyr::bind_rows() + + res_obvs <- sampleObservations(sim, tdat) + expect_equal(res_obvs$pt, tdat$pt) + expect_equal(res_obvs$arm, tdat$arm) + expect_equal(res_obvs$study, tdat$study) + expect_equal(res_obvs$time, tdat$time) + expect_equal(nrow(res_obvs), nrow(tdat)) + expect_equal( + names(res_obvs), + c( + "pt", "arm", "study", "psi_b", "psi_s", "psi_g", "psi_phi", "time", + "mu_sld", "dsld", "ttg", "sld", "log_haz_link" + ) + ) +}) diff --git a/tests/testthat/test-SimLongitudinalRandomSlope.R b/tests/testthat/test-SimLongitudinalRandomSlope.R new file mode 100644 index 00000000..46b588ce --- /dev/null +++ b/tests/testthat/test-SimLongitudinalRandomSlope.R @@ -0,0 +1,89 @@ + +test_that("SimLongitudinalRandomSlope works as expected", { + sim <- SimLongitudinalRandomSlope( + times = c(-100, 0, 50), + intercept = c(100, 50), + slope_mu = c(10, 30), + slope_sigma = 0.0000001, + sigma = 2, + link_dsld = 0, + link_identity = 0 + ) + expect_true(is(sim, "SimLongitudinalRandomSlope")) + + subs <- dplyr::tibble( + pt = c("1", "2", "3"), + arm = factor(c("A", "B", "A")), + study = factor(c("study1", "study1", "study2")) + ) + res_subs <- sampleSubjects(sim, subs) + expect_equal(res_subs$pt, subs$pt) + expect_equal(res_subs$arm, subs$arm) + expect_equal(res_subs$study, subs$study) + expect_equal(res_subs$intercept, c(100, 100, 50)) + expect_equal(res_subs$slope_ind, c(10, 30, 10), tolerance = 0.0000001) + expect_equal(nrow(res_subs), nrow(subs)) + expect_equal( + names(res_subs), + c("pt", "arm", "study", "intercept", "slope_ind") + ) + + + tdat <- purrr::map( + sim@times, + \(time) { + res_subs$time <- time + res_subs + } + ) |> + dplyr::bind_rows() + + res_obvs <- sampleObservations(sim, tdat) + expect_equal(res_obvs$pt, tdat$pt) + expect_equal(res_obvs$arm, tdat$arm) + expect_equal(res_obvs$study, tdat$study) + expect_equal(res_obvs$time, tdat$time) + expect_equal(nrow(res_obvs), nrow(tdat)) + expect_equal( + names(res_obvs), + c( + "pt", "arm", "study", "intercept", "slope_ind", "time", + "err", "sld_mu", "sld", "log_haz_link" + ) + ) + expect_equal( + res_obvs$sld_mu + res_obvs$err, + res_obvs$sld + ) +}) + +test_that("SimLongitudinalRandomSlope correctly generates a dataset with known parameters", { + set.seed(3521) + sim_data <- SimJointData( + design = list( + SimGroup(200, "Arm-A", "Study-X"), + SimGroup(200, "Arm-A", "Study-Y") + ), + longitudinal = SimLongitudinalRandomSlope( + times = c(-100, -50, -10, 0, 10, 50, 150, 200, 250, 301, 425, 532), + intercept = c(50, 70), + slope_mu = 10, + slope_sigma = 0.5, + sigma = 2, + link_dsld = 0, + link_identity = 0 + ), + survival = SimSurvivalExponential(lambda = 1 / 100, time_max = 10, time_step = 1), + .silent = TRUE + ) + + mod <- lme4::lmer( + dat = sim_data@longitudinal, + formula = sld ~ 0 + study + time + (0 + time | pt), + ) + + ests <- lme4::fixef(mod) + ests_se <- sqrt(diag(as.matrix(vcov(mod)))) + z_score <- (ests - c(50, 70, 10)) / ests_se + expect_true(all(abs(z_score) < qnorm(0.99))) +}) diff --git a/tests/testthat/test-SimLongitudinalSteinFojo.R b/tests/testthat/test-SimLongitudinalSteinFojo.R new file mode 100644 index 00000000..c831e323 --- /dev/null +++ b/tests/testthat/test-SimLongitudinalSteinFojo.R @@ -0,0 +1,59 @@ + +test_that("SimLongitudinalSteinFojo works as expected", { + sim <- SimLongitudinalSteinFojo( + times = c(-100, 0, 50), + sigma = 0.00000001, + mu_b = c(60, 90), + mu_s = c(0.6, 0.4), + mu_g = c(0.25, 0.35), + omega_b = 0.000000001, + omega_s = 0.000000001, + omega_g = 0.000000001, + link_dsld = 0, + link_ttg = 0, + link_identity = 0 + ) + expect_true(is(sim, "SimLongitudinalSteinFojo")) + + subs <- dplyr::tibble( + pt = c("1", "2", "3"), + arm = factor(c("A", "B", "A")), + study = factor(c("study1", "study1", "study2")) + ) + res_subs <- sampleSubjects(sim, subs) + expect_equal(res_subs$pt, subs$pt) + expect_equal(res_subs$arm, subs$arm) + expect_equal(res_subs$study, subs$study) + expect_equal(res_subs$psi_b, c(60, 60, 90), tolerance = 0.00001) + expect_equal(res_subs$psi_s, c(0.6, 0.4, 0.6), tolerance = 0.00001) + expect_equal(res_subs$psi_g, c(0.25, 0.35, 0.25), tolerance = 0.0001) + expect_equal(nrow(res_subs), nrow(subs)) + expect_equal( + names(res_subs), + c("pt", "arm", "study", "psi_b", "psi_s", "psi_g") + ) + + + tdat <- purrr::map( + sim@times, + \(time) { + res_subs$time <- time + res_subs + } + ) |> + dplyr::bind_rows() + + res_obvs <- sampleObservations(sim, tdat) + expect_equal(res_obvs$pt, tdat$pt) + expect_equal(res_obvs$arm, tdat$arm) + expect_equal(res_obvs$study, tdat$study) + expect_equal(res_obvs$time, tdat$time) + expect_equal(nrow(res_obvs), nrow(tdat)) + expect_equal( + names(res_obvs), + c( + "pt", "arm", "study", "psi_b", "psi_s", "psi_g", "time", + "mu_sld", "dsld", "ttg", "sld", "log_haz_link" + ) + ) +}) diff --git a/tests/testthat/test-SimSurvival.R b/tests/testthat/test-SimSurvival.R new file mode 100644 index 00000000..5af49391 --- /dev/null +++ b/tests/testthat/test-SimSurvival.R @@ -0,0 +1,83 @@ + +test_that("SimSurvivalExponential creates a dataset with the correct parameter", { + set.seed(321) + sim_data <- SimJointData( + design = list( + SimGroup(200, "Arm-A", "Study-X"), + SimGroup(200, "Arm-B", "Study-X") + ), + longitudinal = SimLongitudinalRandomSlope(times = c(0)), + survival = SimSurvivalExponential( + time_max = 2000, + time_step = 1, + lambda_censor = 1 / 5000, + lambda = 1 / 100, + beta_cont = 1.2, + beta_cat = c("A" = 0, "B" = -0.6, "C" = 0.9) + ), + .silent = TRUE + ) + + mod <- survival::survreg( + survival::Surv(time, event) ~ cov_cont + cov_cat, + data = sim_data@survival, + dist = "exponential" + ) + + par_real <- c(-log(1 / 100), 1.2, -0.6, 0.9) + par_obs <- c(coef(mod)[1], -coef(mod)[-1]) + par_obs_se <- sqrt(diag(vcov(mod))) + z_score <- (par_obs - par_real) / par_obs_se + expect_true(all(abs(z_score) < qnorm(0.99))) +}) + + +test_that("SimSurvivalWeibullPH creates a dataset with the correct parameter", { + set.seed(1310) + lambda_real <- 1 / 100 + gamma_real <- 0.9 + sim_data <- SimJointData( + design = list( + SimGroup(400, "Arm-A", "Study-X"), + SimGroup(400, "Arm-B", "Study-X") + ), + longitudinal = SimLongitudinalRandomSlope(times = c(0)), + survival = SimSurvivalWeibullPH( + time_max = 2000, + time_step = 1, + lambda_censor = 1 / 5000, + lambda = lambda_real, + gamma = gamma_real, + beta_cont = 1.2, + beta_cat = c("A" = 0, "B" = -0.6, "C" = 0.9) + ), + .silent = TRUE + ) + mod <- survival::survreg( + survival::Surv(time, event) ~ cov_cont + cov_cat, + data = sim_data@survival, + dist = "weibull" + ) + scale <- mod$scale + + ## Check covariate coefficients + alpha_real <- -c(1.2, -0.6, 0.9) * scale # Convert from PH to scale-location formulation + alpha_obs <- coef(mod)[-1] + alpha_se <- sqrt(diag(vcov(mod)[2:4, 2:4])) + z_score <- (alpha_obs - alpha_real) / alpha_se + expect_true(all(abs(z_score) < qnorm(0.9))) + + ## Check Lambda parameter + int_real <- -log(lambda_real) * (scale) + int_obs <- coef(mod)[1] + int_obs_se <- sqrt(vcov(mod)[1, 1]) + z_score <- (int_real - int_obs) / int_obs_se + expect_true(abs(z_score) < qnorm(0.9)) + + ## Check Scale Parameter + log_scale_real <- log(1 / gamma_real) + log_scale_obs <- log(scale) + log_scale_se <- sqrt(vcov(mod)["Log(scale)", "Log(scale)"]) + z_score <- (log_scale_obs - log_scale_real) / log_scale_se + expect_true(abs(z_score) < qnorm(0.9)) +}) diff --git a/tests/testthat/test-SurvivalExponential.R b/tests/testthat/test-SurvivalExponential.R index 466fc20e..cc59290c 100644 --- a/tests/testthat/test-SurvivalExponential.R +++ b/tests/testthat/test-SurvivalExponential.R @@ -31,15 +31,17 @@ test_that("SurvivalExponential can recover true parameter (including covariates) ) ) - mp <- sampleStanModel( - jm, - data = jdat, - iter_sampling = 400, - iter_warmup = 400, - chains = 1, - refresh = 0, - parallel_chains = 1 - ) + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_sampling = 400, + iter_warmup = 400, + chains = 1, + refresh = 0, + parallel_chains = 1 + ) + }) # Variables to extract (order important) vars <- c("sm_exp_lambda", "beta_os_cov") diff --git a/tests/testthat/test-SurvivalLoglogistic.R b/tests/testthat/test-SurvivalLoglogistic.R index 6fd0c668..5f6b4859 100644 --- a/tests/testthat/test-SurvivalLoglogistic.R +++ b/tests/testthat/test-SurvivalLoglogistic.R @@ -8,7 +8,7 @@ test_that("sim_os_loglogistic() is consistant with flexsurv", { t <- c(1, 4, 50, 200, 600) expect_equal( log(flexsurv::hllogis(t, scale = 400, shape = 2)), - sim_os_loglogistic(a = 400, b = 2)(t) + SimSurvivalLogLogistic(a = 400, b = 2)@loghazard(t) ) }) @@ -56,15 +56,17 @@ test_that("SurvivalLogLogistic can recover known values", { ) ) - mp <- sampleStanModel( - jm, - data = jdat, - iter_warmup = 300, - iter_sampling = 400, - chains = 1, - refresh = 0, - parallel_chains = 1 - ) + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_warmup = 300, + iter_sampling = 400, + chains = 1, + refresh = 0, + parallel_chains = 1 + ) + }) # Variables to extract (order important) vars <- c("sm_loglogis_a", "sm_loglogis_b", "beta_os_cov") diff --git a/tests/testthat/test-SurvivalQuantities.R b/tests/testthat/test-SurvivalQuantities.R index 7d403f85..9946763c 100644 --- a/tests/testthat/test-SurvivalQuantities.R +++ b/tests/testthat/test-SurvivalQuantities.R @@ -8,7 +8,7 @@ test_that("SurvivalQuantities and autoplot.SurvivalQuantities works as expected" survsamps <- SurvivalQuantities( test_data_1$jsamples, - list("a" = c("pt_00001", "pt_00002")), + list("a" = c("pt_001", "pt_002")), c(10, 20, 200, 300) ) preds <- summary(survsamps) @@ -30,7 +30,7 @@ test_that("SurvivalQuantities and autoplot.SurvivalQuantities works as expected" survsamps <- SurvivalQuantities( test_data_1$jsamples, - c("pt_00001", "pt_00003") + c("pt_001", "pt_003") ) preds <- preds <- summary(survsamps) expect_equal(nrow(preds), 2 * 201) # 201 default time points for 2 subjects @@ -40,18 +40,18 @@ test_that("SurvivalQuantities and autoplot.SurvivalQuantities works as expected" # Check that the relationship between the quantitites is preservered e.g. # that `surv = exp(-cumhaz)` preds1 <- SurvivalQuantities( - test_data_1$jsamples, "pt_00001", c(200, 300) + test_data_1$jsamples, "pt_001", c(200, 300) ) |> summary() preds2 <- SurvivalQuantities( - test_data_1$jsamples, "pt_00001", c(200, 300), + test_data_1$jsamples, "pt_001", c(200, 300), type = "cumhaz" ) |> summary() preds3 <- SurvivalQuantities( - test_data_1$jsamples, "pt_00001", c(200, 300), + test_data_1$jsamples, "pt_001", c(200, 300), type = "haz" ) |> summary() preds4 <- SurvivalQuantities( - test_data_1$jsamples, "pt_00001", c(200, 300), + test_data_1$jsamples, "pt_001", c(200, 300), type = "loghaz" ) |> summary() expect_equal(unique(preds1$type), "surv") @@ -68,7 +68,7 @@ test_that("SurvivalQuantities and autoplot.SurvivalQuantities works as expected" test_that("autoplot.SurvivalSamples works as expected", { samps <- SurvivalQuantities( test_data_1$jsamples, - c("pt_00011", "pt_00061") + c("pt_011", "pt_061") ) p <- autoplot( samps, @@ -85,7 +85,7 @@ test_that("autoplot.SurvivalSamples works as expected", { samps <- SurvivalQuantities( test_data_1$jsamples, - groups = list("a" = c("pt_00011", "pt_00061"), "b" = c("pt_00001", "pt_00002")), + groups = list("a" = c("pt_011", "pt_061"), "b" = c("pt_001", "pt_002")), type = "loghaz", time_grid = c(10, 20, 50, 200) ) diff --git a/tests/testthat/test-SurvivalWeibullPH.R b/tests/testthat/test-SurvivalWeibullPH.R index 050c3e51..ee628328 100644 --- a/tests/testthat/test-SurvivalWeibullPH.R +++ b/tests/testthat/test-SurvivalWeibullPH.R @@ -59,15 +59,17 @@ test_that("SurvivalWeibullPH can recover known values", { ) ) - mp <- sampleStanModel( - jm, - data = jdat, - iter_warmup = 300, - iter_sampling = 400, - chains = 1, - refresh = 0, - parallel_chains = 1 - ) + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_warmup = 300, + iter_sampling = 400, + chains = 1, + refresh = 0, + parallel_chains = 1 + ) + }) # Variables to extract (order important) vars <- c("sm_weibull_ph_lambda", "sm_weibull_ph_gamma", "beta_os_cov") diff --git a/tests/testthat/test-brierScore.R b/tests/testthat/test-brierScore.R index a0e12f5e..5a3ede15 100644 --- a/tests/testthat/test-brierScore.R +++ b/tests/testthat/test-brierScore.R @@ -17,7 +17,7 @@ test_that("brierScore(SurvivalQuantities) returns same results as survreg", { mp <- test_data_1$jsamples ### Get our internal bayesian estimate - t_grid <- c(1, 50, 75, 350, 860) + t_grid <- c(1, 25, 60, 425, 750) sq <- SurvivalQuantities( mp, time_grid = t_grid, diff --git a/tests/testthat/test-extract_quantities.R b/tests/testthat/test-extract_quantities.R index b9202d0c..f4e3b008 100644 --- a/tests/testthat/test-extract_quantities.R +++ b/tests/testthat/test-extract_quantities.R @@ -57,10 +57,12 @@ test_that("extract_quantities() works as expected", { ) stan_fitted <- posterior::as_draws_matrix(list(null_y_mean = 1:2)) - gq <- mod$generate_quantities( - data = stan_data, - fitted_params = stan_fitted - ) + gq <- run_quietly({ + mod$generate_quantities( + data = stan_data, + fitted_params = stan_fitted + ) + }) run_test <- function(vals, keyword) { actual <- extract_quantities(gq, type = keyword) diff --git a/tests/testthat/test-misc_models.R b/tests/testthat/test-misc_models.R index 7a372058..9e1a2b10 100644 --- a/tests/testthat/test-misc_models.R +++ b/tests/testthat/test-misc_models.R @@ -9,7 +9,7 @@ test_that("Longitudinal Model doesn't print sampler rejection messages", { mp <- capture_messages({ devnull_out <- capture.output({ devnull_model <- sampleStanModel( - jm, + test_data_1$jmodel, data = test_data_1$jdata, iter_sampling = 3, iter_warmup = 3, diff --git a/tests/testthat/test-model_multi_chain.R b/tests/testthat/test-model_multi_chain.R index 8ab587d5..38731f84 100644 --- a/tests/testthat/test-model_multi_chain.R +++ b/tests/testthat/test-model_multi_chain.R @@ -1,9 +1,15 @@ test_that("Can recover known distribution parameters from random slope model when using multiple chains", { jm <- JointModel( - longitudinal = LongitudinalRandomSlope(), - survival = SurvivalExponential(), - link = link_dsld() + longitudinal = LongitudinalRandomSlope( + intercept = prior_normal(30, 5), + slope_sigma = prior_lognormal(log(0.2), sigma = 0.5), + sigma = prior_lognormal(log(3), sigma = 0.5) + ), + survival = SurvivalExponential( + lambda = prior_lognormal(log(1 / 200), 0.5) + ), + link = link_dsld(prior = prior_normal(0.1, 0.2)) ) set.seed(3251) @@ -52,15 +58,17 @@ test_that("Can recover known distribution parameters from random slope model whe ) ) - mp <- sampleStanModel( - jm, - data = jdat, - iter_sampling = 400, - iter_warmup = 200, - chains = 3, - refresh = 0, - parallel_chains = 3 - ) + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_sampling = 400, + iter_warmup = 200, + chains = 3, + parallel_chains = 3 + ) + }) + vars <- c( "sm_exp_lambda" = 1 / 200,