| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- import _ from 'lodash'
- import { alg } from 'graphlibrary'
- import feasibleTree from './feasible-tree'
- import { slack, longestPath as initRank } from './util'
- import { simplify } from '../util'
- const { preorder, postorder } = alg
- // Expose some internals for testing purposes
- networkSimplex.initLowLimValues = initLowLimValues
- networkSimplex.initCutValues = initCutValues
- networkSimplex.calcCutValue = calcCutValue
- networkSimplex.leaveEdge = leaveEdge
- networkSimplex.enterEdge = enterEdge
- networkSimplex.exchangeEdges = exchangeEdges
- /*
- * The network simplex algorithm assigns ranks to each node in the input graph
- * and iteratively improves the ranking to reduce the length of edges.
- *
- * Preconditions:
- *
- * 1. The input graph must be a DAG.
- * 2. All nodes in the graph must have an object value.
- * 3. All edges in the graph must have "minlen" and "weight" attributes.
- *
- * Postconditions:
- *
- * 1. All nodes in the graph will have an assigned "rank" attribute that has
- * been optimized by the network simplex algorithm. Ranks start at 0.
- *
- *
- * A rough sketch of the algorithm is as follows:
- *
- * 1. Assign initial ranks to each node. We use the longest path algorithm,
- * which assigns ranks to the lowest position possible. In general this
- * leads to very wide bottom ranks and unnecessarily long edges.
- * 2. Construct a feasible tight tree. A tight tree is one such that all
- * edges in the tree have no slack (difference between length of edge
- * and minlen for the edge). This by itself greatly improves the assigned
- * rankings by shorting edges.
- * 3. Iteratively find edges that have negative cut values. Generally a
- * negative cut value indicates that the edge could be removed and a new
- * tree edge could be added to produce a more compact graph.
- *
- * Much of the algorithms here are derived from Gansner, et al., "A Technique
- * for Drawing Directed Graphs." The structure of the file roughly follows the
- * structure of the overall algorithm.
- */
- function networkSimplex (g) {
- g = simplify(g)
- initRank(g)
- const t = feasibleTree(g)
- initLowLimValues(t)
- initCutValues(t, g)
- let e
- let f
- while ((e = leaveEdge(t))) {
- f = enterEdge(t, g, e)
- exchangeEdges(t, g, e, f)
- }
- }
- /*
- * Initializes cut values for all edges in the tree.
- */
- function initCutValues (t, g) {
- let vs = postorder(t, t.nodes())
- vs = vs.slice(0, vs.length - 1)
- _.forEach(vs, function (v) {
- assignCutValue(t, g, v)
- })
- }
- function assignCutValue (t, g, child) {
- const childLab = t.node(child)
- const parent = childLab.parent
- t.edge(child, parent).cutvalue = calcCutValue(t, g, child)
- }
- /*
- * Given the tight tree, its graph, and a child in the graph calculate and
- * return the cut value for the edge between the child and its parent.
- */
- function calcCutValue (t, g, child) {
- const childLab = t.node(child)
- const parent = childLab.parent
- // True if the child is on the tail end of the edge in the directed graph
- let childIsTail = true
- // The graph's view of the tree edge we're inspecting
- let graphEdge = g.edge(child, parent)
- // The accumulated cut value for the edge between this node and its parent
- let cutValue = 0
- if (!graphEdge) {
- childIsTail = false
- graphEdge = g.edge(parent, child)
- }
- cutValue = graphEdge.weight
- _.forEach(g.nodeEdges(child), function (e) {
- const isOutEdge = e.v === child
- const other = isOutEdge ? e.w : e.v
- if (other !== parent) {
- const pointsToHead = isOutEdge === childIsTail
- const otherWeight = g.edge(e).weight
- cutValue += pointsToHead ? otherWeight : -otherWeight
- if (isTreeEdge(t, child, other)) {
- const otherCutValue = t.edge(child, other).cutvalue
- cutValue += pointsToHead ? -otherCutValue : otherCutValue
- }
- }
- })
- return cutValue
- }
- function initLowLimValues (tree, root) {
- if (arguments.length < 2) {
- root = tree.nodes()[0]
- }
- dfsAssignLowLim(tree, {}, 1, root)
- }
- function dfsAssignLowLim (tree, visited, nextLim, v, parent) {
- const low = nextLim
- const label = tree.node(v)
- visited[v] = true
- _.forEach(tree.neighbors(v), function (w) {
- if (!_.has(visited, w)) {
- nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v)
- }
- })
- label.low = low
- label.lim = nextLim++
- if (parent) {
- label.parent = parent
- } else {
- // TODO should be able to remove this when we incrementally update low lim
- delete label.parent
- }
- return nextLim
- }
- function leaveEdge (tree) {
- return _.find(tree.edges(), function (e) {
- return tree.edge(e).cutvalue < 0
- })
- }
- function enterEdge (t, g, edge) {
- let v = edge.v
- let w = edge.w
- // For the rest of this function we assume that v is the tail and w is the
- // head, so if we don't have this edge in the graph we should flip it to
- // match the correct orientation.
- if (!g.hasEdge(v, w)) {
- v = edge.w
- w = edge.v
- }
- const vLabel = t.node(v)
- const wLabel = t.node(w)
- let tailLabel = vLabel
- let flip = false
- // If the root is in the tail of the edge then we need to flip the logic that
- // checks for the head and tail nodes in the candidates function below.
- if (vLabel.lim > wLabel.lim) {
- tailLabel = wLabel
- flip = true
- }
- const candidates = _.filter(g.edges(), function (edge) {
- return flip === isDescendant(t, t.node(edge.v), tailLabel) &&
- flip !== isDescendant(t, t.node(edge.w), tailLabel)
- })
- return _.minBy(candidates, function (edge) { return slack(g, edge) })
- }
- function exchangeEdges (t, g, e, f) {
- const v = e.v
- const w = e.w
- t.removeEdge(v, w)
- t.setEdge(f.v, f.w, {})
- initLowLimValues(t)
- initCutValues(t, g)
- updateRanks(t, g)
- }
- function updateRanks (t, g) {
- const root = _.find(t.nodes(), function (v) { return !g.node(v).parent })
- let vs = preorder(t, root)
- vs = vs.slice(1)
- _.forEach(vs, function (v) {
- const parent = t.node(v).parent
- let edge = g.edge(v, parent)
- let flipped = false
- if (!edge) {
- edge = g.edge(parent, v)
- flipped = true
- }
- g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen)
- })
- }
- /*
- * Returns true if the edge is in the tree.
- */
- function isTreeEdge (tree, u, v) {
- return tree.hasEdge(u, v)
- }
- /*
- * Returns true if the specified node is descendant of the root node per the
- * assigned low and lim attributes in the tree.
- */
- function isDescendant (tree, vLabel, rootLabel) {
- return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim
- }
- export default networkSimplex
|