graphinius
Version:
Generic graph library in Typescript
346 lines (290 loc) • 10.2 kB
text/typescript
import {GraphMode, GraphStats, MinAdjacencyListDict} from '../core/interfaces';
import * as $N from '../core/base/BaseNode';
import * as $E from '../core/base/BaseEdge';
import * as $G from '../core/base/BaseGraph';
import * as $CB from '../utils/CallbackUtils';
import * as $BH from '../datastructs/BinaryHeap';
export const DEFAULT_WEIGHT: number = 1;
export interface PFS_Config {
result: { [id: string]: PFS_ResultEntry };
callbacks: PFS_Callbacks;
dir_mode: GraphMode;
goal_node: $N.IBaseNode;
messages?: PFS_Messages;
filters?: any;
evalPriority: any;
evalObjID: any;
}
export interface PFS_ResultEntry {
distance: number; // evaluated by a
parent: $N.IBaseNode;
counter: number; // order of discovery
}
export interface PFS_Callbacks {
init_pfs?: Array<Function>;
new_current?: Array<Function>;
not_encountered?: Array<Function>;
node_open?: Array<Function>;
node_closed?: Array<Function>;
better_path?: Array<Function>;
equal_path?: Array<Function>;
goal_reached?: Array<Function>;
}
export interface PFS_Messages {
init_pfs_msgs?: Array<string>;
new_current_msgs?: Array<string>;
not_enc_msgs?: Array<string>;
node_open_msgs?: Array<string>;
node_closed_msgs?: Array<string>;
better_path_msgs?: Array<string>;
equal_path_msgs?: Array<string>;
goal_reached_msgs?: Array<string>;
}
export interface PFS_Scope {
// OPEN is the heap we use for getting the best choice
OPEN_HEAP: $BH.BinaryHeap;
OPEN: { [id: string]: $N.NeighborEntry };
CLOSED: { [id: string]: $N.NeighborEntry };
nodes: { [id: string]: $N.IBaseNode };
root_node: $N.IBaseNode;
current: $N.NeighborEntry;
adj_nodes: Array<$N.NeighborEntry>;
next: $N.NeighborEntry;
proposed_dist: number;
}
/**
* Priority first search
*
* Like BFS, we are not necessarily visiting the
* whole graph, but only what's reachable from
* a given start node.
*
* @param graph the graph to perform PFS only
* @param v the node from which to start PFS
* @param config a config object similar to that used
* in BFS, automatically instantiated if not given..
*/
function PFS(graph: $G.IGraph,
v: $N.IBaseNode,
config?: PFS_Config): { [id: string]: PFS_ResultEntry } {
config = config || preparePFSStandardConfig();
let callbacks = config.callbacks,
dir_mode = config.dir_mode,
evalPriority = config.evalPriority,
evalObjID = config.evalObjID;
/**
* We are not traversing an empty graph...
*/
if (graph.getMode() === GraphMode.INIT) {
throw new Error('Cowardly refusing to traverse graph without edges.');
}
/**
* We are not traversing a graph taking NO edges into account
*/
if (dir_mode === GraphMode.INIT) {
throw new Error('Cannot traverse a graph with dir_mode set to INIT.');
}
// Root NeighborEntries
let start_ne: $N.NeighborEntry = {
node: v,
edge: new $E.BaseEdge('virtual start edge', v, v, { weighted: true, weight: 0 }),
best: 0
};
let scope: PFS_Scope = {
OPEN_HEAP: new $BH.BinaryHeap($BH.BinaryHeapMode.MIN, evalPriority, evalObjID),
OPEN: {},
CLOSED: {},
nodes: graph.getNodes(),
root_node: v,
current: start_ne,
adj_nodes: [],
next: null,
proposed_dist: Number.POSITIVE_INFINITY,
};
/**
* HOOK 1: PFS INIT
*/
callbacks.init_pfs && $CB.execCallbacks(callbacks.init_pfs, scope);
//initializes the result entry, gives the start node the final values, and default values for all others
scope.OPEN_HEAP.insert(start_ne);
scope.OPEN[start_ne.node.getID()] = start_ne;
/**
* Main loop
*/
while (scope.OPEN_HEAP.size()) {
scope.current = scope.OPEN_HEAP.pop();
// console.log(`node: ${scope.current.node.getID()}`); //LOG!
// console.log(`best: ${scope.current.best}`); //LOG!
/**
* HOOK 2: NEW CURRENT
*/
callbacks.new_current && $CB.execCallbacks(callbacks.new_current, scope);
if (scope.current == null) {
console.log("HEAP popped undefined - HEAP size: " + scope.OPEN_HEAP.size());
}
// remove from OPEN
scope.OPEN[scope.current.node.getID()] = undefined;
// add it to CLOSED
scope.CLOSED[scope.current.node.getID()] = scope.current;
// TODO what if we already reached the goal?
if (scope.current.node === config.goal_node) {
/**
* HOOK 3: Goal node reached
*/
config.callbacks.goal_reached && $CB.execCallbacks(config.callbacks.goal_reached, scope);
// If a goal node is set from the outside & we reach it, we stop.
return config.result;
}
/**
* Extend the current node, also called
* "create n's successors"...
*/
// TODO: Reverse callback logic to NOT merge anything by default!!!
if (dir_mode === GraphMode.MIXED) {
scope.adj_nodes = scope.current.node.reachNodes();
}
else if (dir_mode === GraphMode.UNDIRECTED) {
scope.adj_nodes = scope.current.node.connNodes();
}
else if (dir_mode === GraphMode.DIRECTED) {
scope.adj_nodes = scope.current.node.nextNodes();
}
else {
throw new Error('Unsupported traversal mode. Please use directed, undirected, or mixed');
}
/**
* EXPAND AND EXAMINE NEIGHBORHOOD
*/
for (let adj_idx in scope.adj_nodes) {
scope.next = scope.adj_nodes[adj_idx];
// console.log("scopeNext now:"); //LOG!
// console.log(scope.next.node.getID());
if (scope.CLOSED[scope.next.node.getID()]) {
/**
* HOOK 4: Goal node already closed
*/
config.callbacks.node_closed && $CB.execCallbacks(config.callbacks.node_closed, scope);
continue;
}
if (scope.OPEN[scope.next.node.getID()]) {
// First let's recover the previous best solution from our OPEN structure,
// as the node's neighborhood-retrieving function cannot know it...
// console.log("MARKER - ALREADY OPEN"); //LOG!
scope.next.best = scope.OPEN[scope.next.node.getID()].best;
/**
* HOOK 5: Goal node already visited, but not yet closed
*/
config.callbacks.node_open && $CB.execCallbacks(config.callbacks.node_open, scope);
scope.proposed_dist = scope.current.best + (isNaN(scope.next.edge.getWeight()) ? DEFAULT_WEIGHT : scope.next.edge.getWeight());
/**
* HOOK 6: Better path found
*/
if (scope.next.best > scope.proposed_dist) {
config.callbacks.better_path && $CB.execCallbacks(config.callbacks.better_path, scope);
// HEAP operations are necessary for internal traversal,
// so we handle them here in the main loop
// removing next with the old weight and re-adding it with updated value
scope.OPEN_HEAP.remove(scope.next);
// console.log("MARKER - BETTER DISTANCE");
// console.log(scope.OPEN_HEAP);
scope.next.best = scope.proposed_dist;
scope.OPEN_HEAP.insert(scope.next);
scope.OPEN[scope.next.node.getID()].best = scope.proposed_dist;
}
/**
* HOOK 7: Equal path found (same weight)
*/
//at the moment, this callback array is empty here in the PFS and in the Dijkstra, but used in the Johnsons
else if (scope.next.best === scope.proposed_dist) {
config.callbacks.equal_path && $CB.execCallbacks(config.callbacks.equal_path, scope);
}
continue;
}
// NODE NOT ENCOUNTERED
config.callbacks.not_encountered && $CB.execCallbacks(config.callbacks.not_encountered, scope);
// HEAP operations are necessary for internal traversal,
// so we handle them here in the main loop
scope.OPEN_HEAP.insert(scope.next);
scope.OPEN[scope.next.node.getID()] = scope.next;
// console.log("MARKER-NOT ENCOUNTERED"); //LOG!
}
}
return config.result;
}
function preparePFSStandardConfig(): PFS_Config {
let config: PFS_Config = {
result: {},
callbacks: {
init_pfs: [],
new_current: [],
not_encountered: [],
node_open: [],
node_closed: [],
better_path: [],
equal_path: [],
goal_reached: []
},
messages: {
init_pfs_msgs: [],
new_current_msgs: [],
not_enc_msgs: [],
node_open_msgs: [],
node_closed_msgs: [],
better_path_msgs: [],
equal_path_msgs: [],
goal_reached_msgs: []
},
dir_mode: GraphMode.MIXED,
goal_node: null,
evalPriority: function (ne: $N.NeighborEntry) {
return ne.best || DEFAULT_WEIGHT;
},
evalObjID: function (ne: $N.NeighborEntry) {
return ne.node.getID();
}
};
let callbacks = config.callbacks;
let count = 0;
let counter = function () {
return count++;
};
// Standard INIT callback
let initPFS = function (context: PFS_Scope) {
// initialize all nodes to infinite distance
for (let key in context.nodes) {
config.result[key] = {
distance: Number.POSITIVE_INFINITY,
parent: null,
counter: -1
};
}
// initialize root node entry
// maybe take heuristic into account right here...??
config.result[context.root_node.getID()] = {
distance: 0,
parent: context.root_node,
counter: counter()
};
};
callbacks.init_pfs.push(initPFS);
// Node not yet encountered callback
let notEncountered = function (context: PFS_Scope) {
// setting it's best score to actual distance + edge weight
// and update result structure
context.next.best = context.current.best + (isNaN(context.next.edge.getWeight()) ? DEFAULT_WEIGHT : context.next.edge.getWeight());
config.result[context.next.node.getID()] = {
distance: context.next.best,
parent: context.current.node,
counter: undefined
};
};
callbacks.not_encountered.push(notEncountered);
// Callback for when we find a better solution
let betterPathFound = function (context: PFS_Scope) {
config.result[context.next.node.getID()].distance = context.proposed_dist;
config.result[context.next.node.getID()].parent = context.current.node;
};
callbacks.better_path.push(betterPathFound);
return config;
}
export { PFS, preparePFSStandardConfig };