Development#
DEVELOPMENT.md at the repository root is the canonical environment guide and
stays that way: it is what a maintainer reads in a terminal, before the
documentation is buildable. This section is everything around it — how to
contribute, what the test tiers are, what CI and the review bots do, and how
versioning works.
Environment#
Set up with Installation, then read DEVELOPMENT.md
for the parts that only matter once you are producing evidence:
the dependency-provisioning rules (inducer stack from Git sources,
pyfmmlibwith OpenMP and the batched P2M wrappers, the post-provisioning traversal sanity check, and the thread caps to record in run metadata);remote setup for heavier numerical experiments;
lint, type-check and test commands.
Lint and types#
ruff.toml at the repository root is the single lint configuration — 85
columns, Python 3.11 target — and it replaced the former
[flake8]/[isort]/[pycodestyle] sections of setup.cfg.
uvx ruff@0.13.0 check
uvx ruff@0.13.0 check --fix # fixable rules only; re-read every hunk
uvx basedpyright -p pyproject.toml --level error
Two conventions are worth knowing before the first pull request.
The [lint.per-file-ignores] block is a baseline, not a preference. It is
a per-file record of the rules a file still violates, measured rather than
hand-written: exactly the set each file trips with the block emptied. New files
start clean. When you clean a file, delete its baseline entry in the same
commit. Entries appear and disappear as the tree changes, so never edit the
block by hand — re-measure it.
ruff format is not a gate. Adopting it would reformat 92 of 117 files
(about 5.7k changed lines), and enabling flake8-quotes (Q) would rewrite
448 single-quoted literals. Either is a tree-wide rewrite that collides with
every open branch, so the [format] section of ruff.toml records the
intended style for a future coordinated reformat and nothing enforces it today.
Documentation#
The site is built with Sphinx and pydata-sphinx-theme; the doc extra
carries everything it needs. The build imports volumential, so it needs an
environment with the OpenCL stack (pyopencl, loopy) installed.
# Name every extra you want: uv sync is exact, so --extra doc alone would
# uninstall pytest and the rest of the test extra.
uv sync --extra test --extra doc
# The build CI Full runs: -W makes every warning an error, and --keep-going
# reports all of them instead of stopping at the first. No warning class is
# suppressed in conf.py, so a malformed docstring fails the build like a bad
# cross-reference does.
sphinx-build -W --keep-going -b html doc/source doc/build/html
# External links.
sphinx-build -b linkcheck doc/source doc/build/linkcheck
# Live preview at http://127.0.0.1:8000, rebuilding on save. --watch is what
# picks up an edit to a notebook: they live outside doc/source, and the
# staging copy in conf.py only runs when a build starts.
sphinx-autobuild --watch examples doc/source doc/build/html
Writing pages#
New pages are MyST Markdown (.md). The reStructuredText pages that remain are
substantial existing documents kept where they are rather than rewritten;
either format is read by the same build, and myst_enable_extensions turns on
amsmath, colon_fence, deflist and dollarmath, so $...$ and $$...$$
math works in Markdown.
The three constructs worth knowing, since they are the ones that differ from plain Markdown:
```{note}
A directive. Any Sphinx directive works with this fence.
```
{doc}`../user-guide/index` and {mod}`volumential.volume_fmm` are roles.
```{toctree}
:maxdepth: 1
some-page
```
A page reaches the sidebar by being in a toctree, and a page that is in none
is a build warning — which, under -W, is a build failure.
Where the API pages come from#
doc/source/api/generated/ is generated — and only that subdirectory.
sphinx.ext.autosummary writes one page per module there from the templates in
doc/source/_templates/autosummary/, so a new module needs no edit and the
tree is not committed. doc/source/api/index.rst above it is committed and
hand-written: it holds the autosummary seed, the module map and the note about
the unsupported volumential.qbfem package, so a change to how the API is
introduced goes there.
The build is nitpicky, which means an unresolvable cross-reference in a
docstring fails it. When the name belongs to a dependency, add an intersphinx
target in doc/source/conf.py. Add a nitpick_ignore entry — with a comment
saying why — only when no usable inventory target exists: either the project
publishes no objects.inv at all (mpmath, pyfmmlib), or it publishes one
that does not document the referenced object (boxtree no longer documents
boxtree.tools.DeviceDataRecord, though its inventory is otherwise fine).
Reproducible gallery assets#
Curated figures live under doc/source/gallery/, but numerical
figures are generated by the maintained examples rather than by Sphinx.
doc/tools/render_gallery.py runs them on an explicitly selected OpenCL
device and writes a manifest.json provenance record beside the figures.
Gallery assets records the regeneration commands, the manifest, the CI
preview artifact, and the rule that documentation builds never launch OpenCL
workloads.
Example notebooks#
The notebooks are maintained in examples/, beside the scripts they
demonstrate. Sphinx reads only what is under doc/source, so conf.py copies
examples/*.ipynb into doc/source/examples/notebooks/ on builder-inited;
that directory is generated and git-ignored, like api/generated/. Add a
notebook to examples/ and it gets a page, because
Examples globs the staged directory — but add the paragraph
that says what it costs to run, since a reader cannot tell from the rendering.
Nothing is executed: nb_execution_mode = "off". Every notebook needs a
working OpenCL device, and the two Poisson tutorials run co-refinement studies
far past a documentation build’s budget; the Helmholtz one calls
run_convergence_study(smoke_mode=True) and would be cheap, but a docs build
is still not where it belongs. So a page shows the prose, the code and whatever
outputs the notebook carries in the repository — today, none. Commit them
stripped. Above 2 MB the
staged copy drops the outputs anyway rather than shipping them into the page;
the file in examples/ is never modified.
Docstring and API coverage#
Two different questions, and CI answers both in the Documentation job of
CI — on every pull request — uploading the answers as a docs-coverage-*
artifact next to the docs-html-* preview of the site itself. Both are
maintained tools configured in the repository, not checkers written here.
sphinx-build -q -W --keep-going -b coverage asks whether every module,
function, class and method of the package reaches a page of this site. -q is
load-bearing rather than tidiness: sphinx.ext.coverage logs an undocumented
object at info level unless the app is quiet, in which case it logs a
warning — and only a warning is something -W fails on. The report it writes,
doc/build/coverage/python.txt, lists the undocumented objects per module;
coverage_show_missing_items is what puts the names in it.
It does not see properties: the builder inspects a class attribute only when
it is a method or a function, so a @property that fell off a page would go
unreported. Nothing here documents a property anywhere but on its class’s
page, so the gap has no reach today; know about it before relying on the
report for a new kind of page. It is at 100% and should
stay there. coverage_modules in conf.py is what makes it a real check:
without it the builder looks only at the modules it already saw on a page, so a
module that fell out of the autosummary tree would not be examined at all and
the total would stay at 100%. With it, a module in the package but not on a
page — and a module on a page but not in the package — is a warning, which
under -W fails the job.
It is not a docstring check. undoc-members is what puts the whole public
surface on the API pages, and an object with no docstring still gets an entry
there and still counts as covered.
interrogate asks the other question:
does every public object have a docstring? It reads the syntax tree, so it
needs no OpenCL stack, no import and no Sphinx, and runs anywhere:
interrogate -v volumential
-v prints the per-file table, and -vv names every object it counted and
says whether it is covered; without either the command prints only the
percentage and its verdict.
The configuration is [tool.interrogate] in pyproject.toml, and it decides
two things. What counts as public is what the API reference means by it: not a
name with an underscore-prefixed component, not __init__ (documented by its
class), not a nested helper, and nothing under qbfem. Modules do count, so a
module that opens with __copyright__ instead of a docstring is a gap like any
other. And fail-under is the percentage the tree measures today rather than a
target, so a new undocumented public object fails the check. Raise the number
in the commit that earns it; lowering it is a deliberate edit that has to say
why.
Neither of these is a review. undoc-members and a one-line docstring both
satisfy a coverage tool; whether the sentence is true is what a reader
checks, and a plausible-sounding docstring on a numerical routine nobody has
run is worse than none.
Redirect stubs for the old flat URLs#
Before the 2026-09 restructure every page lived directly under the site root,
and the published build is replaced in place, so an external link to
/nearfield_symmetry.html would 404 against the new tree. conf.py writes a
meta-refresh stub at each of those old paths on build-finished; the mapping
is _LEGACY_REDIRECTS, it never shadows a real page, and an entry can be
deleted once its inbound links have aged out. Moving a page again means adding
an entry there in the same commit.
Documentation layout#
The site follows a Diátaxis-style split, and a new page belongs in exactly one of these:
Section |
For |
Example |
|---|---|---|
Getting started |
A reader who has not run the code yet |
installing, a first potential |
Examples |
What each program under |
the gallery |
User guide |
Understanding a mechanism you are using |
the Helmholtz split |
Design notes |
Why a mechanism has its shape; no derivations |
windowed channels |
Benchmarks |
What a measurement has to record to be worth quoting |
first call versus warm |
API reference |
|
the module map |
Development |
The project, not the library |
this page |
When you add a kernel, a mode or a derivative path, update Validation Matrix in the same pull request that adds the tests.