network-simplex.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import _ from 'lodash'
  2. import { alg } from 'graphlibrary'
  3. import feasibleTree from './feasible-tree'
  4. import { slack, longestPath as initRank } from './util'
  5. import { simplify } from '../util'
  6. const { preorder, postorder } = alg
  7. // Expose some internals for testing purposes
  8. networkSimplex.initLowLimValues = initLowLimValues
  9. networkSimplex.initCutValues = initCutValues
  10. networkSimplex.calcCutValue = calcCutValue
  11. networkSimplex.leaveEdge = leaveEdge
  12. networkSimplex.enterEdge = enterEdge
  13. networkSimplex.exchangeEdges = exchangeEdges
  14. /*
  15. * The network simplex algorithm assigns ranks to each node in the input graph
  16. * and iteratively improves the ranking to reduce the length of edges.
  17. *
  18. * Preconditions:
  19. *
  20. * 1. The input graph must be a DAG.
  21. * 2. All nodes in the graph must have an object value.
  22. * 3. All edges in the graph must have "minlen" and "weight" attributes.
  23. *
  24. * Postconditions:
  25. *
  26. * 1. All nodes in the graph will have an assigned "rank" attribute that has
  27. * been optimized by the network simplex algorithm. Ranks start at 0.
  28. *
  29. *
  30. * A rough sketch of the algorithm is as follows:
  31. *
  32. * 1. Assign initial ranks to each node. We use the longest path algorithm,
  33. * which assigns ranks to the lowest position possible. In general this
  34. * leads to very wide bottom ranks and unnecessarily long edges.
  35. * 2. Construct a feasible tight tree. A tight tree is one such that all
  36. * edges in the tree have no slack (difference between length of edge
  37. * and minlen for the edge). This by itself greatly improves the assigned
  38. * rankings by shorting edges.
  39. * 3. Iteratively find edges that have negative cut values. Generally a
  40. * negative cut value indicates that the edge could be removed and a new
  41. * tree edge could be added to produce a more compact graph.
  42. *
  43. * Much of the algorithms here are derived from Gansner, et al., "A Technique
  44. * for Drawing Directed Graphs." The structure of the file roughly follows the
  45. * structure of the overall algorithm.
  46. */
  47. function networkSimplex (g) {
  48. g = simplify(g)
  49. initRank(g)
  50. const t = feasibleTree(g)
  51. initLowLimValues(t)
  52. initCutValues(t, g)
  53. let e
  54. let f
  55. while ((e = leaveEdge(t))) {
  56. f = enterEdge(t, g, e)
  57. exchangeEdges(t, g, e, f)
  58. }
  59. }
  60. /*
  61. * Initializes cut values for all edges in the tree.
  62. */
  63. function initCutValues (t, g) {
  64. let vs = postorder(t, t.nodes())
  65. vs = vs.slice(0, vs.length - 1)
  66. _.forEach(vs, function (v) {
  67. assignCutValue(t, g, v)
  68. })
  69. }
  70. function assignCutValue (t, g, child) {
  71. const childLab = t.node(child)
  72. const parent = childLab.parent
  73. t.edge(child, parent).cutvalue = calcCutValue(t, g, child)
  74. }
  75. /*
  76. * Given the tight tree, its graph, and a child in the graph calculate and
  77. * return the cut value for the edge between the child and its parent.
  78. */
  79. function calcCutValue (t, g, child) {
  80. const childLab = t.node(child)
  81. const parent = childLab.parent
  82. // True if the child is on the tail end of the edge in the directed graph
  83. let childIsTail = true
  84. // The graph's view of the tree edge we're inspecting
  85. let graphEdge = g.edge(child, parent)
  86. // The accumulated cut value for the edge between this node and its parent
  87. let cutValue = 0
  88. if (!graphEdge) {
  89. childIsTail = false
  90. graphEdge = g.edge(parent, child)
  91. }
  92. cutValue = graphEdge.weight
  93. _.forEach(g.nodeEdges(child), function (e) {
  94. const isOutEdge = e.v === child
  95. const other = isOutEdge ? e.w : e.v
  96. if (other !== parent) {
  97. const pointsToHead = isOutEdge === childIsTail
  98. const otherWeight = g.edge(e).weight
  99. cutValue += pointsToHead ? otherWeight : -otherWeight
  100. if (isTreeEdge(t, child, other)) {
  101. const otherCutValue = t.edge(child, other).cutvalue
  102. cutValue += pointsToHead ? -otherCutValue : otherCutValue
  103. }
  104. }
  105. })
  106. return cutValue
  107. }
  108. function initLowLimValues (tree, root) {
  109. if (arguments.length < 2) {
  110. root = tree.nodes()[0]
  111. }
  112. dfsAssignLowLim(tree, {}, 1, root)
  113. }
  114. function dfsAssignLowLim (tree, visited, nextLim, v, parent) {
  115. const low = nextLim
  116. const label = tree.node(v)
  117. visited[v] = true
  118. _.forEach(tree.neighbors(v), function (w) {
  119. if (!_.has(visited, w)) {
  120. nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v)
  121. }
  122. })
  123. label.low = low
  124. label.lim = nextLim++
  125. if (parent) {
  126. label.parent = parent
  127. } else {
  128. // TODO should be able to remove this when we incrementally update low lim
  129. delete label.parent
  130. }
  131. return nextLim
  132. }
  133. function leaveEdge (tree) {
  134. return _.find(tree.edges(), function (e) {
  135. return tree.edge(e).cutvalue < 0
  136. })
  137. }
  138. function enterEdge (t, g, edge) {
  139. let v = edge.v
  140. let w = edge.w
  141. // For the rest of this function we assume that v is the tail and w is the
  142. // head, so if we don't have this edge in the graph we should flip it to
  143. // match the correct orientation.
  144. if (!g.hasEdge(v, w)) {
  145. v = edge.w
  146. w = edge.v
  147. }
  148. const vLabel = t.node(v)
  149. const wLabel = t.node(w)
  150. let tailLabel = vLabel
  151. let flip = false
  152. // If the root is in the tail of the edge then we need to flip the logic that
  153. // checks for the head and tail nodes in the candidates function below.
  154. if (vLabel.lim > wLabel.lim) {
  155. tailLabel = wLabel
  156. flip = true
  157. }
  158. const candidates = _.filter(g.edges(), function (edge) {
  159. return flip === isDescendant(t, t.node(edge.v), tailLabel) &&
  160. flip !== isDescendant(t, t.node(edge.w), tailLabel)
  161. })
  162. return _.minBy(candidates, function (edge) { return slack(g, edge) })
  163. }
  164. function exchangeEdges (t, g, e, f) {
  165. const v = e.v
  166. const w = e.w
  167. t.removeEdge(v, w)
  168. t.setEdge(f.v, f.w, {})
  169. initLowLimValues(t)
  170. initCutValues(t, g)
  171. updateRanks(t, g)
  172. }
  173. function updateRanks (t, g) {
  174. const root = _.find(t.nodes(), function (v) { return !g.node(v).parent })
  175. let vs = preorder(t, root)
  176. vs = vs.slice(1)
  177. _.forEach(vs, function (v) {
  178. const parent = t.node(v).parent
  179. let edge = g.edge(v, parent)
  180. let flipped = false
  181. if (!edge) {
  182. edge = g.edge(parent, v)
  183. flipped = true
  184. }
  185. g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen)
  186. })
  187. }
  188. /*
  189. * Returns true if the edge is in the tree.
  190. */
  191. function isTreeEdge (tree, u, v) {
  192. return tree.hasEdge(u, v)
  193. }
  194. /*
  195. * Returns true if the specified node is descendant of the root node per the
  196. * assigned low and lim attributes in the tree.
  197. */
  198. function isDescendant (tree, vLabel, rootLabel) {
  199. return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim
  200. }
  201. export default networkSimplex