Skip to content

Commit

Permalink
fix: louvain data pollution (#76)
Browse files Browse the repository at this point in the history
* fix: louvain data pollution

* chore: refine
  • Loading branch information
Yanyan-Wang authored Oct 16, 2023
1 parent f6a34cd commit 305a20a
Show file tree
Hide file tree
Showing 8 changed files with 587 additions and 430 deletions.
80 changes: 55 additions & 25 deletions __tests__/unit/louvain.spec.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,68 @@
import { Graph } from "@antv/graphlib";
import { louvain, iLouvain } from "../../packages/graph/src";
import * as propertiesGraphData from "../data/cluster-origin-properties-data.json";
import { Graph } from '@antv/graphlib';
import { louvain, iLouvain } from '../../packages/graph/src';
import * as propertiesGraphData from '../data/cluster-origin-properties-data.json';

describe('Louvain', () => {
it('simple louvain', () => {
const graph = new Graph<any, any>({
nodes: [
{ id: '0', data: {} }, { id: '1', data: {} }, { id: '2', data: {} }, { id: '3', data: {} }, { id: '4', data: {} },
{ id: '5', data: {} }, { id: '6', data: {} }, { id: '7', data: {} }, { id: '8', data: {} }, { id: '9', data: {} },
{ id: '10', data: {} }, { id: '11', data: {} }, { id: '12', data: {} }, { id: '13', data: {} }, { id: '14', data: {} },
{ id: '0', data: {} },
{ id: '1', data: {} },
{ id: '2', data: {} },
{ id: '3', data: {} },
{ id: '4', data: {} },
{ id: '5', data: {} },
{ id: '6', data: {} },
{ id: '7', data: {} },
{ id: '8', data: {} },
{ id: '9', data: {} },
{ id: '10', data: {} },
{ id: '11', data: {} },
{ id: '12', data: {} },
{ id: '13', data: {} },
{ id: '14', data: {} },
],
edges: [
{ id: 'e1', source: '0', target: '1', data: {} }, { id: 'e2', source: '0', target: '2', data: {} }, { id: 'e3', source: '0', target: '3', data: {} }, { id: 'e4', source: '0', target: '4', data: {} },
{ id: 'e5', source: '1', target: '2', data: {} }, { id: 'e6', source: '1', target: '3', data: {} }, { id: 'e7', source: '1', target: '4', data: {} },
{ id: 'e8', source: '2', target: '3', data: {} }, { id: 'e9', source: '2', target: '4', data: {} },
{ id: 'e1', source: '0', target: '1', data: {} },
{ id: 'e2', source: '0', target: '2', data: {} },
{ id: 'e3', source: '0', target: '3', data: {} },
{ id: 'e4', source: '0', target: '4', data: {} },
{ id: 'e5', source: '1', target: '2', data: {} },
{ id: 'e6', source: '1', target: '3', data: {} },
{ id: 'e7', source: '1', target: '4', data: {} },
{ id: 'e8', source: '2', target: '3', data: {} },
{ id: 'e9', source: '2', target: '4', data: {} },
{ id: 'e10', source: '3', target: '4', data: {} },
{ id: 'e11', source: '0', target: '0', data: {} },
{ id: 'e12', source: '0', target: '0', data: {} },
{ id: 'e13', source: '0', target: '0', data: {} },

{ id: 'e14', source: '5', target: '6', data: {weight: 5} }, { id: 'e15', source: '5', target: '7', data: {} }, { id: 'e16', source: '5', target: '8', data: {} }, { id: 'e17', source: '5', target: '9', data: {} },
{ id: 'e18', source: '6', target: '7', data: {} }, { id: 'e19', source: '6', target: '8', data: {} }, { id: 'e20', source: '6', target: '9', data: {} },
{ id: 'e21', source: '7', target: '8', data: {} }, { id: 'e22', source: '7', target: '9', data: {} },
{ id: 'e23',source: '8', target: '9', data: {} },

{ id: 'e24',source: '10', target: '11', data: {} }, { id: 'e25',source: '10', target: '12', data: {} }, { id: 'e26',source: '10', target: '13', data: {} }, { id: 'e27',source: '10', target: '14', data: {} },
{ id: 'e28',source: '11', target: '12', data: {} }, { id: 'e29',source: '11', target: '13', data: {} }, { id: 'e30',source: '11', target: '14', data: {} },
{ id: 'e31',source: '12', target: '13', data: {} }, { id: 'e32',source: '12', target: '14', data: {} },
{ id: 'e33',source: '13', target: '14', data: { weight: 5 } },

{ id: 'e34',source: '0', target: '5', data: {}},
{ id: 'e35',source: '5', target: '10', data: {} },
{ id: 'e36',source: '10', target: '0', data: {} },
{ id: 'e37',source: '10', target: '0', data: {} },

{ id: 'e14', source: '5', target: '6', data: { weight: 5 } },
{ id: 'e15', source: '5', target: '7', data: {} },
{ id: 'e16', source: '5', target: '8', data: {} },
{ id: 'e17', source: '5', target: '9', data: {} },
{ id: 'e18', source: '6', target: '7', data: {} },
{ id: 'e19', source: '6', target: '8', data: {} },
{ id: 'e20', source: '6', target: '9', data: {} },
{ id: 'e21', source: '7', target: '8', data: {} },
{ id: 'e22', source: '7', target: '9', data: {} },
{ id: 'e23', source: '8', target: '9', data: {} },

{ id: 'e24', source: '10', target: '11', data: {} },
{ id: 'e25', source: '10', target: '12', data: {} },
{ id: 'e26', source: '10', target: '13', data: {} },
{ id: 'e27', source: '10', target: '14', data: {} },
{ id: 'e28', source: '11', target: '12', data: {} },
{ id: 'e29', source: '11', target: '13', data: {} },
{ id: 'e30', source: '11', target: '14', data: {} },
{ id: 'e31', source: '12', target: '13', data: {} },
{ id: 'e32', source: '12', target: '14', data: {} },
{ id: 'e33', source: '13', target: '14', data: { weight: 5 } },

{ id: 'e34', source: '0', target: '5', data: {} },
{ id: 'e35', source: '5', target: '10', data: {} },
{ id: 'e36', source: '10', target: '0', data: {} },
{ id: 'e37', source: '10', target: '0', data: {} },
],
});
const clusteredData = louvain(graph, false, 'weight');
Expand Down Expand Up @@ -64,4 +94,4 @@ describe('Louvain', () => {
expect(clusteredData.clusters[2].sumTot).toBe(4);
expect(clusteredData.clusterEdges.length).toBe(7);
});
});
});
30 changes: 17 additions & 13 deletions __tests__/utils/data.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { NodeID, INode, IEdge } from "../../packages/graph/src/types";
import { ID } from '@antv/graphlib';
import { INode, IEdge } from '../../packages/graph/src/types';
/**
* Convert the old version of the data format to the new version
* @param data old data
* @return {{nodes:INode[],edges:IEdge[]}} new data
*/
export const dataTransformer = (data: { nodes: { id: NodeID, [key: string]: any }[], edges: { source: NodeID, target: NodeID, [key: string]: any }[] }): { nodes: INode[], edges: IEdge[] } => {
const { nodes, edges } = data;
return {
nodes: nodes.map((n) => {
const { id, ...rest } = n;
return { id, data: rest ? rest : {} };
}),
edges: edges.map((e, i) => {
const { id, source, target, ...rest } = e;
return { id: id ? id : `edge-${i}`, target, source, data: rest };
}),
};
export const dataTransformer = (data: {
nodes: { id: ID; [key: string]: any }[];
edges: { source: ID; target: ID; [key: string]: any }[];
}): { nodes: INode[]; edges: IEdge[] } => {
const { nodes, edges } = data;
return {
nodes: nodes.map((n) => {
const { id, ...rest } = n;
return { id, data: rest ? rest : {} };
}),
edges: edges.map((e, i) => {
const { id, source, target, ...rest } = e;
return { id: id ? id : `edge-${i}`, target, source, data: rest };
}),
};
};
27 changes: 16 additions & 11 deletions packages/graph/src/bfs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ID } from '@antv/graphlib';
import Queue from './structs/queue';
import { Graph, IAlgorithmCallbacks, NodeID } from './types';
import { Graph, IAlgorithmCallbacks } from './types';

/**
* @param startNodeId The ID of the bfs traverse starting node.
Expand All @@ -8,11 +9,14 @@ import { Graph, IAlgorithmCallbacks, NodeID } from './types';
- enterNode: Called when BFS visits a node.
- leaveNode: Called after BFS visits the node.
*/
function initCallbacks(callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) {
function initCallbacks(
callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks
) {
const initiatedCallback = callbacks;
const stubCallback = () => { };
const stubCallback = () => {};
const allowTraversalCallback = () => true;
initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback;
initiatedCallback.allowTraversal =
callbacks.allowTraversal || allowTraversalCallback;
initiatedCallback.enter = callbacks.enter || stubCallback;
initiatedCallback.leave = callbacks.leave || stubCallback;
return initiatedCallback;
Expand All @@ -26,19 +30,19 @@ Performs breadth-first search (BFS) traversal on a graph.
*/
export const breadthFirstSearch = (
graph: Graph,
startNodeId: NodeID,
originalCallbacks?: IAlgorithmCallbacks,
startNodeId: ID,
originalCallbacks?: IAlgorithmCallbacks
) => {
const visit = new Set<NodeID>();
const visit = new Set<ID>();
const callbacks = initCallbacks(originalCallbacks);
const nodeQueue = new Queue<NodeID>();
const nodeQueue = new Queue<ID>();
// init Queue. Enqueue node ID.
nodeQueue.enqueue(startNodeId);
visit.add(startNodeId);
let previousNodeId: NodeID = '';
let previousNodeId: ID = '';
// 遍历队列中的所有顶点
while (!nodeQueue.isEmpty()) {
const currentNodeId: NodeID = nodeQueue.dequeue();
const currentNodeId: ID = nodeQueue.dequeue();
callbacks.enter({
current: currentNodeId,
previous: previousNodeId,
Expand All @@ -52,7 +56,8 @@ export const breadthFirstSearch = (
previous: previousNodeId,
current: currentNodeId,
next: nextNodeId,
}) && !visit.has(nextNodeId)
}) &&
!visit.has(nextNodeId)
) {
visit.add(nextNodeId);
nodeQueue.enqueue(nextNodeId);
Expand Down
18 changes: 11 additions & 7 deletions packages/graph/src/connected-component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Graph, INode, NodeID } from './types';
import { ID } from '@antv/graphlib';
import { Graph, INode } from './types';
/**
* Generate all connected components for an undirected graph
* @param graph
*/
export const detectConnectedComponents = (graph: Graph): INode[][] => {
const nodes = graph.getAllNodes();
const allComponents: INode[][] = [];
const visited: { [key: NodeID]: boolean } = {};
const visited: { [key: ID]: boolean } = {};
const nodeStack: INode[] = [];
const getComponent = (node: INode) => {
nodeStack.push(node);
Expand Down Expand Up @@ -49,9 +50,9 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
const nodes = graph.getAllNodes();
const nodeStack: INode[] = [];
// Assist to determine whether it is already in the stack to reduce the search overhead
const inStack: { [key: NodeID]: boolean } = {};
const indices: { [key: NodeID]: number } = {};
const lowLink: { [key: NodeID]: number } = {};
const inStack: { [key: ID]: boolean } = {};
const indices: { [key: ID]: number } = {};
const lowLink: { [key: ID]: number } = {};
const allComponents: INode[][] = [];
let index = 0;
const getComponent = (node: INode) => {
Expand All @@ -61,7 +62,7 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
index += 1;
nodeStack.push(node);
inStack[node.id] = true;
const relatedEdges = graph.getRelatedEdges(node.id, "out");
const relatedEdges = graph.getRelatedEdges(node.id, 'out');
for (let i = 0; i < relatedEdges.length; i++) {
const targetNodeID = relatedEdges[i].target;
if (!indices[targetNodeID] && indices[targetNodeID] !== 0) {
Expand Down Expand Up @@ -98,7 +99,10 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
return allComponents;
};

export function getConnectedComponents(graph: Graph, directed?: boolean): INode[][] {
export function getConnectedComponents(
graph: Graph,
directed?: boolean
): INode[][] {
if (directed) return detectStrongConnectComponents(graph);
return detectConnectedComponents(graph);
}
Loading

0 comments on commit 305a20a

Please sign in to comment.