Friday, 1 September 2017

Column Generation: Shortest path problem with time constraint

Column generation technique is used in integer (pure or mixed) programming problem in which enumeration of variables is very large. It can be viewed as matrix with small number of rows with very large number of columns. Since, it is difficult to enumerate the variables and incorporate in the problem column generation technique is used which iteratively adds variables that seems feasible to enter. It is similar to simplex method in which new variables are added into the basis.

We look here column generation problem on shortest path problem with time constraint. A set of nodes and arcs are present with arc time and arc cost associated with each arc. The objective is to reach from start node to end node while satisfying total path time within threshold time value. The problem selected (Desaulniers, G., Desrosiers, J. and Solomon, M.M., Column generation, Chapter 1,Springer, 2005, DOI: 10.1007/b135457) is shown in below figure:


It is required to reach from node 1 to node 6 in shortest time while satisfying the total time constraint. The problem can be stated in MILP form as:


The problem involves in enumeration of paths such that objective is met. However, there are nine paths and it would become difficult for a problem involving thousands of nodes. In such cases, we would decompose the original problem into master problem and sub-problem. Start the master problem by randomly selecting a path and an artificial variable with very large coefficient. Upon solving the master problem without integer constraint and additional path variable. The sum of path variable would be one and time restriction constraints of original problem are used. The master problem which is so called as restricted master problem is formulated as:

The sub-problem is same as original problem except the objective is modified with reduced cost for the constrain (1.11) and (1.12) as:


Upon construction of the restricted master problem and sub-problem, iterate it till objective of sub-problem is greater than or equal to zero. Check for the solution to the problem and if it is not integer, than we would perform column generation at each node with branch and bound technique. Here, we would consider the first solution of column generation with iterations till the sub-problem objective becomes greater than or equal to zero. The data consists of four headers in .csv file with headers: fromnode, tonode, Cost, Time. Minimum path time threshold is 14.


from pulp import*
import pandas as pd

arcdf = pd.DataFrame.from_csv('.....SPTRESData.csv', index_col=['fromnode', 'tonode'])



class MasterProblem:
    
    def __init__(self,maxresrtime,nodes,arcdf,arcs,arccost,arctime,initialpaths,problemname):
        self.maxresrtime = maxresrtime
        self.nodes = nodes
        self.arcdf = arcdf
        self.arcs = arcs
        self.arccost = arccost
        self.arctime = arctime
        self.initialpaths = initialpaths
        
        self.prob = LpProblem(problemname,LpMinimize)
        
        self.obj = LpConstraintVar("obj")
        self.prob.setObjective(self.obj)
        
        self.PathVars = []
        
        self.FirstConstr = LpConstraintVar("TimeResrConstr",LpConstraintLE,self.maxresrtime)
        self.prob += self.FirstConstr
        
        self.SecondConstr = LpConstraintVar("LambdaConstr",LpConstraintEQ,1.0)
        self.prob += self.SecondConstr
        
        
        
        
        artfvar = LpVariable("ArtVar",0,None,LpContinuous,
                            lpSum(self.obj * 1000 + self.FirstConstr + self.SecondConstr))
        self.PathVars.append(artfvar)
        
        
        
        
        pathtime = 0
        pathcost = 0
        
        for i,x in enumerate(self.initialpaths):
            if x>0:
                pathtime += (self.arctime[i]) 
                pathcost += (self.arccost[i]) 
            
        
        
                    
        var = LpVariable("Path"+str(len(self.initialpaths)),0,None,LpContinuous,
                            lpSum(self.obj * pathcost + pathtime * self.FirstConstr + self.SecondConstr))
        self.PathVars.append(var)
            
    def solve(self):
        self.prob.writeLP("MasterProb.lp")
        self.prob.solve()
        
        return [self.prob.constraints[i].pi for i in self.prob.constraints]
    
    def addPath(self,path):
        self.initialpaths.append(path)
        pathtime = 0
        pathcost = 0
        
        for j,y in enumerate(path):
            if y>0:
                pathtime += (self.arctime[j]) 
                pathcost += (self.arccost[j]) 
                
        var = LpVariable("Path"+str(len(self.initialpaths)),0,None,LpContinuous,
                         lpSum(self.obj * pathcost + pathtime * self.FirstConstr + self.SecondConstr))
        self.PathVars.append(var)
        
    
    def startSubProblem(self,duals):
        newSubProb = SubProblem(duals,self.nodes,self.arcdf,self.arcs,self.arccost,self.arctime)
        path = newSubProb.returnPath()
        return path
    
    def setRelaxed(self,relaxed):  
        if relaxed==False:
            for var in self.prob.variables():
                var.cat = LpBinary
                
    def getObjective(self):
        return value(self.prob.objective)
    
    def getUsedPaths(self):
        usedPathList=[]
        for i,x in enumerate(self.PathVars):
            if value(x)>0:
                usedPathList.append((value(x),self.initialpaths[i]))
        return usedPathList       




class SubProblem:
    
    def __init__(self,duals,nodes,arcdf,arcs,arccost,arctime):
        
        
        f = -duals[0]
            
        s = -duals[1]
            
        
        self.subprob = LpProblem("Sub Solver",LpMinimize)
            
        self.varList = LpVariable.dicts("Arc_variable", ((i,j) for i,j in arcs),cat='Binary')
        
        self.subprob += lpSum([self.varList[i,j] * arcdf.loc[(i,j),'cost'] for i,j in arcdf.index] + 
                             [self.varList[i,j] * f * arcdf.loc[(i,j),'time'] for i,j in arcdf.index]) + s 
        
        self.subprob += lpSum([self.varList[i,j] for i,j in arcs if i == 1]) == 1
        
        self.subprob += lpSum([self.varList[i,j] for i,j in arcs if j == 6]) == 1
        
        for n in nodes:
            if n != 1:
                if n != 6:
                    self.subprob += (lpSum([self.varList[i,j] for i,j in arcs if i == n]) == lpSum([self.varList[i,j] for i,j in arcs if j == n]))     
        
        
        self.subprob.writeLP('subprob.lp')
        self.subprob.solve() 
        
        
    
    def returnPath(self):
        path=False
        if value(self.subprob.objective) < 0:
            path=[]
            for v in self.subprob.variables():
                path.append(v.varValue)
        return path





nodes = [1,2,3,4,5,6]
arcs = [[1,2],[1,3],[2,4],[2,5],[3,2],[3,4],[3,5],[4,5],[4,6],[5,6]]
arccost = [1,10,1,2,1,5,12,10,1,2]
arctime = [10,3,1,3,2,7,3,1,7,2]
paths = [1,0,1,0,0,0,0,0,1,0]



print ("\n\nTrying to solve problem")
CGprob=MasterProblem(14,nodes,arcdf,arcs,arccost,arctime,paths,'Shortest Path Problem with time constraint')

relaxed=True
while relaxed==True:   # once no more new columns can be generated set relaxed to False
    duals=CGprob.solve()

    newPath=CGprob.startSubProblem(duals)

    print ('New path: %s' % newPath)

    if newPath:
        CGprob.addPath(newPath)
    else:
        CGprob.setRelaxed(False)
        CGprob.solve()
        relaxed=False

print ("\n\nSolution: %s Optimal path cost is" % CGprob.getObjective())

t=CGprob.getUsedPaths()
for i,x in enumerate(t):
    print ("Path %s: selected %s times %s" % (i,x[0],x[1]))

No comments:

Post a Comment