UNPKG

graphinius

Version:

Generic graph library in Typescript

294 lines (237 loc) 9.01 kB
import {MinAdjacencyListArray, MinAdjacencyListDict, NextArray} from "../interfaces"; import {IGraph} from "../base/BaseGraph"; import {Logger} from "../../utils/Logger"; import {IBaseNode} from "../base/BaseNode"; const logger = new Logger(); const DEFAULT_WEIGHT = 1; export interface NumericHandler { tensor2d: Function; matMul: Function; } export interface IComputeGraph { // REPRESENTATIONS adjListW(incoming?: boolean, include_self?, self_dist?: number): MinAdjacencyListDict; adjMatrix(): MinAdjacencyListArray; adjMatrixW(incoming?: boolean): MinAdjacencyListArray; nextArray(incoming?: boolean): NextArray; // METRICS triadCount(directed?: boolean): number; triangleCount(directed?: boolean): Promise<number>; globalCC(directed?: boolean): Promise<number>; localCC(directed? : boolean) : Promise<{[key: string]: number}>; } class ComputeGraph implements IComputeGraph { private adj_list_uu: Uint32Array; private adj_list_du: Uint32Array; private adj_list_uw: Float32Array; private adj_list_dw: Float32Array; constructor(private _g: IGraph, private _numeric?: NumericHandler) { } checkNumericHandler() { if (!this._numeric || !this._numeric.matMul) { throw new Error("Tensorflow & TF matMul function must be present for fast numeric computations."); } } /** * @param incoming * @todo analyze and make faster */ nextArray(incoming: boolean = false): NextArray { let next = [], node_keys = Object.keys(this._g.getNodes()); const adjDict = this.adjListW(incoming, true, 0); for (let i = 0; i < this._g.nrNodes(); ++i) { next.push([]); for (let j = 0; j < this._g.nrNodes(); ++j) { next[i].push([]); next[i][j].push(i === j ? j : isFinite(adjDict[node_keys[i]][node_keys[j]]) ? j : null); } } return next; } adjMatrix(): MinAdjacencyListArray { let adjList = [], node_keys = Object.keys(this._g.getNodes()); const adjDict = this.adjListW(); for (let i = 0; i < this._g.nrNodes(); ++i) { adjList.push([]); for (let j = 0; j < this._g.nrNodes(); ++j) { adjList[i].push(i === j ? 0 : isFinite(adjDict[node_keys[i]][node_keys[j]]) ? 1 : 0); } } return adjList; } /** * @todo rename? it's actually a weight matrix... * * This function iterates over the adjDict in order to use it's advantage * of being able to override edges if edges with smaller weights exist * * However, the order of nodes in the array represents the order of nodes * at creation time, no other implicit alphabetical or collational sorting. * * This has to be considered when further processing the result * * @param incoming whether or not to consider incoming edges * @param include_self contains a distance to itself? * @param self_dist default distance to self */ adjMatrixW(incoming: boolean = false, include_self = false, self_dist = 0): MinAdjacencyListArray { let adjList = [], node_keys = Object.keys(this._g.getNodes()); const adjDict = this.adjListW(incoming, include_self, self_dist); for (let i = 0; i < this._g.nrNodes(); ++i) { adjList.push([]); for (let j = 0; j < this._g.nrNodes(); ++j) { adjList[i].push(i === j ? self_dist : isFinite(adjDict[node_keys[i]][node_keys[j]]) ? adjDict[node_keys[i]][node_keys[j]] : Number.POSITIVE_INFINITY); } } return adjList; } /** * @todo force directed / undirected * -> take undirected edge as 2 directed ones? * -> take directed edge as undirected? * * @param incoming whether or not to consider incoming edges * @param include_self contains a distance to itself? * @param self_dist default distance to self */ adjListW(incoming: boolean = false, include_self = false, self_dist = 0): MinAdjacencyListDict { let adj_list_dict: MinAdjacencyListDict = {}, nodes = this._g.getNodes(), cur_dist: number, key: string, cur_edge_weight: number; for (key in nodes) { adj_list_dict[key] = {}; if (include_self) { adj_list_dict[key][key] = self_dist; } } for (key in nodes) { let neighbors = incoming ? nodes[key].reachNodes().concat(nodes[key].prevNodes()) : nodes[key].reachNodes(); neighbors.forEach((ne) => { cur_dist = adj_list_dict[key][ne.node.getID()] || Number.POSITIVE_INFINITY; cur_edge_weight = isNaN(ne.edge.getWeight()) ? DEFAULT_WEIGHT : ne.edge.getWeight(); if (cur_edge_weight < cur_dist) { adj_list_dict[key][ne.node.getID()] = cur_edge_weight; if (incoming) { // we need to update the 'inverse' entry as well adj_list_dict[ne.node.getID()][key] = cur_edge_weight; } } else { adj_list_dict[key][ne.node.getID()] = cur_dist; if (incoming) { adj_list_dict[ne.node.getID()][key] = cur_dist; } } }); } return adj_list_dict; } /**------------------------------------------------------------- * Triad, triangle, CC & transitivity (global CC) * @todo refactor out into own module: * - name: `general metrics` ? * - modularity / connectivity / CC & Trans *------------------------------------------------------------- */ /** * @description `triad`: is either a completed triangle or a potential triangle, * meaning a connection between 3 nodes that is lacking just 1 edge. * `triplet`: is three nodes that are connected by either two (open triplet) * or three (closed triplet) undirected ties * triad == triplet * @description count all 2-triads * UN-directed scenario for earch node: all pairwise connections could form a triangle * directed scenario for each node: -ins could form triangles with -outs (and vice versa) * * @todo this only works for nodes without self-loops !!! * * @param directed directed or undirected */ triadCount(directed = false): number { let triangle_count = 0; const nodes = Object.values(this._g.getNodes()); let deg; for ( let n of nodes ) { if ( directed ) { triangle_count += ( n.in_deg - n.self_in_deg ) * ( n.out_deg - n.self_out_deg ); } else { deg = n.deg - n.self_deg; triangle_count += deg * ( deg - 1 ) / 2; } } return triangle_count; } /** * @description how many triangles (A-B-C, or A->B->C) are there in the graph * In directed graphs, each triangle is seen thrice (from A, B, C) * In undirected graphs, each triangle is seen six times (from A, B, C, but each in 2 directions) * @param directed directed or undirected network */ async triangleCount(directed = false): Promise<number> { this.checkNumericHandler(); const adj_list = this.adjMatrix(); const a = this._numeric.tensor2d(adj_list); const aux2 = await a.matMul(a).array(); const aux3 = await a.matMul(aux2).array(); // logger.log(aux3); let trace = 0; for (let i = 0; i < aux3.length; i++) { trace += aux3[i][i]; } return directed ? trace / 3 : trace / 6; } /** * @description transitivity (or global clustering coefficient, gCC) is the ratio * of actual triangles to potential triangles, or * (nr. of closed triplets / nr. of all triplets) * It therefore measures the connection potential of the whole graph, * the higher the gCC the lower the future connection potential. * @description should equal the average (local) clustering coefficients of all nodes * * @todo test that avg(lCC) == gCC * @todo there are 4 different ways to define a triplet closure in DIRECTED graphs * @todo using the `undirected` formula results are not consistent with networkx * @todo research & correct the $G <-> networkx inconsistency * @param directed directed or undirected network */ async globalCC(directed = false): Promise<number> { const triangles = await this.triangleCount(directed); const triads = this.triadCount(directed); return 3 * triangles / triads; } /** * @description The CC (also `local` CC) measures how complete the neighborhood of a node is, * i.e. (completed triangles / `potential` triangles), where a potential triangle * could form by adding 1 edge between hitherto unconnected neighbors. * This can be measured by * @param directed directed or undirected network */ async localCC(directed = false) : Promise<{[key: string]: number}> { this.checkNumericHandler(); const result = {}; const adj_list = this.adjMatrix(); const a = this._numeric.tensor2d(adj_list); const aux2 = await a.matMul(a).array(); const aux3 = await a.matMul(aux2).array(); /** * @todo ensure node order is equivalent to aux3 ordering - HOW ?? */ let deg: number; let node: IBaseNode; let cc_i: number; // intermediate const keys = Object.keys(this._g.getNodes()); for ( let i in aux3[0] ) { node = this._g.getNodeById(keys[i]); deg = directed ? node.in_deg + node.out_deg : node.deg; cc_i = (aux3[i][i] / (deg * (deg-1))) || 0; result[i] = directed ? 2 * cc_i : cc_i; } return result; } } export { ComputeGraph }