Contributing¶
Contributions to pymocd are welcome: bug reports, fixes, new detectors, documentation and examples. This page describes how the repository is set up, so that a change passes the same checks locally that it meets in continuous integration (CI). It covers reporting issues, building pymocd from source, running the tests, how the API reference is generated from the Rust sources, the steps for adding a detector, writing documentation, and the commit conventions of the project.
Reporting issues¶
Issues are tracked on
GitHub. Opening a new issue
offers the three templates in .github/ISSUE_TEMPLATE/:
| Template | Its description | It asks for |
|---|---|---|
| Bug report | "Create a report to help us improve" | A description of the bug, steps to reproduce it, the expected behavior, screenshots if applicable, your operating system, and any other context |
| Feature request | "Suggest an idea for this project" | The problem it solves, the solution you would like, alternatives you considered, and any other context |
| Custom issue template | None (placeholder text) | Free text: the template body is empty |
For a bug in a detector, a minimal script is the most useful reproduction: the
graph (ideally one networkx can build, such as nx.karate_club_graph()), the
exact call with its keyword arguments, and the full traceback or the
unexpected output. Include your pymocd and Python versions;
importlib.metadata.version("pymocd") returns the former.
Development setup¶
pymocd is a Rust crate compiled into a Python extension module with PyO3 and maturin. To build it you need:
- a Rust toolchain. The crate uses Rust edition 2024 (
Cargo.toml). The workflows that build the Python package (python.yml,docs.ymlandrelease.yml) install the stable toolchain withdtolnay/rust-toolchain@stable;rust.ymlhas no toolchain step; - Python 3.10 or newer (
requires-python = ">=3.10"inpyproject.toml). The Makefile route below needs Python 3.11 or newer aspython3: some of the versions pinned inres/requirements.txt(networkx 3.6.1, numpy 2.4.2, pandas 3.0.1, scikit-learn 1.8.0) declareRequires-Python >=3.11on PyPI.
Clone the repository and let the Makefile create a development environment:
The Makefile targets used during development:
| Target | What it does |
|---|---|
make dependencies |
Creates a virtual environment in .venv with python3 -m venv, upgrades its pip, and installs the pinned development and benchmark requirements of res/requirements.txt into it (maturin, networkx, igraph, matplotlib and others). |
make stubs |
Runs cargo run --bin stub_gen, which writes the type stub pymocd.pyi to the repository root. |
make build |
make dependencies, then generates the stub, installs pymocd into .venv with maturin develop --release, and deletes pymocd.pyi again. |
make test |
make build, then the Rust test suite with cargo test. |
make all |
make build and make test. |
make docs / make docs-serve |
Build or serve this documentation site (see Writing documentation). |
make clean |
Runs cargo clean and removes .venv, target, build, dist, *.egg-info, pymocd.pyi and every __pycache__ directory. |
Without the Makefile, build with cargo run --bin stub_gen followed by
pip install .; CI runs the same two steps with uv
(uv pip install --system .). See
Building from source.
Leave the extension-module feature off for cargo
Cargo.toml deliberately keeps PyO3's extension-module feature out of
the default features. Its comment explains why: the feature omits the
link against libpython, so with it on by default every cargo build,
cargo test and cargo run fails to link the stub_gen binary. maturin
turns it on when it builds the extension ([tool.maturin] features in
pyproject.toml).
The benchmark* and hard-* targets drive the benchmark harness in
benchmarks/. They are not part of CI.
Running the tests¶
There are two test suites. Rust unit tests live in #[cfg(test)] modules next
to the code they test: in the detector modules under src/core/algorithms/
and in the graph and metrics code under src/core/. The Python suite in
tests/ exercises the installed package:
| File | What it checks |
|---|---|
tests/test_partitions.py |
The detectors (all but gdpso and cdrme) and the rimpso_fronts and mmcomo_fronts accessors return valid partitions of the karate club graph (every node assigned, integer community ids), and hpmocd and rimpso recover both triangles of a graph made of two triangles joined by one edge. |
tests/test_determinism.py |
rimpso and rimpso_fronts return identical results on repeated calls, rimpso returns the same partition under 1 and 4 threads, and repeated hpmocd runs return valid partitions. |
tests/test_sanity.py |
A modularity floor on karate for rimpso and hpmocd, perfect self-agreement of the metrics, community -1 for isolated nodes, rimpso_fronts objective values that match their partitions, and the complete-graph and empty-graph edge cases. |
tests/test_igraph.py |
Every detector accepts an igraph.Graph (Zachary's karate club) and assigns every vertex, rimpso returns the same partition for an igraph graph and a NetworkX copy with the same node and edge order, and an isolated vertex gets community -1. Skipped when python-igraph is not installed. |
Two workflows run them on pull requests:
| Workflow | Runs on | Steps |
|---|---|---|
.github/workflows/rust.yml |
pull requests to master |
cargo build --verbose, then cargo test --verbose |
.github/workflows/python.yml |
every pull request | Python 3.10; cargo run --bin stub_gen; installs networkx, python-igraph and pymocd from the checkout; runs the Python suite from tests/ |
To run the same checks locally, run the Rust suite from the repository root:
and the Python suite from tests/, with a Python interpreter that has pymocd
and networkx installed (after make build, the one in .venv):
test_determinism.py starts child processes to compare thread counts,
because max_cores sizes the thread pool only once per
process.
Docstrings and the type stub¶
The API reference is generated; no signature on it is written by hand. The pipeline is:
- Docstrings are Rust doc comments. Each Python-facing function, and the
HpMocdclass with its methods, carries a///comment, which mkdocstrings parses as a Google-style docstring (docstring_style: googleinmkdocs.yml). Most detector docstrings haveArgs:andReturns:sections; the metric functions andmax_coreshave short summaries only. They live insrc/api/detectors.rs,src/api/metrics.rs,src/core/utils/threading.rs(max_cores) andsrc/core/algorithms/hpmocd/api.rs(theHpMocdclass). The first sentence of a docstring becomes its summary on the API index page. - Annotations select what is stubbed.
#[gen_stub_pyfunction](and#[gen_stub_pyclass],#[gen_stub_pymethods]forHpMocd) make pyo3-stub-gen include an item. Parameter defaults come from the#[pyo3(signature = (...))]attribute. Some functions that return a generic Python object carry a#[gen_stub(override_return_type(...))]attribute that states the precise return type; without one, the stub declarestyping.Any, as it does forrimpso_select. cargo run --bin stub_genwritespymocd.pyi. The binary (src/bin/stub_gen.rs) calls the gatherer thatdefine_stub_info_gatherer!defines insrc/lib.rs. The stub is not tracked in git (.gitignore); maturin packages it, together withpy.typed, into every wheel and source distribution.- The docs build reads the stub.
docs/hooks.pycopiespymocd.pyito.docs-stub/pymocd.py, rewriting PyO3's__new__constructors as__init__, and mkdocstrings renders that copy. The build stops ifpymocd.pyiis missing (runmake stubs), if a public name in the stub has nopymocd.<name>directive indocs/api/detectors.md,docs/api/fronts.mdordocs/api/metrics.md, or if a directive names something the stub does not contain.
To change a signature, default or docstring, edit the Rust source and
regenerate the stub. Never edit pymocd.pyi by hand: the next
cargo run --bin stub_gen overwrites it.
Adding a detector¶
Each detector is a self-contained module under src/core/algorithms/: it owns
its representation, objectives, operators, search engine and entry point. The
steps below follow the existing modules; the
mocd_q entry point serves as the example.
- Write the module. Create
src/core/algorithms/<name>/and declare it withpub mod <name>;insrc/core/algorithms/mod.rs. The existing modules each have amod.rs, anapi.rswith the entry point, and their shipped defaults as named constants (defaults.rsorconfig/). -
Expose it to Python. Add a function to
src/api/detectors.rswith a Google-style docstring and the stub annotation. An abridged excerpt of the existingmocd_qentry point:/// Run Shi-MOCD (Shi, Yan, Cai, Wu 2012) — PESA-II over Shi's /// ... /// Returns: /// ``dict[node, community]``. Isolated nodes get community ``-1``. #[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "mocd_q", signature = (graph, pop_size = mocd::DEFAULT_POP_SIZE, num_gens = mocd::DEFAULT_NUM_GENS, cross_rate = mocd::DEFAULT_CROSS_RATE, mut_rate = mocd::DEFAULT_MUT_RATE))] pub fn mocd_q_fn( graph: &Bound<'_, PyAny>, pop_size: usize, num_gens: usize, cross_rate: f64, mut_rate: f64, ) -> PyResult<Partition> {Every detector returns one partition as
dict[node, community], with isolated nodes in community-1. A front accessor, if the detector has one, is a second function named<name>_fronts. 3. Register it in thepymocdmodule function insrc/lib.rs, next to the other detectors: -
Regenerate the stub with
cargo run --bin stub_genand check that the new name appears inpymocd.pyi. - Add it to the API reference. Put a
pymocd.<name>directive indocs/api/detectors.mdand, if the detector has a front accessor, one for<name>_frontsindocs/api/fronts.md. The docs build fails while a public name is undocumented. - Write its algorithm page,
docs/algorithms/<name>.md, and add it tonavinmkdocs.ymlunder Library methods or Re-implemented baselines. Follow the structure of the existing pages: the paper with full citation and DOI, objectives, search, selection, parameters, complexity, when to use it, and divergences from the paper. The parameter table must be marked with<!-- pymocd:defaults pymocd.<name> -->; the build checks it against the stub (see Writing documentation). Add the method to the Algorithms overview too. - Write the module README,
src/core/algorithms/<name>/README.md. Like the existing ones, assrc/core/algorithms/README.mddescribes them, it carries the full citation, the objective equations, the representation, the search loop, the parameter table, whether the detector is bit-deterministic, and every known divergence from its paper. Add a row for it to the table insrc/core/algorithms/README.md. The top-levelREADME.mdonly points to the documentation, so it needs no change. - Test it. Add a case to
tests/test_partitions.py(a valid partition of the karate club graph, plus the front accessor if there is one), add the entry point to theDETECTORSlist intests/test_igraph.pyso that it is also tested on igraph input, and add Rust unit tests for the module's pieces. - Cite it. For a re-implemented baseline, add its paper, DOI and BibTeX to Citing pymocd and Publications.
To include the detector in the benchmark harness, register a wrapper with the
@algorithm(...) decorator in benchmarks/_exp_synt_net/algorithms.py, next
to the existing wrappers. That file wraps every detector except gdpso and
cdrme.
Writing documentation¶
The site is built with MkDocs and the
Material theme. Its
configuration is mkdocs.yml, its pages are under docs/, and its build
dependencies are listed in docs/requirements.txt. Add any new docs
dependency there, because both the Makefile and the deploy workflow install
from it.
make docs # build the site into site/ with mkdocs build --strict
make docs-serve # serve it locally with live reload
Both targets first build pymocd into .venv and regenerate the stub, because
the code on the pages runs during the build. --strict turns every warning
into an error, including a broken internal link, a missing anchor and a page
that is not listed in nav. The site is deployed to GitHub Pages by
.github/workflows/docs.yml on every push to master; pull requests do not
build it, so run make docs before opening one that touches docs/,
examples/ or a docstring.
Page conventions¶
- Front matter. The guide, algorithm and research pages start with YAML
front matter holding a
description:line, then one#title. -
Executed code. Python examples on the pages are written as markdown-exec blocks with
exec="on", so they run at build time and their output is that of the current package. Blocks that share asessionshare their variables, in page order:```python exec="on" source="above" session="<page-slug>" result="text" import networkx as nx import pymocd G = nx.karate_club_graph() print(len(set(pymocd.rimpso(G).values()))) ```Keep these blocks fast (small graphs, small budgets), because the whole site is rebuilt in CI. Shell commands go in plain
bashblocks, which are not executed. -
API links. Refer to the API with cross-references such as
[`rimpso`][pymocd.rimpso]; any name in the stub works aspymocd.<name>. - Parameter tables. A table placed right after an HTML comment of the form
<!-- pymocd:defaults pymocd.<name> -->is compared with the stub on every build. Its first column is the parameter name in backticks and its second the default in backticks, written as in the stub or as an exact fraction such as1/68. Every parameter exceptgraphmust appear, and no other row may. Several targets can share one table if their shared parameters agree. - Missing facts. Do not guess. When a fact is not known,
write a sentence that is true without it and put an HTML comment starting
with
TODO(docs):right after it, saying what is missing and where it could come from. The build lists every such marker at the end. - Moved pages. Keep URLs stable. When a page moves, add the old path to
redirect_mapsunder theredirectsplugin inmkdocs.yml.
Gallery examples¶
The Examples gallery is generated by
mkdocs-gallery from the scripts in
examples/. Every file whose name starts with plot_ is executed during the
build, and its printed output and figures are rendered into its page. To add
an example:
- put
plot_<topic>.pyin a section folder ofexamples/(basics,benchmarks,frontsorobjectives); each folder has aREADME.mdwhose heading titles the section. Sections appear in alphabetical order of their folder names (thesubsection_orderoption of mkdocs-gallery 0.10.4 recurses forever, so it is not used), and examples within a section by file name; - open the script with a module docstring whose first line is a Markdown
#title, followed by a sentence on what the example shows; - split the script into cells with
# %%lines; the comment lines right after a# %%are rendered as Markdown text between the code cells; - keep it self-contained and quick, and show figures with
plt.show().
The generated pages land in docs/examples/, which is not tracked in git. The
edit button of a gallery page opens the example's source script.
Communities in Zachary's karate club
is a short example to start from.
Commits and pull requests¶
Propose changes as a pull request against master, where the CI workflows
above run on them. The repository has no written commit policy; most commit
subjects in its history follow these conventions:
- A scope, a colon, then a short imperative summary in lower case, with
no trailing period:
rimpso: rename mr_mocd and replace the front selector,tests: move the python suite onto mr_mocd,ci: use setup-python for the interpreter, uv for installs. Some subjects have no scope (vendor the cdrme reference implementation). - The scope names what changed: a detector module (
rimpso,cdrme,gdpso,mmcomo), or an area such asdocs,readme,tests,ci,bench,build(deps)orchore. Older commits also use conventional types with a scope in parentheses, such asrefactor(ccm):andfeat(api):. - One logical change per commit. CDRME, for example, was added as a
series of commits, one per part (
cdrme: add the topology, objective and entry points,cdrme: add the configuration and shipped defaults, and so on). - A body only when it is needed. Most commits are a single line; a body explains changes the subject cannot, such as a replaced selection rule.
Dependabot checks the Cargo dependencies weekly and opens pull requests for
updates (.github/dependabot.yml).
Releases¶
make bump V=<version> sets the version in Cargo.toml and
pyproject.toml, commits it as chore: bump version to <version>, tags the
commit v<version> and pushes master with the tag.
The tag triggers .github/workflows/release.yml, which builds wheels for
Linux, Windows and macOS on Python 3.10 to 3.13, plus a source distribution,
and publishes them to PyPI.
Licence¶
pymocd is licensed under the GNU General Public License, version 3 or later
(GPL-3.0-or-later), as declared by license in Cargo.toml and by the
licence classifier in pyproject.toml; the full text is in
LICENSE. Rust
source files open with a //! summary of the module followed by a licence
header; new files follow the same layout.
See also¶
- Installation: installing from PyPI and building from source.
- API reference: the generated reference this pipeline produces.
- Algorithms: the existing detector pages to model a new one on.