Project
Contributing to ResDAG¶
ResDAG is a PyTorch-native reservoir computing library. Contributions are welcome — components, fixes, docs, benchmarks. This guide covers the mechanics; the design rationale lives in the documentation.
Looking for something to work on? The roadmap is tracked as a dependency-ordered set of issues, grouped under the
type:epictracking issues and organized by thepillar:*labels (correctness, speed, api, pipeline, hpo). Pick any issue labelledstatus:ready.
Development setup¶
git clone https://github.com/El3ssar/ResDAG && cd ResDAG
uv sync --extra dev # lint/format/test toolchain (single source of truth)
uv run pytest --no-cov -q # full suite — ~1,400 tests, a few minutes on CPU
pip install -e ".[dev]" works too if you don't use uv. Add hpo (Optuna) or
docs (MkDocs + the figure pipeline) as needed — e.g. uv sync --extra dev
--extra docs or pip install -e ".[dev,docs]".
Quality gate¶
Run before pushing:
uv run ruff check src/ tests/
uv run black --check src/ tests/
uv run mypy src/resdag/
uv run pytest --no-cov -q # full suite
Type checking¶
CI type-checks the whole package — mypy src/resdag/, not a single file —
and the tree is kept at zero errors. There is deliberately no per-module
baseline and no disable_error_code escape hatch in pyproject.toml: new and
changed code is gated immediately, so a typing error is a failing build, not a
warning. The only standing allowance is ignore_missing_imports (for untyped
third-party libraries such as pytorch_symbolic).
If your change surfaces a type error — even one that looks pre-existing — fix it
in your PR with a real annotation/narrowing rather than suppressing it. Reach
for cast/assert only when a runtime invariant genuinely guarantees the
narrowing (and say so in a short comment); never use them, or a blanket
# type: ignore, to paper over a possible-None/wrong-type bug. If you must
ignore, use a specific # type: ignore[code] with a one-line justification.
Fast local iteration — run only the affected tests¶
The full suite takes a few minutes. While iterating, run only the tests that your change can affect, computed from a static first-party import graph:
# What would run for your branch vs. main, and why:
uv run python tools/affected_tests.py --explain
# Run exactly that subset:
uv run pytest --no-cov -q $(uv run python tools/affected_tests.py --format args)
The selector errs toward running more tests, never fewer (broad-impact
changes such as pyproject.toml, conftest.py, or a package __init__ fan
out to the whole suite). Still do one full pytest run before you push.
Selective CI¶
Pull-request CI runs the same selector and tests only the affected subset,
so a typical single-module PR finishes in seconds instead of minutes. The full
suite is the safety net and still runs on every merge to main and on a nightly
schedule. To force a full run on a PR, add the ci-full label.
The test suite mirrors src/resdag/. Markers:
gpu— CUDA variants, auto-skip on machines without a GPUbenchmark— performance assertions, deselected by default (pytest -m benchmarkto run; they assert the GPU beats the CPU at scale)
Golden-forecast regression fixtures¶
tests/test_models/test_golden_forecast.py pins the behaviour of
ESNModel.forecast for each premade model against committed golden
trajectories under tests/fixtures/golden_forecast/ — a fully-seeded
warmup → forecast on Lorenz-63, in CPU float64. It fails on any drift in the
forecast path. If you change that path — or the canonical lorenz /
prepare_esn_data generators it builds on — regenerate the fixtures and review
the diff before committing:
uv run python tools/regen_golden_forecasts.py --check # verify only (drift check, non-zero exit on drift)
uv run python tools/regen_golden_forecasts.py --all # regenerate every fixture
The model specs, data splits, metric, and tolerances live in
tests/test_models/golden_forecast.py, shared by the test and the tool so they
can never disagree.
Commit messages and releases¶
Releases are live. Every push to
mainwhose commits since the last tag warrant a release publishes automatically (version bump, tag, GitHub release, PyPI via trusted publishing). Never renamerelease.yml— PyPI trusted publishing is bound to that filename. The mechanics below describe what runs.
Releases are fully automated from conventional commits — there are no
manual tags or version edits. On every push to main,
python-semantic-release parses the commits since the last release:
| prefix | effect |
|---|---|
feat: ... |
minor release (0.5.0 → 0.6.0) |
fix: ..., perf: ... |
patch release (0.5.0 → 0.5.1) |
feat!: ... or BREAKING CHANGE: footer |
minor while 0.x, major from 1.0 |
docs:, chore:, ci:, refactor:, test:, style:, build: |
no release |
A release rewrites __version__ in src/resdag/__init__.py and the
mirrored version field in CITATION.cff (both are listed in
version_variables, so they stay in lock-step automatically), tags
vX.Y.Z, publishes a GitHub release with generated notes, and uploads to
PyPI. Documentation deploys on the same push, and version strings in the
docs substitute automatically.
Pull requests are squash-merged, and the PR title becomes the commit
that decides the bump — so PR titles must be conventional
(feat: add Rössler topology). CI checks the title format on every PR.
Inside your branch, commit however you like; only the squash title counts.
Adding components¶
Every component type is an open set with one extension point:
| component | how | docs page |
|---|---|---|
| Topology (graph) | @register_graph_topology("name", **defaults) on a NetworkX generator |
generated automatically from the registry |
| Topology (matrix) | @register_matrix_topology("name", **defaults) on an fn(n) -> matrix |
generated automatically |
| Input/feedback initializer | @register_input_feedback("name") on a function or InputFeedbackInitializer subclass |
generated automatically |
| Readout | subclass ReadoutLayer, implement _fit_impl(states, targets) |
one file in docs/build/readouts/ |
| Reservoir family | implement the ReservoirCell contract, wrap in a BaseReservoirLayer subclass |
one file in docs/build/layers/ |
| Premade architecture | factory function returning a model, in src/resdag/models/ |
one file in docs/build/architectures/ |
Registered topologies and initializers document themselves: their docs pages (figure, parameters, usage) are generated from the registry at build time. For the other component types, drop a Markdown file in the listed folder — navigation and index cards pick it up automatically.
New components need tests (place them in the mirror location under
tests/) and, for anything with tunable structure, a figure — see below.
Documentation¶
uv run mkdocs serve # live preview at 127.0.0.1:8000/ResDAG/
uv run mkdocs build --strict # link-checking build, run before pushing
- Colors and fonts for the entire site and every figure live in
docs/_tooling/theme.json. After editing:uv run python docs/_tooling/apply_theme.py --figures - Figures are generated by the per-category scripts in
docs/_tooling/figures/— never edit images indocs/assets/figures/by hand. - The token block in
docs/css/notebook.cssbetween the===== TOKENS =====markers is generated; edittheme.jsoninstead. - The landing page is a single file:
docs/overrides/home.html.
Pull request checklist¶
- Conventional PR title (it becomes the release-deciding commit)
-
ruff check,black --check, andpytestpass locally - New code has tests; new components have docs (see table above)
-
mkdocs build --strictpasses if you touched docs - Figures regenerated via the scripts if you changed visuals