Installation#
pyproject.toml plus uv are the source of truth
for dependency resolution, and uv.lock records the exact resolution an
environment was built from — a commit for each Git-sourced dependency, a
version and artifact hashes for each one that comes from PyPI. DEVELOPMENT.md at the repository root carries the
same recipe in the form a maintainer runs it; this page is the version a new
user needs.
Prerequisites#
Python 3.12 — the version CI tests, and the only one exercised.
requires-pythonis>=3.11and nothing in Volumential itself needs 3.12.loopyused to importoverridefrom the standard-librarytypingmodule, which gained it only in 3.12, soimport loopyfailed outright under 3.11; at the revisionuv.lockpins that import now comes fromtyping_extensions. Treat 3.11 as untested rather than as known-broken: no job runs it.An OpenCL runtime. PoCL is the default tested backend; a vendor ICD (CUDA, ROCm) works too.
uv.micromamba(orconda/mamba), which is how this recipe provides the OpenCL runtime. Skip it only if the host already has a working ICD and a 3.12 interpreter, in which case create the environment however you normally would and pick the recipe up at thegit clone.gfortranandninja, only for the optionalfmmlibextra.
Install#
curl -LsSf https://astral.sh/uv/install.sh | sh
"${SHELL}" <(curl -L micro.mamba.pm/install.sh) # if micromamba is absent
The OpenCL runtime is easiest to obtain from conda-forge, so create the base
environment there and let uv fill in the rest:
micromamba create -n volumential-dev -c conda-forge -c nodefaults \
python=3.12 pyopencl pocl scipy numpy
eval "$(micromamba shell hook -s bash)" # once per shell, if not shell-init'd
micromamba activate volumential-dev
export UV_PROJECT_ENVIRONMENT="$CONDA_PREFIX"
git clone https://github.com/xywei/volumential.git
cd volumential
uv sync --extra test --extra doc
Two lines there are load-bearing, and they fail differently if you skip them:
without the eval, micromamba activate fails at once because the shell
function does not exist yet; without UV_PROJECT_ENVIRONMENT, uv fails
quietly and installs into a checkout-local .venv instead.
micromamba activate is a shell function, not a binary, so a fresh shell has
to evaluate the hook before it exists. micromamba shell init -s bash makes it
permanent; the eval line above is the per-shell form.
UV_PROJECT_ENVIRONMENT is what makes uv use the conda environment as the
project environment. Activating conda sets CONDA_PREFIX, not VIRTUAL_ENV,
and uv’s --active flag keys on VIRTUAL_ENV — so uv sync --active inside
an activated conda environment does not target it: it creates .venv in
the checkout and installs there, and every later uv run uses that same
.venv. The result builds and imports, and has none of the conda-provided
OpenCL runtime this recipe exists to supply. Export the variable once after
activating and both uv sync and uv run do the right thing.
Why the dependencies come from Git#
[tool.uv.sources] points most of the inducer stack at its main branches,
and uv.lock records the resolved commits: arraycontext, boxtree, cgen,
genpy, gmsh_interop, loopy, meshmode, modepy, pyfmmlib,
pymbolic, pytential, pytools, pyvisfile, sumpy. pyopencl is not
among them — it resolves from PyPI, and uv.lock pins a release
(2026.1.2) rather than a commit, so that is the version an audit of an
evidence environment should expect to find. These projects release rarely, and released wheels have shipped
defects that corrupt results silently, which is a different and worse failure
than a crash.
The specific one that matters here: boxtree must be at or after the upstream
commit that fixed refine_and_coarsen_tree_of_boxes (parent/child id remapping
after the level reorder, the tile-versus-repeat parent assignment, and an
np.inf sentinel on an integer array). Anything older — the 2024.10 wheel
included — silently corrupts List 1 neighbour lists on reordered adaptive
trees. Do not substitute PyPI wheels in an environment that produces evidence,
and capture the locked commits in the metadata of any promoted result.
A patch that exists only inside one environment’s site-packages is an
incident to remediate, never a fix: patches belong upstream, on a tracked fork
branch, or vendored and committed.
The FMMLib backend#
uv sync --extra test --extra doc --extra fmmlib
uv sync is an exact sync: it uninstalls whatever the requested set does not
include. Naming only --extra fmmlib would therefore remove the test and
doc extras installed above, pytest included, so list every extra you want
in the environment on each sync.
This builds pyfmmlib from upstream main, which carries both the restored
OpenMP feature option (inducer/pyfmmlib#93)
and the batched {l,h}{2,3}dformmp_imany wrappers
(inducer/pyfmmlib#94). The PyPI
2024.1.1 release has neither. Since
#135 pyfmmlib has a
[tool.uv.sources] entry like the inducer packages, so the extra resolves to
the commit uv.lock pins; installing it by hand with
uv pip install "pyfmmlib @ git+..." still works but bypasses the lock, and
two hosts provisioned on different days then end up on different revisions.
The openmp feature option defaults to auto, so a host with a usable OpenMP
toolchain needs no extra build flag. Verify the build before trusting any
FMMLib timing — FPNDFMMLibExpansionWrangler falls back to the serial per-box
path without complaining when the batched entry points are missing, so a
mis-provisioned environment is correct but slow:
# Batched wrappers present. The backend picks {l,h}{2,3}dformmp_imany from the
# equation and the dimension, so check all four: a successful 3D Laplace import
# does not rule out a 2D or Helmholtz fallback. This covers charge sources
# only.
python -c "from pyfmmlib import \
h2dformmp_imany, h3dformmp_imany, l2dformmp_imany, l3dformmp_imany"
# Dipole sources (a DirectionalSourceDerivative kernel, i.e. a dipole_vec)
# take a different P2M wrapper, {l,h}{2,3}dformmp_dp_imany, and fall back to
# the per-box routine just as silently when it is absent. pyfmmlib generates
# both families at the revision uv.lock pins, so an import failure here means
# the dipole P2M will run per box, not that the charge path is broken.
python -c "from pyfmmlib import \
h2dformmp_dp_imany, h3dformmp_dp_imany, \
l2dformmp_dp_imany, l3dformmp_dp_imany"
python - <<'PY'
import pathlib
import platform
import subprocess
import pyfmmlib
so = next(pathlib.Path(pyfmmlib.__file__).parent.glob("_internal*.so"))
print(so)
if platform.system() == "Darwin":
# macOS has no ldd, and the OpenMP runtime is libomp rather than libgomp.
subprocess.run(["otool", "-L", str(so)], check=True)
else:
subprocess.run(["ldd", str(so)], check=True)
PY
Expect a line for an OpenMP runtime: libgomp for a GCC build, libomp
for Clang (the usual case on macOS, and a possible one on Linux), libiomp
for Intel. The name follows the compiler, not the operating system, so do not
read a missing libgomp on Linux as a missing OpenMP. A missing runtime and
an unavailable ldd look the same otherwise, which is why the check branches
instead of assuming Linux.
Batched P2M is bit-identical to the per-box path and GEMM L2P agrees at
roundoff (test/test_fmmlib_batched_stages.py), so adopting them needs no
accuracy re-measurement — but any FMMLib-stage timing narrative must be
re-measured after adoption.
Verify the environment#
Imports and quick tests#
These need UV_PROJECT_ENVIRONMENT exported, as above — uv run otherwise
creates and uses .venv rather than the conda environment.
uv run pytest -q test/test_import.py
uv run pytest -q test/test_public_surface.py
uv run pytest -q test/test_duffy_tanh_sinh.py
The traversal check#
Run this once after creating an environment, and again after any inducer-stack
update, before the environment is used for evidence. The driver that builds
the graded 3D case (q_order, initial levels, adapt steps) = (3, 4, 3) and
reports its List 1 diagnostics left the tree with the rest of the benchmark
drivers (Benchmarks and reproducibility), but it is one git archive away:
7c75ed1 is the last revision of main that carries it, and it imports only
its sibling adaptive_timing.py besides the installed library.
mkdir -p build/traversal-check
git archive 7c75ed1 benchmarks | tar -x -C build/traversal-check
python build/traversal-check/benchmarks/adaptive_timing_3d.py --mode full \
--out build/traversal-check/adaptive-timing-3d.csv
In the laplace3d-q3-l4-a3 row of that CSV, cross_level_list1_fraction must
equal 0.16588653810147913, that is n_cross_level_list1_interactions =
3544 out of n_list1_interactions = 21364. A different value means the
tree-of-boxes refinement is the broken one: stop and re-provision. The driver
also fails loudly if the adaptive tree comes out uniform, unbalanced, or free
of cross-level List 1 work; that is the same verdict.
This check is the reason the Git sources above are not optional. It is cheap, it is decisive, and skipping it is how a corrupted neighbour list reaches a plot.
Next#
A first volume potential — a volume potential in about twenty lines.
Device selection — make sure the run lands on the device you meant.
DEVELOPMENT.mdand Development — the maintainer-facing version of all of the above, plus lint, types and the test tiers.