graphinius
Version:
Generic graph library in Typescript
384 lines (313 loc) • 11.2 kB
text/typescript
/**
* Previous version created by ru on 14.09.17 is to be found below.
* Modifications by Rita on 28.02.2018 - now it can handle branchings too.
* CONTENTS:
* Brandes: according to Brandes 2001, it is meant for unweighted graphs (+undirected according to the paper, but runs fine on directed ones, too)
* BrandesForWeighted: according to Brandes 2007, handles WEIGHTED graphs, including graphs with null edges
* PFSdictBased: an alternative for our PFS, not heap based but dictionary based, however, not faster (see BetweennessTests)
*/
import * as $G from '../core/base/BaseGraph';
import * as $N from '../core/base/BaseNode';
import * as $P from '../traversal/PFS';
import * as $BF from '../traversal/BellmanFord';
import * as $JO from '../traversal/Johnsons';
import * as $BH from '../datastructs/BinaryHeap';
import {ComputeGraph, IComputeGraph} from "../core/compute/ComputeGraph";
export interface BrandesHeapEntry {
id: string;
best: number;
}
/**
* @param _graph input graph
* @returns Dict of betweenness centrality values for each node
*/
class Brandes {
private _cg: IComputeGraph;
constructor(private _graph: $G.IGraph) {
this._cg = new ComputeGraph(this._graph);
}
computeUnweighted(normalize: boolean = false, directed: boolean = false): {} {
if (this._graph.nrDirEdges() === 0 && this._graph.nrUndEdges() === 0) {
throw new Error("Cowardly refusing to traverse graph without edges.");
}
let nodes = this._graph.getNodes();
let adjList = this._cg.adjListW();
//Variables for Brandes algorithm
let s: $N.IBaseNode, //source node
v: string, //parent of w, at least one shortest path between s and w leads through v
w: string, //neighbour of v, lies one edge further than v from s
Pred: { [key: string]: string[] } = {}, //list of Predecessors=parent nodes
sigma: { [key: string]: number } = {}, //number of shortest paths from source s to each node as goal node
delta: { [key: string]: number } = {}, //dependency of source node s on a node
dist: { [key: string]: number } = {}, //distances from source node s to each node
Q: string[] = [], //Queue of nodes - nodes to visit
S: string[] = [], //stack of nodes - nodes waiting for their dependency values
CB: { [key: string]: number } = {}; //Betweenness values for each node
//info: push element to array - last position
//array.shift: returns and removes the first element - when used, array behaves as queue
//array.pop: returns and removes last element - when used, array behaves as stack
let closedNodes: { [key: string]: boolean } = {};
for (let n in nodes) {
let node_id = nodes[n].getID();
CB[node_id] = 0;
dist[node_id] = Number.POSITIVE_INFINITY;
sigma[node_id] = 0;
delta[node_id] = 0;
Pred[node_id] = [];
closedNodes[node_id] = false;
}
for (let i in nodes) {
s = nodes[i];
//Initialization
let id = s.getID();
dist[id] = 0;
sigma[id] = 1;
Q.push(id);
closedNodes[id] = true;
let counter = 0;
while (Q.length) { //Queue not empty
v = Q.shift();
S.push(v);
let neighbors = adjList[v];
closedNodes[v] = true;
for (let w in neighbors) {
if (closedNodes[w]) {
continue;
}
//Path discovery: w found for the first time?
if (dist[w] === Number.POSITIVE_INFINITY) {
Q.push(w);
dist[w] = dist[v] + 1;
}
//Path counting: edge (v,w) on shortest path?
if (dist[w] === dist[v] + 1) {
sigma[w] += sigma[v];
Pred[w].push(v);
}
}
}
//Accumulation: back-propagation of dependencies
while (S.length >= 1) {
w = S.pop();
for (let parent of Pred[w]) {
delta[parent] += (sigma[parent] / sigma[w] * (1 + delta[w]));
}
if (w != s.getID()) {
CB[w] += delta[w];
}
// This spares us from having to loop over all nodes again for initialization
sigma[w] = 0;
delta[w] = 0;
dist[w] = Number.POSITIVE_INFINITY;
Pred[w] = [];
closedNodes[w] = false;
}
}
if (normalize) {
this.normalizeScores(CB, this._graph.nrNodes(), directed);
}
return CB;
}
computeWeighted(normalize: boolean, directed: boolean): {} {
if (this._graph.nrDirEdges() === 0 && this._graph.nrUndEdges() === 0) {
throw new Error("Cowardly refusing to traverse graph without edges.");
}
if (this._graph.hasNegativeEdge()) {
var extraNode: $N.IBaseNode = new $N.BaseNode("extraNode");
let graph: $G.IGraph = $JO.addExtraNandE(this._graph, extraNode);
let BFresult = $BF.BellmanFordDict(graph, extraNode);
if (BFresult.neg_cycle) {
throw new Error("The graph contains a negative cycle, thus it can not be processed");
}
else {
let newWeights: {} = BFresult.distances;
graph = $JO.reWeighGraph(graph, newWeights, extraNode);
graph.deleteNode(extraNode);
}
this._graph = graph;
}
let nodes = this._graph.getNodes();
let N = Object.keys(nodes).length;
let adjList = this._cg.adjListW();
const evalPriority = (nb: BrandesHeapEntry) => nb.best;
const evalObjID = (nb: BrandesHeapEntry) => nb.id;
//Variables for Brandes algorithm
let s: $N.IBaseNode, //source node,
v: BrandesHeapEntry, //parent of w, at least one shortest path between s and w leads through v
w: string, //neighbour of v, lies one edge further than v from s, type id nodeID, alias string (got from AdjListDict)
Pred: { [key: string]: string[] } = {}, //list of Predecessors=parent nodes
sigma: { [key: string]: number } = {}, //number of shortest paths from source s to each node as goal node
delta: { [key: string]: number } = {}, //dependency of source node s on a node
dist: { [key: string]: number } = {}, //distances from source node s to each node
S: string[] = [], //stack of nodeIDs - nodes waiting for their dependency values
CB: { [key: string]: number } = {}, //Betweenness values for each node
closedNodes: { [key: string]: boolean } = {},
Q: $BH.BinaryHeap = new $BH.BinaryHeap($BH.BinaryHeapMode.MIN, evalPriority, evalObjID);
for (let n in nodes) {
let currID = nodes[n].getID();
CB[currID] = 0;
dist[currID] = Number.POSITIVE_INFINITY;
sigma[currID] = 0;
delta[currID] = 0;
Pred[currID] = [];
closedNodes[currID] = false;
}
for (let i in nodes) {
s = nodes[i];
let id_s = s.getID();
dist[id_s] = 0;
sigma[id_s] = 1;
let source: BrandesHeapEntry = { id: id_s, best: 0 };
Q.insert(source);
closedNodes[id_s] = true;
while (Q.size() > 0) {
v = Q.pop();
let current_id = v.id;
S.push(current_id);
closedNodes[current_id] = true;
let neighbors = adjList[current_id];
for (let w in neighbors) {
if (closedNodes[w]) {
continue;
}
let new_dist = dist[current_id] + neighbors[w];
let nextNode: BrandesHeapEntry = { id: w, best: dist[w] };
if (dist[w] > new_dist) {
if (isFinite(dist[w])) { //this means the node has already been encountered
let x = Q.remove(nextNode);
nextNode.best = new_dist;
Q.insert(nextNode);
}
else {
nextNode.best = new_dist;
Q.insert(nextNode);
}
sigma[w] = 0;
dist[w] = new_dist;
Pred[w] = [];
}
if (dist[w] === new_dist) {
sigma[w] += sigma[current_id];
Pred[w].push(current_id);
}
}
}
// Accumulation: back-propagation of dependencies
while (S.length >= 1) {
w = S.pop();
for (let parent of Pred[w]) {
delta[parent] += (sigma[parent] / sigma[w] * (1 + delta[w]));
}
if (w != s.getID()) {
CB[w] += delta[w];
}
// reset
sigma[w] = 0;
delta[w] = 0;
dist[w] = Number.POSITIVE_INFINITY;
Pred[w] = [];
closedNodes[w] = false;
}
}
if (normalize) {
this.normalizeScores(CB, N, directed);
}
return CB;
}
/**
*
* @param graph
* @param normalize
* @param directed
*
* @todo decide to remove or not
*/
computePFSbased(normalize: boolean, directed: boolean): {} {
let nodes = this._graph.getNodes();
let adjList = this._cg.adjListW();
//Variables for Brandes algorithm
let Pred: { [key: string]: string[] } = {}, //list of Predecessors=parent nodes
sigma: { [key: string]: number } = {}, //number of shortest paths from source s to each node as goal node
delta: { [key: string]: number } = {}, //dependency of source node s on a node
S: string[] = [], //stack of nodeIDs - nodes waiting for their dependency values
CB: { [key: string]: number } = {}; //Betweenness values for each node
for (let n in nodes) {
let currID = nodes[n].getID();
CB[currID] = 0;
sigma[currID] = 0;
delta[currID] = 0;
Pred[currID] = [];
}
let specialConfig: $P.PFS_Config = $P.preparePFSStandardConfig();
var notEncounteredBrandes = function (context: $P.PFS_Scope) {
context.next.best =
context.current.best + (isNaN(context.next.edge.getWeight()) ? $P.DEFAULT_WEIGHT : context.next.edge.getWeight());
//these needed for betweenness
let next_id = context.next.node.getID();
let current_id = context.current.node.getID();
Pred[next_id] = [current_id];
sigma[next_id] += sigma[current_id];
};
specialConfig.callbacks.not_encountered.splice(0, 1, notEncounteredBrandes);
var newCurrentBrandes = function (context: $P.PFS_Scope) {
S.push(context.current.node.getID());
};
specialConfig.callbacks.new_current.push(newCurrentBrandes);
var betterPathBrandes = function (context: $P.PFS_Scope) {
let next_id = context.next.node.getID();
let current_id = context.current.node.getID();
sigma[next_id] = 0;
sigma[next_id] += sigma[current_id];
Pred[next_id] = [];
Pred[next_id].push(current_id);
};
specialConfig.callbacks.better_path.splice(0, 1, betterPathBrandes);
/**
* @param context
*
* @todo figure out a faster way than .indexOf()
*/
var equalPathBrandes = function (context: $P.PFS_Scope) {
let next_id = context.next.node.getID();
let current_id = context.current.node.getID();
sigma[next_id] += sigma[current_id];
//other approach needed to avoid duplicates
if (Pred[next_id].indexOf(current_id) === -1) {
Pred[next_id].push(current_id);
}
};
specialConfig.callbacks.equal_path.push(equalPathBrandes);
for (let i in nodes) {
let s = nodes[i];
sigma[s.getID()] = 1;
$P.PFS(this._graph, s, specialConfig)
//step: do the scoring, using S, Pred and sigma
while (S.length >= 1) {
let w = S.pop();
for (let parent of Pred[w]) {
delta[parent] += (sigma[parent] / sigma[w] * (1 + delta[w]));
}
if (w != s.getID()) {
CB[w] += delta[w];
}
//This spares us from having to loop over all nodes again for initialization
sigma[w] = 0;
delta[w] = 0;
Pred[w] = [];
}
}
if (normalize) {
this.normalizeScores(CB, this._graph.nrNodes(), directed);
}
return CB;
}
normalizeScores(CB, N, directed) {
let factor = directed ? ((N - 1) * (N - 2)) : ((N - 1) * (N - 2) / 2);
for (let node in CB) {
CB[node] /= factor;
}
}
}
export {
Brandes
}