Skip to content

Contributing

Thanks for your interest in this library. This guide covers the development setup, the checks every change must pass, and how a change gets from your machine into master. Repository conventions that are easy to get wrong (two StudentT classes, probability vs. log space, single-use online likelihood objects, device handling) are in AGENTS.md; read it before touching the algorithms.

Development setup

The library depends only on torch. The test suite also needs numpy and scipy, which the dev extra provides.

git clone https://github.com/hildensia/bayesian_changepoint_detection.git
cd bayesian_changepoint_detection
python -m venv .venv && source .venv/bin/activate   # or: uv venv && source .venv/bin/activate
pip install -e ".[dev]"                             # or: uv pip install -e ".[dev]"
pytest

Tests live in tests/, one file per area (test_online_detection.py, test_priors.py, test_studentt_exactness.py, ...). To run a subset:

pytest tests/test_online_detection.py          # one file
pytest -k "multivariate and not slow"          # by name
pytest -m "not slow"                           # skip tests marked slow
pytest -m gpu                                  # only the GPU tests (they skip without one)
pytest -m math                                 # only the tests that check the mathematics
pytest --cov=bayesian_changepoint_detection    # with coverage (needs pytest-cov, in the dev extra)

CI measures coverage on every Python version and fails below 94% of statements (about 95% in CI when the floor was last raised; a local run on macOS reads about one point higher). The table for Python 3.12 is in the summary of each CI run. If a PR raises coverage, raise the floor in .github/workflows/ci.yml with it. Never lower the floor to make a PR pass.

Every test carries exactly one of two kind markers, and tests/conftest.py fails collection when a selected test is missing one or has both (it runs after -k/-m deselection, so a scoped run only judges the tests it selected; the full run checks all of them):

  • math: the value is checked against an independent computation of the same quantity: a scipy evaluation, an exhaustive enumeration of segmentations, a closed form from a cited paper, or a derivation inside the library that shares no code with the one under test (the offline marginal against the product of online predictives; the negative binomial prior at k = 1 against the geometric prior). These are the tests that fail when the mathematics is wrong.
  • behavior: the test pins how the library behaves today: API contracts, input validation, device handling, the same formula through two code paths (pdf_rows against pdf, one multivariate call against the sum of per-dimension calls), detection on synthetic data with known changepoints, goldens generated by an earlier version. These tests say when behavior changes; they do not say whether the change is correct.

Tag a whole file with pytestmark = pytest.mark.math when it is homogeneous, otherwise decorate each test. A behavior test that pins a number still says in a comment or docstring where the number came from.

slow, gpu, math and behavior are the only registered markers (pyproject.toml, [tool.pytest.ini_options]). --strict-markers is on, so decorating a test with an unregistered marker fails collection; a typo inside a -m expression does not error, it silently selects nothing, so check the collected count.

The full suite takes well under a minute on CPU. On CUDA or Apple's MPS it can take minutes, because small tensors are slower on an accelerator. The default device is the CPU, but tests still pass device="cpu" explicitly unless they are about device handling, and new tests should do the same. New tests that need a GPU must carry @pytest.mark.gpu and skip themselves when none is present; one existing test, test_device_consistency in tests/test_integration.py, predates the marker and only skips at runtime.

CI runs pytest tests/ on Python 3.9 to 3.13 with CPU-only PyTorch, a lint job (ruff check and ruff format --check), an examples job that runs every script in examples/ headless, a docs job that builds the documentation site with mkdocs build --strict, and a build job that makes the sdist and wheel, runs twine check, and installs the wheel into a clean environment to run both detectors once. Run the linter and formatter locally before pushing, or let the pre-commit hooks do it on each commit:

pre-commit install                      # once; hooks come from .pre-commit-config.yaml
ruff check . && ruff format .           # what CI checks (ruff is in the dev extra)

Configuration lives in pyproject.toml under [tool.ruff]: rule sets E, F, W, I, B, UP, line length 88, Python 3.9 as the target, notebooks excluded. mypy settings are kept there too but mypy is not enforced yet.

Documentation site

mkdocs.yml describes the site; the pages are in docs/. Most of them pull sections of README.md in through snippet section markers, HTML comments in the README that open and close a named section (start:usage, end:usage; a comment is invisible on GitHub and PyPI). Edit the README, not the page, and keep the markers when you move text around. The API pages are generated from the docstrings. To preview:

pip install -e ".[docs]"
mkdocs serve        # http://127.0.0.1:8000, rebuilds on save
mkdocs build --strict   # what CI runs; warnings fail the build

CI builds the site on every PR. The published site, https://estcarisimo.github.io/bayesian_changepoint_detection/, is built from master and deployed by a workflow on the maintainer's fork (estcarisimo/bayesian_changepoint_detection, .github/workflows/pages.yml), daily and on demand, because enabling Pages on this repository needs its owner. A merged docs change is live within a day; to publish at once, run that workflow by hand.

Conventions

  • American English for identifiers (functions, methods, classes, arguments, test markers), docstrings, and the agent instructions in AGENTS.md: normalize, behavior, color. Reviewers flag British spellings in new names; renaming an existing public name needs a deprecation alias.
  • NumPy-style docstrings on public functions and classes. When a docstring states a formula, cite the paper and equation it comes from.
  • Every behavior change gets a test under tests/ and a line in CHANGELOG.md under [Unreleased]. Tests that pin a numerical result say where the expected value comes from (an independent reference implementation, a scipy evaluation, a paper); a test that compares the code against itself proves nothing. Mark the test math or behavior (see "Development setup" above).
  • Changes to the mathematics follow the rules in the "Changing the math" section of AGENTS.md: name the source, prove the new form against an independent path, and say in the PR whether outputs change numerically.
  • Keep PRs small and single-purpose. Do not mix refactors with statistical fixes.

Making a change

master is not to be pushed to directly. GitHub does not currently enforce this on the upstream repository, so it is policy, and it applies to maintainers and to any automated agent acting for them:

  • Every change, however small, lands through a pull request opened from a fork or a topic branch.
  • Never force-push to master. If something has to be undone, revert it through a pull request.
  • A PR is merged only when CI is green on lint, every test (3.x) leg, examples and build, and the review is done: request whatever automated code review the repository has enabled, or ask a person where none is available, and address every comment, either with a fix or with a reply in the thread saying why it does not apply.

Step by step:

  1. Branch from the current master: git switch -c <type>/<short-name> (fix/, feat/, docs/, chore/, perf/).
  2. Commit in small, coherent steps.
  3. Push the branch to your fork and open the PR against hildensia/bayesian_changepoint_detection:master. Explain what changes, why, and how it was verified; for numerical changes include the numbers.
  4. Iterate until CI is green and the review is addressed, then merge (squash for a single logical change, merge commit when the individual commits matter). Delete the branch afterwards.

Merging needs write access to the upstream repository. Branch protection, repository secrets, PyPI credentials and review-tool settings need the repository owner.

Releasing

  1. version in pyproject.toml is the only place the version lives; bayesian_changepoint_detection.__version__ reads it from the installed package metadata. Bump it there.
  2. Move the [Unreleased] section of CHANGELOG.md under a new [X.Y.Z] — YYYY-MM-DD heading.
  3. Update version and date-released in CITATION.cff and the version in the README's BibTeX entry, and re-measure the README performance table (benchmarks/performance.py --versions current, see benchmarks/README.md). Open a PR with these changes and merge it.
  4. Optionally rehearse: run the CD workflow manually (gh workflow run cd.yml -f target=testpypi) to build and upload to TestPyPI, and install the result in a clean environment.
  5. Tag and create the GitHub release on the upstream repository, not on a fork: the workflow file is in every clone, but the PyPI publisher is bound to hildensia/bayesian_changepoint_detection: git tag vX.Y.Z && git push <upstream-remote> vX.Y.Z, then gh release create vX.Y.Z --repo hildensia/bayesian_changepoint_detection --generate-notes.
  6. The CD workflow builds the sdist and wheel, runs twine check --strict, refuses to continue if the version in pyproject.toml does not match the tag, uploads to PyPI with Trusted Publishing (OpenID Connect, no stored token or password), and attaches both files to the GitHub release. The distribution is bayesian-changepoint; its PyPI publisher is configured for this repository and the cd.yml workflow with the environment left blank. The upload job runs in the pypi GitHub environment (testpypi for rehearsals) so that the upload shows up under "Deployments" on the repository page. A new distribution name needs a matching publisher on PyPI before the first upload.

Reporting issues

Open a GitHub issue with a minimal reproducible example: the data or a generator with its seed, the likelihood and hazard/prior used, the device, and the versions of Python and PyTorch. For something security-sensitive, contact a maintainer privately through their GitHub profile rather than posting the details in a public issue.