Thanks for your interest in contributing to hBayesDM. This guide replaces the older wiki pages and reflects the 2.0 stack (cmdstanr / cmdstanpy, R ≥ 4.4, Python ≥ 3.13).
- R ≥ 4.4 with cmdstanr and a working CmdStan installation.
- Python ≥ 3.13 with uv for dependency management.
- Git, optionally RStudio, and a code editor.
One-time CmdStan install (do this once per machine, not per clone):
# R side
Rscript -e 'install.packages("cmdstanr", repos = c("https://stan-dev.r-universe.dev", getOption("repos")))'
Rscript -e 'cmdstanr::install_cmdstan()'
# Python side (after `uv sync` in Python/, see below)
uv run python -c "import cmdstanpy; cmdstanpy.install_cmdstan()"hBayesDM follows the Git Flow
model: feature branches branch off develop, merge back into develop, and
develop merges into master at release.
- Fork
CCS-Lab/hBayesDMon GitHub. - Clone your fork and add upstream:
git clone https://github.com/<your-username>/hBayesDM.git cd hBayesDM git remote add upstream https://github.com/CCS-Lab/hBayesDM.git
- Branch off
developwith a descriptive name:Conventions:git fetch upstream git checkout -b feature/<short-description> upstream/develop
- Never push directly to
developormaster. - Branch names:
feature/<description>for new work,bugfix/<description>for fixes,hotfix/<description>for urgent production fixes.
- Never push directly to
- Implement your changes (see Adding a new model below for the model-specific workflow).
- Run the tests locally (see Testing).
- Push to your fork and open a PR against
CCS-Lab/hBayesDM:develop. Maintainers will review and merge.
hBayesDM/
├── DEVELOPERS.md # developer quick reference (toolchain, gotchas)
├── CONTRIBUTING.md # this file
├── README.md
├── commons/ # single source of truth shared by R + Python
│ ├── extdata/ # example data files (tab-separated .txt)
│ ├── models/ # one .yml per model (model spec)
│ ├── stan_files/ # one .stan per model
│ ├── templates/ # R/Python code templates used by generators
│ ├── convert-to-py.py # YAML -> Python wrapper
│ ├── convert-to-r.py # YAML -> R wrapper
│ ├── example.yml # template you copy when adding a new model
│ ├── generate-codes.sh # runs both converters
│ └── utils.py
├── R/ # R package (cmdstanr backend)
│ ├── R/ # source (per-model wrappers + helpers)
│ ├── inst/ # symlinks to commons/{stan_files,extdata}
│ ├── man/ # auto-generated; do NOT hand-edit
│ ├── man-roxygen/ # shared roxygen template
│ ├── tests/testthat/
│ ├── vignettes/
│ ├── DESCRIPTION
│ ├── NAMESPACE # auto-generated; do NOT hand-edit
│ └── README.Rmd
├── Python/ # Python package (cmdstanpy backend)
│ ├── hbayesdm/ # source
│ │ ├── common/ # symlinks to commons/{stan_files,extdata}
│ │ └── models/ # auto-generated wrappers
│ ├── tests/
│ ├── docs/
│ ├── pyproject.toml # uv + hatchling
│ └── Makefile
└── .github/workflows/ # CI
The single source of truth for Stan files and example data is commons/.
R/inst/{stan_files,extdata} and Python/hbayesdm/common/{stan_files,extdata}
are symlinks into commons/. Edit once in commons/; both packages pick it
up.
- Write a model spec YAML in
commons/models/. - Write the Stan file in
commons/stan_files/. - Provide example data in
commons/extdata/(only once per task). - Generate R + Python wrappers with
commons/generate-codes.sh. - Implement the preprocess function in
R/R/preprocess_funcs.RandPython/hbayesdm/preprocess_funcs.py. - Regenerate roxygen man pages in R.
- Install both packages and run the tests.
Copy commons/example.yml to commons/models/<task_code>_<model_code>[_<model_type>].yml
and edit. Required fields are:
task_name(withcode,desc, optionalcite)model_name(withcode,desc, optionalcite)model_type— one of:- empty
code+desc: Hierarchical(default) code: single+desc: Individualcode: multipleB+desc: Multiple-block Hierarchical
- empty
data_columns— must includesubjID;blockis required formultipleB.parameters— for each:desc,info: [lower, plausible, upper].
Optional fields: regressors, postpreds, additional_args, notes,
contributors. See commons/example.yml for inline documentation of every
field.
Naming convention (used as the function name, the Stan filename, the
generated .R and .py filenames, etc.):
<task_code>_<model_code>[_<model_type>]
Examples: ra_prospect, choiceRT_ddm_single, prl_fictitious_multipleB.
Drop your .stan file into commons/stan_files/ with the same base name as
the YAML. Use canonical Stan syntax (current as of CmdStan 2.32+):
array[N, T] real x;(notreal x[N, T];)abs(...)(notfabs(...))- Hierarchical models should use non-centered parameterization
(guide) with a
mu_prvector, asigmavector, and per-subject<param>_prvectors:
parameters {
// Group-level priors
vector[3] mu_pr;
vector<lower=0>[3] sigma;
// Subject-level raw parameters (Matt trick)
vector[N] alpha_pr;
vector[N] beta_pr;
vector[N] gamma_pr;
}Add commons/extdata/<task_code>[_<model_type>]_exampleData.txt. Format:
- Tab-separated.
- Includes a
subjIDcolumn (required) and any other columns referenced in your YAML'sdata_columns. - Reasonably small (~5–10 subjects, ~50–250 trials per subject) so example fits run in seconds.
cd commons
bash generate-codes.shRequires pyyaml, which is part of the dev dependency group in
Python/pyproject.toml — running uv sync in Python/ installs it
into Python/.venv and bash generate-codes.sh will pick it up.
The script:
- Runs
convert-to-r.pyto renderR_CODE_TEMPLATE.txt/R_DOCS_TEMPLATE.txt/R_TEST_TEMPLATE.txtinto per-model files in_r-codes/and_r-tests/. - Runs
convert-to-py.pysimilarly for Python. - Copies the generated files into:
R/R/<model>.RR/tests/testthat/test_<model>.RPython/hbayesdm/models/_<model>.pyPython/tests/test_<model>.py
- Cleans up the staging directories.
Both packages have a single shared file with all preprocess functions:
- R:
R/R/preprocess_funcs.R - Python:
Python/hbayesdm/preprocess_funcs.py
Add a function named <task_code>[_<model_type>]_preprocess_func (one per
task, not per model — multiple models on the same task share it). The
function takes raw_data, general_info, and any additional_args from
the YAML, and returns a dict (Python) / named list (R) that gets passed
straight to Stan as data.
Watch out — R
additional_argsplumbing: when defaulting per-model args, use single-bracket list assignment so aNULLdefault is preserved:args[nm] <- list(additional_args[[nm]]) # keeps NULL # args[[nm]] <- NULL # would *delete* the entrySee
DEVELOPERS.mdfor more gotchas.
cd R
Rscript -e 'roxygen2::roxygenize(".")'This rewrites R/man/<your-model>.Rd and R/NAMESPACE from the roxygen
tags in your generated R/R/<model>.R. Never hand-edit the .Rd files
or NAMESPACE — they'll be overwritten on the next regenerate.
Background (you shouldn't need to act on this): roxygen2 ≥ 8.0 errors on
empty @templateVar X lines. commons/convert-to-r.py strips them before
writing the file, and the shared template at
R/man-roxygen/model-documentation.R uses get0() so the resulting
missing (rather than empty) templateVars degrade gracefully. If you ever
see this error in practice, regenerate with bash commons/generate-codes.sh
rather than hand-editing.
# R side
cd R
R CMD INSTALL --no-docs --no-test-load .
NOT_CRAN=true Rscript -e 'testthat::test_file("tests/testthat/test_<your-model>.R")'
# Python side
cd ../Python
uv sync --all-groups
uv run pytest tests/test_<your-model>.py -vThe first model fit will take ~30 s while CmdStan compiles the binary; subsequent fits reuse the cached binary instantly.
| Scope | R | Python |
|---|---|---|
| Just your new model | testthat::test_file(...) |
pytest tests/test_<model>.py |
| User-facing API | testthat::test_file("tests/testthat/test_user_facing.R") |
pytest tests/test_user_facing.py |
| Everything | testthat::test_dir("tests/testthat") |
pytest tests |
NOT_CRAN=true is required locally for the R test suite — the per-model
tests are gated by skip_on_cran() so CRAN check machines don't try to fit
Stan models.
CI (GitHub Actions) runs the user-facing suite plus a couple of model smoke tests on every PR. The full per-model suite is local-only.
- R: 2-space indent, tidyverse-ish style. roxygen comments on exported functions.
- Python: 4-space indent, ruff-formatted. Type hints on public APIs.
- Stan: 2-space indent, lowercase variable names, blank line between blocks.
- Comments: describe current behavior, not history. Don't write "this
used to be X", "after the refactor", etc. — git log and
R/NEWS.mdare the sources of truth for change history.
By contributing, you agree that your contributions will be licensed under the same terms as hBayesDM (GPL-3).