diff options
author | Harshal Dupare <harshal333hd@gmail.com> | 2021-05-20 02:44:58 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-19 17:14:58 -0400 |
commit | 984f0c262cc15418a98bb967d1edb5df8479ac63 (patch) | |
tree | 324bb18cee2df2edd8c536c0075702ba66af0ea6 | |
parent | 87c127b9b88de237d89c771abb6d964b29fd1356 (diff) | |
download | networkx-984f0c262cc15418a98bb967d1edb5df8479ac63.tar.gz |
restructured networksimplex.py and added test_networksimplex.py (#4685)
* restructured networksimplex.py and added test_networksimplex.py
* formatting changed
* formatting changed
* formatting changed
* removed extra underscore
* added fixture in test_networksimplex.py
* formatting changed
* initilized -> initialized.
* fix typo in comment
Co-authored-by: Ross Barnowski <rossbar@berkeley.edu>
Co-authored-by: Dan Schult <dschult@colgate.edu>
-rw-r--r-- | networkx/algorithms/flow/networksimplex.py | 690 | ||||
-rw-r--r-- | networkx/algorithms/flow/tests/test_networksimplex.py | 383 |
2 files changed, 768 insertions, 305 deletions
diff --git a/networkx/algorithms/flow/networksimplex.py b/networkx/algorithms/flow/networksimplex.py index 561dbb8a..0de9be80 100644 --- a/networkx/algorithms/flow/networksimplex.py +++ b/networkx/algorithms/flow/networksimplex.py @@ -10,6 +10,321 @@ import networkx as nx from networkx.utils import not_implemented_for +class _DataEssentialsAndFunctions: + def __init__( + self, G, multigraph, demand="demand", capacity="capacity", weight="weight" + ): + + # Number all nodes and edges and hereafter reference them using ONLY their numbers + self.node_list = list(G) # nodes + self.node_indices = {u: i for i, u in enumerate(self.node_list)} # node indices + self.node_demands = [ + G.nodes[u].get(demand, 0) for u in self.node_list + ] # node demands + + self.edge_sources = [] # edge sources + self.edge_targets = [] # edge targets + if multigraph: + self.edge_keys = [] # edge keys + self.edge_indices = {} # edge indices + self.edge_capacities = [] # edge capacities + self.edge_weights = [] # edge weights + + if not multigraph: + edges = G.edges(data=True) + else: + edges = G.edges(data=True, keys=True) + + inf = float("inf") + edges = (e for e in edges if e[0] != e[1] and e[-1].get(capacity, inf) != 0) + for i, e in enumerate(edges): + self.edge_sources.append(self.node_indices[e[0]]) + self.edge_targets.append(self.node_indices[e[1]]) + if multigraph: + self.edge_keys.append(e[2]) + self.edge_indices[e[:-1]] = i + self.edge_capacities.append(e[-1].get(capacity, inf)) + self.edge_weights.append(e[-1].get(weight, 0)) + + # spanning tree specific data to be initialized + + self.edge_count = None # number of edges + self.edge_flow = None # edge flows + self.node_potentials = None # node potentials + self.parent = None # parent nodes + self.parent_edge = None # edges to parents + self.subtree_size = None # subtree sizes + self.next_node_dft = None # next nodes in depth-first thread + self.prev_node_dft = None # previous nodes in depth-first thread + self.last_descendent_dft = None # last descendants in depth-first thread + self._spanning_tree_initialized = ( + False # False until initialize_spanning_tree() is called + ) + + def initialize_spanning_tree(self, n, faux_inf): + self.edge_count = len(self.edge_indices) # number of edges + self.edge_flow = list( + chain(repeat(0, self.edge_count), (abs(d) for d in self.node_demands)) + ) # edge flows + self.node_potentials = [ + faux_inf if d <= 0 else -faux_inf for d in self.node_demands + ] # node potentials + self.parent = list(chain(repeat(-1, n), [None])) # parent nodes + self.parent_edge = list( + range(self.edge_count, self.edge_count + n) + ) # edges to parents + self.subtree_size = list(chain(repeat(1, n), [n + 1])) # subtree sizes + self.next_node_dft = list( + chain(range(1, n), [-1, 0]) + ) # next nodes in depth-first thread + self.prev_node_dft = list(range(-1, n)) # previous nodes in depth-first thread + self.last_descendent_dft = list( + chain(range(n), [n - 1]) + ) # last descendants in depth-first thread + self._spanning_tree_initialized = True # True only if all the assignments pass + + def find_apex(self, p, q): + """ + Find the lowest common ancestor of nodes p and q in the spanning tree. + """ + size_p = self.subtree_size[p] + size_q = self.subtree_size[q] + while True: + while size_p < size_q: + p = self.parent[p] + size_p = self.subtree_size[p] + while size_p > size_q: + q = self.parent[q] + size_q = self.subtree_size[q] + if size_p == size_q: + if p != q: + p = self.parent[p] + size_p = self.subtree_size[p] + q = self.parent[q] + size_q = self.subtree_size[q] + else: + return p + + def trace_path(self, p, w): + """ + Returns the nodes and edges on the path from node p to its ancestor w. + """ + Wn = [p] + We = [] + while p != w: + We.append(self.parent_edge[p]) + p = self.parent[p] + Wn.append(p) + return Wn, We + + def find_cycle(self, i, p, q): + """ + Returns the nodes and edges on the cycle containing edge i == (p, q) + when the latter is added to the spanning tree. + + The cycle is oriented in the direction from p to q. + """ + w = self.find_apex(p, q) + Wn, We = self.trace_path(p, w) + Wn.reverse() + We.reverse() + if We != [i]: + We.append(i) + WnR, WeR = self.trace_path(q, w) + del WnR[-1] + Wn += WnR + We += WeR + return Wn, We + + def augment_flow(self, Wn, We, f): + """ + Augment f units of flow along a cycle represented by Wn and We. + """ + for i, p in zip(We, Wn): + if self.edge_sources[i] == p: + self.edge_flow[i] += f + else: + self.edge_flow[i] -= f + + def trace_subtree(self, p): + """ + Yield the nodes in the subtree rooted at a node p. + """ + yield p + l = self.last_descendent_dft[p] + while p != l: + p = self.next_node_dft[p] + yield p + + def remove_edge(self, s, t): + """ + Remove an edge (s, t) where parent[t] == s from the spanning tree. + """ + size_t = self.subtree_size[t] + prev_t = self.prev_node_dft[t] + last_t = self.last_descendent_dft[t] + next_last_t = self.next_node_dft[last_t] + # Remove (s, t). + self.parent[t] = None + self.parent_edge[t] = None + # Remove the subtree rooted at t from the depth-first thread. + self.next_node_dft[prev_t] = next_last_t + self.prev_node_dft[next_last_t] = prev_t + self.next_node_dft[last_t] = t + self.prev_node_dft[t] = last_t + # Update the subtree sizes and last descendants of the (old) acenstors + # of t. + while s is not None: + self.subtree_size[s] -= size_t + if self.last_descendent_dft[s] == last_t: + self.last_descendent_dft[s] = prev_t + s = self.parent[s] + + def make_root(self, q): + """ + Make a node q the root of its containing subtree. + """ + ancestors = [] + while q is not None: + ancestors.append(q) + q = self.parent[q] + ancestors.reverse() + for p, q in zip(ancestors, islice(ancestors, 1, None)): + size_p = self.subtree_size[p] + last_p = self.last_descendent_dft[p] + prev_q = self.prev_node_dft[q] + last_q = self.last_descendent_dft[q] + next_last_q = self.next_node_dft[last_q] + # Make p a child of q. + self.parent[p] = q + self.parent[q] = None + self.parent_edge[p] = self.parent_edge[q] + self.parent_edge[q] = None + self.subtree_size[p] = size_p - self.subtree_size[q] + self.subtree_size[q] = size_p + # Remove the subtree rooted at q from the depth-first thread. + self.next_node_dft[prev_q] = next_last_q + self.prev_node_dft[next_last_q] = prev_q + self.next_node_dft[last_q] = q + self.prev_node_dft[q] = last_q + if last_p == last_q: + self.last_descendent_dft[p] = prev_q + last_p = prev_q + # Add the remaining parts of the subtree rooted at p as a subtree + # of q in the depth-first thread. + self.prev_node_dft[p] = last_q + self.next_node_dft[last_q] = p + self.next_node_dft[last_p] = q + self.prev_node_dft[q] = last_p + self.last_descendent_dft[q] = last_p + + def add_edge(self, i, p, q): + """ + Add an edge (p, q) to the spanning tree where q is the root of a subtree. + """ + last_p = self.last_descendent_dft[p] + next_last_p = self.next_node_dft[last_p] + size_q = self.subtree_size[q] + last_q = self.last_descendent_dft[q] + # Make q a child of p. + self.parent[q] = p + self.parent_edge[q] = i + # Insert the subtree rooted at q into the depth-first thread. + self.next_node_dft[last_p] = q + self.prev_node_dft[q] = last_p + self.prev_node_dft[next_last_p] = last_q + self.next_node_dft[last_q] = next_last_p + # Update the subtree sizes and last descendants of the (new) ancestors + # of q. + while p is not None: + self.subtree_size[p] += size_q + if self.last_descendent_dft[p] == last_p: + self.last_descendent_dft[p] = last_q + p = self.parent[p] + + def update_potentials(self, i, p, q): + """ + Update the potentials of the nodes in the subtree rooted at a node + q connected to its parent p by an edge i. + """ + if q == self.edge_targets[i]: + d = self.node_potentials[p] - self.edge_weights[i] - self.node_potentials[q] + else: + d = self.node_potentials[p] + self.edge_weights[i] - self.node_potentials[q] + for q in self.trace_subtree(q): + self.node_potentials[q] += d + + def reduced_cost(self, i): + """Returns the reduced cost of an edge i.""" + c = ( + self.edge_weights[i] + - self.node_potentials[self.edge_sources[i]] + + self.node_potentials[self.edge_targets[i]] + ) + return c if self.edge_flow[i] == 0 else -c + + def find_entering_edges(self): + """Yield entering edges until none can be found.""" + if self.edge_count == 0: + return + + # Entering edges are found by combining Dantzig's rule and Bland's + # rule. The edges are cyclically grouped into blocks of size B. Within + # each block, Dantzig's rule is applied to find an entering edge. The + # blocks to search is determined following Bland's rule. + B = int(ceil(sqrt(self.edge_count))) # pivot block size + M = (self.edge_count + B - 1) // B # number of blocks needed to cover all edges + m = 0 # number of consecutive blocks without eligible + # entering edges + f = 0 # first edge in block + while m < M: + # Determine the next block of edges. + l = f + B + if l <= self.edge_count: + edges = range(f, l) + else: + l -= self.edge_count + edges = chain(range(f, self.edge_count), range(l)) + f = l + # Find the first edge with the lowest reduced cost. + i = min(edges, key=self.reduced_cost) + c = self.reduced_cost(i) + if c >= 0: + # No entering edge found in the current block. + m += 1 + else: + # Entering edge found. + if self.edge_flow[i] == 0: + p = self.edge_sources[i] + q = self.edge_targets[i] + else: + p = self.edge_targets[i] + q = self.edge_sources[i] + yield i, p, q + m = 0 + # All edges have nonnegative reduced costs. The current flow is + # optimal. + + def residual_capacity(self, i, p): + """Returns the residual capacity of an edge i in the direction away + from its endpoint p. + """ + return ( + self.edge_capacities[i] - self.edge_flow[i] + if self.edge_sources[i] == p + else self.edge_flow[i] + ) + + def find_leaving_edge(self, Wn, We): + """Returns the leaving edge in a cycle represented by Wn and We.""" + j, s = min( + zip(reversed(We), reversed(Wn)), + key=lambda i_p: self.residual_capacity(*i_p), + ) + t = self.edge_targets[j] if self.edge_sources[j] == s else self.edge_sources[j] + return j, s, t + + @not_implemented_for("undirected") def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): r"""Find a minimum cost flow satisfying all demands in digraph G. @@ -180,43 +495,23 @@ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): if len(G) == 0: raise nx.NetworkXError("graph has no nodes") - # Number all nodes and edges and hereafter reference them using ONLY their - # numbers - - N = list(G) # nodes - I = {u: i for i, u in enumerate(N)} # node indices - D = [G.nodes[u].get(demand, 0) for u in N] # node demands - - inf = float("inf") - for p, b in zip(N, D): - if abs(b) == inf: - raise nx.NetworkXError(f"node {p!r} has infinite demand") - multigraph = G.is_multigraph() - S = [] # edge sources - T = [] # edge targets - if multigraph: - K = [] # edge keys - E = {} # edge indices - U = [] # edge capacities - C = [] # edge weights - if not multigraph: - edges = G.edges(data=True) - else: - edges = G.edges(data=True, keys=True) - edges = (e for e in edges if e[0] != e[1] and e[-1].get(capacity, inf) != 0) - for i, e in enumerate(edges): - S.append(I[e[0]]) - T.append(I[e[1]]) - if multigraph: - K.append(e[2]) - E[e[:-1]] = i - U.append(e[-1].get(capacity, inf)) - C.append(e[-1].get(weight, 0)) + # extracting data essential to problem + DEAF = _DataEssentialsAndFunctions( + G, multigraph, demand=demand, capacity=capacity, weight=weight + ) - for e, c in zip(E, C): - if abs(c) == inf: + ########################################################################### + # Quick Error Detection + ########################################################################### + + inf = float("inf") + for u, d in zip(DEAF.node_list, DEAF.node_demands): + if abs(d) == inf: + raise nx.NetworkXError(f"node {u!r} has infinite demand") + for e, w in zip(DEAF.edge_indices, DEAF.edge_weights): + if abs(w) == inf: raise nx.NetworkXError(f"edge {e!r} has infinite weight") if not multigraph: edges = nx.selfloop_edges(G, data=True) @@ -227,13 +522,13 @@ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): raise nx.NetworkXError(f"edge {e[:-1]!r} has infinite weight") ########################################################################### - # Quick infeasibility detection + # Quick Infeasibility Detection ########################################################################### - if sum(D) != 0: + if sum(DEAF.node_demands) != 0: raise nx.NetworkXUnfeasible("total node demand is not zero") - for e, u in zip(E, U): - if u < 0: + for e, c in zip(DEAF.edge_indices, DEAF.edge_capacities): + if c < 0: raise nx.NetworkXUnfeasible(f"edge {e!r} has negative capacity") if not multigraph: edges = nx.selfloop_edges(G, data=True) @@ -252,293 +547,65 @@ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): # spanning tree of the network simplex method. The new edges will used to # trivially satisfy the node demands and create an initial strongly # feasible spanning tree. - n = len(N) # number of nodes - for p, d in enumerate(D): + for i, d in enumerate(DEAF.node_demands): # Must be greater-than here. Zero-demand nodes must have - # edges pointing towards the root to ensure strong - # feasibility. + # edges pointing towards the root to ensure strong feasibility. if d > 0: - S.append(-1) - T.append(p) + DEAF.edge_sources.append(-1) + DEAF.edge_targets.append(i) else: - S.append(p) - T.append(-1) + DEAF.edge_sources.append(i) + DEAF.edge_targets.append(-1) faux_inf = ( 3 * max( chain( - [sum(u for u in U if u < inf), sum(abs(c) for c in C)], - (abs(d) for d in D), + [ + sum(c for c in DEAF.edge_capacities if c < inf), + sum(abs(w) for w in DEAF.edge_weights), + ], + (abs(d) for d in DEAF.node_demands), ) ) or 1 ) - C.extend(repeat(faux_inf, n)) - U.extend(repeat(faux_inf, n)) + + n = len(DEAF.node_list) # number of nodes + DEAF.edge_weights.extend(repeat(faux_inf, n)) + DEAF.edge_capacities.extend(repeat(faux_inf, n)) # Construct the initial spanning tree. - e = len(E) # number of edges - x = list(chain(repeat(0, e), (abs(d) for d in D))) # edge flows - pi = [faux_inf if d <= 0 else -faux_inf for d in D] # node potentials - parent = list(chain(repeat(-1, n), [None])) # parent nodes - edge = list(range(e, e + n)) # edges to parents - size = list(chain(repeat(1, n), [n + 1])) # subtree sizes - next = list(chain(range(1, n), [-1, 0])) # next nodes in depth-first thread - prev = list(range(-1, n)) # previous nodes in depth-first thread - last = list(chain(range(n), [n - 1])) # last descendants in depth-first thread + DEAF.initialize_spanning_tree(n, faux_inf) ########################################################################### # Pivot loop ########################################################################### - def reduced_cost(i): - """Returns the reduced cost of an edge i.""" - c = C[i] - pi[S[i]] + pi[T[i]] - return c if x[i] == 0 else -c - - def find_entering_edges(): - """Yield entering edges until none can be found.""" - if e == 0: - return - - # Entering edges are found by combining Dantzig's rule and Bland's - # rule. The edges are cyclically grouped into blocks of size B. Within - # each block, Dantzig's rule is applied to find an entering edge. The - # blocks to search is determined following Bland's rule. - B = int(ceil(sqrt(e))) # pivot block size - M = (e + B - 1) // B # number of blocks needed to cover all edges - m = 0 # number of consecutive blocks without eligible - # entering edges - f = 0 # first edge in block - while m < M: - # Determine the next block of edges. - l = f + B - if l <= e: - edges = range(f, l) - else: - l -= e - edges = chain(range(f, e), range(l)) - f = l - # Find the first edge with the lowest reduced cost. - i = min(edges, key=reduced_cost) - c = reduced_cost(i) - if c >= 0: - # No entering edge found in the current block. - m += 1 - else: - # Entering edge found. - if x[i] == 0: - p = S[i] - q = T[i] - else: - p = T[i] - q = S[i] - yield i, p, q - m = 0 - # All edges have nonnegative reduced costs. The current flow is - # optimal. - - def find_apex(p, q): - """Find the lowest common ancestor of nodes p and q in the spanning - tree. - """ - size_p = size[p] - size_q = size[q] - while True: - while size_p < size_q: - p = parent[p] - size_p = size[p] - while size_p > size_q: - q = parent[q] - size_q = size[q] - if size_p == size_q: - if p != q: - p = parent[p] - size_p = size[p] - q = parent[q] - size_q = size[q] - else: - return p - - def trace_path(p, w): - """Returns the nodes and edges on the path from node p to its ancestor - w. - """ - Wn = [p] - We = [] - while p != w: - We.append(edge[p]) - p = parent[p] - Wn.append(p) - return Wn, We - - def find_cycle(i, p, q): - """Returns the nodes and edges on the cycle containing edge i == (p, q) - when the latter is added to the spanning tree. - - The cycle is oriented in the direction from p to q. - """ - w = find_apex(p, q) - Wn, We = trace_path(p, w) - Wn.reverse() - We.reverse() - if We != [i]: - We.append(i) - WnR, WeR = trace_path(q, w) - del WnR[-1] - Wn += WnR - We += WeR - return Wn, We - - def residual_capacity(i, p): - """Returns the residual capacity of an edge i in the direction away - from its endpoint p. - """ - return U[i] - x[i] if S[i] == p else x[i] - - def find_leaving_edge(Wn, We): - """Returns the leaving edge in a cycle represented by Wn and We.""" - j, s = min( - zip(reversed(We), reversed(Wn)), key=lambda i_p: residual_capacity(*i_p) - ) - t = T[j] if S[j] == s else S[j] - return j, s, t - - def augment_flow(Wn, We, f): - """Augment f units of flow along a cycle represented by Wn and We.""" - for i, p in zip(We, Wn): - if S[i] == p: - x[i] += f - else: - x[i] -= f - - def trace_subtree(p): - """Yield the nodes in the subtree rooted at a node p.""" - yield p - l = last[p] - while p != l: - p = next[p] - yield p - - def remove_edge(s, t): - """Remove an edge (s, t) where parent[t] == s from the spanning tree.""" - size_t = size[t] - prev_t = prev[t] - last_t = last[t] - next_last_t = next[last_t] - # Remove (s, t). - parent[t] = None - edge[t] = None - # Remove the subtree rooted at t from the depth-first thread. - next[prev_t] = next_last_t - prev[next_last_t] = prev_t - next[last_t] = t - prev[t] = last_t - # Update the subtree sizes and last descendants of the (old) acenstors - # of t. - while s is not None: - size[s] -= size_t - if last[s] == last_t: - last[s] = prev_t - s = parent[s] - - def make_root(q): - """Make a node q the root of its containing subtree.""" - ancestors = [] - while q is not None: - ancestors.append(q) - q = parent[q] - ancestors.reverse() - for p, q in zip(ancestors, islice(ancestors, 1, None)): - size_p = size[p] - last_p = last[p] - prev_q = prev[q] - last_q = last[q] - next_last_q = next[last_q] - # Make p a child of q. - parent[p] = q - parent[q] = None - edge[p] = edge[q] - edge[q] = None - size[p] = size_p - size[q] - size[q] = size_p - # Remove the subtree rooted at q from the depth-first thread. - next[prev_q] = next_last_q - prev[next_last_q] = prev_q - next[last_q] = q - prev[q] = last_q - if last_p == last_q: - last[p] = prev_q - last_p = prev_q - # Add the remaining parts of the subtree rooted at p as a subtree - # of q in the depth-first thread. - prev[p] = last_q - next[last_q] = p - next[last_p] = q - prev[q] = last_p - last[q] = last_p - - def add_edge(i, p, q): - """Add an edge (p, q) to the spanning tree where q is the root of a - subtree. - """ - last_p = last[p] - next_last_p = next[last_p] - size_q = size[q] - last_q = last[q] - # Make q a child of p. - parent[q] = p - edge[q] = i - # Insert the subtree rooted at q into the depth-first thread. - next[last_p] = q - prev[q] = last_p - prev[next_last_p] = last_q - next[last_q] = next_last_p - # Update the subtree sizes and last descendants of the (new) ancestors - # of q. - while p is not None: - size[p] += size_q - if last[p] == last_p: - last[p] = last_q - p = parent[p] - - def update_potentials(i, p, q): - """Update the potentials of the nodes in the subtree rooted at a node - q connected to its parent p by an edge i. - """ - if q == T[i]: - d = pi[p] - C[i] - pi[q] - else: - d = pi[p] + C[i] - pi[q] - for q in trace_subtree(q): - pi[q] += d - - # Pivot loop - for i, p, q in find_entering_edges(): - Wn, We = find_cycle(i, p, q) - j, s, t = find_leaving_edge(Wn, We) - augment_flow(Wn, We, residual_capacity(j, s)) + for i, p, q in DEAF.find_entering_edges(): + Wn, We = DEAF.find_cycle(i, p, q) + j, s, t = DEAF.find_leaving_edge(Wn, We) + DEAF.augment_flow(Wn, We, DEAF.residual_capacity(j, s)) # Do nothing more if the entering edge is the same as the leaving edge. if i != j: - if parent[t] != s: + if DEAF.parent[t] != s: # Ensure that s is the parent of t. s, t = t, s if We.index(i) > We.index(j): # Ensure that q is in the subtree rooted at t. p, q = q, p - remove_edge(s, t) - make_root(q) - add_edge(i, p, q) - update_potentials(i, p, q) + DEAF.remove_edge(s, t) + DEAF.make_root(q) + DEAF.add_edge(i, p, q) + DEAF.update_potentials(i, p, q) ########################################################################### # Infeasibility and unboundedness detection ########################################################################### - if any(x[i] != 0 for i in range(-n, 0)): + if any(DEAF.edge_flow[i] != 0 for i in range(-n, 0)): raise nx.NetworkXUnfeasible("no flow satisfies all node demands") - if any(x[i] * 2 >= faux_inf for i in range(e)) or any( + if any(DEAF.edge_flow[i] * 2 >= faux_inf for i in range(DEAF.edge_count)) or any( e[-1].get(capacity, inf) == inf and e[-1].get(weight, 0) < 0 for e in nx.selfloop_edges(G, data=True) ): @@ -548,9 +615,9 @@ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): # Flow cost calculation and flow dict construction ########################################################################### - del x[e:] - flow_cost = sum(c * x for c, x in zip(C, x)) - flow_dict = {n: {} for n in N} + del DEAF.edge_flow[DEAF.edge_count :] + flow_cost = sum(w * x for w, x in zip(DEAF.edge_weights, DEAF.edge_flow)) + flow_dict = {n: {} for n in DEAF.node_list} def add_entry(e): """Add a flow dict entry.""" @@ -564,14 +631,27 @@ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): d = t d[e[-2]] = e[-1] - S = (N[s] for s in S) # Use original nodes. - T = (N[t] for t in T) # Use original nodes. + DEAF.edge_sources = ( + DEAF.node_list[s] for s in DEAF.edge_sources + ) # Use original nodes. + DEAF.edge_targets = ( + DEAF.node_list[t] for t in DEAF.edge_targets + ) # Use original nodes. if not multigraph: - for e in zip(S, T, x): + for e in zip( + DEAF.edge_sources, + DEAF.edge_targets, + DEAF.edge_flow, + ): add_entry(e) edges = G.edges(data=True) else: - for e in zip(S, T, K, x): + for e in zip( + DEAF.edge_sources, + DEAF.edge_targets, + DEAF.edge_keys, + DEAF.edge_flow, + ): add_entry(e) edges = G.edges(data=True, keys=True) for e in edges: @@ -579,12 +659,12 @@ def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): if e[-1].get(capacity, inf) == 0: add_entry(e[:-1] + (0,)) else: - c = e[-1].get(weight, 0) - if c >= 0: + w = e[-1].get(weight, 0) + if w >= 0: add_entry(e[:-1] + (0,)) else: - u = e[-1][capacity] - flow_cost += c * u - add_entry(e[:-1] + (u,)) + c = e[-1][capacity] + flow_cost += w * c + add_entry(e[:-1] + (c,)) return flow_cost, flow_dict diff --git a/networkx/algorithms/flow/tests/test_networksimplex.py b/networkx/algorithms/flow/tests/test_networksimplex.py new file mode 100644 index 00000000..9a9287bb --- /dev/null +++ b/networkx/algorithms/flow/tests/test_networksimplex.py @@ -0,0 +1,383 @@ +import pytest
+import networkx as nx
+import os
+
+
+@pytest.fixture
+def simple_flow_graph():
+ G = nx.DiGraph()
+ G.add_node("a", demand=0)
+ G.add_node("b", demand=-5)
+ G.add_node("c", demand=50000000)
+ G.add_node("d", demand=-49999995)
+ G.add_edge("a", "b", weight=3, capacity=4)
+ G.add_edge("a", "c", weight=6, capacity=10)
+ G.add_edge("b", "d", weight=1, capacity=9)
+ G.add_edge("c", "d", weight=2, capacity=5)
+ return G
+
+
+@pytest.fixture
+def simple_no_flow_graph():
+ G = nx.DiGraph()
+ G.add_node("s", demand=-5)
+ G.add_node("t", demand=5)
+ G.add_edge("s", "a", weight=1, capacity=3)
+ G.add_edge("a", "b", weight=3)
+ G.add_edge("a", "c", weight=-6)
+ G.add_edge("b", "d", weight=1)
+ G.add_edge("c", "d", weight=-2)
+ G.add_edge("d", "t", weight=1, capacity=3)
+ return G
+
+
+def get_flowcost_from_flowdict(G, flowDict):
+ """Returns flow cost calculated from flow dictionary"""
+ flowCost = 0
+ for u in flowDict.keys():
+ for v in flowDict[u].keys():
+ flowCost += flowDict[u][v] * G[u][v]["weight"]
+ return flowCost
+
+
+def test_infinite_demand_raise(simple_flow_graph):
+ G = simple_flow_graph
+ inf = float("inf")
+ nx.set_node_attributes(G, {"a": {"demand": inf}})
+ pytest.raises(nx.NetworkXError, nx.network_simplex, G)
+
+
+def test_neg_infinite_demand_raise(simple_flow_graph):
+ G = simple_flow_graph
+ inf = float("inf")
+ nx.set_node_attributes(G, {"a": {"demand": -inf}})
+ pytest.raises(nx.NetworkXError, nx.network_simplex, G)
+
+
+def test_infinite_weight_raise(simple_flow_graph):
+ G = simple_flow_graph
+ inf = float("inf")
+ nx.set_edge_attributes(
+ G, {("a", "b"): {"weight": inf}, ("b", "d"): {"weight": inf}}
+ )
+ pytest.raises(nx.NetworkXError, nx.network_simplex, G)
+
+
+def test_nonzero_net_demand_raise(simple_flow_graph):
+ G = simple_flow_graph
+ nx.set_node_attributes(G, {"b": {"demand": -4}})
+ pytest.raises(nx.NetworkXUnfeasible, nx.network_simplex, G)
+
+
+def test_negative_capacity_raise(simple_flow_graph):
+ G = simple_flow_graph
+ nx.set_edge_attributes(G, {("a", "b"): {"weight": 1}, ("b", "d"): {"capacity": -9}})
+ pytest.raises(nx.NetworkXUnfeasible, nx.network_simplex, G)
+
+
+def test_no_flow_satisfying_demands(simple_no_flow_graph):
+ G = simple_no_flow_graph
+ pytest.raises(nx.NetworkXUnfeasible, nx.network_simplex, G)
+
+
+def test_sum_demands_not_zero(simple_no_flow_graph):
+ G = simple_no_flow_graph
+ nx.set_node_attributes(G, {"t": {"demand": 4}})
+ pytest.raises(nx.NetworkXUnfeasible, nx.network_simplex, G)
+
+
+def test_google_or_tools_example():
+ """
+ https://developers.google.com/optimization/flow/mincostflow
+ """
+ G = nx.DiGraph()
+ start_nodes = [0, 0, 1, 1, 1, 2, 2, 3, 4]
+ end_nodes = [1, 2, 2, 3, 4, 3, 4, 4, 2]
+ capacities = [15, 8, 20, 4, 10, 15, 4, 20, 5]
+ unit_costs = [4, 4, 2, 2, 6, 1, 3, 2, 3]
+ supplies = [20, 0, 0, -5, -15]
+ answer = 150
+
+ for i in range(len(supplies)):
+ G.add_node(i, demand=(-1) * supplies[i]) # supplies are negative of demand
+
+ for i in range(len(start_nodes)):
+ G.add_edge(
+ start_nodes[i],
+ end_nodes[i],
+ weight=unit_costs[i],
+ capacity=capacities[i],
+ )
+
+ flowCost, flowDict = nx.network_simplex(G)
+ assert flowCost == answer
+ assert flowCost == get_flowcost_from_flowdict(G, flowDict)
+
+
+def test_google_or_tools_example2():
+ """
+ https://developers.google.com/optimization/flow/mincostflow
+ """
+ G = nx.DiGraph()
+ start_nodes = [0, 0, 1, 1, 1, 2, 2, 3, 4, 3]
+ end_nodes = [1, 2, 2, 3, 4, 3, 4, 4, 2, 5]
+ capacities = [15, 8, 20, 4, 10, 15, 4, 20, 5, 10]
+ unit_costs = [4, 4, 2, 2, 6, 1, 3, 2, 3, 4]
+ supplies = [23, 0, 0, -5, -15, -3]
+ answer = 183
+
+ for i in range(len(supplies)):
+ G.add_node(i, demand=(-1) * supplies[i]) # supplies are negative of demand
+
+ for i in range(len(start_nodes)):
+ G.add_edge(
+ start_nodes[i],
+ end_nodes[i],
+ weight=unit_costs[i],
+ capacity=capacities[i],
+ )
+
+ flowCost, flowDict = nx.network_simplex(G)
+ assert flowCost == answer
+ assert flowCost == get_flowcost_from_flowdict(G, flowDict)
+
+
+def test_large():
+ fname = os.path.join(os.path.dirname(__file__), "netgen-2.gpickle.bz2")
+ G = nx.read_gpickle(fname)
+ flowCost, flowDict = nx.network_simplex(G)
+ assert 6749969302 == flowCost
+ assert 6749969302 == nx.cost_of_flow(G, flowDict)
+
+
+def test_simple_digraph():
+ G = nx.DiGraph()
+ G.add_node("a", demand=-5)
+ G.add_node("d", demand=5)
+ G.add_edge("a", "b", weight=3, capacity=4)
+ G.add_edge("a", "c", weight=6, capacity=10)
+ G.add_edge("b", "d", weight=1, capacity=9)
+ G.add_edge("c", "d", weight=2, capacity=5)
+ flowCost, H = nx.network_simplex(G)
+ soln = {"a": {"b": 4, "c": 1}, "b": {"d": 4}, "c": {"d": 1}, "d": {}}
+ assert flowCost == 24
+ assert nx.min_cost_flow_cost(G) == 24
+ assert H == soln
+
+
+def test_negcycle_infcap():
+ G = nx.DiGraph()
+ G.add_node("s", demand=-5)
+ G.add_node("t", demand=5)
+ G.add_edge("s", "a", weight=1, capacity=3)
+ G.add_edge("a", "b", weight=3)
+ G.add_edge("c", "a", weight=-6)
+ G.add_edge("b", "d", weight=1)
+ G.add_edge("d", "c", weight=-2)
+ G.add_edge("d", "t", weight=1, capacity=3)
+ pytest.raises(nx.NetworkXUnfeasible, nx.network_simplex, G)
+
+
+def test_transshipment():
+ G = nx.DiGraph()
+ G.add_node("a", demand=1)
+ G.add_node("b", demand=-2)
+ G.add_node("c", demand=-2)
+ G.add_node("d", demand=3)
+ G.add_node("e", demand=-4)
+ G.add_node("f", demand=-4)
+ G.add_node("g", demand=3)
+ G.add_node("h", demand=2)
+ G.add_node("r", demand=3)
+ G.add_edge("a", "c", weight=3)
+ G.add_edge("r", "a", weight=2)
+ G.add_edge("b", "a", weight=9)
+ G.add_edge("r", "c", weight=0)
+ G.add_edge("b", "r", weight=-6)
+ G.add_edge("c", "d", weight=5)
+ G.add_edge("e", "r", weight=4)
+ G.add_edge("e", "f", weight=3)
+ G.add_edge("h", "b", weight=4)
+ G.add_edge("f", "d", weight=7)
+ G.add_edge("f", "h", weight=12)
+ G.add_edge("g", "d", weight=12)
+ G.add_edge("f", "g", weight=-1)
+ G.add_edge("h", "g", weight=-10)
+ flowCost, H = nx.network_simplex(G)
+ soln = {
+ "a": {"c": 0},
+ "b": {"a": 0, "r": 2},
+ "c": {"d": 3},
+ "d": {},
+ "e": {"r": 3, "f": 1},
+ "f": {"d": 0, "g": 3, "h": 2},
+ "g": {"d": 0},
+ "h": {"b": 0, "g": 0},
+ "r": {"a": 1, "c": 1},
+ }
+ assert flowCost == 41
+ assert H == soln
+
+
+def test_digraph1():
+ # From Bradley, S. P., Hax, A. C. and Magnanti, T. L. Applied
+ # Mathematical Programming. Addison-Wesley, 1977.
+ G = nx.DiGraph()
+ G.add_node(1, demand=-20)
+ G.add_node(4, demand=5)
+ G.add_node(5, demand=15)
+ G.add_edges_from(
+ [
+ (1, 2, {"capacity": 15, "weight": 4}),
+ (1, 3, {"capacity": 8, "weight": 4}),
+ (2, 3, {"weight": 2}),
+ (2, 4, {"capacity": 4, "weight": 2}),
+ (2, 5, {"capacity": 10, "weight": 6}),
+ (3, 4, {"capacity": 15, "weight": 1}),
+ (3, 5, {"capacity": 5, "weight": 3}),
+ (4, 5, {"weight": 2}),
+ (5, 3, {"capacity": 4, "weight": 1}),
+ ]
+ )
+ flowCost, H = nx.network_simplex(G)
+ soln = {
+ 1: {2: 12, 3: 8},
+ 2: {3: 8, 4: 4, 5: 0},
+ 3: {4: 11, 5: 5},
+ 4: {5: 10},
+ 5: {3: 0},
+ }
+ assert flowCost == 150
+ assert nx.min_cost_flow_cost(G) == 150
+ assert H == soln
+
+
+def test_zero_capacity_edges():
+ """Address issue raised in ticket #617 by arv."""
+ G = nx.DiGraph()
+ G.add_edges_from(
+ [
+ (1, 2, {"capacity": 1, "weight": 1}),
+ (1, 5, {"capacity": 1, "weight": 1}),
+ (2, 3, {"capacity": 0, "weight": 1}),
+ (2, 5, {"capacity": 1, "weight": 1}),
+ (5, 3, {"capacity": 2, "weight": 1}),
+ (5, 4, {"capacity": 0, "weight": 1}),
+ (3, 4, {"capacity": 2, "weight": 1}),
+ ]
+ )
+ G.nodes[1]["demand"] = -1
+ G.nodes[2]["demand"] = -1
+ G.nodes[4]["demand"] = 2
+
+ flowCost, H = nx.network_simplex(G)
+ soln = {1: {2: 0, 5: 1}, 2: {3: 0, 5: 1}, 3: {4: 2}, 4: {}, 5: {3: 2, 4: 0}}
+ assert flowCost == 6
+ assert nx.min_cost_flow_cost(G) == 6
+ assert H == soln
+
+
+def test_digon():
+ """Check if digons are handled properly. Taken from ticket
+ #618 by arv."""
+ nodes = [(1, {}), (2, {"demand": -4}), (3, {"demand": 4})]
+ edges = [
+ (1, 2, {"capacity": 3, "weight": 600000}),
+ (2, 1, {"capacity": 2, "weight": 0}),
+ (2, 3, {"capacity": 5, "weight": 714285}),
+ (3, 2, {"capacity": 2, "weight": 0}),
+ ]
+ G = nx.DiGraph(edges)
+ G.add_nodes_from(nodes)
+ flowCost, H = nx.network_simplex(G)
+ soln = {1: {2: 0}, 2: {1: 0, 3: 4}, 3: {2: 0}}
+ assert flowCost == 2857140
+
+
+def test_deadend():
+ """Check if one-node cycles are handled properly. Taken from ticket
+ #2906 from @sshraven."""
+ G = nx.DiGraph()
+
+ G.add_nodes_from(range(5), demand=0)
+ G.nodes[4]["demand"] = -13
+ G.nodes[3]["demand"] = 13
+
+ G.add_edges_from([(0, 2), (0, 3), (2, 1)], capacity=20, weight=0.1)
+ pytest.raises(nx.NetworkXUnfeasible, nx.network_simplex, G)
+
+
+def test_infinite_capacity_neg_digon():
+ """An infinite capacity negative cost digon results in an unbounded
+ instance."""
+ nodes = [(1, {}), (2, {"demand": -4}), (3, {"demand": 4})]
+ edges = [
+ (1, 2, {"weight": -600}),
+ (2, 1, {"weight": 0}),
+ (2, 3, {"capacity": 5, "weight": 714285}),
+ (3, 2, {"capacity": 2, "weight": 0}),
+ ]
+ G = nx.DiGraph(edges)
+ G.add_nodes_from(nodes)
+ pytest.raises(nx.NetworkXUnbounded, nx.network_simplex, G)
+
+
+def test_multidigraph():
+ """Multidigraphs are acceptable."""
+ G = nx.MultiDiGraph()
+ G.add_weighted_edges_from([(1, 2, 1), (2, 3, 2)], weight="capacity")
+ flowCost, H = nx.network_simplex(G)
+ assert flowCost == 0
+ assert H == {1: {2: {0: 0}}, 2: {3: {0: 0}}, 3: {}}
+
+
+def test_negative_selfloops():
+ """Negative selfloops should cause an exception if uncapacitated and
+ always be saturated otherwise.
+ """
+ G = nx.DiGraph()
+ G.add_edge(1, 1, weight=-1)
+ pytest.raises(nx.NetworkXUnbounded, nx.network_simplex, G)
+
+ G[1][1]["capacity"] = 2
+ flowCost, H = nx.network_simplex(G)
+ assert flowCost == -2
+ assert H == {1: {1: 2}}
+
+ G = nx.MultiDiGraph()
+ G.add_edge(1, 1, "x", weight=-1)
+ G.add_edge(1, 1, "y", weight=1)
+ pytest.raises(nx.NetworkXUnbounded, nx.network_simplex, G)
+
+ G[1][1]["x"]["capacity"] = 2
+ flowCost, H = nx.network_simplex(G)
+ assert flowCost == -2
+ assert H == {1: {1: {"x": 2, "y": 0}}}
+
+
+def test_bone_shaped():
+ # From #1283
+ G = nx.DiGraph()
+ G.add_node(0, demand=-4)
+ G.add_node(1, demand=2)
+ G.add_node(2, demand=2)
+ G.add_node(3, demand=4)
+ G.add_node(4, demand=-2)
+ G.add_node(5, demand=-2)
+ G.add_edge(0, 1, capacity=4)
+ G.add_edge(0, 2, capacity=4)
+ G.add_edge(4, 3, capacity=4)
+ G.add_edge(5, 3, capacity=4)
+ G.add_edge(0, 3, capacity=0)
+ flowCost, H = nx.network_simplex(G)
+ assert flowCost == 0
+ assert H == {0: {1: 2, 2: 2, 3: 0}, 1: {}, 2: {}, 3: {}, 4: {3: 2}, 5: {3: 2}}
+
+
+def test_graphs_type_exceptions():
+ G = nx.Graph()
+ pytest.raises(nx.NetworkXNotImplemented, nx.network_simplex, G)
+ G = nx.MultiGraph()
+ pytest.raises(nx.NetworkXNotImplemented, nx.network_simplex, G)
+ G = nx.DiGraph()
+ pytest.raises(nx.NetworkXError, nx.network_simplex, G)
|