This document describes what ctsApp does that is not visible in the interface: which model inputs are fixed and cannot be changed by the user, which values the application fills in on its own, what it changes silently while the user works, and every calculation it performs itself on top of the simulation output.
It is written for a reviewer or auditor who needs to know exactly which number came from where.
What this document does not cover. It does not
describe or defend the PBPK models, the exposure-response models, or the
parameter estimates behind them. Those are properties of the underlying
PK-Sim models and of the cts package, and are documented in
the white paper and in the model files themselves. This document stops
at the application boundary: it tells you what the application feeds
into the model and what it does with what comes back. Section “Questions
this document cannot answer” at the end makes that boundary
explicit.
Version of record. All file and line references
below point at the ctsApp source at the version shown on the
application’s About page. Version numbers for ctsApp, cts,
and ospsuite are displayed there and should be recorded
alongside any result.
The application itself does not solve any differential equation and contains no PBPK model.
| Step | Performed by |
|---|---|
| Collect user inputs, validate them | ctsApp |
| Assemble a DDI project (compounds, populations, protocols, formulations, simulations) | ctsApp, via cts |
| Sample the virtual population | OSP Suite (ospsuite::createPopulationCharacteristics(),
PK-Sim) |
| Build and run the simulations | PK-Sim, via cts::run_ddi() |
| Compute standard PK parameters (Cmax, tmax, AUC, Ctrough) | OSP Suite, via cts::run_pk_analysis() |
| Unit conversion, percentiles, DDI ratios | ctsApp |
| Cavg and the two PD endpoints (Pearl Index, Ovulation Rate) | ctsApp |
| Plots and value boxes | ctsApp |
The last three rows are the only places where ctsApp produces a number of its own. They are documented in full in “Calculations performed by the application”.
Every compound, population, protocol, formulation and individual offered by the application comes from a single file shipped inside the package:
inst/data/models/ddi_oral_contraceptives.json
It is a PK-Sim snapshot (snapshot format version 80) loaded once per
session (R/mod_compound.R:51). Nothing in the interface can
edit it; the only way to add material is the precipitant “Upload
Compound” option, which adds to the in-session copy only and never
writes back to the file.
Contents of the packaged snapshot:
| Category | Entries |
|---|---|
| Compounds | Itraconazole, Hydroxy-Itraconazole, Keto-Itraconazole, N-desalkyl-Itraconazole, Levonorgestrel, Ketoconazole_Vmax_Km, Ethinylestradiol, Carbamazepine, Rifampicin SHBG, Efavirenz, Drospirenone, Midazolam |
| Populations | Healthy Women, Overweight Women, Class I Obese Women, Class II Obese Women |
| Individuals | Woman, European (P-gp modified, CYP3A4 36 h), Woman_Westhoff et al 2010, Woman SHBG 40% less, Woman SHBG 40% more |
| Formulations | DRSP, LNG (low and high dose), EE, ITZ, KTZ, CBZ, RIF, Efavirenz, MDZ oral tablets |
| Protocols | 19 predefined protocols (see the User Manual for the list) |
The compound dropdowns do not show the snapshot’s compound list as-is
(R/mod_compound.R:187-238):
Ketoconazole_Vmax_Km is shown as “Ketoconazole”, and
Itraconazole is shown as “Itraconazole
(incl. metabolites)”. The internal names are what reach the model.Selecting a compound does not offer all 19 protocols and 10
formulations. A hard-coded allowlist
(R/mod_protocol.R:186-213,
R/mod_formulation.R:339-352) limits the menus:
| Compound | Allowed protocols | Allowed formulations |
|---|---|---|
| Drospirenone | DRSP_3mg 21 days | DRSP oral tablet |
| Levonorgestrel | LNG 0.03 mg 28 days, LNG_150 ug_21 Days, LNG_100 ug_21 Days, LNG 0.75 mg single dose | LNG low dose oral tablet, LNG high dose oral tablet |
| Ethinylestradiol | EE 30ug 21 days, EE 20ug 21 days | EE oral tablet |
| Midazolam | MDZ 1 mg single dose | MDZ oral tablet |
| Rifampicin SHBG | Rifampicin 600mg 21 days, Rifampicin 600mg 10 days | RIF oral tablet |
| Itraconazole | ITZ 100mg 10 days, ITZ 200mg 10 days, ITZ 200mg 21 days, ITZ 100mg 21 days | ITZ oral tablet |
| Ketoconazole_Vmax_Km | KTZ 400 mg 28 days | KTZ oral tablet |
| Carbamazepine | CBZ 400mg 10 days, CBZ 400mg 21 days | CBZ oral tablet |
| Efavirenz | Efavirenz 600 mg for 10, 14 and 21 days | Efavirenz oral tablet |
Note that Drospirenone can only be given as 3 mg for 21 days from the predefined list. Other dose levels require “Create New Protocol”.
Uploaded compounds are not restricted. A compound imported through “Upload Compound” is not in the allowlist, so the app falls back to offering every protocol and every formulation in the session snapshot, including ones intended for other compounds. Combinations that make no pharmaceutical sense are reachable this way and are not blocked.
| Setting | Default | Where |
|---|---|---|
| Object compound | Drospirenone | R/mod_compound.R:200 |
| Precipitant compound | Rifampicin SHBG | R/mod_compound.R:229 |
| Population | Healthy Women | R/mod_population.R:50 |
| Number of individuals | 10 (allowed 1 to 100) | R/mod_population.R:22 |
| Age range | 20 to 60 years | R/mod_population.R:3 |
| Model Ethinylestradiol effects | on | R/app_ui.R:45 |
| Model Midazolam reference | on | R/app_ui.R:70 |
| Simulation duration | 24 h, then overwritten automatically (see below) | R/mod_simulation_params.R:19 |
| Resolution | 1 point per hour | R/mod_simulation_params.R:46 |
| Concentration unit in results | pg/mL | R/utils_units.R:17 |
Rifampicin is the default precipitant by deliberate choice: it is a strong CYP3A4 inducer, so the default scenario shows a large interaction rather than a near-null one. A reviewer opening the app sees a strong-inducer case unless they change it.
Two settings are hard-coded and cannot be reached from the interface
(R/mod_population.R:223,
R/mod_population.R:255-309):
proportionOfFemales = 1. Every
population is entirely female, for every preset.seed = 42. The population sampling
seed is fixed. The same inputs therefore always produce the same virtual
individuals and the same results, on the same platform and software
versions. This makes runs reproducible, and it also means that
run-to-run sampling variability is not represented anywhere in
the application. Repeating a simulation is not an independent
replicate, and the spread shown in the results is the spread across the
10 (or n) sampled individuals of that one fixed sample, not uncertainty
about the sample.The BMI range is locked to the selected population preset’s own band (fallback 16 to 35 kg/m² if the preset does not define one). Only the age range and a range inside the preset’s BMI band can be adjusted. Populations defined by height and weight instead of BMI expose height and weight ranges instead.
SHBG defaults (geometric mean and geometric standard deviation) are read from the selected population preset so that the app reproduces the model’s own distribution; the user can override them.
The Duration and Unit fields are not simply user inputs. An observer
(R/mod_simulation_params.R:73-137) recomputes them whenever
a protocol end time changes, and writes the result back into the visible
fields:
So a user who types a duration, then changes a protocol, will find
their duration replaced. The intent is to keep the simulation window
equal to the administration window, because the PD endpoints are read
from the last dosing interval and a simulation that runs past the last
dose lets off-treatment decay bias them. When the simulation window does
exceed the administration window, the PK-PD tab shows a warning
(R/mod_results_pd.R:462).
Throughout the application one month means exactly 30 days
(720 h) (R/mod_results_pd.R:520,
R/mod_simulation_params.R:101), matching the
cts convention.
This is the single most important item in this document for anyone reading numbers off a fresh session.
The application ships a file of pre-computed results:
inst/extdata/default_simulation_results.rds
At startup, app_server.R:21 loads that file and
populates the Results tab from it. The Results tab is therefore
already populated before the user has run anything, with
results produced by an earlier run, possibly on a different machine, at
a different time, with different software versions. The file carries
metadata recording the object, precipitant, population, duration,
resolution, EE flag, timestamp and platform of that earlier run
(R/utils_simulation_results.R:75-85), but this metadata is
not surfaced anywhere in the interface.
Two consequences:
inputs_are_default()
(R/utils_simulation_results.R:7), exists but is never
called anywhere in the application. Nothing guarantees that the
pre-loaded results correspond to the settings currently displayed in the
sidebar. Only after pressing Run Simulation do the
sidebar and the Results tab describe the same scenario.SAVE_SIMULATION_RESULTS is TRUE
(R/utils_simulation_results.R:64), so after each run the
application writes the new results over that file inside its own
installation directory (R/mod_simulation.R:465-476). In a
long-lived deployment, the “startup results” become whatever was last
run in that container, and the shipped reference results are gone.For any result that is being recorded or reported, press Run Simulation first and take the numbers from that run.
Pressing Run Simulation builds one DDI project and runs, at minimum,
two simulations (R/mod_simulation.R:87-198):
| Simulation name | Contents |
|---|---|
Single Simulation |
Object alone, plus Ethinylestradiol when the EE box is ticked |
DDI Simulation |
Object plus precipitant, plus Ethinylestradiol when the EE box is ticked |
Both are built with add_interactions = TRUE and
add_processes = TRUE, and both request the same output path
per compound:
Organism|PeripheralVenousBlood|<compound>|Plasma (Peripheral Venous Blood),
over 0 to the configured duration at the configured resolution.
Ethinylestradiol is present in both arms, not only the DDI
arm. EE travels with the contraceptive, so when the box is
ticked it is co-administered in the baseline simulation as well
(R/mod_simulation.R:77-88). The baseline is “object + EE”,
not “object alone”.
The Midazolam reference adds two more simulations
(R/mod_simulation.R:201-290),
MDZ Reference Single and MDZ Reference DDI,
sharing the same population and reusing the precipitant’s protocol and
formulation. These are display-only: they exist to draw
a CYP3A index-substrate curve on the concentration-time plot, and no
value box, DDI ratio, or PD endpoint ever reads them. Midazolam’s own
protocol defaults to the object’s schedule so its later doses fall
inside the induction window. Leaving the toggle on costs simulation
time; turning it off changes nothing in any reported number.
PK-Sim returns amounts in µmol/L. The plots convert them
(R/mod_results_ddi.R:166,
R/utils_units.R:28):
concentration = simulationValues (µmol/L) x molWeight (g/mol) x factor
factor: pg/mL = 1000, ng/mL = 1, µg/mL = 0.001
All displayed concentrations are total plasma concentrations (bound plus unbound) in peripheral venous blood. Unbound concentrations are not shown anywhere, and no free-fraction correction is applied.
Cmax, tmax and AUC come from the OSP Suite PK analysis, not from the
app’s own integration. The app reads specific parameter names, with a
fallback (R/mod_results_pk.R:496-510):
| Displayed | Preferred parameter | Fallback if unavailable |
|---|---|---|
| Cmax | C_max_tDLast_tEnd (after the last dose) |
C_max (whole simulation) |
| tmax | t_max_tDLast_tEnd |
t_max |
| AUC | AUC_tDLast_minus_1_tDLast (last dosing interval) |
AUC_tEnd (whole simulation) |
The fallback changes the meaning of the number. For a single-dose protocol there is no last dosing interval, so the AUC shown is the whole-simulation AUC. The only indication is the wording of the tooltip; the label on the value box does not change.
tmax is corrected by the app. OSP Suite documents
t_max_tDLast_tEnd as time after the last application but
returns absolute simulation time. The app subtracts the last dose time
to match the documented meaning
(R/mod_results_pk.R:388-399, issue #66).
AUC unit conversion combines the concentration factor with a
minutes-to-hours conversion (auc_factor() =
conc_factor() / 60), taking µmol·min/L to the selected
concentration unit times hours.
Every reported PK and PD number is a percentile across individuals: the value shown is the median, and the bracketed range is the 5th to 95th percentile, rounded to 4 significant digits. It is a description of the spread across the sampled virtual women, not a confidence interval.
The two PD endpoints are driven by a single quantity, Cavg, computed
by the app (R/mod_results_pd.R:395-414):
Cavg = 0.5 x (Cmax + Ctrough) over the last dosing interval, in µmol/L
Cavg_pmol = Cavg x 1e6 µmol/L to pmol/L
Cavg_used = log10(Cavg_pmol)
Two points matter here:
Cmax and Ctrough here are read through the same fallback mechanism described above, so if the last-dosing-interval parameters are unavailable, Cavg is silently built from whole-simulation values instead.
Both endpoints use one equation
(R/mod_results_pd.R:426-431):
value = BL x (1 - (Imax x tau^hill x Cavg^hill) / ((Kd + Cavg)^hill + tau^hill x Cavg^hill))
with these constants, none of which is exposed in the interface
(R/mod_results_pd.R:160-163):
| Constant | Pearl Index | Ovulation Rate |
|---|---|---|
BL (baseline, no contraception) |
85 | 100 |
Imax |
1 | 1 |
hill |
9.653 | 25.462 |
and these compound-specific parameters
(R/mod_results_pd.R:439-447):
| Compound | Kd (log10 pmol/L) |
tau (Pearl Index) |
tau (Ovulation Rate) |
|---|---|---|---|
| Drospirenone | 2.949 | 2.602 | 1.867 |
| Levonorgestrel | 3.556 | 3.046 | 2.172 |
Behaviour that follows from this and is not stated in the interface:
get_pd_params() returns nothing
for any other object compound, and the PK-PD tab then renders empty
rather than explaining why.Imax is fixed at 1, meaning complete
achievable suppression.Kd, tau, hill, BL)
are treated as exact, and their uncertainty is not propagated. The
intervals shown are therefore not prediction intervals for the Pearl
Index or the ovulation rate; they are narrower than a full uncertainty
analysis would produce, and they do not widen when the prediction moves
into a poorly constrained exposure range.(DDI - baseline) / baseline x 100, shown as
N/A when the baseline median is zero.Export Snapshot
(R/mod_simulation.R:482-492) writes the assembled DDI
project through cts::export_ddi() to
DDI_<object>_<precipitant>_<timestamp>.json.
It captures the full input configuration: compounds, protocols,
formulations, population settings and simulation settings, in a form
that PK-Sim can import.
What the export does not contain: the simulation
results, the PK analysis output, the PD constants applied by the app,
and the versions of ctsApp, cts, ospsuite and
PK-Sim used. Record those separately; the versions are on the About
page.
What makes a result reproducible: the packaged snapshot file, the
fixed seed of 42, the exported configuration, and the recorded software
versions. What can still move a result: a different PK-Sim or
ospsuite build, a different platform, or a change to the
packaged snapshot in a later release of ctsApp.
For convenience, the behaviours above that a reviewer is most likely to want stated plainly:
Some questions about a result produced with this application are questions about the underlying models and the analysis behind them, not about the application. The application applies the exposure-response relationships and PBPK models it is given; it does not calibrate, qualify, or validate them. Specifically, out of scope here:
Those belong to the PK-Sim models, the cts package, and
the white paper. What this document does provide is the application-side
half of each of those questions: exactly which constants are applied
(see “Pearl Index and Ovulation Rate”), what the displayed intervals do
and do not include, and the fact that no extrapolation guard exists. A
reviewer can pair those facts with the model documentation to see the
whole picture.