Using column generation to minimize production waste

Minimizing production waste can result in significant cost savings. However, calculating just the right production configuration can be a tedious task.

Let’s take the example of a firm which delivers sheet metal. The sheets it delivers are all of the same width but can differ in length. The raw material used by the company are metal sheets of 3 meters wide by 20 meters long. Clients can order any length metal sheets (as long as it doesn’t exceed 20 meters).

Let’s say there is demand for the following lengths of sheet metal (all are 3 meters wide). The number of demands are denoted in the next line. E.g. for 40 items of length 6 have to be produced, 5 items of length 9 have to be produced and so on.

items_demands

How should we cut the raw material metal sheets, which are 20 meters long, so that we satisfy demand but minimize waste? Column generation is a very effective method to solve this problem (often referred to as the cutting-stock problem).

The column generation method consists of a master linear program and a slave integer program.

The master sets up the problem with initial feasible (but probably not optimal) cutting patterns. The example that follows is build using pulp-or, an LP-modeler for Python.

First let’s generate the above items and item demands;

random.seed(2012)

nrItems=12
lengthSheets=20

itemLengths=[]
itemDemands=[]

while len(itemLengths)!=nrItems:
	length=random.randint(5, lengthSheets-2)
	demand=random.randint(5, 100)
	if length not in itemLengths:
		itemLengths.append(length)
		itemDemands.append(demand)

print "Item lengts  : %s" % itemLengths
print "Item demands : %s\n\n" % itemDemands

Then we have to generate a set of feasible starting cut patterns. In this example I generate very simple patterns, however, much better starting patterns can be found, which would result in much faster solve times in large problems. The naive starting patterns are generated as follows.

patterns=[]
print "Generating start patterns:"
## generate simple start patterns
for x in range(nrItems):
	temp=[0.0 for y in range(x)]
	temp.append(1.0)
	temp+=[0.0 for y in range(nrItems-x-1)]
	patterns.append(temp)
	print temp

This code generates the following patterns:

genpatterns

Next we have to set up a master model and a slave model. The master model will solve the problem using the generated start patterns. Following that, we will use the resulting dual prices to set up the slave model. The results of the slave model will then be used to add a new pattern to the master problem.

Let’s take a look at the master model;

class MasterProblem:
	def __init__(self, maxValue, itemLengths, itemDemands, initialPatterns, problemname):

		self.maxValue=maxValue
		self.itemLengths=itemLengths
		self.itemDemands=itemDemands
		self.initialPatterns=initialPatterns

		self.prob = LpProblem(problemname,LpMinimize)	# set up the problem

		self.obj = LpConstraintVar("obj")   # generate a constraint variable that will be used as the objective
		self.prob.setObjective(self.obj)

		self.PatternVars=[]
		self.constraintList=[]   # list to save constraint variables in
		for i,x in enumerate(itemDemands):		# create variables & set the constraints, in other words: set the minimum amount of items to be produced
			var=LpConstraintVar("C"+str(i),LpConstraintGE,x)  # create constraintvar and set to >= demand for item
			self.constraintList.append(var)
			self.prob+=var

		for i,x in enumerate(self.initialPatterns):  #save initial patterns and set column constraints
			temp=[]
			for j,y in enumerate(x):
				if y>0:
					temp.append(j)

			var=LpVariable("Pat"+str(i)	, 0, None, LpContinuous, lpSum(self.obj+[self.constraintList[v] for v in temp]))  # create decision variable: will determine how often pattern x should be produced
			self.PatternVars.append(var)

	def solve(self):
		self.prob.writeLP('prob.lp')
		self.prob.solve()  # start solve

		return [self.prob.constraints[i].pi for i in self.prob.constraints]

	def addPattern(self,pattern):  # add new pattern to existing model

		self.initialPatterns.append(pattern)
		temp=[]

		for j,y in enumerate(pattern):
			if y>0:
				temp.append(j)

		var=LpVariable("Pat"+str(len(self.initialPatterns))	, 0, None, LpContinuous, lpSum(self.obj+[pattern[v]*self.constraintList[v] for v in temp]))
		self.PatternVars.append(var)

	def startSlave(self,duals):  # create/run new slave and return new pattern (if available)

		newSlaveProb=SlaveProblem(duals,self.itemLengths,self.maxValue)

		pattern=newSlaveProb.returnPattern()
		return pattern

	def setRelaxed(self,relaxed):  # if no new patterns are available, solve model as IP problem
		if relaxed==False:
			for var in self.prob.variables():
				var.cat = LpInteger

	def getObjective(self):
		return value(self.prob.objective)

	def getUsedPatterns(self):
		usedPatternList=[]
		for i,x in enumerate(self.PatternVars):
			if value(x)>0:
				usedPatternList.append((value(x),self.initialPatterns[i]))
		return usedPatternList

The slave model will then use the dual prices to generate a new column. This process will go on until the value of the solved slave model is –1, in other words, no better column can be generated (-1.0001 is used to account for rounding errors). The slave model is set up as follows;

class SlaveProblem:
    def __init__(self,duals, itemLengths,maxValue):
        self.slaveprob=LpProblem("Slave solver",LpMinimize)
        self.varList=[LpVariable('S'+str(i),0,None,LpInteger) for i,x in enumerate(duals)]
        self.slaveprob+=-lpSum([duals[i]*x for i,x in enumerate(self.varList)])  #use duals to set objective coefficients
        self.slaveprob+=lpSum([itemLengths[i]*x for i,x in enumerate(self.varList)])
        self.slaveprob.writeLP('slaveprob.lp')
        self.slaveprob.solve()
        self.slaveprob.roundSolution() #to avoid rounding problems

    def returnPattern(self):
        pattern=False
        if value(self.slaveprob.objective) < -1.00001:
            pattern=[]
            for v in self.varList:
                pattern.append(value(v))
        return pattern

Then let’s set up the problem;


print "\n\nTrying to solve problem"
CGprob=MasterProblem(lengthSheets,itemLengths,itemDemands,patterns,'1D cutting stock')

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

	newPattern=CGprob.startSlave(duals)

	print 'New pattern: %s' % newPattern

	if newPattern:
		CGprob.addPattern(newPattern)
	else:
		CGprob.setRelaxed(False)
		CGprob.solve()
		relaxed=False

print "\n\nSolution: %s sheets required" % CGprob.getObjective()

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

And finally, the optimal cutting patterns will be outputted to the screen.

The complete .py file can be found here (dependency: pulp-or).

December 11, 2012Permalink Leave a comment