--- title: "Running a simulation" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Running a simulation} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} # Evaluate the runtime chunks only when the OSPSuite .NET runtime initialised # successfully (native libraries + .NET). On machines without a working runtime # the code is shown but not executed, so the vignette still renders. .ospRuntimeAvailable <- isTRUE(tryCatch( requireNamespace("ospsuite", quietly = TRUE) && ospsuite::getOSPSuiteSetting("initialized"), error = function(e) FALSE )) knitr::opts_chunk$set( eval = .ospRuntimeAvailable, collapse = TRUE, comment = "#>", fig.showtext = TRUE ) ``` ## Running individual simulation and retrieving the results Once the simulation is loaded (see [Loading a simulation and accessing entities](load-get.html)), it can be run using `runSimulations()` to produce an object of the class `SimulationResults`. Keep in mind that `runSimulations()` produces a list of `SimulationResults` objects. ```{r runSim} library(ospsuite) # Load the simulation simFilePath <- system.file("extdata", "Aciclovir.pkml", package = "ospsuite") sim <- loadSimulation(simFilePath) simulationResults <- runSimulations(simulations = sim) # Extract `SimulationResults` by simulation id simulationResults <- simulationResults[[sim$id]] print(simulationResults) ``` The advantage of storing the results in a object is the option to keep different results of the same simulation produced with different settings (e.g., model parameters). Simulated time-value pairs for a specific output from the `SimulationResults`-object can be accessed with the method `getOutputValues`. The user can provide either the path(s) of the output (which can be a molecule, a parameter, or an observer), or the object(s) of the type `Molecule`, `Parameter`, or `Quantity` (for observers) with the argument `quantitiesOrPaths`. If no output is specified, all outputs available in the simulation results are returned. The paths of all available outputs can be accessed via ```{r getAllOutputSelections} simulationResults$allQuantityPaths ``` `getOutputValues` returns a list with two entries: `data` and `metadata`: * `data` is a dataframe with two predefined columns (IndividualId and Time) as well as one column for each requested output * `IndividualId` (not relevant for an individual simulation) * `Time` a vector with simulated time values (in minutes, equal for all outputs) * a vector with simulated entries for each output requested. * `metaData` is a list containing one entry for each requested output. Each entry contains information pertinent to the output such as its dimension or the unit. ```{r getOutputValues} # Get simulated results by path resultsPath <- simulationResults$allQuantityPaths[[1]] print(resultsPath) resultsData <- getOutputValues( simulationResults, quantitiesOrPaths = resultsPath ) resultsTime <- resultsData$data$Time resultsValues <- resultsData$data$`Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)` plot(resultsTime, resultsValues, type = "l") ``` The results can be stored in and imported from a *.csv file with the methods `exportResultsToCSV` and `importResultsFromCSV`. ```{r SimResultsExport} # Load and run the simulation simFilePath <- system.file("extdata", "Aciclovir.pkml", package = "ospsuite") sim <- loadSimulation(simFilePath) # `runSimulations` returns a list of `SimulationResults`. For single simulation, # we directly extract the first element, as only one object is created. simulationResults <- runSimulations(simulations = sim)[[1]] # Export to csv csvResultsPath <- system.file("extdata", "SimResults.csv", package = "ospsuite") exportResultsToCSV(results = simulationResults, filePath = csvResultsPath) # Load from csv resultsLoaded <- importResultsFromCSV( filePaths = csvResultsPath, simulation = sim ) print(resultsLoaded) ``` ## Running multiple individual simulations and retrieving the results In some cases, the user might want to run multiple simulations in parallel. This can be achieved easily by simply passing a list of simulations to the `runSimulations()` function. However, only individual simulations (i.e., the `population` argument must remain empty) are supported. By default, the simulations will be executed in parallel by using up to all cores available on the machine minus 1. So if there are 8 cores and the user simulates 4 simulations, 4 cores will be used. On the other hand, if the user simulates 10 simulations with only 8 cores available, 7 will be used and then the first 3 available. ```{r MultipleSimRun} # Load and run multiple simulations concurrently. simFilePath <- system.file("extdata", "Aciclovir.pkml", package = "ospsuite") # We load 3 times the same simulation for convenience. But in real life scenarios, # they should be different simulations sim1 <- loadSimulation(simFilePath) sim2 <- loadSimulation(simFilePath) sim3 <- loadSimulation(simFilePath) simulationResults <- runSimulations(simulations = list(sim1, sim2, sim3)) ``` The results in this case will be a named list of `SimulationResults`-object , i.e. one for each simulation. The names of the entries of the list are the ids of the simulation to which the results correspond. This way, it is easy to retrieve the correct results for the specific simulation ```{r MultipleRunSimResultsIds} # Get the id of the second simulation id <- sim2$id print(id) # get the corresponding result sim2Results <- simulationResults[[id]] print(sim2Results$simulation$id) ``` ## Adding new outputs By default, only outputs that were selected in PK-Sim or MoBi prior to the export of the simulation to `pkml` are generated. The user can add new outputs to the simulation with the method `addOutputs`. The outputs can be provided either as objects of the type(s) `Molecule`, `Parameter`, or `Quantity`, or as path strings. The output list is a property of the `simulation`. After adding or removing outputs, the corresponding simulation needs to be re-run in order to generate updated results. ```{r addOutputs} # Clear the list of generated outputs clearOutputs(sim) # Add new outputs as objects molecule <- getMolecule("Organism|Kidney|Intracellular|Aciclovir", sim) observer <- getQuantity("Organism|Lumen|Aciclovir|Fraction dissolved", sim) addOutputs(c(molecule, observer), simulation = sim) # Add new outputs as path strings addOutputs( c( "Organism|Lumen|Stomach|Aciclovir", "Organism|PeripheralVenousBlood|Aciclovir|Whole Blood (Peripheral Venous Blood)" ), simulation = sim ) # Run simulation simulationResults <- runSimulations(simulations = sim) # Retrieve all generated outputs (e.g. omitting the quantitiesOrPaths property # will return all available values) resultsData <- getOutputValues(simulationResults) # Note that "Organism|PeripheralVenousBlood|Aciclovir|Plasma (Peripheral Venous Blood)" # is not in the list of generated results any more names(resultsData$data) ``` `clearOutputs()` and `addOutputs()` can be combined with the function `setOutputs()`. ## Changing simulation intervals The simulation interval (i.e., the simulation times at which results are stored) are stored as the property `$outputSchema` of a `Simulation` object. ```{r $outputSchema} print(sim$outputSchema) ``` To change the simulation interval, the user can use one of the functions `clearOutputIntervals()`, `addOutputInterval()`, and `setOutputInterval()`. ```{r changeOutputInterval, error = TRUE, purl = FALSE} # Remove all output intervals - simulation not possible! clearOutputIntervals(simulation = sim) runSimulations(simulations = sim) # Add an interval addOutputInterval( simulation = sim, startTime = 0, endTime = 20, resolution = 60, intervalName = "highRes" ) print(sim$outputSchema) # Add a second interval addOutputInterval( simulation = sim, startTime = 30, endTime = 2000, resolution = 4, intervalName = "lowRes" ) print(sim$outputSchema) # Replace the existing interval(s) with a new one setOutputInterval( simulation = sim, startTime = 0, endTime = 2000, resolution = 4 ) print(sim$outputSchema) ``` ## Adjusting solver settings The solver settings control the numerical integration algorithm used to solve the differential equations in the simulation. These settings can be accessed and modified through the `$solver` property of a `Simulation` object. ```{r viewSolverSettings} # View current solver settings print(sim$solver) ``` In some cases, a simulation may fail to run successfully due to numerical instability or convergence issues. One common approach to resolve such issues is to reduce the solver tolerances, which makes the solver use smaller step sizes and more precise calculations. The two main tolerance parameters are: * `absTol`: Absolute tolerance - controls the accuracy of the solution in absolute terms * `relTol`: Relative tolerance - controls the accuracy of the solution relative to the magnitude of the values ```{r adjustSolverTolerances} # If a simulation fails to run, try reducing the tolerances # Reduce absolute tolerance by a factor of 10 sim$solver$absTol <- sim$solver$absTol * 1e-1 # Optionally, the relative tolerance can also be reduced # sim$solver$relTol <- sim$solver$relTol * 1e-1 # After changing solver settings, re-run the simulation simulationResults <- runSimulations(simulations = sim)[[1]] ``` Other solver settings that can be adjusted include: * `h0`: Initial time step size * `hMin`: Minimum step size allowed * `hMax`: Maximum step size allowed * `mxStep`: Maximum number of (internal) steps to be taken by the solver in its attempt to reach the next output time * `useJacobian`: Indicates whether to use the analytically calculated Jacobian matrix during calculations. When disabled, an approximation by difference quotients is used, which is much slower in the majority of cases For more details on solver properties and their effects, see the [MoBi documentation on ODE Solver Properties](https://docs.open-systems-pharmacology.org/working-with-mobi/mobi-documentation/setting-up-simulation#ode-solver-properties).