66##############
77## Settings ##
88##############
9- TIME_LIMIT = 120.0
10- TIME_INCREMENT = 10.0
11- DEBUG_SWITCH = False
9+ TIME_LIMIT = 300.0 # Time (in seconds) to run the solver
10+ TIME_INCREMENT = 13.0 # Time (in seconds) in between heuristic measurements
11+ DEBUG_SWITCH = False # Displays intermediate heuristic info when True
12+ MAX_LNS_NEIGHBOURHOODS = 1000 # Maximum number of neighbours to explore in LNS
13+
1214
1315##############################
1416## Neighbourhood Generators ##
1517##############################
1618
1719def _neighbours_random (data , perm , num = 1 ):
20+ # Returns <num> random job permutations, including the current one
1821 candidates = [perm ]
1922 for i in range (num ):
2023 candidate = perm [:]
@@ -23,6 +26,7 @@ def _neighbours_random(data, perm, num = 1):
2326 return candidates
2427
2528def _neighbours_swap (data , perm ):
29+ # Returns the permutations corresponding to swapping every pair of jobs
2630 candidates = [perm ]
2731 for (i ,j ) in combinations (range (len (perm )), 2 ):
2832 candidate = perm [:]
@@ -31,10 +35,20 @@ def _neighbours_swap(data, perm):
3135 return candidates
3236
3337def _neighbours_LNS (data , perm , size = 2 ):
38+ # Returns the Large Neighbourhood Search neighbours
3439 candidates = [perm ]
35- for subset in combinations (range (len (perm )), size ):
40+
41+ # Bound the number of neighbourhoods in case there are too many jobs
42+ neighbourhoods = list (combinations (range (len (perm )), size ))
43+ random .shuffle (neighbourhoods )
44+
45+ for subset in neighbourhoods [:MAX_LNS_NEIGHBOURHOODS ]:
46+
47+ # Keep track of the best candidate for each neighbourhood
3648 best_make = makespan (data , perm )
3749 best_perm = perm
50+
51+ # Enumerate every permutation of the selected neighbourhood
3852 for ordering in permutations (subset ):
3953 candidate = perm [:]
4054 for i in range (len (ordering )):
@@ -43,12 +57,17 @@ def _neighbours_LNS(data, perm, size = 2):
4357 if res < best_make :
4458 best_make = res
4559 best_perm = candidate
60+
61+ # Record the best candidate as part of the larger neighbourhood
4662 candidates .append (best_perm )
63+
4764 return candidates
4865
4966def _neighbours_idle (data , perm , size = 4 ):
50-
67+ # Returns the permutations of the most <size> idle jobs
5168 candidates = [perm ]
69+
70+ # Compute the idle time for each job
5271 sol = compile_solution (data , perm )
5372 results = []
5473
@@ -57,8 +76,10 @@ def _neighbours_idle(data, perm, size=4):
5776 idle_time = (finish_time - sol [0 ][i ]) - sum ([time for time in data [perm [i ]]])
5877 results .append ((idle_time , perm [i ]))
5978
79+ # Take the <size> most idle jobs
6080 subset = [job for (idle , job ) in list (reversed (results ))[:size ]]
6181
82+ # Enumerate the permutations of the idle jobs
6283 for ordering in permutations (subset ):
6384 candidate = perm [:]
6485 for i in range (len (ordering )):
@@ -73,13 +94,16 @@ def _neighbours_idle(data, perm, size=4):
7394################
7495
7596def _heur_hillclimbing (data , candidates ):
97+ # Returns the best candidate in the list
7698 scores = [(makespan (data , perm ), perm ) for perm in candidates ]
7799 return sorted (scores )[0 ][1 ]
78100
79101def _heur_random (data , candidates ):
102+ # Returns a random candidate choice
80103 return random .choice (candidates )
81104
82105def _heur_random_hillclimbing (data , candidates ):
106+ # Returns a candidate with probability proportional to its rank in sorted quality
83107 scores = sorted ([(makespan (data , perm ), perm ) for perm in candidates ])
84108 i = 0
85109 while (random .random () < 0.5 ) and (i < len (scores ) - 1 ):
@@ -89,6 +113,7 @@ def _heur_random_hillclimbing(data, candidates):
89113
90114################################
91115
116+ # Define the neighbourhoods (and parameters) we would like to investigate
92117NEIGHBOURHOODS = [
93118 ('Random Permutation' , partial (_neighbours_random , num = 100 )),
94119 ('Swapped Pairs' , _neighbours_swap ),
@@ -99,33 +124,42 @@ def _heur_random_hillclimbing(data, candidates):
99124 ('Idle Neighbourhood (5)' , partial (_neighbours_idle , size = 5 ))
100125]
101126
127+ # Define the heuristics we would like to investigate
102128HEURISTICS = [
103129 ('Hill Climbing' , _heur_hillclimbing ),
104130 ('Random Selection' , _heur_random ),
105131 ('Biased Random Selection' , _heur_random_hillclimbing )
106132]
107133
134+ # Combine every neighbourhood and heuristic strategy
108135STRATEGIES = []
109136for (n , h ) in product (NEIGHBOURHOODS , HEURISTICS ):
110- STRATEGIES .append ({'name' : "%s / %s" % (n [0 ], h [0 ]),
111- 'neigh' : n [1 ],
112- 'heur' : h [1 ],
113- 'weight' : 1 ,
114- 'usage' : 0 })
137+ STRATEGIES .append ({'name' : "%s / %s" % (n [0 ], h [0 ]), # Unique name
138+ 'neigh' : n [1 ], # Neighbourhood function
139+ 'heur' : h [1 ], # Heuristic function
140+ 'weight' : 1 , # Weight to determine chance of usage
141+ 'usage' : 0 }) # Number of times the strategy is used
142+
115143
116144def _pick_strategy (strategies ):
145+ # Picks a random strategy based on its weight: roulette wheel selection
117146 total = sum ([strat ['weight' ] for strat in strategies ])
118147 pick = random .uniform (0 , total )
119148 count = strategies [0 ]['weight' ]
149+
120150 i = 0
121151 while pick > count :
122152 count += strategies [i + 1 ]['weight' ]
123153 i += 1
154+
124155 return (strategies [i ],i )
125156
126157
127158def parse_problem (filename ):
128- print "Parsing..."
159+ """Parse the first instance of a Taillard problem file"""
160+
161+ print "\n Parsing..."
162+
129163 with open (filename , 'r' ) as f :
130164 problem_line = 'number of jobs, number of machines, initial seed, upper bound and lower bound :'
131165 lines = map (str .strip , f .readlines ())
@@ -136,10 +170,12 @@ def parse_problem(filename):
136170
137171
138172def makespan (data , perm ):
173+ """Computes the makespan of the provided solution"""
139174 return compile_solution (data , perm )[- 1 ][- 1 ] + data [perm [- 1 ]][- 1 ]
140175
141176
142177def compile_solution (data , perm ):
178+ """Compiles a scheduling on the machines given a permutation of jobs"""
143179
144180 nmach = len (data [0 ])
145181
@@ -148,12 +184,19 @@ def compile_solution(data, perm):
148184 # Assign the initial job to the machines
149185 mach_times [0 ].append (0 )
150186 for mach in range (1 ,nmach ):
187+ # Start the next task in the job when the previous finishes
151188 mach_times [mach ].append (mach_times [mach - 1 ][0 ] + data [perm [0 ]][mach - 1 ])
152189
153190 # Assign the remaining jobs
154191 for i in range (1 , len (perm )):
192+
193+ # The first machine never contains any idle time
155194 job = perm [i ]
156195 mach_times [0 ].append (mach_times [0 ][- 1 ] + data [perm [i - 1 ]][0 ])
196+
197+ # For the remaining machines, the start time is the max of when the
198+ # previous task in the job completed, or when the current machine
199+ # completes the task for the previous job.
157200 for mach in range (1 , nmach ):
158201 mach_times [mach ].append (max (mach_times [mach - 1 ][i ] + data [perm [i ]][mach - 1 ],
159202 mach_times [mach ][i - 1 ] + data [perm [i - 1 ]][mach ]))
@@ -162,18 +205,23 @@ def compile_solution(data, perm):
162205
163206
164207def solve (data ):
165-
208+ """Solves an instance of the flow shop scheduling problem"""
166209 global STRATEGIES
167210
211+ # Record the improvements made by each heuristic and the time they are used
168212 improvements = [0 ] * len (STRATEGIES )
169213 time_spent = [0 ] * len (STRATEGIES )
170214
215+ # Start with a random permutation of the jobs
171216 perm = range (len (data ))
217+ random .shuffle (perm )
172218
219+ # Keep track of the best solution
173220 best_make = makespan (data , perm )
174221 best_perm = perm
175222 res = best_make
176223
224+ # Maintain statistics and timing for the iterations
177225 iteration = 0
178226 time_limit = time .time () + TIME_LIMIT
179227 time_last_switch = time .time ()
@@ -182,7 +230,7 @@ def solve(data):
182230 checkpoint = time .time () + time_delta
183231 percent_complete = 10
184232
185- print "Solving ..."
233+ print "\n Solving ..."
186234
187235 while time .time () < time_limit :
188236
@@ -193,27 +241,43 @@ def solve(data):
193241
194242 iteration += 1
195243
244+ # Heuristically choose the best strategy
196245 (s ,i ) = _pick_strategy (STRATEGIES )
197246
247+ # Use the strategy to change the solution
198248 old_val = res
199249 old_time = time .time ()
200250 perm = s ['heur' ](data , s ['neigh' ](data , perm ))
201251 res = makespan (data , perm )
202252
253+ # Record the statistics on how the strategy did
203254 improvements [i ] += res - old_val
204255 time_spent [i ] += time .time () - old_time
205256 STRATEGIES [i ]['usage' ] += 1
206257
258+ if res < best_make :
259+ best_make = res
260+ best_perm = perm [:]
261+
262+ # At regular intervals, switch the weighting on the strategies available
207263 if time .time () > time_last_switch + TIME_INCREMENT :
264+
265+ # Normalize the improvements made by the time it takes to make them
208266 results = sorted ([(float (improvements [i ]) / max (0.001 , time_spent [i ]), i ) for i in range (len (STRATEGIES ))])
209267
210268 if DEBUG_SWITCH :
211269 print "\n Computing another switch..."
212270 print "Best performer: %s (%d)" % (STRATEGIES [results [0 ][1 ]]['name' ], results [0 ][0 ])
213271 print "Worst performer: %s (%d)" % (STRATEGIES [results [- 1 ][1 ]]['name' ], results [- 1 ][0 ])
214272
273+ # Boost the weight for the successful strategies
215274 for i in range (len (STRATEGIES )):
216275 STRATEGIES [results [i ][1 ]]['weight' ] += len (STRATEGIES ) - i
276+
277+ # Additionally boost the unused strategies to avoid starvation
278+ if 0 == results [i ][0 ]:
279+ STRATEGIES [results [i ][1 ]]['weight' ] += len (STRATEGIES )
280+
217281 time_last_switch = time .time ()
218282
219283 if DEBUG_SWITCH :
@@ -224,12 +288,6 @@ def solve(data):
224288 time_spent = [0 ] * len (STRATEGIES )
225289
226290
227- if res < best_make :
228- best_make = res
229- best_perm = perm [:]
230-
231-
232-
233291 print " %d %%\n " % percent_complete
234292 print "\n Went through %d iterations." % iteration
235293
@@ -242,6 +300,7 @@ def solve(data):
242300
243301
244302def print_solution (data , perm ):
303+ """Prints statistics on the computed solution"""
245304
246305 sol = compile_solution (data , perm )
247306
@@ -256,23 +315,20 @@ def print_solution(data, perm):
256315 idle_time = (finish_time - sol [mach ][0 ]) - sum ([job [mach ] for job in data ])
257316 print row_format .format (mach + 1 , sol [mach ][0 ], finish_time , idle_time )
258317
259- print "\n "
260- print row_format .format ('Job' , 'Start Time' , 'Finish Time' , 'Idle Time' )
261318 results = []
262319 for i in range (len (data )):
263320 finish_time = sol [- 1 ][i ] + data [perm [i ]][- 1 ]
264321 idle_time = (finish_time - sol [0 ][i ]) - sum ([time for time in data [perm [i ]]])
265322 results .append ((perm [i ]+ 1 , sol [0 ][i ], finish_time , idle_time ))
266323
324+ print "\n "
325+ print row_format .format ('Job' , 'Start Time' , 'Finish Time' , 'Idle Time' )
267326 for r in sorted (results ):
268327 print row_format .format (* r )
269328
270-
271329 print "\n \n Note: Idle time does not include initial or final wait time.\n "
272330
273331
274-
275-
276332if __name__ == '__main__' :
277333 data = parse_problem (sys .argv [1 ])
278334 (perm , ms ) = solve (data )
0 commit comments