This guide covers how to set up a development environment, add new R or C++ code, and validate changes before opening a PR. For a full picture of the package's structure and design decisions, see architecture.md first.
- A recent R version
- RStudio (recommended for interactive development)
- Build toolchain for your OS: Rtools on Windows, Xcode CLI on macOS,
build-essentialon Linux
install.packages(c(
"devtools",
"roxygen2",
"testthat",
"profvis"
))Optional — needed to run real-data examples:
install.packages(c("Lahman", "MM", "MGLM"))For R-only changes, you can reload the source tree from the package root with:
devtools::load_all(".")This is useful for interactive development, but it is not the same as testing the installed package.
For changes involving C++ code, Rcpp exports, NAMESPACE, or build files, use a clean rebuild/install cycle:
Rcpp::compileAttributes(".")
devtools::document(".")
devtools::install(".", force = TRUE, upgrade = "never", dependencies = FALSE)Then restart R and test the installed package:
library(MMLN)
verify_installation()Before opening a PR, run a clean check:
devtools::check()- Build > Load All — equivalent to
devtools::load_all() - Build > Check Package — equivalent to
devtools::check() - Build > Test Package — equivalent to
devtools::test()
The R layer is organized into four files — see architecture.md for what belongs where. The typical workflow:
- Add or modify a function in the appropriate
R/*.Rfile. - Document it with a roxygen2 block (
#' @export,@param,@return, etc.). - Regenerate docs and reload for interactive development:
devtools::document(".")
devtools::load_all(".")For changes that affect exports, NAMESPACE, or installed-package behavior, also test a clean install:
devtools::install(".", force = TRUE, upgrade = "never", dependencies = FALSE)Restart R, then run:
library(MMLN)
verify_installation()- Add a test in
tests/testthat/or a script intesting/and verify it runs. - Run
devtools::check()before submitting.
Key conventions (also in architecture.md):
Yis always a count matrix; pass it throughcompress_counts()before ALR-transforming.- Latent variables live in ALR space (
d = ncol(Y) - 1). - The
proposalargument controls MH behavior:"norm","beta", or"normbeta".
The package uses Rcpp + RcppArmadillo + RcppEigen. See architecture.md for how the C++ modules are structured and how they connect to R via .Call().
- Add or update implementation in
src/*.cpp. New utility functions belong inutils.cpp; new performance-critical paths should get their own file. - Annotate functions to expose to R:
// [[Rcpp::export]]
arma::vec my_function(arma::mat X) { ... }- Regenerate the Rcpp wrappers, rebuild, and test the installed package:
Rcpp::compileAttributes(".")
devtools::document(".")
devtools::install(".", force = TRUE, upgrade = "never", dependencies = FALSE)Then restart R and verify the compiled backend:
library(MMLN)
verify_installation()- Validate — see Validating C++ Changes below.
- Keep matrix/vector dimensions explicit and validated early.
- Prefer numerically stable transforms (
log-sum-exp, bounded probabilities) to reduce under/overflow risk. - Don't break MCMC reproducibility — existing utilities depend on deterministic behavior given a seed.
- Keep R-facing interface contracts stable unless you're intentionally making an API change.
src/Makevars and src/Makevars.win set:
PKG_CXXFLAGS = -Wno-ignored-attributes
PKG_LIBS = $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS)
PKG_CXXFLAGS suppresses an Eigen-specific warning on newer compilers while leaving all other warnings active.
PKG_LIBS is required for RcppArmadillo linear algebra calls that use BLAS/LAPACK routines. On Windows, omitting this line can produce linker errors involving symbols such as dgemm_, dgemv, dsyev_, or related BLAS/LAPACK routines.
Add new flags here if needed.
The package should include the generated Rcpp files:
R/RcppExports.R
src/RcppExports.cpp
These are generated by:
Rcpp::compileAttributes(".")The package-level roxygen file R/package.R should include:
#' MMLN: Mixed Effects Multinomial Logistic Normal Models
#'
#' @useDynLib MMLN, .registration = TRUE
#' @importFrom Rcpp evalCpp
"_PACKAGE"After running devtools::document("."), confirm that NAMESPACE contains:
useDynLib(MMLN, .registration = TRUE)
importFrom(Rcpp,evalCpp)If the compiled DLL is not loaded after library(MMLN), verify_installation() and other Rcpp-backed functions may fail.
When modifying any C++ path, work through all four of these before opening a PR:
- Correctness — compare outputs to prior behavior on fixed seeds; verify floating-point differences stay within expected tolerances (
~1e-8per operation,~1e-6over 1000 iterations). - Performance — profile before and after on the same input and seed; record where hotspots moved.
- Stability — run multiple chains on representative datasets; confirm acceptance rates and diagnostics still look reasonable.
- Package check —
devtools::check()with no new warnings from your code path.
Use profvis for interactive hotspot analysis:
library(profvis)
library(MMLN)
prof <- profvis({
set.seed(1)
sim <- simulate_mixed_mln_data(
m = 8, n_i = 8, p = 3, d = 2,
beta = matrix(c(0.5, -1, 0.2, 0.3, 0.7, -0.4), 3, 2),
Sigma = diag(2),
Phi = 5 * diag(2),
n_mean = 100
)
fit <- FMLN(
Y = sim$Y,
X = sim$X,
n_iter = 300,
burn_in = 100,
thin = 2,
proposal = "normbeta",
verbose = FALSE
)
})
profTips:
- Warm up once before profiling to avoid first-run overhead.
- Profile realistic workloads — small inputs can hide true bottlenecks.
- Save profile artifacts to
profiling/with descriptive names.
| Tool | Purpose |
|---|---|
profvis |
Interactive flame graph profiling in RStudio |
bench |
Mid-size benchmarking with memory tracking |
microbenchmark |
Fast comparative timing for small kernels |
lintr |
R style and static analysis |
goodpractice |
Package-level quality checks |
covr |
Test coverage reports |
valgrind (Linux/macOS) |
Native memory diagnostics for C++ |
gdb / lldb |
Step-level native debugging for C++ crashes |
Benchmark pattern with bench:
library(bench)
mark(
baseline = {
# old path / behavior
},
candidate = {
# new path / behavior
},
iterations = 20,
check = FALSE
)- Code compiles cleanly on your platform
-
Rcpp::compileAttributes(".")run if any exported C++ functions changed -
devtools::document(".")run if any exported functions, roxygen docs, or namespace directives changed -
devtools::install(".", force = TRUE, upgrade = "never", dependencies = FALSE)succeeds - After restarting R,
library(MMLN)andverify_installation()both work - Scripts in
testing/still run end-to-end - C++ changes validated for correctness, performance, and stability
-
devtools::check()passes with no new warnings
Small cross-platform numeric differences are expected with compiled C++ due to compiler and BLAS/LAPACK variation. Treat differences below ~1e-6 as normal unless they materially affect diagnostics, rankings, or substantive inference.