defmodule Yog.Flow.NetworkSimplex do @moduledoc """ Minimum cost flow algorithm using the Network Simplex method. 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 The Network Simplex algorithm is a specialized primal simplex method for network flows. In practice, it runs extremely fast (polynomial-like time) and handles arbitrary flows/demands. Unlike pseudo-polynomial algorithms (like Successive Shortest Path), its performance is independent of the total flow capacity (F), making it suitable for large networks. ## 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 - **Reduced Costs**: Modified costs ensuring optimality conditions - **Spanning Tree**: Basis for the simplex method on networks ## Implementation Notes - Internal state is stored in maps keyed by integer indices (nodes and edges). For very dense graphs, switching to Erlang `:array` could reduce memory and lookup overhead, but it would be a sizeable refactor. - The pivot step uses `Enum.find_index/2` to locate the entering and leaving edges inside the fundamental cycle. Each call is O(cycle length); building a position map would speed up hot paths on large instances. - The unboundedness test checks whether any original edge flow is at least half of the artificial big-M cost (`faux_inf / 2`). This 2x buffer is a defensive choice; NetworkX uses `flow >= faux_inf` directly. - Zero-capacity edges are filtered out before solving. Self-loops are kept and treated as non-tree edges, so finite negative-cost self-loops are saturated when beneficial. """ alias Yog.Model # Capacity value used internally to represent infinity. @infinity_threshold 100_000_000_000 @typedoc """ A flow vector assigning an amount of flow to each edge. Each tuple is `{from_node, to_node, flow_amount}`. Note: the name `flow_map` is historical; the type is a list of flow tuples, not a map. """ @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 - `:unbounded` - The network contains a negative-cost cycle with infinite capacity - `:timeout` - The algorithm did not converge within the allowed number of pivots """ @type min_cost_flow_error :: :infeasible | :unbalanced_demands | :unbounded | :timeout @doc """ Solves the minimum cost flow problem using the Network Simplex 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 - `{:error, :unbounded}` - Negative cycle with infinite capacity found - `{:error, :timeout}` - Maximum pivot limit exceeded ## Examples ```elixir graph = Yog.directed() |> Yog.add_node(1, {-20, nil}) |> Yog.add_node(2, {10, nil}) |> Yog.add_node(3, {10, nil}) |> Yog.add_edges([ {1, 2, {10, 3}}, {1, 3, {15, 2}}, {2, 3, {5, 1}} ]) get_demand = fn {d, _} -> d end get_capacity = fn {c, _} -> c end get_cost = fn {_, c} -> c end {:ok, result} = Yog.Flow.NetworkSimplex.min_cost_flow(graph, get_demand, get_capacity, get_cost) ``` """ @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 Network Simplex algorithm solve_min_cost_flow(nodes, edges, demands) end end end defp solve_min_cost_flow(nodes, edges, demands) do # Map nodes to integer indices node_to_idx = Map.new(Enum.with_index(nodes)) idx_to_node = Map.new(Enum.map(node_to_idx, fn {k, v} -> {v, k} end)) n = length(nodes) node_demands = Map.new(0..(n - 1), fn idx -> node = Map.fetch!(idx_to_node, idx) {idx, Map.fetch!(demands, node)} end) # Separate zero-capacity edges (they can never carry flow). {valid_edges, _zero_cap_edges} = Enum.split_with(edges, fn {_from, _to, capacity, _cost} -> capacity != 0 end) # Check for unboundedness in self loops. A self loop with negative cost and # effectively infinite capacity can absorb an unbounded amount of flow. is_unbounded_self_loop = Enum.any?(edges, fn {from, to, capacity, cost} -> from == to and capacity >= @infinity_threshold and cost < 0 end) if is_unbounded_self_loop do {:error, :unbounded} else # Build indexing maps for valid edges indexed_edges = Enum.with_index(valid_edges) m = length(valid_edges) edge_sources = Map.new(indexed_edges, fn {{from, _to, _cap, _cost}, idx} -> {idx, Map.fetch!(node_to_idx, from)} end) edge_targets = Map.new(indexed_edges, fn {{_from, to, _cap, _cost}, idx} -> {idx, Map.fetch!(node_to_idx, to)} end) edge_capacities = Map.new(indexed_edges, fn {{_from, _to, cap, _cost}, idx} -> {idx, cap} end) edge_costs = Map.new(indexed_edges, fn {{_from, _to, _cap, cost}, idx} -> {idx, cost} end) # Calculate faux_inf sum_caps = edge_capacities |> Map.values() |> Enum.filter(&(&1 < @infinity_threshold)) |> Enum.sum() sum_costs = edge_costs |> Map.values() |> Enum.map(&abs/1) |> Enum.sum() sum_demands = node_demands |> Map.values() |> Enum.map(&abs/1) |> Enum.sum() faux_inf = 3 * Enum.max([sum_caps, sum_costs, sum_demands]) faux_inf = if faux_inf == 0, do: 1, else: faux_inf # Append artificial edges (indices m to m + n - 1) # Root node is index n {edge_sources, edge_targets, edge_capacities, edge_costs, flows} = Enum.reduce( 0..(n - 1), {edge_sources, edge_targets, edge_capacities, edge_costs, %{}}, fn i, {srcs, tgts, caps, csts, fls} -> d = Map.fetch!(node_demands, i) art_idx = m + i {src, tgt} = if d > 0, do: {n, i}, else: {i, n} srcs = Map.put(srcs, art_idx, src) tgts = Map.put(tgts, art_idx, tgt) caps = Map.put(caps, art_idx, faux_inf) csts = Map.put(csts, art_idx, faux_inf) fls = Map.put(fls, art_idx, abs(d)) {srcs, tgts, caps, csts, fls} end ) # Initialize flow on original edges to 0 flows = if m > 0 do Enum.reduce(0..(m - 1), flows, fn idx, fls -> Map.put(fls, idx, 0) end) else flows end # Initialize spanning tree attributes node_potentials = Enum.reduce(0..(n - 1), %{n => 0}, fn i, pots -> d = Map.fetch!(node_demands, i) val = if d <= 0, do: faux_inf, else: -faux_inf Map.put(pots, i, val) end) parents = Enum.reduce(0..(n - 1), %{n => nil}, fn i, pars -> Map.put(pars, i, n) end) parent_edges = Enum.reduce(0..(n - 1), %{n => nil}, fn i, pe -> Map.put(pe, i, m + i) end) sizes = Enum.reduce(0..(n - 1), %{n => n + 1}, fn i, szs -> Map.put(szs, i, 1) end) nexts = if n == 1 do %{0 => 1, 1 => 0} else nxts = Map.new(0..(n - 2), fn i -> {i, i + 1} end) nxts |> Map.put(n - 1, n) |> Map.put(n, 0) end prevs = if n == 1 do %{0 => 1, 1 => 0} else prvs = Map.new(1..(n - 1), fn i -> {i, i - 1} end) prvs |> Map.put(0, n) |> Map.put(n, n - 1) end lasts = Enum.reduce(0..(n - 1), %{n => n - 1}, fn i, lsts -> Map.put(lsts, i, i) end) # Internal state is map-based. See moduledoc "Implementation Notes" for # discussion of potential `:array` optimizations. state = %{ node_count: n, edge_count: m, node_demands: node_demands, edge_sources: edge_sources, edge_targets: edge_targets, edge_capacities: edge_capacities, edge_costs: edge_costs, flows: flows, node_potentials: node_potentials, parents: parents, parent_edges: parent_edges, sizes: sizes, nexts: nexts, prevs: prevs, lasts: lasts, blockmark: 0 } limit = max(5000, 100 * m) case pivot_loop(state, 0, limit) do {:error, :timeout} -> {:error, :timeout} {:ok, final_state} -> reconstruct_result(final_state, m, n, faux_inf, idx_to_node) end end end defp reconstruct_result(final_state, m, n, faux_inf, idx_to_node) do # Check for infeasibility: non-zero flow on any artificial edge has_artificial_flow = Enum.any?(m..(m + n - 1), fn idx -> Map.fetch!(final_state.flows, idx) != 0 end) # Check for unboundedness: flow on any original edge exceeds half of faux_inf. # See moduledoc "Implementation Notes" for the 2x buffer discussion. has_unbounded_flow = if m > 0 do Enum.any?(0..(m - 1), fn idx -> Map.fetch!(final_state.flows, idx) * 2 >= faux_inf end) else false end cond do has_artificial_flow -> {:error, :infeasible} has_unbounded_flow -> {:error, :unbounded} true -> # Reconstruct final flows and total cost {flow_list, total_cost} = if m > 0 do Enum.reduce(0..(m - 1), {[], 0}, fn idx, {f_acc, cost_acc} -> flow = Map.fetch!(final_state.flows, idx) if flow > 0 do from_idx = Map.fetch!(final_state.edge_sources, idx) to_idx = Map.fetch!(final_state.edge_targets, idx) from_node = Map.fetch!(idx_to_node, from_idx) to_node = Map.fetch!(idx_to_node, to_idx) cost = Map.fetch!(final_state.edge_costs, idx) {[{from_node, to_node, flow} | f_acc], cost_acc + flow * cost} else {f_acc, cost_acc} end end) else {[], 0} end {:ok, %{cost: total_cost, flow: Enum.reverse(flow_list)}} end end defp pivot_loop(state, count, limit) do if count >= limit do {:error, :timeout} else case find_entering_edge(state) do :optimal -> {:ok, state} {:ok, i, p, q, state} -> {wn, we} = find_cycle(state, i, p, q) {j, s, t} = find_leaving_edge(state, wn, we) flow_delta = residual_capacity(state, j, s) state = augment_flow(state, wn, we, flow_delta) state = if i != j do # Ensure s is parent of t {s, t} = if Map.fetch!(state.parents, t) != s do {t, s} else {s, t} end # Ensure q is in the subtree rooted at t. # These scans are O(cycle length); see moduledoc notes. i_idx = Enum.find_index(we, &(&1 == i)) j_idx = Enum.find_index(we, &(&1 == j)) {p, q} = if i_idx > j_idx do {q, p} else {p, q} end state |> remove_edge(s, t) |> make_root(q) |> add_edge(i, p, q) |> update_potentials(i, p, q) else state end pivot_loop(state, count + 1, limit) end end end defp find_entering_edge(state) do edge_count = state.edge_count if edge_count == 0 do :optimal else b = :math.sqrt(edge_count) |> Float.ceil() |> round() m_blocks = div(edge_count + b - 1, b) search_entering_edge(state, 0, m_blocks, b, state.blockmark) end end defp search_entering_edge(_state, m, m_blocks, _b, _f) when m >= m_blocks do :optimal end defp search_entering_edge(state, m, m_blocks, b, f) do edge_count = state.edge_count l = f + b edges = if l <= edge_count do Enum.to_list(f..(l - 1)) else l_wrap = l - edge_count Enum.to_list(f..(edge_count - 1)) ++ Enum.to_list(0..(l_wrap - 1)) end next_f = rem(l, edge_count) best_edge = Enum.min_by(edges, fn i -> reduced_cost(state, i) end) best_cost = reduced_cost(state, best_edge) if best_cost >= 0 do search_entering_edge(state, m + 1, m_blocks, b, next_f) else flow = Map.fetch!(state.flows, best_edge) {p, q} = if flow == 0 do {Map.fetch!(state.edge_sources, best_edge), Map.fetch!(state.edge_targets, best_edge)} else {Map.fetch!(state.edge_targets, best_edge), Map.fetch!(state.edge_sources, best_edge)} end {:ok, best_edge, p, q, %{state | blockmark: next_f}} end end defp reduced_cost(state, i) do w = Map.fetch!(state.edge_costs, i) source = Map.fetch!(state.edge_sources, i) target = Map.fetch!(state.edge_targets, i) s_pot = Map.fetch!(state.node_potentials, source) t_pot = Map.fetch!(state.node_potentials, target) c = w - s_pot + t_pot flow = Map.fetch!(state.flows, i) if flow == 0, do: c, else: -c end defp find_cycle(state, i, p, q) do w = find_apex(state, p, q) {wn, we} = trace_path(state, p, w) wn_rev = Enum.reverse(wn) we_rev = if we != [i] do Enum.reverse([i | we]) else Enum.reverse(we) end {wn_r, we_r} = trace_path(state, q, w) wn_r_len = length(wn_r) wn_r_init = if wn_r_len > 0, do: Enum.take(wn_r, wn_r_len - 1), else: [] {wn_rev ++ wn_r_init, we_rev ++ we_r} end defp find_apex(state, p, q) do size_p = Map.fetch!(state.sizes, p) size_q = Map.fetch!(state.sizes, q) do_find_apex(state, p, size_p, q, size_q) end defp do_find_apex(state, p, size_p, q, size_q) do cond do size_p < size_q -> parent_p = Map.fetch!(state.parents, p) do_find_apex(state, parent_p, Map.fetch!(state.sizes, parent_p), q, size_q) size_p > size_q -> parent_q = Map.fetch!(state.parents, q) do_find_apex(state, p, size_p, parent_q, Map.fetch!(state.sizes, parent_q)) true -> if p != q do parent_p = Map.fetch!(state.parents, p) parent_q = Map.fetch!(state.parents, q) do_find_apex( state, parent_p, Map.fetch!(state.sizes, parent_p), parent_q, Map.fetch!(state.sizes, parent_q) ) else p end end end defp trace_path(state, p, w) do do_trace_path(state, p, w, [], []) end defp do_trace_path(_state, p, w, nodes, edges) when p == w do {Enum.reverse([p | nodes]), Enum.reverse(edges)} end defp do_trace_path(state, p, w, nodes, edges) do edge = Map.fetch!(state.parent_edges, p) parent = Map.fetch!(state.parents, p) do_trace_path(state, parent, w, [p | nodes], [edge | edges]) end defp find_leaving_edge(state, wn, we) do pairs = Enum.zip(Enum.reverse(we), Enum.reverse(wn)) {j, s} = Enum.min_by(pairs, fn {i_idx, p_idx} -> residual_capacity(state, i_idx, p_idx) end) source = Map.fetch!(state.edge_sources, j) t = if source == s, do: Map.fetch!(state.edge_targets, j), else: source {j, s, t} end defp residual_capacity(state, i, p) do cap = Map.fetch!(state.edge_capacities, i) flow = Map.fetch!(state.flows, i) source = Map.fetch!(state.edge_sources, i) if source == p, do: cap - flow, else: flow end defp augment_flow(state, wn, we, f) do new_flows = Enum.zip(we, wn) |> Enum.reduce(state.flows, fn {i, p}, flows_acc -> source = Map.fetch!(state.edge_sources, i) current_flow = Map.fetch!(flows_acc, i) new_flow = if source == p, do: current_flow + f, else: current_flow - f Map.put(flows_acc, i, new_flow) end) %{state | flows: new_flows} end defp remove_edge(state, s, t) do size_t = Map.fetch!(state.sizes, t) prev_t = Map.fetch!(state.prevs, t) last_t = Map.fetch!(state.lasts, t) next_last_t = Map.fetch!(state.nexts, last_t) parents = Map.put(state.parents, t, nil) parent_edges = Map.put(state.parent_edges, t, nil) nexts = state.nexts |> Map.put(prev_t, next_last_t) |> Map.put(last_t, t) prevs = state.prevs |> Map.put(next_last_t, prev_t) |> Map.put(t, last_t) state = %{ state | parents: parents, parent_edges: parent_edges, nexts: nexts, prevs: prevs } update_ancestors_remove(state, s, size_t, last_t, prev_t) end defp update_ancestors_remove(state, nil, _size_t, _last_t, _prev_t) do state end defp update_ancestors_remove(state, s, size_t, last_t, prev_t) do sizes = Map.update!(state.sizes, s, &(&1 - size_t)) lasts = if Map.fetch!(state.lasts, s) == last_t do Map.put(state.lasts, s, prev_t) else state.lasts end parent = Map.fetch!(state.parents, s) update_ancestors_remove( %{state | sizes: sizes, lasts: lasts}, parent, size_t, last_t, prev_t ) end defp make_root(state, q) do ancestors = get_ancestors(state, q, []) do_make_root_loop(state, ancestors) end defp get_ancestors(_state, nil, acc), do: acc defp get_ancestors(state, curr, acc) do parent = Map.fetch!(state.parents, curr) get_ancestors(state, parent, [curr | acc]) end defp do_make_root_loop(state, [_single_node]) do state end defp do_make_root_loop(state, [p, q | rest]) do size_p = Map.fetch!(state.sizes, p) last_p_init = Map.fetch!(state.lasts, p) prev_q = Map.fetch!(state.prevs, q) last_q = Map.fetch!(state.lasts, q) next_last_q = Map.fetch!(state.nexts, last_q) parents = state.parents |> Map.put(p, q) |> Map.put(q, nil) parent_edges = state.parent_edges |> Map.put(p, Map.fetch!(state.parent_edges, q)) |> Map.put(q, nil) size_q_val = Map.fetch!(state.sizes, q) sizes = state.sizes |> Map.put(p, size_p - size_q_val) |> Map.put(q, size_p) nexts_temp = state.nexts |> Map.put(prev_q, next_last_q) |> Map.put(last_q, q) prevs_temp = state.prevs |> Map.put(next_last_q, prev_q) |> Map.put(q, last_q) {lasts_temp, last_p_val} = if last_p_init == last_q do {Map.put(state.lasts, p, prev_q), prev_q} else {state.lasts, last_p_init} end prevs_updated = prevs_temp |> Map.put(p, last_q) |> Map.put(q, last_p_val) nexts_updated = nexts_temp |> Map.put(last_q, p) |> Map.put(last_p_val, q) lasts_updated = Map.put(lasts_temp, q, last_p_val) state = %{ state | parents: parents, parent_edges: parent_edges, sizes: sizes, nexts: nexts_updated, prevs: prevs_updated, lasts: lasts_updated } do_make_root_loop(state, [q | rest]) end defp add_edge(state, i, p, q) do last_p = Map.fetch!(state.lasts, p) next_last_p = Map.fetch!(state.nexts, last_p) size_q = Map.fetch!(state.sizes, q) last_q = Map.fetch!(state.lasts, q) parents = Map.put(state.parents, q, p) parent_edges = Map.put(state.parent_edges, q, i) nexts = state.nexts |> Map.put(last_p, q) |> Map.put(last_q, next_last_p) prevs = state.prevs |> Map.put(q, last_p) |> Map.put(next_last_p, last_q) state = %{ state | parents: parents, parent_edges: parent_edges, nexts: nexts, prevs: prevs } update_ancestors_add(state, p, size_q, last_p, last_q) end defp update_ancestors_add(state, nil, _size_q, _last_p, _last_q) do state end defp update_ancestors_add(state, p, size_q, last_p, last_q) do sizes = Map.update!(state.sizes, p, &(&1 + size_q)) lasts = if Map.fetch!(state.lasts, p) == last_p do Map.put(state.lasts, p, last_q) else state.lasts end parent = Map.fetch!(state.parents, p) update_ancestors_add( %{state | sizes: sizes, lasts: lasts}, parent, size_q, last_p, last_q ) end defp update_potentials(state, i, p, q) do q_pot = Map.fetch!(state.node_potentials, q) p_pot = Map.fetch!(state.node_potentials, p) weight = Map.fetch!(state.edge_costs, i) target = Map.fetch!(state.edge_targets, i) d = if q == target do p_pot - weight - q_pot else p_pot + weight - q_pot end subtree_nodes = trace_subtree(state, q) node_potentials = Enum.reduce(subtree_nodes, state.node_potentials, fn node, pots -> Map.update!(pots, node, &(&1 + d)) end) %{state | node_potentials: node_potentials} end defp trace_subtree(state, p) do limit = Map.fetch!(state.lasts, p) do_trace_subtree(state, p, limit, [p]) end defp do_trace_subtree(_state, p, limit, acc) when p == limit do Enum.reverse(acc) end defp do_trace_subtree(state, p, limit, acc) do nxt = Map.fetch!(state.nexts, p) do_trace_subtree(state, nxt, limit, [nxt | acc]) end end