monoprop

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 by uv (or pip);
  • the C++ unit tests — built within the same uv (or pip) invocation.

MPI is off by default in every build path; you enable it explicitly. The mechanism differs by build:

BuildEnable 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

OptionDefaultEffect
monoprop_ENABLE_ARCH_FLAGSON (OFF for the published wheels)Compile with -march=native / -xHost.
monoprop_WIDE_TERM_INDEXOFF64-bit term indices, for partitions holding more than ~2^32 terms.
monoprop_ENABLE_MPIOFFMulti-rank support, as above.
monoprop_SPARSE_ROW_MIN_MODESISA-dependentMode 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:

ValueEffect
auto (default, or unset)Sparse at or above monoprop_SPARSE_ROW_MIN_MODES storage modes, dense below.
denseAlways dense monomials.
sparseAlways 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 uv package manager (for the bindings)
  • an MPI implementation such as Open MPI (only for MPI builds)
  • hwloc (version 2.9+) and pkg-config (required so CMake can locate hwloc)

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 -v

This 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=ON

The same override works with pip when installing from a checkout:

pip install . --config-settings=cmake.define.monoprop_ENABLE_MPI=ON

Verify 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.py

For 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.py

See 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/Release

This 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/Debug
  • Use just test-wide for the 64-bit monoprop_WIDE_TERM_INDEX configuration.
  • Use just code-coverage for the coverage build.
  • Use ctest --test-dir build/editable/Release -L serial or -L mpi-2 to filter the discovered C++ test set.

See also

On this page