Custom Objectives
Replace PRISM’s multi-resolution modularity with your own Python objective functions.
Overview
By default, PRISM optimizes three built-in multi-resolution modularity objectives in Rust: -Q_0.5, -Q_1.0, and -Q_2.0. These are evaluated in parallel across the swarm and cover the common “I want resolution-diverse community structure” use case.
Just like HP-MOCD, PRISM accepts an objectives= list of Python callables. When provided, the Rust objective evaluator is bypassed entirely — the swarm, the archive, the leader-selection tournament, and the Pareto front are all driven by your callbacks.
Writing an Objective Function
Each objective must follow this signature:
def objective(graph, partition: dict[int, int]) -> float:
...
| Argument | Type | Description |
|---|---|---|
| graph | Your original graph object | The same NetworkX or igraph graph you passed to Prism. |
| partition | dict[int, int] | Maps each node ID to its community ID. |
| return | float | The objective value — minimized by the algorithm. |
Simple Example
Replace the default -Q_γ triple with a single negated-modularity objective:
import networkx as nx
from pymocd import Prism
def negative_modularity(G, partition):
communities = {}
for node, cid in partition.items():
communities.setdefault(cid, []).append(node)
return -nx.community.modularity(G, communities.values())
G = nx.karate_club_graph()
alg = Prism(
G,
objectives=[negative_modularity],
swarm_size=50,
num_gens=50,
)
solution = alg.run()
Any number of objectives is accepted — PRISM’s archive stores the non-dominated front across all of them:
alg = Prism(G, objectives=[obj_1, obj_2, obj_3])
Factory Pattern for Performance
Each objective is called once per particle, per generation — swarm_size × num_gens times. Recomputing graph-level constants inside that hot path is wasteful.
The factory pattern fixes this: a factory receives the graph once, precomputes anything that does not depend on the partition, and returns a closure that is what PRISM actually calls:
import numpy as np
import scipy.sparse as sp
def make_conductance(G):
nodes = list(G.nodes())
idx = {v: i for i, v in enumerate(nodes)}
n = len(nodes)
src = [idx[u] for u, v in G.edges()] + [idx[v] for u, v in G.edges()]
dst = [idx[v] for u, v in G.edges()] + [idx[u] for u, v in G.edges()]
A = sp.csr_matrix((np.ones(len(src)), (src, dst)), shape=(n, n))
degrees = np.asarray(A.sum(axis=1)).ravel()
total_vol = degrees.sum()
rows, cols = A.nonzero()
def _obj(_G, partition):
if total_vol == 0:
return 0.0
labels = np.array([partition[v] for v in nodes], dtype=np.int32)
n_comms = labels.max() + 1
is_cut = (labels[rows] != labels[cols]).astype(np.float64)
cut_comm = np.bincount(labels[rows], weights=is_cut, minlength=n_comms)
vol_comm = np.bincount(labels, weights=degrees, minlength=n_comms)
vol_comp = total_vol - vol_comm
denom = np.minimum(vol_comm, vol_comp)
mask = denom > 0
return float((cut_comm[mask] / denom[mask]).mean()) if mask.any() else 0.0
return _obj
Usage — note the factory is invoked once with G at construction time, and the returned closure is what PRISM calls repeatedly:
alg = Prism(
G,
objectives=[make_conductance(G)],
swarm_size=50,
num_gens=50,
)
solution = alg.run()
Changing Objectives After Construction
set_objectives() swaps the callable list on an existing instance. Pass an empty list to revert to the built-in Rust -Q_γ triple:
alg = Prism(G)
# switch to custom objectives
alg.set_objectives([make_conductance(G)])
# revert to built-in Rust multi-resolution modularity
alg.set_objectives([])
Progress Tracking
Register a callback with set_on_generation() — invoked after every generation of the swarm:
from tqdm import tqdm
alg = Prism(G, objectives=[make_conductance(G)], swarm_size=50, num_gens=50)
bar = tqdm(total=alg.num_gens, desc="PRISM", unit="gen")
def on_gen(generation, num_gens, archive_size):
bar.set_postfix(archive=archive_size)
bar.update(1)
if generation == num_gens - 1:
bar.close()
alg.set_on_generation(on_gen)
solution = alg.run()
| Argument | Type | Description |
|---|---|---|
| generation | int | Current generation (0-indexed). |
| num_gens | int | Total number of generations. |
| archive_size | int | Number of members currently in the archive. |
Performance Considerations
Recommendations:
- Reduce the evolutionary budget:
swarm_size=50, num_gens=50instead of the default100/100. - Use the factory pattern: precompute anything that does not depend on the partition.
- Vectorize:
numpy+scipy.sparseis 10–100× faster than pure-Python loops. - Profile first: the built-in Rust
-Q_γobjectives are typically 10–30× faster per evaluation. Only reach for custom objectives when the metric you actually care about is not expressible in the defaults.
Last updated 18 Apr 2026, 22:19 -0300 .