Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ jobs:
wget https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-noble.deb
sudo apt-get install -y ./apache-arrow-apt-source-latest-noble.deb
sudo apt-get update
sudo apt-get install -y clang-20 libomp-20-dev ninja-build libarrow-dev
sudo apt-get install -y clang-20 libomp-20-dev ninja-build libarrow-dev=23.0.1-1

- name: Install prerequisites (Linux)
if: matrix.os == 'linux' && matrix.compiler == 'gcc-14'
run: |
wget https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-noble.deb
sudo apt-get install -y ./apache-arrow-apt-source-latest-noble.deb
sudo apt-get update
sudo apt-get install -y g++-14 ninja-build libarrow-dev
sudo apt-get install -y g++-14 ninja-build libarrow-dev=23.0.1-1

- name: Install prerequisites (macOS)
if: matrix.os == 'macos'
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ CTestTestfile.cmake

### MacOS
.DS_Store

### uv
uv.lock
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ if(NETWORKIT_BUILD_CORE AND NETWORKIT_MONOLITH)
endif()

target_link_libraries(networkit PRIVATE OpenMP::OpenMP_CXX PUBLIC tlx Arrow::arrow_shared)
if(CMAKE_DL_LIBS)
target_link_libraries(networkit PRIVATE ${CMAKE_DL_LIBS})
endif()

set_target_properties(networkit PROPERTIES
CXX_STANDARD ${NETWORKIT_CXX_STANDARD}
Expand Down
91 changes: 91 additions & 0 deletions examples/parallel_leiden_scoring_extension_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python3

import argparse
import os
from pathlib import Path

import networkit as nk


def build_demo_graph():
graph = nk.Graph(6, weighted=False, directed=False)
for u, v in [(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5), (2, 3)]:
graph.addEdge(u, v)
return graph


def parse_args():
parser = argparse.ArgumentParser(
description="Demo ParallelLeidenView's shared-library move scoring extension mechanism."
)
parser.add_argument(
"--plugin",
type=Path,
help=(
"Path to a scorer shared library. "
"Example: build/networkit/cpp/community/libnetworkit_parallel_leiden_modularity_extension.dylib"
),
)
parser.add_argument(
"--use-env",
action="store_true",
help="Load the plugin through NETWORKIT_LEIDEN_MOVE_SCORING_LIB instead of the Python API.",
)
parser.add_argument("--iterations", type=int, default=2, help="Number of Leiden iterations.")
parser.add_argument("--gamma", type=float, default=1.0, help="Resolution parameter.")
parser.add_argument(
"--randomize",
action="store_true",
help="Randomize node order. Disabled by default to keep output stable.",
)
return parser.parse_args()


def main():
args = parse_args()
graph = build_demo_graph()
plugin_loaded_via = None

if args.use_env:
if args.plugin is None:
raise SystemExit("--use-env requires --plugin")
os.environ["NETWORKIT_LEIDEN_MOVE_SCORING_LIB"] = str(args.plugin.resolve())
plugin_loaded_via = "env"

leiden = nk.community.ParallelLeidenView(
graph, iterations=args.iterations, randomize=args.randomize, gamma=args.gamma
)

if args.plugin is not None and not args.use_env:
if hasattr(leiden, "loadMoveScoringExtension"):
leiden.loadMoveScoringExtension(str(args.plugin.resolve()))
plugin_loaded_via = "api"
else:
# Fallback for older Python extension builds where the C++ env hook exists
# but the wrapper method has not been rebuilt yet.
os.environ["NETWORKIT_LEIDEN_MOVE_SCORING_LIB"] = str(args.plugin.resolve())
leiden = nk.community.ParallelLeidenView(
graph, iterations=args.iterations, randomize=args.randomize, gamma=args.gamma
)
plugin_loaded_via = "env-fallback"

leiden.run()
partition = leiden.getPartition()

print("communities:", partition.numberOfSubsets())
print("assignment:", [partition[i] for i in range(graph.numberOfNodes())])
if args.plugin is None:
print("scorer: built-in modularity")
elif plugin_loaded_via == "api":
print("scorer: plugin loaded via ParallelLeidenView.loadMoveScoringExtension()")
elif plugin_loaded_via == "env":
print("scorer: plugin loaded from NETWORKIT_LEIDEN_MOVE_SCORING_LIB")
elif plugin_loaded_via == "env-fallback":
print("scorer: plugin loaded from NETWORKIT_LEIDEN_MOVE_SCORING_LIB")
print("note: Python wrapper method not available in this build, used env fallback")
else:
print("scorer: plugin requested, but no loading path was used")


if __name__ == "__main__":
main()
68 changes: 68 additions & 0 deletions include/networkit/community/ParallelLeidenScoringExtension.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* ParallelLeidenScoringExtension.hpp
*
* Shared-library ABI for custom ParallelLeidenView move scoring metrics.
*/

#ifndef NETWORKIT_COMMUNITY_PARALLEL_LEIDEN_SCORING_EXTENSION_HPP_
#define NETWORKIT_COMMUNITY_PARALLEL_LEIDEN_SCORING_EXTENSION_HPP_

#include <networkit/Globals.hpp>

namespace NetworKit {

using ParallelLeidenCommunityScoreFunction =
double (*)(double cutWeight, double degree, double communityVolume, count subsetSize,
count communitySize, double gamma, double inverseGraphVolume);

using ParallelLeidenRefineSetConditionFunction =
bool (*)(double cutWeight, double subsetVolume, count subsetSize, double targetVolume,
count targetSize, double sourceVolume, count sourceSize, double gamma,
double inverseGraphVolume);

} // namespace NetworKit

extern "C" {

/**
* Required: score a candidate community during the move phase.
*/
double networkitParallelLeidenCommunityScore(double cutWeight, double degree, double communityVolume,
NetworKit::count subsetSize,
NetworKit::count communitySize, double gamma,
double inverseGraphVolume);

/**
* Optional: override the current-community stay threshold used to accept or reject the best move.
* When omitted, ParallelLeidenView falls back to the built-in modularity threshold.
*/
double networkitParallelLeidenCurrentCommunityThreshold(double cutWeight, double degree,
double communityVolume,
NetworKit::count subsetSize,
NetworKit::count communitySize,
double gamma,
double inverseGraphVolume);

/**
* Optional: override the refine-phase R-set condition.
* When omitted, ParallelLeidenView falls back to the built-in modularity condition.
*/
bool networkitParallelLeidenRefineRSetCondition(double cutWeight, double subsetVolume,
NetworKit::count subsetSize, double targetVolume,
NetworKit::count targetSize, double sourceVolume,
NetworKit::count sourceSize, double gamma,
double inverseGraphVolume);

/**
* Optional: override the refine-phase T-set condition.
* When omitted, ParallelLeidenView falls back to the built-in modularity condition.
*/
bool networkitParallelLeidenRefineTSetCondition(double cutWeight, double subsetVolume,
NetworKit::count subsetSize, double targetVolume,
NetworKit::count targetSize, double sourceVolume,
NetworKit::count sourceSize, double gamma,
double inverseGraphVolume);

} // extern "C"

#endif // NETWORKIT_COMMUNITY_PARALLEL_LEIDEN_SCORING_EXTENSION_HPP_
95 changes: 92 additions & 3 deletions include/networkit/community/ParallelLeidenView.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <limits>
#include <memory>
#include <mutex>
#include <string>
#include <omp.h>
#include <thread>
#include <tlx/unused.hpp>
Expand All @@ -27,6 +28,7 @@
#include <networkit/community/CommunityDetectionAlgorithm.hpp>
#include <networkit/community/Modularity.hpp>
#include <networkit/community/PLM.hpp>
#include <networkit/community/ParallelLeidenScoringExtension.hpp>
#include <networkit/graph/Graph.hpp>
#include <networkit/structures/Partition.hpp>

Expand Down Expand Up @@ -59,6 +61,17 @@ class ParallelLeidenView final : public CommunityDetectionAlgorithm {

void run() override;

/**
* Load a shared library that customizes the move-phase scoring metric.
*
* The library must export `networkitParallelLeidenCommunityScore`. It may additionally export
* `networkitParallelLeidenCurrentCommunityThreshold` to replace the default modularity-based
* stay threshold as well.
*/
void loadMoveScoringExtension(const std::string &sharedLibraryPath);

void unloadMoveScoringExtension();

int VECTOR_OVERSIZE = 10000;

private:
Expand All @@ -81,14 +94,79 @@ class ParallelLeidenView final : public CommunityDetectionAlgorithm {
template <typename GraphType>
Partition parallelRefine(const GraphType &graph);

inline double modularityDelta(double cutD, double degreeV, double volD) const {
static count nodeSize(const Graph &graph, node u);

static count nodeSize(const CoarsenedGraphView &graph, node u);

static double modularityCommunityScore(double cutD, double degreeV, double volD,
count subsetSize, count sizeD, double gamma,
double inverseGraphVolume) {
tlx::unused(subsetSize);
tlx::unused(sizeD);
return cutD - gamma * degreeV * volD * inverseGraphVolume;
};
}

inline double modularityThreshold(double cutC, double volC, double degreeV) const {
static double modularityThresholdScore(double cutC, double degreeV, double volC,
count subsetSize, count sizeC, double gamma,
double inverseGraphVolume) {
tlx::unused(subsetSize);
tlx::unused(sizeC);
return cutC - gamma * (volC - degreeV) * degreeV * inverseGraphVolume;
}

static bool modularityRefineRSetCondition(double cutWeight, double subsetVolume,
count subsetSize, double targetVolume,
count targetSize, double sourceVolume,
count sourceSize, double gamma,
double inverseGraphVolume) {
tlx::unused(subsetSize);
tlx::unused(targetSize);
tlx::unused(sourceVolume);
tlx::unused(sourceSize);
return cutWeight >= gamma * subsetVolume * targetVolume * inverseGraphVolume;
}

static bool modularityRefineTSetCondition(double cutWeight, double subsetVolume,
count subsetSize, double targetVolume,
count targetSize, double sourceVolume,
count sourceSize, double gamma,
double inverseGraphVolume) {
tlx::unused(subsetSize);
tlx::unused(targetSize);
tlx::unused(sourceVolume);
tlx::unused(sourceSize);
return cutWeight >= gamma * subsetVolume * targetVolume * inverseGraphVolume;
}

inline double scoreCommunity(double cutWeight, double degree, double communityVolume,
count subsetSize, count communitySize) const {
return communityScoreFunction_(cutWeight, degree, communityVolume, subsetSize,
communitySize, gamma, inverseGraphVolume);
}

inline double scoreCurrentCommunityThreshold(double cutWeight, double degree,
double communityVolume, count subsetSize,
count communitySize) const {
return currentCommunityThresholdFunction_(cutWeight, degree, communityVolume, subsetSize,
communitySize, gamma, inverseGraphVolume);
}

inline bool refineRSetCondition(double cutWeight, double subsetVolume, count subsetSize,
double targetVolume, count targetSize, double sourceVolume,
count sourceSize) const {
return refineRSetConditionFunction_(cutWeight, subsetVolume, subsetSize, targetVolume,
targetSize, sourceVolume, sourceSize, gamma,
inverseGraphVolume);
}

inline bool refineTSetCondition(double cutWeight, double subsetVolume, count subsetSize,
double targetVolume, count targetSize, double sourceVolume,
count sourceSize) const {
return refineTSetConditionFunction_(cutWeight, subsetVolume, subsetSize, targetVolume,
targetSize, sourceVolume, sourceSize, gamma,
inverseGraphVolume);
}

static inline void lockLowerFirst(index a, index b, std::vector<std::mutex> &locks) {
if (a < b) {
locks[a].lock();
Expand All @@ -104,6 +182,7 @@ class ParallelLeidenView final : public CommunityDetectionAlgorithm {
double inverseGraphVolume; // 1/vol(V)

std::vector<double> communityVolumes;
std::vector<count> communitySizes;

std::vector<node> composedMapping;

Expand Down Expand Up @@ -131,6 +210,16 @@ class ParallelLeidenView final : public CommunityDetectionAlgorithm {
// Optional convergence stop: minimum relative reduction in community count per inner iter.
// 0.0 disables this criterion.
double minCommunityReduction = 0.0;

void *scoringExtensionHandle_ = nullptr;
ParallelLeidenCommunityScoreFunction communityScoreFunction_ = &modularityCommunityScore;
ParallelLeidenCommunityScoreFunction currentCommunityThresholdFunction_ =
&modularityThresholdScore;
ParallelLeidenRefineSetConditionFunction refineRSetConditionFunction_ =
&modularityRefineRSetCondition;
ParallelLeidenRefineSetConditionFunction refineTSetConditionFunction_ =
&modularityRefineTSetCondition;
std::string scoringExtensionPath_;
};

} // namespace NetworKit
Expand Down
26 changes: 25 additions & 1 deletion networkit/community.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,8 @@ cdef extern from "<networkit/community/ParallelLeidenView.hpp>":
cdef cppclass _ParallelLeidenView "NetworKit::ParallelLeidenView"(_CommunityDetectionAlgorithm):
_ParallelLeidenView(_Graph _G) except +
_ParallelLeidenView(_Graph _G, int iterations, bool_t randomize, double gamma) except +
void loadMoveScoringExtension(const string &sharedLibraryPath) except +
void unloadMoveScoringExtension() except +

cdef class ParallelLeiden(CommunityDetector):
"""
Expand Down Expand Up @@ -777,6 +779,28 @@ cdef class ParallelLeidenView(CommunityDetector):
self._G = G
self._this = new _ParallelLeidenView(dereference(G._this),iterations,randomize,gamma)

def loadMoveScoringExtension(self, shared_library_path):
"""
loadMoveScoringExtension(shared_library_path)

Load a shared library that overrides the move-phase community scoring used by
ParallelLeidenView. The library must export
`networkitParallelLeidenCommunityScore` and may additionally export
`networkitParallelLeidenCurrentCommunityThreshold`.
"""
(<_ParallelLeidenView*>(self._this)).loadMoveScoringExtension(stdstring(shared_library_path))
return self

def unloadMoveScoringExtension(self):
"""
unloadMoveScoringExtension()

Unload a previously configured move-scoring extension and restore the
built-in modularity scorer.
"""
(<_ParallelLeidenView*>(self._this)).unloadMoveScoringExtension()
return self

cdef extern from "<networkit/community/LouvainMapEquation.hpp>":
cdef cppclass _LouvainMapEquation "NetworKit::LouvainMapEquation"(_CommunityDetectionAlgorithm):
_LouvainMapEquation(_Graph, bool, count, string ) except +
Expand Down Expand Up @@ -2257,4 +2281,4 @@ class SpectralPartitioner:
networkit.Partition
The resulting partition. Only valid if :code:`run()` was called before.
"""
return self.partition
return self.partition
Loading
Loading