Current section
Files
Jump to
Current section
Files
lib/yog/flow/network_simplex.ex
defmodule Yog.Flow.NetworkSimplex do
@moduledoc """
Minimum cost flow algorithms for network flow optimization.
This module solves the [minimum cost flow problem](https://en.wikipedia.org/wiki/Minimum-cost_flow_problem):
find the cheapest way to send a given amount of flow through a network where
edges have both capacities and costs per unit of flow.
## Algorithm
| Algorithm | Function | Complexity | Best For |
|-----------|----------|------------|----------|
| [Successive Shortest Path](https://en.wikipedia.org/wiki/Minimum-cost_flow_problem) | `min_cost_flow/4` | O(F · E log V) | Small to medium networks |
The implementation uses the Successive Shortest Path algorithm.
## Key Concepts
- **Minimum Cost Flow**: Route flow to satisfy demands at minimum total cost
- **Node Demands**: Supply nodes (negative demand) and demand nodes (positive demand)
- **Edge Costs**: Price per unit of flow on each edge
## Problem Formulation
Given:
- Graph G = (V, E) with capacities u(e) and costs c(e)
- Node demands b(v) where Σb(v) = 0 (conservation)
Find flow f(e) that:
- Minimizes: Σ c(e) × f(e) (total cost)
- Subject to: 0 ≤ f(e) ≤ u(e) (capacity constraints)
- And: flow conservation at each node
## Use Cases
- **Transportation planning**: Minimize shipping costs with vehicle capacities
- **Supply chain**: Optimize distribution from warehouses to retailers
- **Telecommunications**: Route calls at minimum cost with link capacities
## Example
# Store demand/capacity/cost data in node and edge data tuples
graph =
Yog.directed()
# Node data: {demand, nil} where negative=supply, positive=demand
|> Yog.add_node(1, {-20, nil}) # warehouse: supply 20
|> Yog.add_node(2, {10, nil}) # store_a: demand 10
|> Yog.add_node(3, {10, nil}) # store_b: demand 10
# Edge data: {capacity, cost_per_unit}
|> Yog.add_edges([
{1, 2, {10, 3}}, # capacity 10, cost $3
{1, 3, {15, 2}}, # capacity 15, cost $2
{2, 3, {5, 1}} # capacity 5, cost $1
])
# Extract demand from node data
get_demand = fn {d, _} -> d end
# Extract capacity from edge data
get_capacity = fn {c, _} -> c end
# Extract cost from edge data
get_cost = fn {_, c} -> c end
case Yog.Flow.NetworkSimplex.min_cost_flow(graph, get_demand, get_capacity, get_cost) do
{:ok, result} ->
IO.puts("Min cost: \#{result.cost}")
IO.inspect(result.flow)
{:error, :infeasible} ->
IO.puts("Cannot satisfy demands with given capacities")
{:error, :unbalanced_demands} ->
IO.puts("Total demand does not equal total supply")
end
## References
- [Wikipedia: Minimum-Cost Flow Problem](https://en.wikipedia.org/wiki/Minimum-cost_flow_problem)
- [CP-Algorithms: Min-Cost Flow](https://cp-algorithms.com/graph/min_cost_flow.html)
"""
alias Yog.Model
@typedoc """
A flow vector assigning an amount of flow to each edge.
Each tuple is `{from_node, to_node, flow_amount}`.
"""
@type flow_map :: [{Yog.node_id(), Yog.node_id(), integer()}]
@typedoc """
Result of a successful minimum cost flow computation.
"""
@type min_cost_flow_result :: %{
cost: integer(),
flow: flow_map()
}
@typedoc """
Errors that can occur during minimum cost flow optimization.
- `:infeasible` - The demands cannot be satisfied given the edge capacities
- `:unbalanced_demands` - The sum of all node demands does not equal 0
"""
@type min_cost_flow_error :: :infeasible | :unbalanced_demands
@doc """
Solves the minimum cost flow problem using the Successive Shortest Path algorithm.
Given a network with edge capacities and costs, and node demands/supplies,
finds the flow assignment that satisfies all demands at minimum total cost.
## Parameters
- `graph` - The flow network (directed graph with edge data representing capacities)
- `get_demand` - Function `(node_data) -> demand` where negative = supply, positive = demand
- `get_capacity` - Function `(edge_data) -> capacity`
- `get_cost` - Function `(edge_data) -> cost_per_unit`
Note: These functions take the node/edge **data** stored in the graph, not node IDs.
## Returns
- `{:ok, result}` - Successful computation with `%{cost: integer(), flow: flow_map()}`
- `{:error, :infeasible}` - Demands cannot be satisfied
- `{:error, :unbalanced_demands}` - Total supply ≠total demand
## Examples
iex> graph = Yog.directed()
...> |> Yog.add_node(1, "s")
...> |> Yog.add_node(2, "t")
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 10)
iex> get_demand = fn
...> "s" -> -5 # supply 5
...> "t" -> 5 # demand 5
...> _ -> 0
...> end
iex> get_capacity = fn w -> w end
iex> get_cost = fn _ -> 1 end
iex> result = Yog.Flow.NetworkSimplex.min_cost_flow(
...> graph, get_demand, get_capacity, get_cost
...> )
iex> match?({:ok, _} , result) or match?({:error, _}, result)
true
## Notes
- All demands must sum to 0 (conservation of flow)
- Costs and capacities must be integers
"""
@spec min_cost_flow(
Yog.graph(),
(any() -> integer()),
(any() -> integer()),
(any() -> integer())
) :: {:ok, min_cost_flow_result()} | {:error, min_cost_flow_error()}
def min_cost_flow(graph, get_demand, get_capacity, get_cost) do
nodes = Model.all_nodes(graph)
# Compute demands for all nodes
demands =
Map.new(nodes, fn node ->
data = Model.node(graph, node)
{node, get_demand.(data)}
end)
# Check if demands are balanced
total_demand = Enum.sum(Map.values(demands))
if total_demand != 0 do
{:error, :unbalanced_demands}
else
# Handle trivial case: no flow needed
total_supply = -Enum.sum(Enum.filter(Map.values(demands), &(&1 < 0)))
if total_supply == 0 do
{:ok, %{cost: 0, flow: []}}
else
# Build edge information
edges =
Enum.flat_map(nodes, fn from ->
Model.successors(graph, from)
|> Enum.map(fn {to, data} ->
capacity = get_capacity.(data)
cost = get_cost.(data)
{from, to, capacity, cost}
end)
end)
# Run successive shortest path algorithm
case solve_min_cost_flow(nodes, edges, demands) do
{:ok, flow, total_cost} ->
{:ok, %{cost: total_cost, flow: flow}}
:infeasible ->
{:error, :infeasible}
end
end
end
end
# Successive Shortest Path algorithm
defp solve_min_cost_flow(nodes, edges, demands) do
# Build adjacency with costs
adj = build_adjacency(edges)
# Find supply and demand nodes
supply_nodes = Enum.filter(nodes, fn n -> demands[n] < 0 end)
demand_nodes = Enum.filter(nodes, fn n -> demands[n] > 0 end)
# Iteratively find shortest paths and augment flow
do_ssp(supply_nodes, demand_nodes, edges, demands, adj, %{}, 0)
end
defp build_adjacency(edges) do
Enum.reduce(edges, %{}, fn {u, v, _cap, cost}, acc ->
Map.update(acc, u, [{v, cost}], fn existing -> [{v, cost} | existing] end)
end)
end
defp do_ssp(_supply, [], _edges, _demands, _adj, flow, cost) do
{:ok, flow_to_list(flow), cost}
end
defp do_ssp([], _demand, _edges, _demands, _adj, _flow, _cost) do
:infeasible
end
defp do_ssp(supply_nodes, demand_nodes, edges, demands, adj, flow, cost) do
# Find a supply node with remaining supply
source = Enum.find(supply_nodes, fn n -> demands[n] < 0 end)
# Find a demand node with remaining demand
sink = Enum.find(demand_nodes, fn n -> demands[n] > 0 end)
if source == nil or sink == nil do
{:ok, flow_to_list(flow), cost}
else
# Find shortest path from source to sink
case shortest_path(adj, source, sink) do
nil ->
:infeasible
{path, path_cost} ->
# Compute bottleneck
source_supply = -demands[source]
sink_demand = demands[sink]
bottleneck = min(source_supply, sink_demand)
# Update demands
new_demands =
demands
|> Map.update!(source, &(&1 + bottleneck))
|> Map.update!(sink, &(&1 - bottleneck))
# Update flow
new_flow = update_flow(flow, path, bottleneck)
# Update cost
new_cost = cost + path_cost * bottleneck
do_ssp(supply_nodes, demand_nodes, edges, new_demands, adj, new_flow, new_cost)
end
end
end
# Bellman-Ford for shortest path (handles negative costs)
defp shortest_path(adj, source, sink) do
nodes = Map.keys(adj) |> Enum.uniq()
initial_dist = Map.new(nodes, fn n -> {n, :infinity} end)
dist = Map.put(initial_dist, source, 0)
prev = %{}
# Relax edges
{final_dist, final_prev} =
Enum.reduce(1..(length(nodes) - 1)//1, {dist, prev}, fn _, {d, p} ->
relax_edges(adj, d, p)
end)
if final_dist[sink] == :infinity do
nil
else
case reconstruct_path(final_prev, source, sink) do
nil -> nil
path -> {path, final_dist[sink]}
end
end
end
defp relax_edges(adj, dist, prev) do
Enum.reduce(adj, {dist, prev}, fn {u, neighbors}, {d, _p} = acc ->
if d[u] == :infinity do
acc
else
Enum.reduce(neighbors, acc, fn {v, cost}, {d2, p2} = acc2 ->
new_dist = d2[u] + cost
if d2[v] == :infinity or new_dist < d2[v] do
{Map.put(d2, v, new_dist), Map.put(p2, v, u)}
else
acc2
end
end)
end
end)
end
defp reconstruct_path(prev, source, sink) do
do_reconstruct(prev, sink, [sink], source)
end
defp do_reconstruct(_prev, current, path, target) when current == target do
Enum.reverse(path)
end
defp do_reconstruct(prev, current, path, target) do
case Map.get(prev, current) do
nil -> nil
parent -> do_reconstruct(prev, parent, [parent | path], target)
end
end
defp update_flow(flow, path, amount) do
edges = path_to_edges(path)
Enum.reduce(edges, flow, fn {u, v}, acc ->
key = {u, v}
current = Map.get(acc, key, 0)
Map.put(acc, key, current + amount)
end)
end
defp path_to_edges([_]), do: []
defp path_to_edges([a, b | rest]) do
[{a, b} | path_to_edges([b | rest])]
end
defp flow_to_list(flow) do
flow
|> Enum.filter(fn {_, f} -> f > 0 end)
|> Enum.map(fn {{u, v}, f} -> {u, v, f} end)
end
end