Purpose and scope

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.

What runs where

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”.

Fixed model inputs the interface does not expose

The packaged model project

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)

Compound choices are filtered

The compound dropdowns do not show the snapshot’s compound list as-is (R/mod_compound.R:187-238):

  • Object is restricted to Drospirenone and Levonorgestrel. No other compound can be selected as the object, and the app defaults to Drospirenone.
  • Precipitant excludes Drospirenone and Levonorgestrel, and hides three Itraconazole metabolites (Hydroxy-, Keto-, N-desalkyl-Itraconazole). The metabolites are hidden from the menu only: they remain part of the Itraconazole model and are simulated whenever Itraconazole is selected.
  • Two compounds are renamed for display: Ketoconazole_Vmax_Km is shown as “Ketoconazole”, and Itraconazole is shown as “Itraconazole (incl. metabolites)”. The internal names are what reach the model.

Protocols and formulations are restricted per compound

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.

Values the application fills in by itself

Startup defaults

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.

Population sampling: fixed seed, women only

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.

Simulation duration is silently overwritten

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:

  1. It takes the longest administration end time across the object and precipitant protocols.
  2. It picks a unit automatically: months above 60 days, weeks above 28 days, days above 48 h, minutes below 1 h, otherwise hours.
  3. It rounds to the nearest whole number in that unit, with a floor of 1.

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.

The results shown at startup were not simulated

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:

  • The cached results are not checked against the current sidebar. A function intended to make that comparison, 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.
  • Every successful run overwrites the cache. 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.

What each simulation actually contains

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.

Calculations performed by the application

Concentrations

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.

PK parameters

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.

Cavg

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:

  • This is a mean of two extremes, not a time-weighted average. It is not AUC over the dosing interval divided by the interval length. For a profile with a sharp peak, the arithmetic mean of Cmax and Ctrough sits above the true time-weighted average.
  • The PD model operates on log10(pmol/L), and its Kd parameter is on the same log10 scale. Cavg values are not concentrations in the usual sense once they enter the PD equation.

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.

Pearl Index and Ovulation Rate

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:

  • PD endpoints exist only for Drospirenone and Levonorgestrel. get_pd_params() returns nothing for any other object compound, and the PK-PD tab then renders empty rather than explaining why.
  • PD endpoints are not shown for single-dose protocols; the tab shows a message instead, because Cavg is defined on a dosing interval.
  • A baseline Pearl Index of 85 is assumed for the untreated state, and Imax is fixed at 1, meaning complete achievable suppression.
  • The 5th to 95th percentile bars on the PD panels come only from the spread of Cavg across individuals. The PD parameters (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.
  • The equation is evaluated at whatever Cavg the PBPK run produces, without any range check. If a strong inducer drops exposure far below the range in which the exposure-response relationship was established, the app still returns a number, formatted identically to a well-supported one, and does not mark it as an extrapolation.
  • The reported “Δ median” is a relative change in the medians, (DDI - baseline) / baseline x 100, shown as N/A when the baseline median is zero.

Traceability and reproducibility

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.

Known limitations, collected

For convenience, the behaviours above that a reviewer is most likely to want stated plainly:

  1. The Results tab is populated at startup from cached results that are not verified against the sidebar. Always press Run Simulation before reading numbers.
  2. Every run overwrites the cached results file inside the installation.
  3. Population sampling uses a fixed seed, so there is no run-to-run variability and the app cannot show sampling uncertainty.
  4. All populations are 100% female by construction; BMI is confined to the selected preset’s band.
  5. Cavg is the mean of Cmax and Ctrough, not a time-weighted average.
  6. PD parameters are fixed point values; their uncertainty is not propagated into the displayed intervals.
  7. The PD equation is evaluated without any check that Cavg falls in the range where the exposure-response relationship was established.
  8. PD endpoints are available for Drospirenone and Levonorgestrel only, and only for repeated-dose protocols.
  9. Concentrations and exposure metrics are total, not unbound.
  10. PK parameters silently fall back from last-dosing-interval to whole-simulation definitions when the former are unavailable.
  11. Uploaded compounds bypass the protocol and formulation allowlists.
  12. The Midazolam overlay is display-only and never enters any reported number.
  13. The simulation duration field is overwritten automatically when protocols change.

Questions this document cannot answer

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:

  • how the exposure-response relationships for Pearl Index and ovulation rate were calibrated, from which studies, and over which exposure range those data extend;
  • whether ovulation-assessment methods were harmonized across the pooled source studies;
  • how historical Pearl Index values relate to contemporary observed rates;
  • how uncertainty propagates across the PBPK, exposure-response, and ovulation-to-Pearl-Index steps taken together;
  • whether the PBPK models for any given compound are qualified for the intended use.

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.