Quantcast
Channel: Yet Another Math Programming Consultant
Viewing all articles
Browse latest Browse all 804

Solving as network with lowerbounds

$
0
0

In [1], we looked at the following problem:


Mathematical Model
\[ \begin{align} \min& \sum_{i,j} \color{darkblue}a_{i,j} \cdot \color{darkred}x_{i,j} \\ & \sum_j \color{darkblue}a_{i,j}\cdot \color{darkred}x_{i,j} \ge \color{darkblue}r_i && \forall i \\ & \sum_i \color{darkblue}a_{i,j}\cdot \color{darkred}x_{i,j} \ge \color{darkblue}c_j && \forall j \\ & \color{darkred}x_{i,j} \in \{0,1\} \end{align}\]


This is a network problem, so instead of solving it as a MIP, we can solve it as a pure LP or as a min-cost flow network problem. 

Here we try to use some open-source network codes. And thus, we need to formulate the problem as a pure network. This would be relatively straightforward if the algorithm supports lower bounds on the flows. However, most open-source network algorithms don't support these. Here is a workaround:

Assume we have an edge \(i \rightarrow j\) with bounds \(\ell,u\). We can reformulate this as:

    • demand in node \(i\) is \(\ell\)
    • supply in node \(j\) is \(\ell\)
    • capacity of the edge  \(i \rightarrow j\)  becomes \(u-\ell\).

A streamlined version of this is used in my implementations below. 

I will use the following network for our problem:


The orange nodes are supply nodes, while the blue ones are demand nodes.

  • The source node src has a supply of \(\sum_{i,j} \color{darkblue}a_{i,j}-\sum_i \color{darkblue}r_i\). We subtract \(\sum_i \color{darkblue}r_i\) to simulate the lower bounds on the flows \(\mathit{src}\rightarrow i_k\). Part of the output of the source node flows to the assignment network; the rest is unused and goes directly to the sink node snk
  • The nodes \(i\) have a supply of \(\color{darkblue}r_i\). This is to make sure each \(i\) receives at least \(\color{darkblue}r_i\). 
  • The nodes \(j\) have a demand of \(\color{darkblue}c_j\). This is to make sure we obey the lower bound of \(\color{darkblue}c_j\).
  • Finally, the node snk has a demand of \(\sum_{i,j} \color{darkblue}a_{i,j}-\sum_j \color{darkblue}c_j\). 
  • You can verify that the total supply is equal to the total demand.
  • The assignment arcs \(i \rightarrow j\) have a cost of 1 and a capacity of 1.
  • All other arcs have no cost and have unlimited capacity.  I used \(\sum_{i,j} \color{darkblue}a_{i,j}\) to represent "unlimited capacity" where needed.

The Python package networkx has some network solvers. The complete implementation of the model is shown in an appendix. The results are:


We get the same objective function value as reported in [1]. The performance is not something to write home about.

Another popular network algorithm can be found in ortools. This is much faster:


Here the network setup is actually more expensive than solving it. 

Conclusions


We can solve the problem in [1] using open-source min-cost flow network solvers, such as the algorithms in networkx and ortools. As they don't support lower bounds on the flows, the modeling requires a bit of thought. 

Compared to an LP model, the code is a bit less structure-revealing. To make this more comprehensible, you really need to show the mathematical model and a picture of the network. And also a description of the demands and supplies.

For this example, the performance difference between the two solvers is very large: a factor of 160!

References


Appendix: networkx implementation


import networkx as nx
import numpy as np
import pickle

#--------------------------------------------------------# import data#--------------------------------------------------------

data = pickle.load(open("c:/tmp/test/arc.pck","rb"))

a = data['a']  # 2d numpy array
r = data['r']  # list of floats
c = data['c']  # list of floats

I = range(len(r))
J = range(len(c))

# these are all integers represented as double precision numbers# convert to integers
a = a.astype(int)
r = [int(r[i]) for i in I]
c = [int(c[j]) for j in J]

suma = np.sum(a)
sumr = sum(r)
sumc = sum(c)

#--------------------------------------------------------# form network#--------------------------------------------------------

G = nx.DiGraph()

## x(i,j) form the bipartite part#for i in I: G.add_node(f'i{i}',demand=-r[i])
for j in J: G.add_node(f'j{j}',demand=c[j])
for i in I:
    for j in J:
        if a[i,j]==1: G.add_edge(f'i{i}',f'j{j}',weight=1,capacity=1)

## add src and snk node#
G.add_node('src',demand=sumr-suma)
G.add_node('snk',demand=suma-sumc)      
for i in I: G.add_edge('src',f'i{i}')
for j in J: G.add_edge(f'j{j}','snk')
G.add_edge('src','snk')

#--------------------------------------------------------# solve#--------------------------------------------------------

print(G)
flowCost, flowDict = nx.network_simplex(G)
print(f'objective:{flowCost}')


Appendix: ortools implementation


import pickle
import numpy as np
from ortools.graph.python import min_cost_flow

#--------------------------------------------------------# import data#--------------------------------------------------------

data = pickle.load(open("c:/tmp/test/arc.pck","rb"))

a = data['a']  # 2d numpy array
r = data['r']  # list of floats
c = data['c']  # list of floats

I = range(len(r))
J = range(len(c))

# these are all integers represented as double precision numbers# convert to integers
a = a.astype(int)
r = [int(r[i]) for i in I]
c = [int(c[j]) for j in J]

suma = np.sum(a) # also functions as largest possible capacity
sumr = sum(r)
sumc = sum(c)

#--------------------------------------------------------# form network#--------------------------------------------------------

mcf = min_cost_flow.SimpleMinCostFlow()

# numbering scheme for the nodes:# i   = 0..len(r)-1# j   = len(r)..len(r)+len(c)-1# src = len(r)+len(c)# snk = len(r)+len(c)+1

src = len(r)+len(c)
snk = src + 1
lenr = len(r)

# assignment part of the graph, i->jfor i in I: 
    for j in J:
        if a[i,j]==1: mcf.add_arcs_with_capacity_and_unit_cost(i,lenr+j,1,1)
# src -> ifor i in I: mcf.add_arcs_with_capacity_and_unit_cost(src,i,suma,0) 
# j -> snkfor j in J: mcf.add_arcs_with_capacity_and_unit_cost(lenr+j,snk,suma,0)
# src -> snk
mcf.add_arcs_with_capacity_and_unit_cost(src,snk,suma,0)

# suppliesfor i in I: mcf.set_node_supply(i,r[i])
for j in J: mcf.set_node_supply(lenr+j,-c[j])
mcf.set_node_supply(src,suma-sumr)
mcf.set_node_supply(snk,sumc-suma)

#--------------------------------------------------------# solve#--------------------------------------------------------

print(f'nodes:{mcf.num_nodes()}, arcs:{mcf.num_arcs()}')

status=mcf.solve()
print(status)

print(f'objective:{mcf.optimal_cost()}')

Viewing all articles
Browse latest Browse all 804

Trending Articles