tests/testthat/test-sensitivity-calculation.R

# Save old options
old_opts <- options()

options(
  tibble.width = Inf,
  pillar.min_title_chars = Inf,
  pillar.sigfig = 4,
  digits = 4,
  scipen = 999
)

# Single output path ------------------------------------------------------

# Load simulation and set paths for tests
simPath <- system.file("extdata", "Aciclovir.pkml", package = "ospsuite")
simulation <- loadSimulation(simPath)
outputPaths <- "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)"
parameterPaths <- c(
  "Aciclovir|Lipophilicity",
  "Applications|IV 250mg 10min|Application_1|ProtocolSchemaItem|Dose",
  "Neighborhoods|Kidney_pls_Kidney_ur|Aciclovir|Glomerular Filtration-GFR|GFR fraction"
)
variationRange <- c(0.1, 2, 20) # 1.0 is deliberately left out for testing

set.seed(123)
results <- sensitivityCalculation(
  simulation = simulation,
  outputPaths = outputPaths,
  parameterPaths = parameterPaths,
  variationRange = variationRange
)

# Validate outputPaths ----------------------------------------------------

test_that("sensitivityCalculation fails with invalid `outputPaths`", {
  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = NULL,
      parameterPaths = parameterPath
    ),
    "The argument `outputPaths` cannot be NULL"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = c(1, 2, 3),
      parameterPaths = parameterPath
    ),
    'The argument `outputPaths` must be of type "character"'
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = "",
      parameterPaths = parameterPath
    ),
    "The argument `outputPaths` contains empty strings"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = c("", "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)"),
      parameterPaths = parameterPath
    ),
    "The argument `outputPaths` contains empty strings"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = rep("Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)", 2),
      parameterPaths = parameterPath
    ),
    "The argument `outputPaths` must contain only distinct values"
  )
})

# Validate parameterPaths -------------------------------------------------

test_that("sensitivityCalculation fails with invalid `parameterPaths`", {
  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = NULL
    ),
    "The argument `parameterPaths` cannot be NULL"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = c(1, 2, 3)
    ),
    'The argument `parameterPaths` must be of type "character"'
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = ""
    ),
    "The argument `parameterPaths` contains empty strings"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = c(
        "Aciclovir|Lipophilicity",
        "",
        "Neighborhoods|Kidney_pls_Kidney_ur|Aciclovir|Glomerular Filtration-GFR|GFR fraction"
      )
    ),
    "The argument `parameterPaths` contains empty strings"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = c(parameterPaths, parameterPaths[1])
    ),
    "The argument `parameterPaths` must contain only distinct values"
  )
})

# Validate pkParameters ---------------------------------------------------

test_that("sensitivityCalculation fails with invalid `pkParameters`", {
  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      pkParameters = c(1, 2, 3),
      outputPaths = outputPaths,
      parameterPaths = parameterPaths
    ),
    'The argument `pkParameters` must be of type "character"'
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      pkParameters = "",
      outputPaths = outputPaths,
      parameterPaths = parameterPaths
    ),
    "The argument `pkParameters` contains empty strings"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      pkParameters = c("", "C_max"),
      outputPaths = outputPaths,
      parameterPaths = parameterPaths
    ),
    "The argument `pkParameters` contains empty strings"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      pkParameters = c("C_max", "C_max"),
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = parameterPaths
    ),
    "The argument `pkParameters` must contain only distinct values"
  )

  expect_message(
    sensitivityCalculation(
      simulation = simulation,
      pkParameters = c("C_max", "abc", "xyz"),
      outputPaths = "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
      parameterPaths = parameterPaths
    ),
    "Following PK parameters are specified but were not calculated:\nabc\nxyz\n",
    fixed = TRUE
  )
})

test_that("sensitivityCalculation works with user-defined `pkParameters`", {
  # Create a new parameter based on the standard AUC parameter
  myAUC <- addUserDefinedPKParameter(
    name = "MyAUC",
    standardPKParameter = StandardPKParameter$AUC_tEnd
  )

  results <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = variationRange,
    pkParameters = c("C_max", "MyAUC")
  )

  expect_true(isOfType(results, "SensitivityCalculation"))
  expect_equal(unique(results$pkData$PKParameter), c("C_max", "MyAUC"))
})

# Validate variationRange -------------------------------------------------

test_that("sensitivityCalculation fails with invalid `variationRange`", {
  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c("x", "y", "z")
    ),
    "argument 'variationRange' is of type 'character', but expected 'numeric, or integer'!"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = list(c(0.1, 1, 10), c("x", "y", "z"), c(0.1, 1, 10)),
    ),
    "argument 'variationRange' is of type 'character', but expected 'numeric, or integer'!"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = list(c(0.1, 1, 10), c(0.1, 1, 10)),
    ),
    "`variationRange` must be either a vector or a list equal to the length of `parameterPaths`"
  )
})

# Validate customOutputFunctions ------------------------------------------

test_that("sensitivityCalculation fails with invalid `customOutputFunctions`", {
  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      customOutputFunctions = "invalid"
    ),
    "argument 'customOutputFunctions' is of type 'character', but expected 'list'!"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      customOutputFunctions = list("invalid" = "function")
    ),
    "argument 'customOutputFunctions' is of type 'list', but expected 'function'!"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      customOutputFunctions = list(
        function(x) x, function(y) y
      )
    ),
    "argument 'customOutputFunctions' is not a named list!"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      customOutputFunctions = list(
        "funA" = function(x) x, function(y) y, "funC" = function(x) x^2
      )
    ),
    "argument 'customOutputFunctions' is not a named list!"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      customOutputFunctions = list("invalid" = function(x, y, z) {
        x / y * z
      })
    ),
    "The user-defined function must have either 'x', 'y', or both 'x' and 'y'"
  )

  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      customOutputFunctions = list("invalid" = \(x, y, z) x / y * z)
    ),
    "The user-defined function must have either 'x', 'y', or both 'x' and 'y'"
  )
})

# Check SensitivityCalculation object -------------------------------------

test_that("sensitivityCalculation returns a valid `SensitivityCalculation` object", {
  expect_true(isOfType(results, "SensitivityCalculation"))

  expect_equal(
    length(results$simulationResults), length(parameterPaths)
  )

  expect_equal(
    length(results$simulationResults[[1]]), length(variationRange) + 1L
  )

  expect_equal(
    length(results$parameterPaths), length(parameterPaths)
  )
})

# Test variationRange -----------------------------------------------------

test_that("sensitivityCalculation works with absolute values of `variationRange`", {
  variationRangeAbs <- list(
    -0.097 * variationRange,
    0.00025 * variationRange,
    1 * variationRange
  )

  set.seed(123)
  resultsAbs <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = variationRangeAbs,
    variationType = "absolute"
  )

  expect_equal(results$pkData, resultsAbs$pkData)
})

# Check PK tidy data ------------------------------------------------------

test_that("sensitivityCalculation returns correct PK parameters dataframe", {
  expect_equal(
    colnames(results$pkData),
    c(
      "OutputPath", "ParameterPath", "ParameterFactor", "ParameterValue",
      "ParameterUnit", "ParameterPathUserName", "PKParameter", "PKParameterValue",
      "PKPercentChange", "Unit", "SensitivityPKParameter"
    )
  )
})

test_that("sensitivityCalculation PK parameters tidy dataframe is as expected", {
  # base scaling should be present
  expect_equal(unique(results$pkData$ParameterFactor), c(0.1, 1, 2, 20))

  set.seed(123)
  df1_pk <- summarizer(results$pkData, parameterPaths[1])
  expect_snapshot(df1_pk)

  set.seed(123)
  df2_pk <- summarizer(results$pkData, parameterPaths[2])
  expect_snapshot(df2_pk)

  set.seed(123)
  df3_pk <- summarizer(results$pkData, parameterPaths[3])
  expect_snapshot(df3_pk)
})

# Test customOutputFunctions ----------------------------------------------

test_that("sensitivityCalculation returns expected results with single custom function", {
  # list with custom function using only `y` parameter
  customFunctions <- list("minmax" = function(y) min(y[y != 0]) / max(y))
  customFunctionsLambda <- list("minmax" = \(y) min(y[y != 0]) / max(y))

  results <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    customOutputFunctions = customFunctions,
    variationRange = variationRange
  )

  resultsLambda <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    customOutputFunctions = customFunctionsLambda,
    variationRange = variationRange
  )

  expect_equal(results$pkData, resultsLambda$pkData)

  customPKData <- dplyr::filter(
    results$pkData,
    PKParameter %in% names(customFunctions)
  )
  expect_snapshot(customPKData)
})

test_that("sensitivityCalculation returns expected results with multiple custom functions", {
  # List with multiple custom functions using `x` and `y` parameter
  customFunctions <- list(
    "minmax" = function(y) {
      max(y) / min(y[y != 0])
    },
    "max_slope" = function(x, y) {
      slopes <- diff(y) / diff(x)
      max(slopes)
    }
  )

  # Perform the sensitivity calculation
  results <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    customOutputFunctions = customFunctions,
    variationRange = variationRange
  )

  # Filter the custom PK data
  customPKData <- results$pkData %>%
    dplyr::filter(PKParameter %in% names(customFunctions))

  # Expect snapshot
  expect_snapshot(customPKData)
})

# Test saving to xlsx file ------------------------------------------------

test_that("sensitivityCalculation saves PK data to xlsx file", {
  path <- "mydata.xlsx"

  set.seed(123)
  results <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = c(0.1, 2, 20),
    saOutputFilePath = path
  )

  expect_true(file.exists(path))

  on.exit(unlink(path))
})

test_that("sensitivityCalculation errors if file extension is incorrect", {
  path <- "mydata.csv"

  set.seed(123)
  expect_error(
    sensitivityCalculation(
      simulation = simulation,
      outputPaths = outputPaths,
      parameterPaths = parameterPaths,
      variationRange = c(0.1, 2, 20),
      saOutputFilePath = path
    ),
    "Provided file has extension 'csv', while 'xlsx' was expected instead."
  )
})

# Check PK wide data ------------------------------------------------------

pkDataWideColumns <- c(
  "OutputPath", "ParameterPath", "ParameterFactor", "ParameterValue", "ParameterUnit",
  "ParameterPathUserName", "C_max", "C_max_norm", "C_max_Unit", "C_max_norm_Unit",
  "C_max_PKPercentChange", "C_max_norm_PKPercentChange", "C_max_Sensitivity",
  "C_max_norm_Sensitivity", "t_max", "t_max_Unit", "t_max_PKPercentChange",
  "t_max_Sensitivity", "AUC_tEnd", "AUC_tEnd_norm", "AUC_tEnd_Unit", "AUC_tEnd_norm_Unit",
  "AUC_tEnd_PKPercentChange", "AUC_tEnd_norm_PKPercentChange", "AUC_tEnd_Sensitivity",
  "AUC_tEnd_norm_Sensitivity", "AUC_inf", "AUC_inf_norm", "AUC_inf_Unit",
  "AUC_inf_norm_Unit", "AUC_inf_PKPercentChange", "AUC_inf_norm_PKPercentChange",
  "AUC_inf_Sensitivity", "AUC_inf_norm_Sensitivity", "CL", "FractionAucLastToInf",
  "CL_Unit", "FractionAucLastToInf_Unit", "CL_PKPercentChange", "FractionAucLastToInf_PKPercentChange",
  "CL_Sensitivity", "FractionAucLastToInf_Sensitivity", "MRT",
  "MRT_Unit", "MRT_PKPercentChange", "MRT_Sensitivity", "Thalf",
  "Thalf_Unit", "Thalf_PKPercentChange", "Thalf_Sensitivity", "Vss",
  "Vss_Unit", "Vss_PKPercentChange", "Vss_Sensitivity", "Vd", "Vd_Unit",
  "Vd_PKPercentChange", "Vd_Sensitivity"
)

test_that("sensitivityCalculation converts output to wide format as expected", {
  set.seed(123)
  results2 <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = c(0.1, 2, 20),
    pkParameters = NULL
  )
  pkDataWide <- esqlabsR:::.convertToWide(results2$pkData)

  expect_equal(dim(pkDataWide), c(12L, 58L))
  expect_equal(colnames(pkDataWide), pkDataWideColumns)
})

test_that("sensitivityCalculation converts output to wide format as expected with `customOutputFunctions`", {
  customFunctions <- list(
    "minmax" = function(y) {
      max(y) / min(y[y != 0])
    },
    "max_slope" = function(x, y) {
      slopes <- diff(y) / diff(x)
      max(slopes)
    }
  )
  pkDataWideColumns <- c(
    pkDataWideColumns, "minmax", "minmax_Unit",
    "minmax_PKPercentChange", "minmax_Sensitivity",
    "max_slope", "max_slope_Unit", "max_slope_PKPercentChange",
    "max_slope_Sensitivity"
  )

  set.seed(123)
  results2 <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = c(0.1, 2, 20),
    customOutputFunctions = customFunctions,
    pkParameters = NULL
  )
  pkParameterNames <- c(
    names(ospsuite::StandardPKParameter),
    names(customFunctions)
  )
  pkDataWide <- esqlabsR:::.convertToWide(results2$pkData, pkParameterNames)

  expect_equal(dim(pkDataWide), c(12L, 66L))
  expect_equal(colnames(pkDataWide), pkDataWideColumns)
})

# Test sensitivityCalculation when simulation fails -----------------------

test_that("sensitivityCalculation handles simulation failure", {
  expect_warning(
    expect_warning(
      resultsSimFailure <- sensitivityCalculation(
        simulation = simulation,
        outputPaths = outputPaths,
        parameterPaths = parameterPaths,
        variationRange = c(-1, 2, 10)
      ),
      "Simulation run failed"
    )
  )

  expect_true(isOfType(resultsSimFailure, "SensitivityCalculation"))

  expect_equal(
    length(resultsSimFailure$simulationResults),
    length(parameterPaths)
  )

  expect_equal(
    length(resultsSimFailure$simulationResults[[1]]),
    length(variationRange) + 1L
  )

  expect_equal(
    # path with failed simulation
    length(resultsSimFailure$simulationResults[[2]]),
    length(variationRange)
  )

  expect_equal(
    length(resultsSimFailure$parameterPaths),
    length(parameterPaths)
  )
})

# Multiple output paths ---------------------------------------------------

simPath <- system.file("extdata", "Aciclovir.pkml", package = "ospsuite")
simulation <- loadSimulation(simPath)
outputPaths <- c(
  "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)",
  "Organism|Age",
  "Organism|ArterialBlood|Plasma|Aciclovir"
)
parameterPaths <- c(
  "Aciclovir|Lipophilicity",
  "Applications|IV 250mg 10min|Application_1|ProtocolSchemaItem|Dose",
  "Neighborhoods|Kidney_pls_Kidney_ur|Aciclovir|Glomerular Filtration-GFR|GFR fraction"
)
variationRange <- c(0.1, 5, 10)

resultsMultiple <- sensitivityCalculation(
  simulation = simulation,
  outputPaths = outputPaths,
  parameterPaths = parameterPaths,
  variationRange = variationRange
)

test_that("sensitivityCalculation extracts data for multiple output paths", {
  expect_identical(nrow(resultsMultiple$pkData), 108L)
  expect_equal(unique(resultsMultiple$pkData$OutputPath), outputPaths)
})

test_that("sensitivityCalculation applies absolute `variationRange` for multiple paths", {
  variationRangeAbs <- list(
    -0.097 * variationRange,
    0.00025 * variationRange,
    1 * variationRange
  )

  set.seed(123)
  resultsMultipleAbs <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = variationRangeAbs,
    variationType = "absolute"
  )

  expect_equal(resultsMultiple$pkData, resultsMultipleAbs$pkData)
})

test_that("sensitivityCalculation applies custom PK function with multiple output paths", {
  # list with custom function using only `y` parameter
  customFunctions <- list("minmax" = function(y) min(y[y != 0]) / max(y))

  results_multiple <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    customOutputFunctions = customFunctions,
    variationRange = c(1, 5, 10)
  )

  customPKDataMultiple <- dplyr::filter(
    results_multiple$pkData,
    PKParameter %in% names(customFunctions)
  )
  expect_snapshot(customPKDataMultiple)
})

test_that("sensitivityCalculation saves PK data to xlsx for multiple output paths", {
  path <- "mydata.xlsx"

  set.seed(123)
  resultsMultiple <- sensitivityCalculation(
    simulation = simulation,
    outputPaths = outputPaths,
    parameterPaths = parameterPaths,
    variationRange = c(0.1, 5, 10),
    saOutputFilePath = path
  )

  expect_true(file.exists(path))

  on.exit(unlink(path))
})

test_that("sensitivityCalculation handles simulation failure for multiple output paths", {
  expect_warning(
    expect_warning(
      resultsMultipleSimFailure <- sensitivityCalculation(
        simulation = simulation,
        outputPaths = outputPaths,
        parameterPaths = parameterPaths,
        variationRange = c(-1, 2, 10)
      ),
      "Simulation run failed"
    )
  )

  expect_identical(nrow(resultsMultipleSimFailure$pkData), 99L)
  expect_equal(unique(resultsMultiple$pkData$OutputPath), outputPaths)
})

# Restore old options
on.exit(options(old_opts), add = TRUE)
esqLABS/esqlabsR documentation built on April 17, 2025, 10:51 a.m.