Tutorial: computing 2D structural entropy

Worked, byte-checked examples — some with integer answers. ← benchmark · gallery · repo

The formula

For a graph with degrees dv, total degree 2m = Σv dv, and a partition into communities j with volume Vj = Σv∈j dv and cut gj (edge weight leaving j), the 2D structural entropy is

H2 = − Σj (gj/2m) log2(Vj/2m)  (module / cut term)  − Σv (dv/2m) log2(dv/Vc(v))  (within term)

The 1D entropy H1 = −Σv(dv/2m)log2(dv/2m) is the same quantity with no partition (every node its own "module" sharing the whole graph as parent); it is an upper bound that H2 improves on whenever the graph has community structure. Below, the module and within columns are the per-community contributions to each sum; the displayed totals are reproduced exactly by selib.

Single edge

01
n=2, m=1, 2m=2; colour = community

Two nodes, one edge. Whatever the partition, both H¹ and H² equal exactly 1 bit: one bit to locate a random walker on one of two symmetric endpoints.

jmembersVjgjmodulewithin
0{0, 1}200.00001.0000
totals0.00001.0000

H1 = 1.0000  ·  H2 = 0.0000 + 1.0000 = 1.0000 = 1 bit exactly

selib.structural_entropy(G, [0, 0])  # -> 1.0000

Four disjoint edges

01234567
n=8, m=4, 2m=8; colour = community

Eight nodes in four independent pairs. The partition-free 1D entropy is log₂8 = 3 bits; describing each pair as its own community drives it down to exactly 1 bit — the cut term vanishes (no edges leave a pair, g=0) and only the within-pair bit remains.

jmembersVjgjmodulewithin
0{0, 1}200.00000.2500
1{2, 3}200.00000.2500
2{4, 5}200.00000.2500
3{6, 7}200.00000.2500
totals0.00001.0000

H1 = 3.0000  ·  H2 = 0.0000 + 1.0000 = 1.0000 = 1 bit exactly

selib.structural_entropy(G, [0, 0, 1, 1, 2, 2, 3, 3])  # -> 1.0000

4-cycle, split in two

0123
n=4, m=4, 2m=8; colour = community

A 4-cycle has 1D entropy log₂4 = 2 bits. Cutting it into two adjacent pairs costs a half-bit of cut entropy but saves a full bit inside the communities → H² = 1.5 bits.

jmembersVjgjmodulewithin
0{0, 1}420.25000.5000
1{2, 3}420.25000.5000
totals0.50001.0000

H1 = 2.0000  ·  H2 = 0.5000 + 1.0000 = 1.5000

selib.structural_entropy(G, [0, 0, 1, 1])  # -> 1.5000

Two triangles + bridge

012345
n=6, m=7, 2m=14; colour = community

Two triangles joined by a single bridge edge. The SE-optimal partition is the two triangles; the lone bridge is the only cut, so the module term is tiny and H² ≈ 1.70 bits — the textbook case where SE recovers the obvious two communities.

jmembersVjgjmodulewithin
0{0, 1, 2}710.07140.7783
1{3, 4, 5}710.07140.7783
totals0.14291.5567

H1 = 2.5567  ·  H2 = 0.1429 + 1.5567 = 1.6995

selib.structural_entropy(G, [0, 0, 0, 1, 1, 1])  # -> 1.6995

Definitions & conventions (what selib actually computes)

Structural entropy is usually motivated by a random walk: on a connected graph the stationary distribution is πv = dv/2m, and the formulas above are its expected description length under a partition. But the quantities they use — degrees, community volumes Vj, and cuts gj — are purely combinatorial, so the entropy is well-defined for any graph, connected or not. selib computes it directly from those quantities. Concretely:

Cross-checked against independent implementations

The 2D-SE numbers here are validated against code that implements the formula independently. Full per-run report: se_crosscheck.json.

Every number here is computed by selib.calc and asserted equal to the hand-derived breakdown in tutorial.json — if the arithmetic and the library ever disagreed, the build would fail.