Building from Source
Build the Python bindings and the C++ unit-test tree, with or without MPI.
monoprop has one supported from-source build workflow:
- the Python bindings — the nanobind extension behind
import monoprop, built with scikit-build-core and driven byuv(orpip); - the C++ unit tests — built within the same
uv(orpip) invocation.
MPI is off by default in every build path; you enable it explicitly. The mechanism differs by build:
| Build | Enable MPI with |
|---|---|
Python bindings and C++ tests (scikit-build / uv / pip) | --config-settings=cmake.define.monoprop_ENABLE_MPI=ON (or export SKBUILD_CMAKE_ARGS="-Dmonoprop_ENABLE_MPI=ON") |
The prebuilt wheels published to PyPI (pip install monoprop) are also built
without MPI, so a from-source build is required for multi-rank runs.
Other build-time options
| Option | Default | Effect |
|---|---|---|
monoprop_ENABLE_ARCH_FLAGS | ON (OFF for the published wheels) | Compile with -march=native / -xHost. |
monoprop_WIDE_TERM_INDEX | OFF | 64-bit term indices, for partitions holding more than ~2^32 terms. |
monoprop_ENABLE_MPI | OFF | Multi-rank support, as above. |
monoprop_SPARSE_ROW_MIN_MODES | ISA-dependent | Mode count at or above which the operator store prefers sparse rows. |
monoprop_SPARSE_ROW_MIN_MODES follows monoprop_ENABLE_ARCH_FLAGS unless you set
it explicitly: 768 with architecture flags on, 256 without them. The crossover
moves because dense monomials cost one pass per storage word while sparse rows are
flat in the width, so a target without a vector popcount reaches the crossover
sooner. The number is build-time because the ISA is.
Both values are measured end to end through the propagator, at the first whole
32-mode storage block where sparse rows win by more than the run-to-run spread.
Expect about one block of machine dependence either way — near the crossing the two
backends are within a few percent of each other, so a threshold that is one block off
costs very little. If you are running a width close to it and care, measure your own
workload both ways with monoprop_ROW_STORE and pin the value.
Choosing the row backend at run time
A propagator stores its terms either as dense monomials or as sparse rows (a list of
the modes it occupies plus two bits per mode). The choice is made once, from the
storage width against the crossover above. Set monoprop_ROW_STORE to override it
for every propagator in the process:
| Value | Effect |
|---|---|
auto (default, or unset) | Sparse at or above monoprop_SPARSE_ROW_MIN_MODES storage modes, dense below. |
dense | Always dense monomials. |
sparse | Always sparse rows. |
Anything else is an error rather than a silent fallback to auto, because the point
of setting it is to know which backend ran.
Both backends compute the same terms and the same expectation value. They hash rows differently, so they differ in term order — and therefore in floating-point accumulation order, which is why a cross-backend comparison is a tolerance check and not a byte diff. Overriding is mainly a testing and benchmarking tool; leave it unset in production.
Prerequisites
- a C++23-compliant compiler; on Linux the minimum supported versions are GCC 14 and Clang 18
- CMake and Ninja
- Python 3.11 or newer and the
uvpackage manager (for the bindings) - an MPI implementation such as Open MPI (only for MPI builds)
hwloc(version 2.9+) andpkg-config(required so CMake can locatehwloc)
The repository ships a DevContainer with all of the above pre-configured; opening the folder in VS Code and rebuilding the container is the quickest route to a working environment.
Building the Python bindings
uv creates a virtual environment, installs the Python dependencies, and
compiles the nanobind extension in editable mode. Re-run the sync command whenever
the dependency graph or the C++ sources change.
Without MPI (default)
uv sync --all-extras -vThis produces a single-process build with no MPI dependency.
With MPI
Pass a config-settings override to enable MPI:
uv sync --all-extras -v \
--config-settings=cmake.define.monoprop_ENABLE_MPI=ONThe same override works with pip when installing from a checkout:
pip install . --config-settings=cmake.define.monoprop_ENABLE_MPI=ONVerify the install
uv run python -c "import monoprop as mp; print(mp.__version__)"Running the bindings
A serial run is just a normal Python invocation:
uv run python your_script.pyFor a multi-rank run, launch the same script under mpiexec (requires an MPI
build) and pass comm=MPI.COMM_WORLD to the simulator:
mpiexec -n 8 uv run python your_script.pySee Parallelism and distribution for the communicator options and the operator-partitioning controls.
Building the C++ unit tests
The supported C++ workflow reuses the build tree produced by uv sync. Do not
run cmake --preset ... to configure this project directly: the top-level CMake
configuration expects scikit-build-core to provide Python, nanobind, and related
cache variables. Instead, first create the tree with uv sync, then invoke ctest
directly to run the C++ unit tests.
Release tree
uv sync --all-extras -v
ctest --test-dir build/editable/ReleaseThis uses the scikit-build-core Release tree at build/editable/Release and
runs bin/monoprop_unit_tests.x there.
Debug tree
uv sync --all-extras -v --config-settings=cmake.build-type=Debug
ctest --test-dir build/editable/DebugRelated workflows
- Use
just test-widefor the 64-bitmonoprop_WIDE_TERM_INDEXconfiguration. - Use
just code-coveragefor the coverage build. - Use
ctest --test-dir build/editable/Release -L serialor-L mpi-2to filter the discovered C++ test set.
See also
- Getting Started — installing a prebuilt release from PyPI.
- Parallelism and distribution — running across MPI ranks and shared-memory threads.
- Testing — the full Python and C++ test workflow.
- How to Contribute — contributor workflow and documentation checks.