UNPKG

graphinius

Version:

Generic graph library in Typescript

800 lines (673 loc) 20 kB
import { DIR, GraphMode, GraphStats, NextArray, MinAdjacencyListArray, MinAdjacencyListDict } from '../interfaces'; import { IBaseNode, BaseNode } from './BaseNode'; import { BaseEdgeConfig, IBaseEdge, BaseEdge } from './BaseEdge'; import { prepareBFSStandardConfig, BFS, BFS_Scope } from '../../traversal/BFS'; import { DFS } from '../../traversal/DFS'; import { BellmanFordDict, BellmanFordArray } from '../../traversal/BellmanFord'; import { reWeighGraph, addExtraNandE } from '../../traversal/Johnsons'; import { TypedGraph } from "../typed/TypedGraph"; const DEFAULT_WEIGHT = 1; export interface IGraph { /** * Getters */ readonly label: string; readonly mode: GraphMode; readonly stats: GraphStats; // readonly adj_list: MinAdjacencyListArray; // ANALYSIS getMode(): GraphMode; getStats(): GraphStats; // HISTOGRAM readonly inHist: Set<number>[]; readonly outHist: Set<number>[]; readonly connHist: Set<number>[]; // NODES addNode(node: IBaseNode): IBaseNode; addNodeByID(id: string, opts?: {}): IBaseNode; hasNodeID(id: string): boolean; getNodeById(id: string): IBaseNode; n(id: string): IBaseNode; getNodes(): { [key: string]: IBaseNode }; nrNodes(): number; getRandomNode(): IBaseNode; deleteNode(node): void; // EDGES addEdge(edge: IBaseEdge): IBaseEdge; addEdgeByID(label: string, node_a: IBaseNode, node_b: IBaseNode, opts?: {}): IBaseEdge; addEdgeByNodeIDs(label: string, node_a_id: string, node_b_id: string, opts?: {}): IBaseEdge; hasEdgeID(id: string): boolean; getEdgeById(id: string): IBaseEdge; getDirEdgeByNodeIDs(node_a_id: string, node_b_id: string): IBaseEdge; getUndEdgeByNodeIDs(node_a_id: string, node_b_id: string): IBaseEdge; getDirEdges(): { [key: string]: IBaseEdge }; getUndEdges(): { [key: string]: IBaseEdge }; getDirEdgesArray(): Array<IBaseEdge>; getUndEdgesArray(): Array<IBaseEdge>; nrDirEdges(): number; nrUndEdges(): number; deleteEdge(edge: IBaseEdge): void; getRandomDirEdge(): IBaseEdge; getRandomUndEdge(): IBaseEdge; // NEGATIVE EDGES AND CYCLES hasNegativeEdge(): boolean hasNegativeCycles(node?: IBaseNode): boolean; // REINTERPRETING EDGES toDirectedGraph(copy?): IGraph; toUndirectedGraph(): IGraph; // PROPERTIES pickRandomProperty(propList): any; pickRandomProperties(propList, amount): Array<string>; // HANDLE ALL EDGES OF NODES deleteInEdgesOf(node: IBaseNode): void; deleteOutEdgesOf(node: IBaseNode): void; deleteDirEdgesOf(node: IBaseNode): void; deleteUndEdgesOf(node: IBaseNode): void; deleteAllEdgesOf(node: IBaseNode): void; // HANDLE ALL EDGES IN GRAPH clearAllDirEdges(): void; clearAllUndEdges(): void; clearAllEdges(): void; // CLONING cloneStructure(): IGraph; cloneSubGraphStructure(start: IBaseNode, cutoff: Number): IGraph; // REWEIGHTING reweighIfHasNegativeEdge(clone: boolean): IGraph; } class BaseGraph implements IGraph { protected _nr_nodes = 0; protected _nr_dir_edges = 0; protected _nr_und_edges = 0; protected _mode: GraphMode = GraphMode.INIT; protected _nodes: { [key: string]: IBaseNode } = {}; protected _dir_edges: { [key: string]: IBaseEdge } = {}; protected _und_edges: { [key: string]: IBaseEdge } = {}; constructor(protected _label) { } static isTyped(arg: any): arg is TypedGraph { return !!arg.type; } get label(): string { return this._label; } get mode(): GraphMode { return this._mode; } get stats(): GraphStats { return this.getStats(); } get inHist(): Set<number>[] { return this.degreeHist(DIR.in); } get outHist(): Set<number>[] { return this.degreeHist(DIR.out); } get connHist(): Set<number>[] { return this.degreeHist(DIR.und); } private degreeHist(dir: string): Set<number>[] { let result = []; for (let nid in this._nodes) { let node = this._nodes[nid]; let deg; switch (dir) { case DIR.in: deg = node.in_deg; break; case DIR.out: deg = node.out_deg; break; default: deg = node.deg; } if (!result[deg]) { result[deg] = new Set([node]); } else { result[deg].add(node); } } return result; } /** * * @param clone * * @comment Convenience method - * Tests to be found in test suites for * BaseGraph, BellmanFord and Johnsons */ reweighIfHasNegativeEdge(clone: boolean = false): IGraph { if (this.hasNegativeEdge()) { let result_graph: IGraph = clone ? this.cloneStructure() : this; let extraNode: IBaseNode = new BaseNode("extraNode"); result_graph = addExtraNandE(result_graph, extraNode); let BFresult = BellmanFordDict(result_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; result_graph = reWeighGraph(result_graph, newWeights, extraNode); result_graph.deleteNode(extraNode); } return result_graph; } } /** * Version 1: do it in-place (to the object you receive) * Version 2: clone the graph first, return the mutated clone */ toDirectedGraph(copy = false): IGraph { let result_graph = copy ? this.cloneStructure() : this; // if graph has no edges, we want to throw an exception if (this._nr_dir_edges === 0 && this._nr_und_edges === 0) { throw new Error("Cowardly refusing to re-interpret an empty graph.") } return result_graph; } /** * @todo implement!!! */ toUndirectedGraph(): IGraph { return this; } /** * what to do if some edges are not weighted at all? * Since graph traversal algortihms (and later maybe graphs themselves) * use default weights anyways, I am simply ignoring them for now... * @todo figure out how to test this... */ hasNegativeEdge(): boolean { let has_neg_edge = false, edge: IBaseEdge; // negative und_edges are always negative cycles for (let edge_id in this._und_edges) { edge = this._und_edges[edge_id]; if (!edge.isWeighted()) { continue; } if (edge.getWeight() < 0) { return true; } } for (let edge_id in this._dir_edges) { edge = this._dir_edges[edge_id]; if (!edge.isWeighted()) { continue; } if (edge.getWeight() < 0) { has_neg_edge = true; break; } } return has_neg_edge; } /** * Do we want to throw an error if an edge is unweighted? * Or shall we let the traversal algorithm deal with DEFAULT weights like now? */ hasNegativeCycles(node?: IBaseNode): boolean { if (!this.hasNegativeEdge()) { return false; } let negative_cycle = false, start = node ? node : this.getRandomNode(); /** * Now do Bellman Ford over all graph components */ DFS(this, start).forEach(comp => { let min_count = Number.POSITIVE_INFINITY, comp_start_node: string = ""; Object.keys(comp).forEach(node_id => { if (min_count > comp[node_id].counter) { min_count = comp[node_id].counter; comp_start_node = node_id; } }); if (BellmanFordArray(this, this._nodes[comp_start_node]).neg_cycle) { negative_cycle = true; } }); return negative_cycle; } getMode(): GraphMode { return this._mode; } getStats(): GraphStats { return { mode: this._mode, nr_nodes: this._nr_nodes, nr_und_edges: this._nr_und_edges, nr_dir_edges: this._nr_dir_edges, density_dir: this._nr_dir_edges / (this._nr_nodes * (this._nr_nodes - 1)), density_und: 2 * this._nr_und_edges / (this._nr_nodes * (this._nr_nodes - 1)) } } nrNodes(): number { return this._nr_nodes; } nrDirEdges(): number { return this._nr_dir_edges; } nrUndEdges(): number { return this._nr_und_edges; } /** * * @param id * @param opts * * @todo addNode functions should check if a node with a given ID already exists -> node IDs have to be unique... */ addNodeByID(id: string, opts?: {}): IBaseNode { if (this.hasNodeID(id)) { throw new Error("Won't add node with duplicate ID."); } let node = new BaseNode(id, opts); return this.addNode(node) ? node : null; } addNode(node: IBaseNode): IBaseNode { if (this.hasNodeID(node.getID())) { throw new Error("Won't add node with duplicate ID."); } this._nodes[node.getID()] = node; this._nr_nodes += 1; return node; } hasNodeID(id: string): boolean { return !!this._nodes[id]; } getNodeById(id: string): IBaseNode { return this._nodes[id]; } n(id: string): IBaseNode { return this.getNodeById(id); } getNodes(): { [key: string]: IBaseNode } { return this._nodes; } /** * CAUTION - This function takes linear time in # nodes */ getRandomNode(): IBaseNode { return this.pickRandomProperty(this._nodes); } deleteNode(node): void { let rem_node = this._nodes[node.getID()]; if (!rem_node) { throw new Error('Cannot remove a foreign node.'); } // Edges? let in_deg = node.in_deg; let out_deg = node.out_deg; let deg = node.deg; // Delete all edges brutally... if (in_deg) { this.deleteInEdgesOf(node); } if (out_deg) { this.deleteOutEdgesOf(node); } if (deg) { this.deleteUndEdgesOf(node); } delete this._nodes[node.getID()]; this._nr_nodes -= 1; } hasEdgeID(id: string): boolean { return !!this._dir_edges[id] || !!this._und_edges[id]; } getEdgeById(id: string): IBaseEdge { let edge = this._dir_edges[id] || this._und_edges[id]; if (!edge) { throw new Error("cannot retrieve edge with non-existing ID."); } return edge; } static checkExistanceOfEdgeNodes(node_a: IBaseNode, node_b: IBaseNode): void { if (!node_a) { throw new Error(`Cannot find edge. Node A does not exist (in graph).`); } if (!node_b) { throw new Error("Cannot find edge. Node B does not exist (in graph)."); } } // get the edge from node_a to node_b (or undirected) getDirEdgeByNodeIDs(node_a_id: string, node_b_id: string) { const node_a = this.getNodeById(node_a_id); const node_b = this.getNodeById(node_b_id); BaseGraph.checkExistanceOfEdgeNodes(node_a, node_b); // check for outgoing directed edges let edges_dir = node_a.outEdges(), edges_dir_keys = Object.keys(edges_dir); for (let i = 0; i < edges_dir_keys.length; i++) { let edge = edges_dir[edges_dir_keys[i]]; if (edge.getNodes().b.getID() == node_b_id) { return edge; } } // if we managed to arrive here, there is no edge! throw new Error(`Cannot find edge. There is no edge between Node ${node_a_id} and ${node_b_id}.`); } getUndEdgeByNodeIDs(node_a_id: string, node_b_id: string) { const node_a = this.getNodeById(node_a_id); const node_b = this.getNodeById(node_b_id); BaseGraph.checkExistanceOfEdgeNodes(node_a, node_b); // check for undirected edges let edges_und = node_a.undEdges(), edges_und_keys = Object.keys(edges_und); for (let i = 0; i < edges_und_keys.length; i++) { let edge = edges_und[edges_und_keys[i]]; let b: string; (edge.getNodes().a.getID() == node_a_id) ? (b = edge.getNodes().b.getID()) : (b = edge.getNodes().a.getID()); if (b == node_b_id) { return edge; } } } getDirEdges(): { [key: string]: IBaseEdge } { return this._dir_edges; } getUndEdges(): { [key: string]: IBaseEdge } { return this._und_edges; } getDirEdgesArray(): Array<IBaseEdge> { let edges = []; for (let e_id in this._dir_edges) { edges.push(this._dir_edges[e_id]); } return edges; } getUndEdgesArray(): Array<IBaseEdge> { let edges = []; for (let e_id in this._und_edges) { edges.push(this._und_edges[e_id]); } return edges; } addEdgeByNodeIDs(label: string, node_a_id: string, node_b_id: string, opts?: {}): IBaseEdge { let node_a = this.getNodeById(node_a_id), node_b = this.getNodeById(node_b_id); if (!node_a) { throw new Error("Cannot add edge. Node A does not exist"); } else if (!node_b) { throw new Error("Cannot add edge. Node B does not exist"); } else { return this.addEdgeByID(label, node_a, node_b, opts); } } /** * @description now all test cases pertaining addEdge() call this one... */ addEdgeByID(id: string, node_a: IBaseNode, node_b: IBaseNode, opts?: BaseEdgeConfig): IBaseEdge { let edge = new BaseEdge(id, node_a, node_b, opts || {}); return this.addEdge(edge) ? edge : null; } /** * @todo test cases should be reversed / completed * @todo make transactional */ addEdge(edge: IBaseEdge): IBaseEdge { let node_a = edge.getNodes().a, node_b = edge.getNodes().b; if (!this.hasNodeID(node_a.getID()) || !this.hasNodeID(node_b.getID()) || this._nodes[node_a.getID()] !== node_a || this._nodes[node_b.getID()] !== node_b ) { throw new Error("can only add edge between two nodes existing in graph"); } // connect edge to first node anyways node_a.addEdge(edge); if (edge.isDirected()) { // add edge to second node too node_b.addEdge(edge); this._dir_edges[edge.getID()] = edge; this._nr_dir_edges += 1; this.updateGraphMode(); } else { // add edge to both nodes, except they are the same... if (node_a !== node_b) { node_b.addEdge(edge); } this._und_edges[edge.getID()] = edge; this._nr_und_edges += 1; this.updateGraphMode(); } return edge; } deleteEdge(edge: IBaseEdge): void { let dir_edge = this._dir_edges[edge.getID()]; let und_edge = this._und_edges[edge.getID()]; if (!dir_edge && !und_edge) { throw new Error('cannot remove non-existing edge.'); } let nodes = edge.getNodes(); nodes.a.removeEdge(edge); if (nodes.a !== nodes.b) { nodes.b.removeEdge(edge); } if (dir_edge) { delete this._dir_edges[edge.getID()]; this._nr_dir_edges -= 1; } else { delete this._und_edges[edge.getID()]; this._nr_und_edges -= 1; } this.updateGraphMode(); } // Some atomicity / rollback feature would be nice here... deleteInEdgesOf(node: IBaseNode): void { this.checkConnectedNodeOrThrow(node); let in_edges = node.inEdges(); let key: string, edge: IBaseEdge; for (key in in_edges) { edge = in_edges[key]; edge.getNodes().a.removeEdge(edge); delete this._dir_edges[edge.getID()]; this._nr_dir_edges -= 1; } node.clearInEdges(); this.updateGraphMode(); } // Some atomicity / rollback feature would be nice here... deleteOutEdgesOf(node: IBaseNode): void { this.checkConnectedNodeOrThrow(node); let out_edges = node.outEdges(); let key: string, edge: IBaseEdge; for (key in out_edges) { edge = out_edges[key]; edge.getNodes().b.removeEdge(edge); delete this._dir_edges[edge.getID()]; this._nr_dir_edges -= 1; } node.clearOutEdges(); this.updateGraphMode(); } // Some atomicity / rollback feature would be nice here... deleteDirEdgesOf(node: IBaseNode): void { this.deleteInEdgesOf(node); this.deleteOutEdgesOf(node); } // Some atomicity / rollback feature would be nice here... deleteUndEdgesOf(node: IBaseNode): void { this.checkConnectedNodeOrThrow(node); let und_edges = node.undEdges(); let key: string, edge: IBaseEdge; for (key in und_edges) { edge = und_edges[key]; let conns = edge.getNodes(); conns.a.removeEdge(edge); if (conns.a !== conns.b) { conns.b.removeEdge(edge); } delete this._und_edges[edge.getID()]; this._nr_und_edges -= 1; } node.clearUndEdges(); this.updateGraphMode(); } // Some atomicity / rollback feature would be nice here... deleteAllEdgesOf(node: IBaseNode): void { this.deleteDirEdgesOf(node); this.deleteUndEdgesOf(node); } /** * Remove all the (un)directed edges in the graph */ clearAllDirEdges(): void { for (let edge in this._dir_edges) { this.deleteEdge(this._dir_edges[edge]); } } clearAllUndEdges(): void { for (let edge in this._und_edges) { this.deleteEdge(this._und_edges[edge]); } } clearAllEdges(): void { this.clearAllDirEdges(); this.clearAllUndEdges(); } /** * CAUTION - This function is linear in # directed edges */ getRandomDirEdge(): IBaseEdge { return this.pickRandomProperty(this._dir_edges); } /** * CAUTION - This function is linear in # undirected edges */ getRandomUndEdge(): IBaseEdge { return this.pickRandomProperty(this._und_edges); } cloneStructure(): IGraph { let new_graph = new BaseGraph(this._label), old_nodes = this.getNodes(), old_edge: IBaseEdge, new_node_a = null, new_node_b = null; for ( let node_id in old_nodes ) { new_graph.addNode(old_nodes[node_id].clone()); } [this.getDirEdges(), this.getUndEdges()].forEach((old_edges) => { for (let edge_id in old_edges) { old_edge = old_edges[edge_id]; new_node_a = new_graph.getNodeById(old_edge.getNodes().a.getID()); new_node_b = new_graph.getNodeById(old_edge.getNodes().b.getID()); new_graph.addEdge(old_edge.clone(new_node_a, new_node_b)) } }); return new_graph; } cloneSubGraphStructure(root: IBaseNode, cutoff: Number): IGraph { let new_graph = new BaseGraph(this._label); let config = prepareBFSStandardConfig(); let bfsNodeUnmarkedTestCallback = function (context: BFS_Scope) { if (config.result[context.next_node.getID()].counter > cutoff) { context.queue = []; } else { //This means we only add cutoff -1 nodes to the cloned graph, # of nodes is then equal to cutoff new_graph.addNode(context.next_node.clone()); } }; config.callbacks.node_unmarked.push(bfsNodeUnmarkedTestCallback); BFS(this, root, config); let old_edge: IBaseEdge, new_node_a = null, new_node_b = null; [this.getDirEdges(), this.getUndEdges()].forEach((old_edges) => { for (let edge_id in old_edges) { old_edge = old_edges[edge_id]; new_node_a = new_graph.getNodeById(old_edge.getNodes().a.getID()); new_node_b = new_graph.getNodeById(old_edge.getNodes().b.getID()); if (new_node_a != null && new_node_b != null) new_graph.addEdge(old_edge.clone(new_node_a, new_node_b)); } }); return new_graph; } protected checkConnectedNodeOrThrow(node: IBaseNode) { let inGraphNode = this._nodes[node.getID()]; if (!inGraphNode) { throw new Error('Cowardly refusing to delete edges of a foreign node.'); } } protected updateGraphMode() { let nr_dir = this._nr_dir_edges, nr_und = this._nr_und_edges; if (nr_dir && nr_und) { this._mode = GraphMode.MIXED; } else if (nr_dir) { this._mode = GraphMode.DIRECTED; } else if (nr_und) { this._mode = GraphMode.UNDIRECTED; } else { this._mode = GraphMode.INIT; } } pickRandomProperty(propList): any { let tmpList = Object.keys(propList); let randomPropertyName = tmpList[Math.floor(Math.random() * tmpList.length)]; return propList[randomPropertyName]; } /** * In some cases we need to return a large number of objects * in one swoop, as calls to Object.keys() are really slow * for large input objects. * * In order to do this, we only extract the keys once and then * iterate over the key list and add them to a result array * with probability = amount / keys.length * * We also mark all used keys in case we haven't picked up * enough entities for the result array after the first round. * We then just fill up the rest of the result array linearly * with as many unused keys as necessary * * * @todo include generic Test Cases * @todo check if amount is larger than propList size * @todo This seems like a simple hack - filling up remaining objects * Could be replaced by a better fraction-increasing function above... * * @param propList * @param amount * @returns {Array} */ pickRandomProperties(propList, amount): Array<string> { let ids = []; let keys = Object.keys(propList); let fraction = amount / keys.length; let used_keys = {}; for (let i = 0; ids.length < amount && i < keys.length; i++) { if (Math.random() < fraction) { ids.push(keys[i]); used_keys[keys[i]] = i; } } let diff = amount - ids.length; for (let i = 0; i < keys.length && diff; i++) { if (used_keys[keys[i]] == null) { ids.push(keys[i]); diff--; } } return ids; } } export { BaseGraph };