library(rxode2)
#> rxode2 5.1.8.9000 using 2 threads (see ?getRxThreads)
#> no cache: create with `rxCreateCache()`Why chunked solving?
Population pharmacokinetic/pharmacodynamic simulations can involve tens or hundreds of thousands of subjects. Each subject requires solver workspace, output arrays, and covariate storage, so the combined memory footprint grows linearly with the number of subjects. A 100,000-subject simulation with a two-compartment model and rich sampling can easily require 10-50 GB of RAM – more than many workstations or cloud instances provide.
rxode2 addresses this by chunked (out-of-memory)
solving: subjects are split into batches, each batch is solved
independently, and the result of each batch is written to a temporary
file (parquet or rds). The final return value is a lightweight
rxSolveOom object that reads chunks on demand rather than
holding the entire dataset in RAM.
For running chunks in parallel across multiple CPU cores or compute nodes, see the Parallel rxode2 solving with mirai vignette.
The model and event table
Throughout this vignette we use a one-compartment oral PK model with
between- subject variability on clearance and absorption rate.
Parameters are defined using ini() and model()
blocks – the standard rxode2 UI approach.
pkModel <- function() {
ini({
tka <- log(0.5); label("Typical log Ka (1/h)")
tcl <- log(4); label("Typical log CL (L/h)")
tv <- log(70); label("Typical log V (L)")
eta.ka ~ 0.09
eta.cl ~ 0.04
prop.err <- 0.15
})
model({
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv)
d/dt(depot) <- -ka * depot
d/dt(center) <- ka * depot - cl / v * center
cp <- center / v
cp ~ prop(prop.err)
})
}The event table includes 10 subjects. With subject IDs in the event
table, rxode2 draws one set of etas per subject from the
eta.ka ~ 0.09 and eta.cl ~ 0.04 variances
defined in ini().
Estimating memory usage
Before solving you can inspect the expected memory footprint with
rxMemoryEstimate(). Pass the event table (as a data frame)
and the compiled model; the function calls the same allocator formulas
used internally by rxSolve().
mod <- rxode2(pkModel)
#> ℹ parameter labels from comments are typically ignored in non-interactive mode
#> ℹ Need to run with the source intact to parse comments
rxMemoryEstimate(as.data.frame(ev), model = mod)
#> rxSolve() memory estimate
#> Total: 44.82 KB
#>
#> inds_global (per-subject structs) 17.36 KB (38.7%)
#> outputData (estimated returned data) 11.53 KB (25.7%)
#> gall_times (event time/dv/amt/ii/limit) 8.00 KB (17.8%)
#> gsolve (double buffer total) 3.70 KB ( 8.2%)
#> |_ n0: ODE state output matrix 3.20 KB ( 7.1%)
#> gevid (event IDs) 2.40 KB ( 5.4%)
#> gon (int buffer) 1.05 KB ( 2.3%)
#> gpars (parameters) 400 B ( 0.9%)
#> invCdfMemo 320 B ( 0.7%)
#> ordId (subject ordering) 40 B ( 0.1%)
#> gInfusionRate (per-thread infusion) 28 B ( 0.1%)
#> gcov (covariates) 0 B ( 0.0%)
#> gomega (omega matrix) 0 B ( 0.0%)
#> gsigma (sigma matrix) 0 B ( 0.0%)
#> gall_timesS (extra sim times) 0 B ( 0.0%)
#> indLinExpCache (per-thread exponential cache) 0 B ( 0.0%)
#> indLinWork (per-thread indLin scratch) 0 B ( 0.0%)
#> indOwnAlloc (per-individual event/solve arrays) 0 B ( 0.0%)
#> gSampleCov (resampled covariate index) 0 B ( 0.0%)
#> gEtaPre (pre-generated eta draws) 0 B ( 0.0%)
#> delayHist (per-individual delay() history, bound) 0 B ( 0.0%)
#> linCmtRateHist (per-individual linCmt rates, bound) 0 B ( 0.0%)
#>
#> Subjects: 10 | sizeof(rx_solving_options_ind): 1736 B | 0.0% of RAM (16.77 GB) | 0.0% of available memory (14.34 GB available)The output lists every major allocation (ODE state buffers,
event-time arrays, output data, etc.) as well as the fraction of total
and free RAM the solve would consume. The freeRamBytes line
is the key guard: if total exceeds 90 % of free RAM, rxode2
will refuse to proceed without file.
Automatic OOM detection
When rxode2 detects that a solve would use more than 90 % of free RAM
it stops immediately and prints a message pointing to the
file argument:
# This would error on a machine with insufficient RAM:
rxSolve(pkModel, ev)
#> Error: Solve requires 12.3 GB but only 6.1 GB appears free.
#> Re-run with rxSolve(..., file = 'path/prefix') to solve in chunks.Chunked solving with rxSolve()
Pass file to opt in to chunked solving. rxode2 will:
- Auto-compute a chunk size from available free RAM (or use
chunkSizeif you supply it). - Solve each chunk with a separate
rxSolve()call. - Write each chunk’s result to
<file>_chunk_NNNNN.parquet(or.rdsif thearrowpackage is unavailable). - Save a manifest to
<file>_manifest.rds. - Return an
rxSolveOomobject.
res <- rxSolve(pkModel, ev,
file = tempfile("rxChunk"),
chunkSize = 3) # 3 subjects per chunk
#> ℹ parameter labels from comments are typically ignored in non-interactive mode
#> ℹ Need to run with the source intact to parse comments
res
#> ── Solved rxode2 object ──
#> ── Parameters (x$params): ──
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> # A tibble: 10 × 7
#> id tka tcl tv prop.err eta.ka eta.cl
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 -0.693 1.39 4.25 0.15 0.0448 -0.309
#> 2 2 -0.693 1.39 4.25 0.15 -0.166 -0.179
#> 3 3 -0.693 1.39 4.25 0.15 0.230 0.343
#> 4 4 -0.693 1.39 4.25 0.15 0.0225 0.180
#> 5 5 -0.693 1.39 4.25 0.15 -0.392 0.0824
#> 6 6 -0.693 1.39 4.25 0.15 -0.0601 0.0137
#> 7 7 -0.693 1.39 4.25 0.15 0.536 0.129
#> 8 8 -0.693 1.39 4.25 0.15 -0.0798 0.256
#> 9 9 -0.693 1.39 4.25 0.15 0.225 0.0241
#> 10 10 -0.693 1.39 4.25 0.15 0.0533 0.491
#> ── Initial Conditions (x$inits): ──
#> depot center
#> 0 0
#> ── First part of data (object): ──
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> # A tibble: 6 × 10
#> id time ka cl v cp ipredSim sim depot center
#> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 0 0.523 2.94 70 0 0 0 100 0
#> 2 1 4 0.523 2.94 70 1.12 1.12 1.02 12.3 78.5
#> 3 1 8 0.523 2.94 70 1.09 1.09 0.963 1.52 76.1
#> 4 1 12 0.523 2.94 70 0.936 0.936 0.969 0.188 65.5
#> 5 1 16 0.523 2.94 70 0.793 0.793 0.689 0.0233 55.5
#> 6 1 20 0.523 2.94 70 0.671 0.671 0.603 0.00287 47.0
#> <rxSolveOom: 4 chunks, 190 total rows, prefix='/tmp/RtmpJPR0o2/rxChunk62998b073c7' [DuckDB/Arrow-backed]>The print method reports how many chunks were created and the total
row count. With chunkSize = 3 and 10 subjects, rxode2
creates four chunks (3 + 3 + 3 + 1).
Accessing results
Materialise the full data frame
df <- as.data.frame(res)
head(df)
#> id time ka cl v cp ipredSim sim depot
#> 1 1 0 0.5229038 2.937553 70 0.0000000 0.0000000 0.0000000 1.000000e+02
#> 2 1 4 0.5229038 2.937553 70 1.1214032 1.1214032 1.0203791 1.234877e+01
#> 3 1 8 0.5229038 2.937553 70 1.0865946 1.0865946 0.9625345 1.524917e+00
#> 4 1 12 0.5229038 2.937553 70 0.9357858 0.9357858 0.9692756 1.883077e-01
#> 5 1 16 0.5229038 2.937553 70 0.7932925 0.7932925 0.6890626 2.325360e-02
#> 6 1 20 0.5229038 2.937553 70 0.6709674 0.6709674 0.6030925 2.871524e-03
#> center
#> 1 0.00000
#> 2 78.49822
#> 3 76.06162
#> 4 65.50501
#> 5 55.53047
#> 6 46.96772This reads and rbinds every chunk file. For very large
results keep the rxSolveOom object in place and only
materialise columns you actually need.
Lazy column access
cp_all <- res$cp
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
length(cp_all)
#> [1] 190res$col reads only the requested column from each chunk,
avoiding loading columns you do not need.
Row and dimension queries
nrow(res)
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> [1] 190
dim(res)
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> [1] 190 10Faking low memory in demos and tests
The easiest way to demonstrate or test the chunked path on a small
dataset is to set chunkSize directly – no need to modify
system RAM. With chunkSize = 2 on 10 subjects you get 5
chunks of 2 subjects each:
res_small <- rxSolve(pkModel, ev,
file = tempfile("rxDemo"),
chunkSize = 2)
res_small
#> ── Solved rxode2 object ──
#> ── Parameters (x$params): ──
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> # A tibble: 10 × 7
#> id tka tcl tv prop.err eta.ka eta.cl
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 -0.693 1.39 4.25 0.15 -0.134 -0.112
#> 2 2 -0.693 1.39 4.25 0.15 -0.230 -0.369
#> 3 3 -0.693 1.39 4.25 0.15 -0.102 -0.0439
#> 4 4 -0.693 1.39 4.25 0.15 -0.517 -0.00782
#> 5 5 -0.693 1.39 4.25 0.15 -0.0234 -0.428
#> 6 6 -0.693 1.39 4.25 0.15 -0.119 -0.513
#> 7 7 -0.693 1.39 4.25 0.15 -0.114 -0.232
#> 8 8 -0.693 1.39 4.25 0.15 -0.559 0.0137
#> 9 9 -0.693 1.39 4.25 0.15 -0.0484 -0.0114
#> 10 10 -0.693 1.39 4.25 0.15 -0.417 0.442
#> ── Initial Conditions (x$inits): ──
#> depot center
#> 0 0
#> ── First part of data (object): ──
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> # A tibble: 6 × 10
#> id time ka cl v cp ipredSim sim depot center
#> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 0 0.437 3.58 70 0 0 0 100 0
#> 2 1 4 0.437 3.58 70 1.04 1.04 0.844 17.4 72.6
#> 3 1 8 0.437 3.58 70 1.03 1.03 0.953 3.02 71.8
#> 4 1 12 0.437 3.58 70 0.868 0.868 0.864 0.525 60.7
#> 5 1 16 0.437 3.58 70 0.713 0.713 0.855 0.0912 49.9
#> 6 1 20 0.437 3.58 70 0.582 0.582 0.726 0.0159 40.7
#> <rxSolveOom: 5 chunks, 190 total rows, prefix='/tmp/RtmpJPR0o2/rxDemo629970e3e800' [DuckDB/Arrow-backed]>In production code, omit chunkSize entirely and rxode2
auto-computes an appropriate chunk size from
rxMemoryEstimate().
File-backed event tables
For datasets so large that even the event table does not fit in RAM,
save it to a file first and wrap the path with
rxEventTableFile():
et_path <- tempfile(fileext = ".parquet")
arrow::write_parquet(as.data.frame(ev), et_path)
et_file <- rxEventTableFile(et_path, format = "parquet")
res_file <- rxSolve(pkModel, et_file,
file = tempfile("rxFile"),
chunkSize = 3)
res_file
#> ── Solved rxode2 object ──
#> ── Parameters (x$params): ──
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> # A tibble: 10 × 7
#> id tka tcl tv prop.err eta.ka eta.cl
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 -0.693 1.39 4.25 0.15 -0.132 0.432
#> 2 2 -0.693 1.39 4.25 0.15 -0.336 -0.00790
#> 3 3 -0.693 1.39 4.25 0.15 -0.374 -0.154
#> 4 4 -0.693 1.39 4.25 0.15 0.351 0.192
#> 5 5 -0.693 1.39 4.25 0.15 0.0977 0.00740
#> 6 6 -0.693 1.39 4.25 0.15 0.199 -0.263
#> 7 7 -0.693 1.39 4.25 0.15 0.0423 -0.106
#> 8 8 -0.693 1.39 4.25 0.15 -0.102 -0.151
#> 9 9 -0.693 1.39 4.25 0.15 -0.0752 0.0679
#> 10 10 -0.693 1.39 4.25 0.15 0.137 -0.00752
#> ── Initial Conditions (x$inits): ──
#> depot center
#> 0 0
#> ── First part of data (object): ──
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpJPR0o2/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> # A tibble: 6 × 10
#> id time ka cl v cp ipredSim sim depot center
#> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 0 0.438 6.16 70 0 0 0 100 0
#> 2 1 4 0.438 6.16 70 0.947 0.947 0.917 17.3 66.3
#> 3 1 8 0.438 6.16 70 0.830 0.830 0.935 3.01 58.1
#> 4 1 12 0.438 6.16 70 0.612 0.612 0.762 0.521 42.9
#> 5 1 16 0.438 6.16 70 0.435 0.435 0.533 0.0904 30.5
#> 6 1 20 0.438 6.16 70 0.307 0.307 0.325 0.0157 21.5
#> <rxSolveOom: 4 chunks, 190 total rows, prefix='/tmp/RtmpJPR0o2/rxFile629990f4178' [DuckDB/Arrow-backed]>rxEventTableFile() also accepts "csv",
"fst", and "rds" formats.
Persistent output files
By default file points to a tempfile()
prefix, so the chunk files and manifest are cleaned up when R’s garbage
collection removes the temp directory. To keep results across sessions,
point file at a directory in your project:
res_persist <- rxSolve(pkModel, ev,
file = "output/my_sim",
chunkSize = 5000)Files written will be output/my_sim_chunk_00001.parquet,
…, output/my_sim_manifest.rds. Reload the manifest
later:
manifest <- readRDS("output/my_sim_manifest.rds")Working with Arrow datasets
The combination of chunked solving and Arrow datasets means rxode2 can handle populations that are too large to fit in memory at any stage – not just during the solve, but also when post-processing the results. Chunk files stay on disk; Arrow reads only what your query needs.
When the arrow package is installed (which it must be
for parquet chunk files), rxode2 exposes two Arrow-native accessors on
the rxSolveOom object.
Lazy Arrow Dataset – querying without loading everything
as.arrow() (an rxode2 generic; arrow itself has no such
function) opens all chunk files as a single lazy
arrow::Dataset. No rows are read from disk until you
explicitly collect them. You can push filters, column selections, and
aggregations down to the Arrow engine so that only the result – not the
full dataset – is brought into R memory:
ds <- as.arrow(res)
ds
#> FileSystemDataset with 4 Parquet files
#> 10 columns
#> id: int32
#> time: double
#> ka: double
#> cl: double
#> v: double
#> cp: double
#> ipredSim: double
#> sim: double
#> depot: double
#> center: double
#>
#> See $metadata for additional Schema metadataBecause ds is lazy you can chain dplyr verbs and only
call dplyr::collect() at the end to materialize the (small)
result:
# Compute the peak concentration per subject without loading all rows.
# Arrow cannot translate argmax expressions like time[which.max(cp)], so
# tmax comes from a lazy self-join: keep the rows where cp hits the
# per-subject maximum.
cmax_tbl <- ds |>
dplyr::group_by(id) |>
dplyr::summarise(cmax = max(cp, na.rm = TRUE))
pk_summary <- ds |>
dplyr::inner_join(cmax_tbl, by = "id") |>
dplyr::filter(cp == cmax) |>
dplyr::select(id, cmax, tmax = time) |>
dplyr::collect() |>
dplyr::arrange(id)
pk_summary
#> # A tibble: 10 × 3
#> id cmax tmax
#> <int> <dbl> <dbl>
#> 1 1 1.78 52
#> 2 2 1.59 52
#> 3 3 1.25 52
#> 4 4 1.33 52
#> 5 5 1.29 52
#> 6 6 1.45 52
#> 7 7 1.44 52
#> 8 8 1.25 52
#> 9 9 1.50 52
#> 10 10 1.12 52This is the key advantage over materialising with
as.data.frame(): for a 100,000-subject simulation that
would require 40 GB of RAM as an R data frame, the summary above reads
each chunk sequentially and returns only one row per subject – a result
that fits easily in memory even when the full dataset does not.
Other useful patterns:
# Keep only observations after the first dose interval
late_obs <- ds |>
dplyr::filter(time > 24) |>
dplyr::select(id, time, cp) |>
dplyr::collect()
head(late_obs)
#> # A tibble: 6 × 3
#> id time cp
#> <int> <dbl> <dbl>
#> 1 4 28 1.28
#> 2 4 32 1.11
#> 3 4 36 0.863
#> 4 4 40 0.658
#> 5 4 44 0.501
#> 6 4 48 0.381In-memory Arrow Table
arrow::as_arrow_table() reads and concatenates all chunk
files into a single arrow::Table in columnar memory. Use
this when the full result fits in RAM but you want the speed of Arrow
columnar operations rather than R’s row-oriented
rbind():
tbl <- arrow::as_arrow_table(res)
tbl
#> Table
#> 190 rows x 10 columns
#> $id <int32>
#> $time <double>
#> $ka <double>
#> $cl <double>
#> $v <double>
#> $cp <double>
#> $ipredSim <double>
#> $sim <double>
#> $depot <double>
#> $center <double>
#>
#> See $metadata for additional Schema metadataMaterialisation via Arrow
When arrow is available, as.data.frame(),
tibble::as_tibble(), and
data.table::as.data.table() all route through
as_arrow_table() internally, so they benefit from Arrow’s
concatenation speed rather than R’s rbind():
df <- as.data.frame(res) # Arrow path when arrow installed
tib <- tibble::as_tibble(res) # also Arrow-backedParallel chunk solving
Each chunk is independent and can be solved on a separate CPU or
compute node using the mirai package. Pass
parallel to control the number of worker processes:
See the Parallel rxode2 solving with mirai vignette for a full walkthrough of daemon setup, performance tuning, and custom parallel workflows.
Session information
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.5 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
#> [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
#> [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
#> [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] rxode2_5.1.8.9000
#>
#> loaded via a namespace (and not attached):
#> [1] utf8_1.2.6 sass_0.4.10 generics_0.1.4
#> [4] digest_0.6.39 magrittr_2.0.5 evaluate_1.0.5
#> [7] grid_4.6.1 RColorBrewer_1.1-3 fastmap_1.2.0
#> [10] lotri_1.0.5 jsonlite_2.0.0 rxode2ll_2.0.18
#> [13] backports_1.5.1 DBI_1.3.0 purrr_1.2.2
#> [16] scales_1.4.0 textshaping_1.0.5 jquerylib_0.1.4
#> [19] duckdb_2.0.0 cli_3.6.6 crayon_1.5.3
#> [22] rlang_1.3.0 bit64_4.8.6 withr_3.0.3
#> [25] cachem_1.1.0 yaml_2.3.12 otel_0.2.0
#> [28] tools_4.6.1 memoise_2.0.1 checkmate_2.3.4
#> [31] dplyr_1.2.1 rxode2lincmt_0.1.1 ggplot2_4.0.3
#> [34] assertthat_0.2.1 vctrs_0.7.3 R6_2.6.1
#> [37] lifecycle_1.0.5 fs_2.1.0 htmlwidgets_1.6.4
#> [40] bit_4.6.0 ragg_1.5.2 PreciseSums_0.7
#> [43] arrow_26.0.0 pkgconfig_2.0.3 desc_1.4.3
#> [46] pkgdown_2.2.1 pillar_1.11.1 bslib_0.12.0
#> [49] gtable_0.3.6 data.table_1.18.6.1 glue_1.8.1
#> [52] Rcpp_1.1.2 systemfonts_1.3.2 xfun_0.62
#> [55] tibble_3.3.1 tidyselect_1.2.1 sys_3.4.3
#> [58] knitr_1.52 farver_2.1.2 dparser_1.3.1-14
#> [61] htmltools_0.5.9 rmarkdown_2.32 compiler_4.6.1
#> [64] S7_0.2.2