From b0126158b35688e88f852e25f5d93d0cdde1c75a Mon Sep 17 00:00:00 2001 From: Ron Keizer Date: Mon, 21 Sep 2026 09:07:03 -0700 Subject: [PATCH 1/2] Rename CLAUDE.md to AGENTS.md and update Use the harness-agnostic AGENTS.md filename so any AI coding agent picks up the repo instructions, not just one vendor's tool. Extend the content with: the ADVAN vs. ODE dispatch in sim(), the two distinct compilation paths (src/ at install time vs. user models at runtime), ODE compartment index conventions and as_is, the model-as- package scaffold in inst/template/, model_library.R, nlmixr2/NONMEM interop, and the setup.R shared-fixture / NOT_CRAN testing pattern. Also add AGENTS.md to .Rbuildignore so R CMD check --as-cran does not report it as a non-standard top-level file. Co-Authored-By: Claude Opus 5 (1M context) --- .Rbuildignore | 1 + AGENTS.md | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 81 ------------------------- 3 files changed, 166 insertions(+), 81 deletions(-) create mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/.Rbuildignore b/.Rbuildignore index bb1b0ca0..44861dba 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -18,3 +18,4 @@ vignettes/variability.Rmd Dockerfile docker-compose.yml ^CRAN-SUBMISSION$ +^AGENTS\.md$ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..90dc7b73 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,165 @@ +# AGENTS.md + +This file provides guidance to AI coding agents when working with code in this +repository. It is harness-agnostic: any agent that reads repo-level instructions +should use it. + +## Overview + +PKPDsim is an R package for simulating pharmacokinetic-pharmacodynamic (PK-PD) +mixed-effects models. It uses ODE-based numerical integration (via Rcpp + Boost +odeint) and ADVAN-style analytical solutions. + +The package is on CRAN, so changes must keep `R CMD check --as-cran` clean. + +## Commands + +```r +# Load package for interactive development +devtools::load_all() + +# Run all tests +devtools::test() + +# Run a single test file +testthat::test_file("tests/testthat/test_advan.R") + +# Run a subset of test files (preferred: this also runs tests/testthat/setup.R, +# which many test files depend on for their shared, pre-compiled models) +devtools::test(filter = "advan") + +# Regenerate NAMESPACE and man/ from roxygen2 comments +devtools::document() + +# Full R CMD check +devtools::check() + +# Build documentation site +pkgdown::build_site() +``` + +Notes: + +- Run `devtools::document()` after touching any roxygen2 block; `NAMESPACE` and + `man/*.Rd` are generated, never edit them by hand. +- Tests and examples that compile models are slow. Several are gated behind + `NOT_CRAN=true` (see `tests/testthat/setup.R`); set that env var locally to + exercise the full suite. +- C++ in `src/` is compiled at package install time. After editing `src/*.cpp`, + re-run `devtools::load_all()` (which recompiles) rather than relying on a + cached build. If a `src/RcppExports.cpp` signature changes, run + `Rcpp::compileAttributes()`. + +## Architecture + +### Core simulation pipeline + +``` +new_ode_model() / (pre-built ADVAN models) + ↓ +sim() → sim_core() → C++ ODE solver (Boost odeint RK4) + ↓ +data.frame output (compartment amounts × time) +``` + +`sim()` branches: if `analytical` is supplied it skips ODE solving entirely and +routes through `advan()` / `advan_create_data()` / `advan_parse_output()` +(`R/sim.R`, around the "Use analytical equations (ADVAN)" block). Otherwise it +builds an event table and calls the dynamically compiled ODE solver. + +### Key function files + +- **`R/new_ode_model.R`** — Defines ODE models: takes user-supplied C++ ODE code as a string, transforms it (index-shifting, auto-declaring variables, injecting infusion rate terms), then compiles it dynamically via `Rcpp::sourceCpp()`. This is the entry point for custom models. +- **`R/sim.R`** — Main user-facing simulation function. Handles input validation, covariate interpolation, IIV/IOV sampling, and dispatch to `sim_core()` or the ADVAN analytical path. +- **`R/sim_core.R`** — Stripped-down simulation wrapper for iterative use (estimation, optimal design); skips validation for performance. +- **`R/advan.R`** — Large (~41KB) file implementing ADVAN1–5 analytical solutions for standard PK models without ODE solving. +- **`R/compile_sim_cpp.R`** — Transforms user ODE strings into compilable C++ and calls `Rcpp::sourceCpp()`. +- **`R/shift_state_indices.R`** — Rewrites compartment indices in ODE code (R is 1-based, C++ is 0-based) before compilation. +- **`R/translate_ode.R`** — Translates ODE code between PKPDsim and RxODE syntax. +- **`R/calculate_parameters.R`** — Applies IIV/IOV variability to typical-value parameters. +- **`R/create_event_table.R`** — Builds the design matrix (event table) used by the C++ solver. +- **`R/model_library.R`** — Named library of standard structural models (`pk_1cmt_iv`, `pk_2cmt_iv_auc`, `pk_1cmt_iv_mm`, `pk_*_oral`, …), each a list of ODE code plus `obs`/`dose` defaults. This is what `new_ode_model("pk_1cmt_iv")` resolves against. +- **`R/new_regimen.R`**, **`R/new_covariate.R`** — Constructors for the two other main user-facing inputs to `sim()`; regimen joining/shifting helpers live in `R/join_regimen.R`, `R/shift_regimen.R`, `R/merge_regimen.R`. + +### C++ layer + +- **`inst/cpp/sim.cpp`**, **`inst/cpp/sim_header.cpp`** — Template for the dynamically compiled ODE solver using Boost odeint. User ODE code is injected here at compile time. +- **`src/*.cpp`** — Pre-compiled Rcpp functions for standard analytical PK models (1-, 2-, 3-compartment IV/oral). +- **`src/Makevars`** — Sets `PKG_CPPFLAGS = -D_HAS_AUTO_PTR_ETC=0` (needed for Boost with newer C++ standards). + +Two distinct compilation paths exist and should not be confused: `src/` is +compiled once when the package is installed, while user ODE models are compiled +at *runtime* into a temporary shared object (or an installed model package). + +### Model definition formats + +- Custom models: C++ ODE string passed to `new_ode_model(code = "...")` +- Library models: named entries in `R/model_library.R`, e.g. `new_ode_model("pk_1cmt_iv")` +- Literature models: JSON5 files in `inst/models/`, loaded via `read_model_json()` and installable as standalone packages via `install_default_literature_model()` +- API models: fetched at runtime via `model_from_api()` + +When `new_ode_model(package = "...")` is used, the model is emitted as a real R +package scaffolded from **`inst/template/`** (its `R/` files carry parameters, +IIV, IOV, RUV, fixed parameters and the compiled model object). Changes to model +metadata usually need a matching change in that template. + +### ODE code conventions + +User-facing ODE code may index compartments from either `A[0]` or `A[1]`. +`shift_state_indices()` subtracts 1 from every index, but returns the code +untouched if that would produce a negative index — so already-0-based code is +left alone. `new_ode_model(as_is = TRUE)` disables this rewriting (and +line-ending substitution) entirely. Variables that are not +parameters or covariates must be listed in `declare_variables`, otherwise the +generated C++ will not compile. `pk_code` runs before the derivatives each step; +`state_init` initializes compartment amounts. + +### Variability modeling + +IIV and IOV are applied in `calculate_parameters.R` before simulation. Supported distributions: exponential (`TVPAR * exp(ETA)`) and additive (`TVPAR + ETA`). Mixture models and quasi-random (Halton/Sobol) sampling are supported via `randtoolbox`. + +### Interoperability + +`R/pkpdsim_to_nlmixr.R`, `R/nlmixr_parse_parameters.R`, `R/regimen_to_nm.R` and +`R/nm_to_regimen.R` convert models and regimens to/from nlmixr2 and NONMEM. + +## Testing patterns + +Tests live in `tests/testthat/` (testthat edition 3). + +- **`tests/testthat/setup.R` compiles a set of shared models once per test + session** (`mod_1cmt_iv`, `pk_iov`, `mod_mixture`, …). Reuse those objects + instead of calling `new_ode_model()` inside a test — compilation dominates + runtime. Add new shared fixtures there when several files need them. +- Expensive fixtures in `setup.R` are wrapped in + `if (identical(Sys.getenv("NOT_CRAN"), "true"))`; tests that use them must be + skipped under the same condition (`skip_on_cran()`). +- The test suite does not hit the network: `test_model_from_api.R` points + `model_from_api()` at local fixtures in `tests/testthat/sample_json/`. Keep it + that way when adding tests for API-backed models. +- Tests that install a model as a package (`test_model_from_api.R`, + `test_install_default_literature_model.R`) write to explicit temp dirs and are + gated with `skip_on_cran()`. + +## CI + +GitHub Actions (`.github/workflows/`): +- `R-CMD-check.yaml`: `R CMD check --as-cran` on ubuntu/windows/macOS (R release), run for each push to `master` and each commit in a PR. Fails only on errors, not warnings/notes. +- `pkgdown.yaml`: Documentation website, run for each commit in a PR +- `rhub.yaml`: Run tests on R-hub, runs only on request + +## Dependencies + +Core: `Rcpp` (≥1.0.13), `BH` (Boost headers), `data.table`, `stringr`, `MASS`, `randtoolbox`, `jsonlite`, `stats`, `parallel`, `magrittr`. Suggested: `httr`, `testthat` (≥3.0.0), `knitr`, `rmarkdown`. R ≥ 4.0.0 required. + +Keep the dependency footprint small — this is a CRAN package, and new hard +dependencies need a good reason. + +## Repository conventions + +- Documentation is roxygen2 with markdown enabled (`Roxygen: list(markdown = TRUE)`). +- Vignettes live in `vignettes/`; several are excluded from the build via + `.Rbuildignore` because they are slow or need optional packages. +- Non-standard top-level files must be listed in `.Rbuildignore` or + `R CMD check --as-cran` reports a NOTE. +- User-visible changes belong in `NEWS.md`. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 6d51889e..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,81 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -PKPDsim is an R package for simulating pharmacokinetic-pharmacodynamic (PK-PD) mixed-effects models. It uses ODE-based numerical integration (via Rcpp + Boost odeint) and ADVAN-style analytical solutions. - -## Commands - -```r -# Load package for interactive development -devtools::load_all() - -# Run all tests -devtools::test() - -# Run a single test file -testthat::test_file("tests/testthat/test_advan.R") - -# Regenerate NAMESPACE and man/ from roxygen2 comments -devtools::document() - -# Full R CMD check -devtools::check() - -# Build documentation site -pkgdown::build_site() -``` - -## Architecture - -### Core simulation pipeline - -``` -new_ode_model() / (pre-built ADVAN models) - ↓ -sim() → sim_core() → C++ ODE solver (Boost odeint RK4) - ↓ -data.frame output (compartment amounts × time) -``` - -### Key function files - -- **`R/new_ode_model.R`** — Defines ODE models: takes user-supplied C++ ODE code as a string, transforms it (index-shifting, auto-declaring variables, injecting infusion rate terms), then compiles it dynamically via `Rcpp::sourceCpp()`. This is the entry point for custom models. -- **`R/sim.R`** — Main user-facing simulation function. Handles input validation, covariate interpolation, IIV/IOV sampling, and dispatch to `sim_core()` or the ADVAN analytical path. -- **`R/sim_core.R`** — Stripped-down simulation wrapper for iterative use (estimation, optimal design); skips validation for performance. -- **`R/advan.R`** — Large (~41KB) file implementing ADVAN1–5 analytical solutions for standard PK models without ODE solving. -- **`R/compile_sim_cpp.R`** — Transforms user ODE strings into compilable C++ and calls `Rcpp::sourceCpp()`. -- **`R/calculate_parameters.R`** — Applies IIV/IOV variability to typical-value parameters. -- **`R/create_event_table.R`** — Builds the design matrix (event table) used by the C++ solver. - -### C++ layer - -- **`inst/cpp/sim.cpp`** — Template for the dynamically compiled ODE solver using Boost odeint. User ODE code is injected here at compile time. -- **`src/*.cpp`** — Pre-compiled Rcpp functions for standard analytical PK models (1-, 2-, 3-compartment IV/oral). - -### Model definition formats - -- Custom models: C++ ODE string passed to `new_ode_model(code = "...")` -- Library models: JSON5 files in `inst/models/`, loaded via `read_model_json()` -- API models: fetched at runtime via `model_from_api()` - -### Variability modeling - -IIV and IOV are applied in `calculate_parameters.R` before simulation. Supported distributions: exponential (`TVPAR * exp(ETA)`) and additive (`TVPAR + ETA`). Mixture models and quasi-random (Halton/Sobol) sampling are supported via `randtoolbox`. - -### Testing patterns - -Tests live in `tests/testthat/`. - -## CI - -GitHub Actions (`.github/workflows/`): -- `pkgdown.yaml`: Documentation website, run for each commit in a PR -- `R-CMD-check.yaml` : R tests, run for each commit in a PR -- `rhub.yaml`: Run tests on Rhub, runs only on request - -## Dependencies - -Core: `Rcpp` (≥1.0.13), `BH` (Boost headers), `data.table`, `stringr`, `MASS`, `randtoolbox`, `jsonlite`, `parallel`, `magrittr`. R ≥ 4.0.0 required. From dce5eb26ef44d8600376184232fe6d01eaa631bc Mon Sep 17 00:00:00 2001 From: Ron Keizer Date: Mon, 21 Sep 2026 10:59:38 -0700 Subject: [PATCH 2/2] Address Codex review feedback on AGENTS.md - sim() does not dispatch to sim_core(); they are sibling entry points that both call the compiled ode object directly. - calculate_parameters.R is a sim_ode() wrapper for effective parameters, not where IIV/IOV is applied. Point to R/sim.R (mvrnorm2 + omega_type) for IIV and R/new_ode_model.R + compiled solver for IOV. - omega_type accepts 'exponential' and 'normal' (not 'additive'). - pk_code runs per event interval, not per derivative evaluation. - Narrow the temp-dir/skip_on_cran testing note to the one installing test in test_model_from_api.R. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 90dc7b73..804a80d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,11 +57,14 @@ Notes: ``` new_ode_model() / (pre-built ADVAN models) ↓ -sim() → sim_core() → C++ ODE solver (Boost odeint RK4) +sim() → compiled C++ ODE solver (Boost odeint RK4) ↓ data.frame output (compartment amounts × time) ``` +`sim()` and `sim_core()` are siblings, not a chain: `sim()` never calls +`sim_core()`. Both end up calling the compiled `ode` object directly. + `sim()` branches: if `analytical` is supplied it skips ODE solving entirely and routes through `advan()` / `advan_create_data()` / `advan_parse_output()` (`R/sim.R`, around the "Use analytical equations (ADVAN)" block). Otherwise it @@ -70,13 +73,13 @@ builds an event table and calls the dynamically compiled ODE solver. ### Key function files - **`R/new_ode_model.R`** — Defines ODE models: takes user-supplied C++ ODE code as a string, transforms it (index-shifting, auto-declaring variables, injecting infusion rate terms), then compiles it dynamically via `Rcpp::sourceCpp()`. This is the entry point for custom models. -- **`R/sim.R`** — Main user-facing simulation function. Handles input validation, covariate interpolation, IIV/IOV sampling, and dispatch to `sim_core()` or the ADVAN analytical path. -- **`R/sim_core.R`** — Stripped-down simulation wrapper for iterative use (estimation, optimal design); skips validation for performance. +- **`R/sim.R`** — Main user-facing simulation function. Handles input validation, covariate interpolation, IIV sampling and application, event-table construction, and then either calls the compiled `ode` object or routes to the ADVAN analytical path. +- **`R/sim_core.R`** — Separate fast entry point for iterative use (estimation, optimal design). It consumes a precalculated design — e.g. the object from `sim(..., return_design = TRUE)` — and calls the compiled `ode` with no validation. - **`R/advan.R`** — Large (~41KB) file implementing ADVAN1–5 analytical solutions for standard PK models without ODE solving. - **`R/compile_sim_cpp.R`** — Transforms user ODE strings into compilable C++ and calls `Rcpp::sourceCpp()`. - **`R/shift_state_indices.R`** — Rewrites compartment indices in ODE code (R is 1-based, C++ is 0-based) before compilation. - **`R/translate_ode.R`** — Translates ODE code between PKPDsim and RxODE syntax. -- **`R/calculate_parameters.R`** — Applies IIV/IOV variability to typical-value parameters. +- **`R/calculate_parameters.R`** — Convenience wrapper around `sim_ode()` that returns model-specific / effective parameters and variables (e.g. `CLi` from `CL` and covariates). Not used by `sim()` itself. - **`R/create_event_table.R`** — Builds the design matrix (event table) used by the C++ solver. - **`R/model_library.R`** — Named library of standard structural models (`pk_1cmt_iv`, `pk_2cmt_iv_auc`, `pk_1cmt_iv_mm`, `pk_*_oral`, …), each a list of ODE code plus `obs`/`dose` defaults. This is what `new_ode_model("pk_1cmt_iv")` resolves against. - **`R/new_regimen.R`**, **`R/new_covariate.R`** — Constructors for the two other main user-facing inputs to `sim()`; regimen joining/shifting helpers live in `R/join_regimen.R`, `R/shift_regimen.R`, `R/merge_regimen.R`. @@ -111,12 +114,24 @@ untouched if that would produce a negative index — so already-0-based code is left alone. `new_ode_model(as_is = TRUE)` disables this rewriting (and line-ending substitution) entirely. Variables that are not parameters or covariates must be listed in `declare_variables`, otherwise the -generated C++ will not compile. `pk_code` runs before the derivatives each step; -`state_init` initializes compartment amounts. +generated C++ will not compile. `pk_code` runs once at initialization and then +at each event, before that interval is integrated — not before every derivative +evaluation, since RK4 evaluates the ODE several times within an interval, so do +not put per-derivative logic there. `state_init` initializes compartment +amounts. ### Variability modeling -IIV and IOV are applied in `calculate_parameters.R` before simulation. Supported distributions: exponential (`TVPAR * exp(ETA)`) and additive (`TVPAR + ETA`). Mixture models and quasi-random (Halton/Sobol) sampling are supported via `randtoolbox`. +IIV is sampled and applied in `R/sim.R`: `mvrnorm2()` draws the ETAs, and the +`omega_type` block applies them per parameter. Accepted `omega_type` values are +`exponential` (`TVPAR * exp(ETA)`) and `normal` (`TVPAR + ETA`). + +IOV is different: `R/new_ode_model.R` generates the `kappa_*` bin logic into the +model C++ at compile time, and the compiled solver picks the right bin at +runtime from the `iov_bins` passed through `sim()`. + +Mixture models and quasi-random (Halton/Sobol) sampling are supported via +`randtoolbox`. ### Interoperability @@ -137,9 +152,12 @@ Tests live in `tests/testthat/` (testthat edition 3). - The test suite does not hit the network: `test_model_from_api.R` points `model_from_api()` at local fixtures in `tests/testthat/sample_json/`. Keep it that way when adding tests for API-backed models. -- Tests that install a model as a package (`test_model_from_api.R`, - `test_install_default_literature_model.R`) write to explicit temp dirs and are - gated with `skip_on_cran()`. +- The one test that actually installs a model as a package + (`test_model_from_api.R`, "Can install a package from json") writes to an + explicit temp lib dir and is gated with `skip_on_cran()`. Follow that pattern + for any new installing test; tests that only exercise dispatch (e.g. + `test_install_default_literature_model.R`) mock `model_from_api()` instead and + need neither. ## CI