Skip to content

Commit 578f2d9

Browse files
committed
Added comments and put in a limit for the LNS search.
1 parent e39c785 commit 578f2d9

1 file changed

Lines changed: 80 additions & 24 deletions

File tree

flow-shop/flow.py

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
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

1719
def _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

2528
def _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

3337
def _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

4966
def _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

7596
def _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

79101
def _heur_random(data, candidates):
102+
# Returns a random candidate choice
80103
return random.choice(candidates)
81104

82105
def _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
92117
NEIGHBOURHOODS = [
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
102128
HEURISTICS = [
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
108135
STRATEGIES = []
109136
for (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

116144
def _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

127158
def parse_problem(filename):
128-
print "Parsing..."
159+
"""Parse the first instance of a Taillard problem file"""
160+
161+
print "\nParsing..."
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

138172
def 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

142177
def 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

164207
def 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 "\nSolving..."
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 "\nComputing 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 "\nWent through %d iterations." % iteration
235293

@@ -242,6 +300,7 @@ def solve(data):
242300

243301

244302
def 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\nNote: Idle time does not include initial or final wait time.\n"
272330

273331

274-
275-
276332
if __name__ == '__main__':
277333
data = parse_problem(sys.argv[1])
278334
(perm, ms) = solve(data)

0 commit comments

Comments
 (0)