@graphty/algorithms 1.8.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/algorithms.js +518 -525
- package/dist/algorithms.js.map +1 -1
- package/dist/algorithms.standalone.js +519 -526
- package/dist/algorithms.standalone.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +13 -2
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.js +96 -73
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +35 -15
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
- package/dist/src/algorithms/community/leiden.js +214 -222
- package/dist/src/algorithms/community/leiden.js.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.d.ts +28 -72
- package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.js +249 -262
- package/dist/src/algorithms/community/louvain-optimized.js.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.d.ts +15 -7
- package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.js +38 -38
- package/dist/src/algorithms/community/modularity-utils.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +35 -8
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/errors.d.ts +21 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +11 -8
- package/src/algorithms/centrality/eigenvector.ts +103 -84
- package/src/algorithms/community/girvan-newman.ts +38 -17
- package/src/algorithms/community/leiden.ts +270 -275
- package/src/algorithms/community/louvain-optimized.ts +303 -313
- package/src/algorithms/community/modularity-utils.ts +42 -45
- package/src/algorithms/shortest-path/bellman-ford.ts +46 -8
- package/src/errors.ts +26 -0
- package/src/index.ts +3 -0
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -69,15 +69,23 @@ export function getNeighborCommunities(graph: Graph, nodeId: NodeId, communities
|
|
|
69
69
|
* Calculate modularity of a partition
|
|
70
70
|
*
|
|
71
71
|
* Modularity measures the quality of a community partition. It compares the
|
|
72
|
-
*
|
|
72
|
+
* weight of the edges inside communities to what would be expected in a random graph
|
|
73
|
+
* with the same degrees.
|
|
73
74
|
*
|
|
74
|
-
* Formula: Q =
|
|
75
|
+
* Formula: Q = sum over communities c of [ w_in(c)/m - gamma * (K_c / 2m)^2 ]
|
|
75
76
|
* where:
|
|
76
|
-
* - m = total edge weight
|
|
77
|
-
* -
|
|
78
|
-
* -
|
|
79
|
-
* -
|
|
80
|
-
*
|
|
77
|
+
* - m = total edge weight, each undirected edge counted once
|
|
78
|
+
* - w_in(c) = summed weight of the edges with both endpoints inside community c
|
|
79
|
+
* - K_c = summed weighted degree of community c's nodes
|
|
80
|
+
* - gamma = resolution parameter (higher values favor smaller communities)
|
|
81
|
+
*
|
|
82
|
+
* This is the per-community form of Q = (1/2m) * sum_ij [A_ij - gamma * k_i * k_j / (2m)] over the
|
|
83
|
+
* pairs in the same community.
|
|
84
|
+
* The double sum runs over every PAIR of nodes inside a community, so collecting the null-model
|
|
85
|
+
* term only where an edge exists leaves the penalty far too small and rewards coarseness: the
|
|
86
|
+
* single-community partition, whose modularity is 0 by definition, would otherwise outscore every
|
|
87
|
+
* real split. The form below visits each community once and each edge once, so it counts every
|
|
88
|
+
* pair exactly once without a pass over the whole adjacency matrix.
|
|
81
89
|
* @param graph - The input graph
|
|
82
90
|
* @param communities - Map from node IDs to community IDs
|
|
83
91
|
* @param resolution - Resolution parameter (default: 1.0)
|
|
@@ -89,46 +97,35 @@ export function calculateModularity(graph: Graph, communities: Map<NodeId, numbe
|
|
|
89
97
|
return 0;
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
// Summed weighted degree of each community's nodes.
|
|
101
|
+
const degreeSum = new Map<number, number>();
|
|
102
|
+
for (const node of graph.nodes()) {
|
|
103
|
+
const community = communities.get(node.id);
|
|
104
|
+
if (community === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (countedEdges.has(edgeKey)) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
countedEdges.add(edgeKey);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (communities.get(nodeI.id) === communities.get(nodeJ.id)) {
|
|
113
|
-
const edge = graph.getEdge(nodeI.id, nodeJ.id);
|
|
114
|
-
const reverseEdge = !graph.isDirected ? graph.getEdge(nodeJ.id, nodeI.id) : null;
|
|
115
|
-
|
|
116
|
-
let edgeWeight = 0;
|
|
117
|
-
if (edge) {
|
|
118
|
-
edgeWeight += edge.weight ?? 1;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (reverseEdge && nodeI.id !== nodeJ.id) {
|
|
122
|
-
edgeWeight += reverseEdge.weight ?? 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const degreeI = getNodeDegree(graph, nodeI.id);
|
|
126
|
-
const degreeJ = getNodeDegree(graph, nodeJ.id);
|
|
127
|
-
|
|
128
|
-
modularity += edgeWeight - (resolution * degreeI * degreeJ) / (2 * totalEdgeWeight);
|
|
129
|
-
}
|
|
108
|
+
degreeSum.set(community, (degreeSum.get(community) ?? 0) + getNodeDegree(graph, node.id));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Summed weight of the edges that stay inside a community.
|
|
112
|
+
const internalWeight = new Map<number, number>();
|
|
113
|
+
for (const edge of graph.edges()) {
|
|
114
|
+
const communityI = communities.get(edge.source);
|
|
115
|
+
const communityJ = communities.get(edge.target);
|
|
116
|
+
|
|
117
|
+
if (communityI === undefined || communityI !== communityJ) {
|
|
118
|
+
continue;
|
|
130
119
|
}
|
|
120
|
+
|
|
121
|
+
internalWeight.set(communityI, (internalWeight.get(communityI) ?? 0) + (edge.weight ?? 1));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let modularity = 0;
|
|
125
|
+
for (const [community, degrees] of degreeSum) {
|
|
126
|
+
const share = degrees / (2 * totalEdgeWeight);
|
|
127
|
+
modularity += (internalWeight.get(community) ?? 0) / totalEdgeWeight - resolution * share * share;
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
return modularity
|
|
130
|
+
return modularity;
|
|
134
131
|
}
|
|
@@ -42,6 +42,43 @@ export interface BellmanFordResult {
|
|
|
42
42
|
negativeCycleNodes: NodeId[];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* One relaxable arc: a direction an edge can actually be traversed in.
|
|
47
|
+
*/
|
|
48
|
+
interface Arc {
|
|
49
|
+
from: NodeId;
|
|
50
|
+
to: NodeId;
|
|
51
|
+
weight: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Expand the graph's edges into the arcs Bellman-Ford may relax.
|
|
56
|
+
*
|
|
57
|
+
* A directed graph yields one arc per edge. An UNDIRECTED graph yields two, because
|
|
58
|
+
* `graph.edges()` reports each undirected edge once, arbitrarily oriented from one of its
|
|
59
|
+
* endpoints. Relaxing only that orientation makes every edge one-way and leaves most of an
|
|
60
|
+
* undirected graph unreachable from the source -- the distances come back as Infinity for
|
|
61
|
+
* nodes that are plainly connected.
|
|
62
|
+
*
|
|
63
|
+
* Note that expanding is not merely a convenience: on an undirected graph a single
|
|
64
|
+
* negative-weight edge IS a negative cycle, since it can be traversed back and forth forever.
|
|
65
|
+
* Producing both arcs is what lets the cycle check below report that correctly.
|
|
66
|
+
* @param graph - The graph whose edges are being expanded.
|
|
67
|
+
* @returns Every arc that may be relaxed, in edge order.
|
|
68
|
+
*/
|
|
69
|
+
function relaxableArcs(graph: Graph): Arc[] {
|
|
70
|
+
const arcs: Arc[] = [];
|
|
71
|
+
for (const edge of graph.edges()) {
|
|
72
|
+
const weight = edge.weight ?? 1;
|
|
73
|
+
arcs.push({ from: edge.source, to: edge.target, weight });
|
|
74
|
+
if (!graph.isDirected) {
|
|
75
|
+
arcs.push({ from: edge.target, to: edge.source, weight });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return arcs;
|
|
80
|
+
}
|
|
81
|
+
|
|
45
82
|
/**
|
|
46
83
|
* Find shortest paths from source using Bellman-Ford algorithm
|
|
47
84
|
* @param graph - The graph to search
|
|
@@ -57,6 +94,7 @@ export function bellmanFord(graph: Graph, source: NodeId, options: BellmanFordOp
|
|
|
57
94
|
const distances = new Map<NodeId, number>();
|
|
58
95
|
const predecessors = new Map<NodeId, NodeId | null>();
|
|
59
96
|
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
97
|
+
const arcs = relaxableArcs(graph);
|
|
60
98
|
|
|
61
99
|
// Initialize distances
|
|
62
100
|
for (const nodeId of nodes) {
|
|
@@ -68,10 +106,10 @@ export function bellmanFord(graph: Graph, source: NodeId, options: BellmanFordOp
|
|
|
68
106
|
for (let i = 0; i < nodes.length - 1; i++) {
|
|
69
107
|
let updated = false;
|
|
70
108
|
|
|
71
|
-
for (const
|
|
72
|
-
const u =
|
|
73
|
-
const v =
|
|
74
|
-
const weight =
|
|
109
|
+
for (const arc of arcs) {
|
|
110
|
+
const u = arc.from;
|
|
111
|
+
const v = arc.to;
|
|
112
|
+
const {weight} = arc;
|
|
75
113
|
|
|
76
114
|
const distanceU = distances.get(u);
|
|
77
115
|
const distanceV = distances.get(v);
|
|
@@ -102,10 +140,10 @@ export function bellmanFord(graph: Graph, source: NodeId, options: BellmanFordOp
|
|
|
102
140
|
const negativeCycleNodes: NodeId[] = [];
|
|
103
141
|
let hasNegativeCycle = false;
|
|
104
142
|
|
|
105
|
-
for (const
|
|
106
|
-
const u =
|
|
107
|
-
const v =
|
|
108
|
-
const weight =
|
|
143
|
+
for (const arc of arcs) {
|
|
144
|
+
const u = arc.from;
|
|
145
|
+
const v = arc.to;
|
|
146
|
+
const {weight} = arc;
|
|
109
147
|
|
|
110
148
|
const distanceU = distances.get(u);
|
|
111
149
|
const distanceV = distances.get(v);
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when an iterative algorithm reaches its iteration cap before meeting its tolerance.
|
|
3
|
+
*
|
|
4
|
+
* The scores it had reached are not returned: an unconverged vector is not the answer, and
|
|
5
|
+
* returning it in silence would pass it off as one. networkx makes the same choice with
|
|
6
|
+
* `PowerIterationFailedConvergence`. Raise `maxIterations`, or loosen `tolerance`, and call again.
|
|
7
|
+
*/
|
|
8
|
+
export class ConvergenceError extends Error {
|
|
9
|
+
override readonly name = "ConvergenceError";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds the error and its message.
|
|
13
|
+
* @param algorithm - The function that gave up, e.g. `"eigenvectorCentrality"`
|
|
14
|
+
* @param iterations - The passes it ran, which is its `maxIterations`
|
|
15
|
+
* @param tolerance - The tolerance it did not meet
|
|
16
|
+
*/
|
|
17
|
+
constructor(
|
|
18
|
+
readonly algorithm: string,
|
|
19
|
+
readonly iterations: number,
|
|
20
|
+
readonly tolerance: number,
|
|
21
|
+
) {
|
|
22
|
+
super(
|
|
23
|
+
`${algorithm} did not converge in ${String(iterations)} iterations (tolerance ${String(tolerance)}); raise maxIterations or tolerance`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|