Network simplex method in Python
Simplex method specialized to min-cost flow, pivoting over spanning tree bases.
- File
network_simplex.py, 139 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/network-simplex/. The authors are listed in prov/SJBDWT.jsonl.
from collections import deque
def network_simplex(
n: int,
edges: list[tuple[int, int, float, float]],
supply: list[float],
) -> tuple[float, list[float]] | None:
"""Minimum-cost flow by the primal network simplex method.
Vertices are 0 .. n-1. Edges (u, v, cap, cost) are directed, with a
capacity cap >= 0 and a cost of any sign, integers or real numbers.
supply[v] is the net amount that must leave v (negative for a demand).
Parallel and antiparallel edges are separate variables. Returns
(cost, flow) with flow[i] on edges[i], or None if no feasible flow exists.
The method keeps a strongly feasible spanning tree rooted at an artificial
vertex, uses the most violating edge to enter (Dantzig's rule), and the
last blocking edge of the cycle to leave (Cunningham's rule).
"""
if len(supply) != n:
raise ValueError("one supply per vertex")
if sum(supply) != 0:
return None
m = len(edges)
tail = [u for u, _, _, _ in edges]
head = [v for _, v, _, _ in edges]
cap = [c for _, _, c, _ in edges]
cost = [k for _, _, _, k in edges]
if any(c < 0 for c in cap):
raise ValueError("capacities must be nonnegative")
r = n
big = (max([abs(k) for k in cost], default=0) + 1) * (n + 1)
flow = [0] * m
for v in range(n): # @line L1
if supply[v] >= 0:
tail.append(v), head.append(r), flow.append(supply[v])
else:
tail.append(r), head.append(v), flow.append(-supply[v])
cap.append(None)
cost.append(big)
in_tree = [False] * m + [True] * n
while True: # @line L2
parent, pedge, depth, pi = tree_structure(n + 1, r, tail, head, cost, in_tree) # @line L3
enter, best = -1, 0
for e in range(m): # @line L4
if in_tree[e] or cap[e] == 0:
continue
rc = cost[e] + pi[tail[e]] - pi[head[e]]
violation = -rc if flow[e] == 0 else rc
if violation > best:
enter, best = e, violation
if enter < 0:
break
if flow[enter] == 0: # @line L5
a, b = tail[enter], head[enter]
else:
a, b = head[enter], tail[enter]
cycle = pivot_cycle(a, b, enter, flow[enter] == 0, parent, pedge, depth, tail)
delta, leave = None, None
for e, forward in cycle: # @line L6
res = residual(e, forward, flow, cap)
if res is not None and (delta is None or res <= delta):
delta, leave = res, e
for e, forward in cycle: # @line L7
flow[e] += delta if forward else -delta
if leave != enter: # @line L8
in_tree[leave] = False
in_tree[enter] = True
if any(flow[e] != 0 for e in range(m, m + n)): # @line L9
return None
return sum(cost[e] * flow[e] for e in range(m)), flow[:m] # @line L10
def residual(e, forward, flow, cap):
"""How much more flow edge e can take in the given direction; None for unbounded."""
if forward:
return None if cap[e] is None else cap[e] - flow[e]
return flow[e]
def tree_structure(size, r, tail, head, cost, in_tree):
"""Parent, parent edge, depth and potentials of the spanning tree, rooted at r.
The potentials make every tree edge (u, v) satisfy cost + pi[u] - pi[v] = 0.
"""
adj = [[] for _ in range(size)]
for e, t in enumerate(in_tree):
if t:
adj[tail[e]].append(e)
adj[head[e]].append(e)
parent = [-1] * size
pedge = [-1] * size
depth = [0] * size
pi = [0] * size
seen = [False] * size
seen[r] = True
queue = deque([r])
while queue:
u = queue.popleft()
for e in adj[u]:
w = head[e] if tail[e] == u else tail[e]
if seen[w]:
continue
seen[w] = True
parent[w], pedge[w], depth[w] = u, e, depth[u] + 1
pi[w] = pi[u] + cost[e] if tail[e] == u else pi[u] - cost[e]
queue.append(w)
return parent, pedge, depth, pi
def pivot_cycle(a, b, enter, enter_forward, parent, pedge, depth, tail):
"""The cycle of the tree plus the entering edge, oriented a -> b along the entering edge.
It is listed as (edge, forward) pairs in the order met when walking along
the orientation from the apex: down to a, across to b, and up to the apex.
forward means the walk goes from the tail of the edge to its head.
"""
up_a, up_b = [], []
x, y = a, b
while x != y:
if depth[x] >= depth[y]:
up_a.append(x)
x = parent[x]
else:
up_b.append(y)
y = parent[y]
cycle = []
for v in reversed(up_a): # walk from the apex down to a: parent[v] -> v
e = pedge[v]
cycle.append((e, tail[e] == parent[v]))
cycle.append((enter, enter_forward))
for v in up_b: # walk from b up to the apex: v -> parent[v]
e = pedge[v]
cycle.append((e, tail[e] == v))
return cycle
test_network_simplex.py 92 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/network-simplex/. The authors are listed in prov/SJBDWT.jsonl.
import itertools
import random
from fractions import Fraction
import pytest
from network_simplex import network_simplex
# s, a, b, t = 0 .. 3; edges (u, v, capacity, cost)
EXAMPLE = [(0, 1, 3, 1), (0, 2, 2, 4), (1, 2, 2, 1), (1, 3, 2, 5), (2, 3, 3, 1)]
def check(n, edges, supply, cost, flow):
net = [0] * n
for (u, v, c, k), x in zip(edges, flow):
assert 0 <= x <= c
net[u] += x
net[v] -= x
assert net == list(supply)
assert cost == sum(k * x for (_, _, _, k), x in zip(edges, flow))
def brute(n, edges, supply):
"""Independent oracle: try every integral flow; integer data has an integral optimum."""
best = None
for flow in itertools.product(*(range(c + 1) for _, _, c, _ in edges)):
net = [0] * n
for (u, v, _, _), x in zip(edges, flow):
net[u] += x
net[v] -= x
if net == list(supply):
cost = sum(k * x for (_, _, _, k), x in zip(edges, flow))
best = cost if best is None else min(best, cost)
return best
def test_example_by_value() -> None:
costs = []
for d in range(6):
cost, flow = network_simplex(4, EXAMPLE, [d, 0, 0, -d])
check(4, EXAMPLE, [d, 0, 0, -d], cost, flow)
costs.append(cost)
assert costs == [0, 3, 6, 11, 17, 25]
assert network_simplex(4, EXAMPLE, [6, 0, 0, -6]) is None
def test_zero_capacity_no_path_and_bad_input() -> None:
assert network_simplex(2, [(0, 1, 0, -5)], [0, 0]) == (0, [0])
assert network_simplex(2, [(0, 1, 0, 1)], [1, -1]) is None
assert network_simplex(3, [(0, 1, 4, 1)], [1, 0, -1]) is None
assert network_simplex(2, [(0, 1, 4, 1)], [1, 0]) is None
with pytest.raises(ValueError):
network_simplex(2, [(0, 1, -1, 1)], [0, 0])
def test_negative_cycle_antiparallel_and_loops() -> None:
edges = [(0, 1, 2, -3), (1, 0, 3, 1), (0, 0, 4, -1), (1, 1, 2, 2)]
cost, flow = network_simplex(2, edges, [0, 0])
assert cost == -4 - 4
assert flow == [2, 2, 4, 0]
check(2, edges, [0, 0], cost, flow)
def test_real_data() -> None:
edges = [(0, 1, Fraction(3, 2), Fraction(1, 3)), (0, 2, Fraction(1, 2), Fraction(-1, 7)), (2, 1, 2, Fraction(1, 5)), (1, 3, 3, 1)]
supply = [Fraction(2), 0, 0, Fraction(-2)]
cost, flow = network_simplex(4, edges, supply)
assert flow == [Fraction(3, 2), Fraction(1, 2), Fraction(1, 2), 2]
check(4, edges, supply, cost, flow)
def test_random_against_brute_force() -> None:
rng = random.Random(77)
for _ in range(600):
n = rng.randint(2, 5)
edges = [(rng.randrange(n), rng.randrange(n), rng.randint(0, 3), rng.randint(-4, 6)) for _ in range(rng.randint(0, 6))]
supply = [0] * n
for _ in range(rng.randint(0, 3)):
u, v = rng.randrange(n), rng.randrange(n)
supply[u] += 1
supply[v] -= 1
result = network_simplex(n, edges, supply)
expected = brute(n, edges, supply)
if expected is None:
assert result is None
else:
cost, flow = result
assert cost == expected
check(n, edges, supply, cost, flow)
What the next level needs
- the tests did not run in this build