UNPKG

graphinius

Version:

Generic graph library in Typescript

384 lines (329 loc) 10.9 kB
import * as $N from '../core/base/BaseNode'; import * as $E from '../core/base/BaseEdge'; import * as $G from '../core/base/BaseGraph'; import * as uuid from 'uuid'; const v4 = uuid.v4; /** * EITHER generate new edges via specified degree span * OR via probability of edge creation from a specified * set of nodes to all others */ export interface NodeDegreeConfiguration { und_degree?: number; dir_degree?: number; min_und_degree?: number; max_und_degree?: number; min_dir_degree?: number; max_dir_degree?: number; probability_dir?: number; probability_und?: number; } export interface ISimplePerturber { // CREATE EDGES PER NODE createEdgesProb(probability: number, directed?: boolean, setOfNodes?: { [key: string] : $N.IBaseNode} ) : void; createEdgesSpan(min: number, max: number, directed?: boolean, setOfNodes?: { [key: string] : $N.IBaseNode} ) : void; // ADD NODES addNodesPercentage(percentage: number, config?: NodeDegreeConfiguration ) : void; addNodesAmount(amount: number, config?: NodeDegreeConfiguration ) : void; // ADD EDGES addUndEdgesPercentage(percentage: number ) : void; addDirEdgesPercentage(percentage: number ) : void; addEdgesAmount(amount: number, config?: $E.BaseEdgeConfig ) : void; // DELETE NODES AND EDGES deleteNodesPercentage(percentage: number ) : void; deleteUndEdgesPercentage(percentage: number ) : void; deleteDirEdgesPercentage(percentage: number ) : void; deleteNodesAmount(amount: number ) : void; deleteUndEdgesAmount(amount: number ) : void; deleteDirEdgesAmount(amount: number ) : void; } class SimplePerturber implements ISimplePerturber { constructor(private _graph: $G.IGraph) {} /** * * @param percentage */ deleteNodesPercentage(percentage: number ) : void { if ( percentage < 0 ) { throw new Error('Cowardly refusing to remove a negative amount of nodes'); } if ( percentage > 100 ) { percentage = 100; } let nr_nodes_to_delete = Math.ceil(this._graph.nrNodes() * percentage/100); this.deleteNodesAmount( nr_nodes_to_delete ); } /** * * @param percentage */ deleteUndEdgesPercentage(percentage: number ) : void { if ( percentage > 100 ) { percentage = 100; } let nr_edges_to_delete = Math.ceil(this._graph.nrUndEdges() * percentage/100); this.deleteUndEdgesAmount( nr_edges_to_delete ); } /** * * @param percentage */ deleteDirEdgesPercentage(percentage: number ) : void { if ( percentage > 100 ) { percentage = 100; } let nr_edges_to_delete = Math.ceil(this._graph.nrDirEdges() * percentage/100); this.deleteDirEdgesAmount( nr_edges_to_delete ); } /** * */ deleteNodesAmount(amount: number ) : void { if ( amount < 0 ) { throw 'Cowardly refusing to remove a negative amount of nodes'; } if ( this._graph.nrNodes() === 0 ) { return; } for ( let nodeID = 0, randomNodes = this._graph.pickRandomProperties(this._graph.getNodes(), amount); nodeID < randomNodes.length; nodeID++ ) { this._graph.deleteNode( this._graph.getNodes()[randomNodes[nodeID]] ); } } /** * */ deleteUndEdgesAmount(amount: number ) : void { if ( amount < 0 ) { throw 'Cowardly refusing to remove a negative amount of edges'; } if ( this._graph.nrUndEdges() === 0 ) { return; } for ( let edgeID = 0, randomEdges = this._graph.pickRandomProperties(this._graph.getUndEdges(), amount); edgeID < randomEdges.length; edgeID++ ) { this._graph.deleteEdge( this._graph.getUndEdges()[randomEdges[edgeID]] ); } } /** * */ deleteDirEdgesAmount(amount: number ) : void { if ( amount < 0 ) { throw 'Cowardly refusing to remove a negative amount of edges'; } if ( this._graph.nrDirEdges() === 0 ) { return; } for ( let edgeID = 0, randomEdges = this._graph.pickRandomProperties(this._graph.getDirEdges(), amount); edgeID < randomEdges.length; edgeID++ ) { this._graph.deleteEdge( this._graph.getDirEdges()[randomEdges[edgeID]] ); } } /** * */ addUndEdgesPercentage(percentage: number ) : void { let nr_und_edges_to_add = Math.ceil(this._graph.nrUndEdges() * percentage/100); this.addEdgesAmount( nr_und_edges_to_add, {directed: false} ); } /** * */ addDirEdgesPercentage(percentage: number ) : void { let nr_dir_edges_to_add = Math.ceil(this._graph.nrDirEdges() * percentage/100); this.addEdgesAmount( nr_dir_edges_to_add, {directed: true} ); } /** * * DEFAULT edge direction: UNDIRECTED */ addEdgesAmount(amount: number, config?: $E.BaseEdgeConfig ) : void { if ( amount <= 0 ) { throw new Error('Cowardly refusing to add a non-positive amount of edges') } let node_a : $N.IBaseNode, node_b : $N.IBaseNode, nodes : {[key: string] : $N.IBaseNode}; let direction = ( config && config.directed ) ? config.directed : false, dir = direction ? "_d" : "_u"; // logger.log("DIRECTION of new edges to create: " + direction ? "directed" : "undirected"); while ( amount > 0 ) { node_a = this._graph.getRandomNode(); while ( ( node_b = this._graph.getRandomNode() ) === node_a ) {} let edge_id = `${node_a.getID()}_${node_b.getID()}${dir}`; if ( node_a.hasEdgeID( edge_id ) ) { // TODO: Check if the whole duplication prevention is really necessary! // logger.log("Duplicate edge creation, continuing..."); continue; } else { /** * Enable random weights for edges ?? */ this._graph.addEdgeByID(edge_id, node_a, node_b, {directed: direction}); --amount; } } // logger.log(`Created ${amount} ${direction ? "directed" : "undirected"} edges...`); } /** * */ addNodesPercentage(percentage: number, config?: NodeDegreeConfiguration ) : void { if ( percentage < 0 ) { throw 'Cowardly refusing to add a negative amount of nodes'; } let nr_nodes_to_add = Math.ceil(this._graph.nrNodes() * percentage/100); this.addNodesAmount( nr_nodes_to_add, config ); } /** * If the degree configuration is invalid * (negative or infinite degree amount / percentage) * the nodes will have been created nevertheless * * @todo is this `perturbation` since it is not `random` ? */ addNodesAmount(amount: number, config?: NodeDegreeConfiguration ) : void { if ( amount < 0 ) { throw 'Cowardly refusing to add a negative amount of nodes'; } let new_nodes : { [key: string] : $N.IBaseNode } = {}; while ( --amount >= 0 ) { let new_node_id = v4(); new_nodes[new_node_id] = this._graph.addNodeByID( new_node_id ); } if ( config == null ) { return; } else { this.createEdgesByConfig( config, new_nodes ); } } /** * Go through the degree_configuration provided and create edges * as requested by config */ private createEdgesByConfig( config: NodeDegreeConfiguration, new_nodes: {[key: string] : $N.IBaseNode} ) { let degree, min_degree, max_degree, deg_probability; if ( config.und_degree != null || config.dir_degree != null || config.min_und_degree != null && config.max_und_degree != null || config.min_dir_degree != null && config.max_dir_degree != null ) { // Ignore min / max undirected degree if specific amount is given if ( ( degree = config.und_degree ) != null ) { this.createEdgesSpan(degree, degree, false, new_nodes); } else if ( ( min_degree = config.min_und_degree) != null && ( max_degree = config.max_und_degree ) != null ) { this.createEdgesSpan(min_degree, max_degree, false, new_nodes); } // Ignore min / max directed degree if specific amount is given if ( degree = config.dir_degree ) { this.createEdgesSpan(degree, degree, true, new_nodes); } else if ( ( min_degree = config.min_dir_degree) != null && ( max_degree = config.max_dir_degree ) != null ) { this.createEdgesSpan(min_degree, max_degree, true, new_nodes); } } else { if ( config.probability_dir != null ) { this.createEdgesProb( config.probability_dir, true, new_nodes ); } if ( config.probability_und != null ) { this.createEdgesProb( config.probability_und, false, new_nodes ); } } } /** * Simple edge generator: * Go through all node combinations, and * add an (un)directed edge with * @param probability and * @param directed true or false * @param new_nodes set of nodes that were added * CAUTION: this algorithm takes quadratic runtime in #nodes */ createEdgesProb(probability: number, directed?: boolean, new_nodes?: { [key: string] : $N.IBaseNode} ) : void { if (0 > probability || 1 < probability) { throw new Error("Probability out of range."); } directed = directed || false; new_nodes = new_nodes || this._graph.getNodes(); let all_nodes = this._graph.getNodes(), node_a, node_b, edge_id, dir = directed ? '_d' : '_u'; for (node_a in new_nodes) { for (node_b in all_nodes) { if (node_a !== node_b && Math.random() <= probability) { edge_id = all_nodes[node_a].getID() + "_" + all_nodes[node_b].getID() + dir; this._graph.addEdgeByID(edge_id, all_nodes[node_a], all_nodes[node_b], {directed: directed}); } } } } /** * Simple edge generator: * Go through all nodes, and * add [min, max] (un)directed edges to * a randomly chosen node * CAUTION: this algorithm could take quadratic runtime in #nodes * but should be much faster */ createEdgesSpan(min: number, max: number, directed?: boolean, setOfNodes?: { [key: string] : $N.IBaseNode} ) : void { if (min < 0) { throw new Error('Minimum degree cannot be negative.'); } if (max >= this._graph.nrNodes()) { throw new Error('Maximum degree exceeds number of reachable nodes.'); } if (min > max) { throw new Error('Minimum degree cannot exceed maximum degree.'); } directed = directed || false; // Do we need to set them integers before the calculations? var min = min | 0, max = max | 0, new_nodes = setOfNodes || this._graph.getNodes(), all_nodes = this._graph.getNodes(), idx_a, node_a, node_b, edge_id, // we want edges to all possible nodes // TODO: enhance with types / filters later on node_keys = Object.keys(all_nodes), keys_len = node_keys.length, rand_idx, rand_deg, dir = directed ? '_d' : '_u'; for (idx_a in new_nodes) { node_a = new_nodes[idx_a]; rand_idx = 0; rand_deg = (Math.random()*(max-min)+min)|0; while (rand_deg) { rand_idx = (keys_len*Math.random())|0; // should never reach keys_len... node_b = all_nodes[node_keys[rand_idx]]; if (node_a !== node_b) { edge_id = node_a.getID() + "_" + node_b.getID() + dir; // Check if edge already exists if (node_a.hasEdgeID(edge_id)) { continue; } this._graph.addEdgeByID(edge_id, node_a, node_b, {directed: directed}); --rand_deg; } } } } } export { SimplePerturber }