Spaces:
Running
Running
""" | |
Capacity scaling minimum cost flow algorithm. | |
""" | |
__all__ = ["capacity_scaling"] | |
from itertools import chain | |
from math import log | |
import networkx as nx | |
from ...utils import BinaryHeap, arbitrary_element, not_implemented_for | |
def _detect_unboundedness(R): | |
"""Detect infinite-capacity negative cycles.""" | |
G = nx.DiGraph() | |
G.add_nodes_from(R) | |
# Value simulating infinity. | |
inf = R.graph["inf"] | |
# True infinity. | |
f_inf = float("inf") | |
for u in R: | |
for v, e in R[u].items(): | |
# Compute the minimum weight of infinite-capacity (u, v) edges. | |
w = f_inf | |
for k, e in e.items(): | |
if e["capacity"] == inf: | |
w = min(w, e["weight"]) | |
if w != f_inf: | |
G.add_edge(u, v, weight=w) | |
if nx.negative_edge_cycle(G): | |
raise nx.NetworkXUnbounded( | |
"Negative cost cycle of infinite capacity found. " | |
"Min cost flow may be unbounded below." | |
) | |
def _build_residual_network(G, demand, capacity, weight): | |
"""Build a residual network and initialize a zero flow.""" | |
if sum(G.nodes[u].get(demand, 0) for u in G) != 0: | |
raise nx.NetworkXUnfeasible("Sum of the demands should be 0.") | |
R = nx.MultiDiGraph() | |
R.add_nodes_from( | |
(u, {"excess": -G.nodes[u].get(demand, 0), "potential": 0}) for u in G | |
) | |
inf = float("inf") | |
# Detect selfloops with infinite capacities and negative weights. | |
for u, v, e in nx.selfloop_edges(G, data=True): | |
if e.get(weight, 0) < 0 and e.get(capacity, inf) == inf: | |
raise nx.NetworkXUnbounded( | |
"Negative cost cycle of infinite capacity found. " | |
"Min cost flow may be unbounded below." | |
) | |
# Extract edges with positive capacities. Self loops excluded. | |
if G.is_multigraph(): | |
edge_list = [ | |
(u, v, k, e) | |
for u, v, k, e in G.edges(data=True, keys=True) | |
if u != v and e.get(capacity, inf) > 0 | |
] | |
else: | |
edge_list = [ | |
(u, v, 0, e) | |
for u, v, e in G.edges(data=True) | |
if u != v and e.get(capacity, inf) > 0 | |
] | |
# Simulate infinity with the larger of the sum of absolute node imbalances | |
# the sum of finite edge capacities or any positive value if both sums are | |
# zero. This allows the infinite-capacity edges to be distinguished for | |
# unboundedness detection and directly participate in residual capacity | |
# calculation. | |
inf = ( | |
max( | |
sum(abs(R.nodes[u]["excess"]) for u in R), | |
2 | |
* sum( | |
e[capacity] | |
for u, v, k, e in edge_list | |
if capacity in e and e[capacity] != inf | |
), | |
) | |
or 1 | |
) | |
for u, v, k, e in edge_list: | |
r = min(e.get(capacity, inf), inf) | |
w = e.get(weight, 0) | |
# Add both (u, v) and (v, u) into the residual network marked with the | |
# original key. (key[1] == True) indicates the (u, v) is in the | |
# original network. | |
R.add_edge(u, v, key=(k, True), capacity=r, weight=w, flow=0) | |
R.add_edge(v, u, key=(k, False), capacity=0, weight=-w, flow=0) | |
# Record the value simulating infinity. | |
R.graph["inf"] = inf | |
_detect_unboundedness(R) | |
return R | |
def _build_flow_dict(G, R, capacity, weight): | |
"""Build a flow dictionary from a residual network.""" | |
inf = float("inf") | |
flow_dict = {} | |
if G.is_multigraph(): | |
for u in G: | |
flow_dict[u] = {} | |
for v, es in G[u].items(): | |
flow_dict[u][v] = { | |
# Always saturate negative selfloops. | |
k: ( | |
0 | |
if ( | |
u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0 | |
) | |
else e[capacity] | |
) | |
for k, e in es.items() | |
} | |
for v, es in R[u].items(): | |
if v in flow_dict[u]: | |
flow_dict[u][v].update( | |
(k[0], e["flow"]) for k, e in es.items() if e["flow"] > 0 | |
) | |
else: | |
for u in G: | |
flow_dict[u] = { | |
# Always saturate negative selfloops. | |
v: ( | |
0 | |
if (u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0) | |
else e[capacity] | |
) | |
for v, e in G[u].items() | |
} | |
flow_dict[u].update( | |
(v, e["flow"]) | |
for v, es in R[u].items() | |
for e in es.values() | |
if e["flow"] > 0 | |
) | |
return flow_dict | |
def capacity_scaling( | |
G, demand="demand", capacity="capacity", weight="weight", heap=BinaryHeap | |
): | |
r"""Find a minimum cost flow satisfying all demands in digraph G. | |
This is a capacity scaling successive shortest augmenting path algorithm. | |
G is a digraph with edge costs and capacities and in which nodes | |
have demand, i.e., they want to send or receive some amount of | |
flow. A negative demand means that the node wants to send flow, a | |
positive demand means that the node want to receive flow. A flow on | |
the digraph G satisfies all demand if the net flow into each node | |
is equal to the demand of that node. | |
Parameters | |
---------- | |
G : NetworkX graph | |
DiGraph or MultiDiGraph on which a minimum cost flow satisfying all | |
demands is to be found. | |
demand : string | |
Nodes of the graph G are expected to have an attribute demand | |
that indicates how much flow a node wants to send (negative | |
demand) or receive (positive demand). Note that the sum of the | |
demands should be 0 otherwise the problem in not feasible. If | |
this attribute is not present, a node is considered to have 0 | |
demand. Default value: 'demand'. | |
capacity : string | |
Edges of the graph G are expected to have an attribute capacity | |
that indicates how much flow the edge can support. If this | |
attribute is not present, the edge is considered to have | |
infinite capacity. Default value: 'capacity'. | |
weight : string | |
Edges of the graph G are expected to have an attribute weight | |
that indicates the cost incurred by sending one unit of flow on | |
that edge. If not present, the weight is considered to be 0. | |
Default value: 'weight'. | |
heap : class | |
Type of heap to be used in the algorithm. It should be a subclass of | |
:class:`MinHeap` or implement a compatible interface. | |
If a stock heap implementation is to be used, :class:`BinaryHeap` is | |
recommended over :class:`PairingHeap` for Python implementations without | |
optimized attribute accesses (e.g., CPython) despite a slower | |
asymptotic running time. For Python implementations with optimized | |
attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better | |
performance. Default value: :class:`BinaryHeap`. | |
Returns | |
------- | |
flowCost : integer | |
Cost of a minimum cost flow satisfying all demands. | |
flowDict : dictionary | |
If G is a digraph, a dict-of-dicts keyed by nodes such that | |
flowDict[u][v] is the flow on edge (u, v). | |
If G is a MultiDiGraph, a dict-of-dicts-of-dicts keyed by nodes | |
so that flowDict[u][v][key] is the flow on edge (u, v, key). | |
Raises | |
------ | |
NetworkXError | |
This exception is raised if the input graph is not directed, | |
not connected. | |
NetworkXUnfeasible | |
This exception is raised in the following situations: | |
* The sum of the demands is not zero. Then, there is no | |
flow satisfying all demands. | |
* There is no flow satisfying all demand. | |
NetworkXUnbounded | |
This exception is raised if the digraph G has a cycle of | |
negative cost and infinite capacity. Then, the cost of a flow | |
satisfying all demands is unbounded below. | |
Notes | |
----- | |
This algorithm does not work if edge weights are floating-point numbers. | |
See also | |
-------- | |
:meth:`network_simplex` | |
Examples | |
-------- | |
A simple example of a min cost flow problem. | |
>>> 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, flowDict = nx.capacity_scaling(G) | |
>>> flowCost | |
24 | |
>>> flowDict | |
{'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}} | |
It is possible to change the name of the attributes used for the | |
algorithm. | |
>>> G = nx.DiGraph() | |
>>> G.add_node("p", spam=-4) | |
>>> G.add_node("q", spam=2) | |
>>> G.add_node("a", spam=-2) | |
>>> G.add_node("d", spam=-1) | |
>>> G.add_node("t", spam=2) | |
>>> G.add_node("w", spam=3) | |
>>> G.add_edge("p", "q", cost=7, vacancies=5) | |
>>> G.add_edge("p", "a", cost=1, vacancies=4) | |
>>> G.add_edge("q", "d", cost=2, vacancies=3) | |
>>> G.add_edge("t", "q", cost=1, vacancies=2) | |
>>> G.add_edge("a", "t", cost=2, vacancies=4) | |
>>> G.add_edge("d", "w", cost=3, vacancies=4) | |
>>> G.add_edge("t", "w", cost=4, vacancies=1) | |
>>> flowCost, flowDict = nx.capacity_scaling( | |
... G, demand="spam", capacity="vacancies", weight="cost" | |
... ) | |
>>> flowCost | |
37 | |
>>> flowDict | |
{'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}} | |
""" | |
R = _build_residual_network(G, demand, capacity, weight) | |
inf = float("inf") | |
# Account cost of negative selfloops. | |
flow_cost = sum( | |
0 | |
if e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0 | |
else e[capacity] * e[weight] | |
for u, v, e in nx.selfloop_edges(G, data=True) | |
) | |
# Determine the maximum edge capacity. | |
wmax = max(chain([-inf], (e["capacity"] for u, v, e in R.edges(data=True)))) | |
if wmax == -inf: | |
# Residual network has no edges. | |
return flow_cost, _build_flow_dict(G, R, capacity, weight) | |
R_nodes = R.nodes | |
R_succ = R.succ | |
delta = 2 ** int(log(wmax, 2)) | |
while delta >= 1: | |
# Saturate Δ-residual edges with negative reduced costs to achieve | |
# Δ-optimality. | |
for u in R: | |
p_u = R_nodes[u]["potential"] | |
for v, es in R_succ[u].items(): | |
for k, e in es.items(): | |
flow = e["capacity"] - e["flow"] | |
if e["weight"] - p_u + R_nodes[v]["potential"] < 0: | |
flow = e["capacity"] - e["flow"] | |
if flow >= delta: | |
e["flow"] += flow | |
R_succ[v][u][(k[0], not k[1])]["flow"] -= flow | |
R_nodes[u]["excess"] -= flow | |
R_nodes[v]["excess"] += flow | |
# Determine the Δ-active nodes. | |
S = set() | |
T = set() | |
S_add = S.add | |
S_remove = S.remove | |
T_add = T.add | |
T_remove = T.remove | |
for u in R: | |
excess = R_nodes[u]["excess"] | |
if excess >= delta: | |
S_add(u) | |
elif excess <= -delta: | |
T_add(u) | |
# Repeatedly augment flow from S to T along shortest paths until | |
# Δ-feasibility is achieved. | |
while S and T: | |
s = arbitrary_element(S) | |
t = None | |
# Search for a shortest path in terms of reduce costs from s to | |
# any t in T in the Δ-residual network. | |
d = {} | |
pred = {s: None} | |
h = heap() | |
h_insert = h.insert | |
h_get = h.get | |
h_insert(s, 0) | |
while h: | |
u, d_u = h.pop() | |
d[u] = d_u | |
if u in T: | |
# Path found. | |
t = u | |
break | |
p_u = R_nodes[u]["potential"] | |
for v, es in R_succ[u].items(): | |
if v in d: | |
continue | |
wmin = inf | |
# Find the minimum-weighted (u, v) Δ-residual edge. | |
for k, e in es.items(): | |
if e["capacity"] - e["flow"] >= delta: | |
w = e["weight"] | |
if w < wmin: | |
wmin = w | |
kmin = k | |
emin = e | |
if wmin == inf: | |
continue | |
# Update the distance label of v. | |
d_v = d_u + wmin - p_u + R_nodes[v]["potential"] | |
if h_insert(v, d_v): | |
pred[v] = (u, kmin, emin) | |
if t is not None: | |
# Augment Δ units of flow from s to t. | |
while u != s: | |
v = u | |
u, k, e = pred[v] | |
e["flow"] += delta | |
R_succ[v][u][(k[0], not k[1])]["flow"] -= delta | |
# Account node excess and deficit. | |
R_nodes[s]["excess"] -= delta | |
R_nodes[t]["excess"] += delta | |
if R_nodes[s]["excess"] < delta: | |
S_remove(s) | |
if R_nodes[t]["excess"] > -delta: | |
T_remove(t) | |
# Update node potentials. | |
d_t = d[t] | |
for u, d_u in d.items(): | |
R_nodes[u]["potential"] -= d_u - d_t | |
else: | |
# Path not found. | |
S_remove(s) | |
delta //= 2 | |
if any(R.nodes[u]["excess"] != 0 for u in R): | |
raise nx.NetworkXUnfeasible("No flow satisfying all demands.") | |
# Calculate the flow cost. | |
for u in R: | |
for v, es in R_succ[u].items(): | |
for e in es.values(): | |
flow = e["flow"] | |
if flow > 0: | |
flow_cost += flow * e["weight"] | |
return flow_cost, _build_flow_dict(G, R, capacity, weight) | |