Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
''' librairies nécéssaires '''
from globals import *
from support import pprint
from support import reset_timer
reset_timer()
if debug:
print("starting to import necessary librairies...")
import pyomo.environ as pyo
from pyomo.opt import SolverFactory
from numpy.random import random as rd
from numpy.random import randint as rdi
from numpy import float64
from numpy import sqrt
from numpy import Infinity
from pyomo.core import Param
import numpy as np
import support
import constraint
import debugging
import sys
import cvrpGoogleOpt as google
import cbc
if debug:
pprint("finished importing all modules")
##################
### HYPOTHESIS ###
##################
'''
the depot will always be at index 0 and have a demand of 0
we code the variable x into a matrix n*n that is symmetric and has a null diagonal. We choose to consider as valid, indices (i,j) such that i>j (all others will not be computed)
a solution is a varialbe x whose values ( for i>j ) are all integer a which verifies all capacity constraints and degree constraints
a feasible solution is a solution that satisfies all constraints (cap and degree) but whose values are not all integer
'''
################
### PATCHING ###
################
'''only needs to be done once (already done)
from pyomo.environ import *
import pyomo.version
if pyomo.version.version_info >= (4, 2, 0, '', 0):
# Pyomo 4.2 mistakenly discards the original expression or rule during
# Expression.construct(). This makes it impossible to reconstruct expressions
# (e.g., for iterated models). So we patch it.
# Test whether patch is still needed:
test_model = ConcreteModel()
test_model.e = Expression(rule=lambda m: 0)
if hasattr(test_model.e, "_init_rule") and test_model.e._init_rule is None:
print ("Patching incompatible version of Pyomo." )
old_construct = pyomo.environ.Expression.construct
def new_construct(self, *args, **kwargs):
# save rule and expression, call the function, then restore them
_init_rule = self._init_rule
_init_expr = self._init_expr
old_construct(self, *args, **kwargs)
self._init_rule = _init_rule
self._init_expr = _init_expr
pyomo.environ.Expression.construct = new_construct
else:
print ( "NOTE: Pyomo no longer removes _init_rule during Expression.construct()." )
print (" The Pyomo patch for Expression.construct() is probably obsolete." )
del test_model
'''
######################
### INITIALISATION ###
######################
''' initalise the solver and the abstract model '''
opt = SolverFactory('glpk')
model = pyo.AbstractModel("CVRP")
''' define model parameters '''
model.number_of_vehicles = pyo.Param(within=pyo.PositiveIntegers)
model.n = pyo.Param(within=pyo.PositiveIntegers) #pour l'instant = nombre de noeuds y compris le dépot
model.capacity = pyo.Param(within=pyo.PositiveIntegers)
model.nodes = pyo.RangeSet(0,model.n-1)
model.locations = pyo.Param(model.nodes,pyo.RangeSet(0,1))
model.demands = pyo.Param(model.nodes)
''' instanciate model with inbound data '''
if len(sys.argv)!=2:
file = "test3.dat"
instance = model.create_instance(file)
else:
file = sys.argv[1]
try:
instance = model.create_instance(file)
except:
print("file not found, using test2.dat instead")
print()
file = "test2.dat"
instance = model.create_instance(file)
''' contruct de distance parameter to save time '''
distances = {}
#distance function (euclidian)
dist = lambda x,y : sqrt( (x[0]-y[0])**2 + (x[1]-y[1])**2 )
for i in instance.nodes:
for j in instance.nodes:
if(i==j):
distances[(i,j)] = 0
else:
distances[(i,j)] = dist((instance.locations[i,0],instance.locations[i,1]),(instance.locations[j,0],instance.locations[j,1]))
instance.costs = Param(instance.nodes,instance.nodes, initialize = distances)
locations = support.to_list_locations(instance)
del(instance.locations)
''' define variable x '''
instance.x = pyo.Var(instance.nodes,instance.nodes, bounds = support.set_bounds)
#deleting uneused variables (i<=j)
for i in instance.nodes:
for j in instance.nodes:
if i<=j:
del(instance.x[i,j])
''' define the objective function '''
instance.objective = pyo.Objective( expr = sum( instance.costs[i,j]*instance.x[i,j] for i in instance.nodes for j in instance.nodes if i>j ) )
''' define degree constraints '''
instance.c_deg = pyo.Constraint(instance.nodes, rule=support.rule_deg)
''' define capacity constraints as an empty list for now '''
instance.c_cap = pyo.ConstraintList() # on utilise cette structure pour pouvoir ajouter et supprimer des contraintes par la suite
#liste vide pour commencer
if debug:
pprint("finished constructing model")
######################
### MAIN FUNCTIONS ###
######################
upper_bound = Infinity #initial best objective function value found
feasible_solution_instances = [] #global list of feasible solutions found at time of cutting
id = support.get_safe_counter() #global inrementing id to name the different instances
class instance_manager():
#CAREFUL class is not thread safe!!
def __init__(self):
self.queue = []
self.upper_bound = google.upper_bound(instance,locations)
self.length = 0
self.best_feasible_integer_solution = None
self.branches_cut = 0
self.partial_solution_recorded = []
self.total_length = 0
def add(self,instance):
if instance.lower_bound <= self.upper_bound:
index = 0
while index<self.length and self.queue[index].lower_bound<instance.lower_bound :
index +=1
if index == self.length:
self.queue.append(instance)
else :
self.queue.insert(index,instance)
self.length += 1
self.total_length += 1
else:
self.branches_cut += 1
def pop(self):
if self.length>0:
instance = self.queue.pop()
self.length -= 1
return instance
return None
def record_feasible_integer_solution(self,instance):
if pyo.value(instance.objective) < self.upper_bound :
upper_bound = pyo.value(instance.objective)
self.best_feasible_integer_solution = instance
def record_partial_solution(self,instance):
index = 0
while index<self.length and self.queue[index].lower_bound<instance.lower_bound :
index +=1
if index == self.length:
self.partial_solution_recorded.append(instance)
else :
self.partial_solution_recorded.insert(index,instance)
def branch(instance,instance_manager):
#branches instance problem into two complementary sub problem instances over a specific index
if debug:
pprint(support.list_to_string(instance.id)+"+++++ entering branching")
# we choose the index for branching whose corresponding value is closest to 0.5
index = -1,-1
dist = 1 # max theoretical dist for bridges considered is actually 0.5
for bridge in instance.x.keys():
#do not consider bridge with depot for their bound is (0,2) and we would have to branch over 3 instances
if 0 in bridge:
continue
if abs(instance.x[bridge].value-0.5)<dist:
index = bridge
dist = abs(instance.x[bridge].value-0.5)
if index==(-1,-1):
raise NameError("No branching index found")
if debug:
pprint(support.list_to_string(instance.id)+"----- branching done over index "+str(index)+" with value "+str(instance.x[index].value))
#creating new instances ! recycling the old one into one of the branches
instance.x[index].fixed = True
instance2 = instance.clone()
instance.x[index].value = 0
instance2.x[index].value = 1
global id
id0 = instance.id
depth = instance.depth
instance2.depth = depth+1
instance.depth = depth+1
instance.id = id0+[id.get_and_increment()]
instance2.id = id0+[id.get_and_increment()]
if debug:
log = instance.log
instance.log = log+str(index)+" : 0 / "
instance.log = log+str(index)+" : 1 / "
print(support.list_to_string(id0)+"----- new value of objective function for instance "+str(instance.id[-1])+" fixed 0 : "+str(round(pyo.value(instance.objective),2)))
pprint(support.list_to_string(id0)+"----- new value of objective function for instance "+str(instance2.id[-1])+" fixed 1 : "+str(round(pyo.value(instance2.objective),2)))
instance.lower_bound = cbc.lower_bound(instance)
instance2.lower_bound = cbc.lower_bound(instance2)
instance_manager.add(instance)
instance_manager.add(instance2)
def column_generation(instance,instance_manager):
#iteratively adds constraints and solves the instance in order to strengthen the linear relaxation
#STRUCTURE OF FUNCTION :
#loop untill the solution is "good enough" OR too many iterations :
#1) find violated constraints in specific order
#2) if non found : exit ; we have found a solution that is feasible and necessarily optimal within the current branch (since it a solution found by the solver)
#2b : if that solution is also integer, we need not continue this branch!
#3) else : add found constraints to constraints list, re-solve linear problem and continue
if debug:
pprint(support.list_to_string(instance.id)+"+++++ entering column generation")
loop_count = 1
failure_count = 0
while support.continue_column_generation(instance,loop_count):
#we first add capacity constraints
success, count = constraint.add_c_cap(instance)
connected_threshold.update()
if debug:
print(support.list_to_string(instance.id)+"----- we found " +str(count) + " capacity cuts with threshold of " +str(connected_threshold.value))
#in the future : add other constraints
#remove_inactive_constraints(instance)
#if we have not found a single cutting plane (=violated constraint) --> feasible and optimal solution found
#ATTENTION : for feasible_integer_found to be true we only need to verify is_integer and connected components heuristic (not all other constraint generation heuristics)
#therefore, when adding new heursitics in the future, this breaking condition will have to be placed else where to happen also right after said heuristic
if not(success):
failure_count+=1
if debug:
print(support.list_to_string(instance.id)+"----- no cutting planes found with threshold of " +str(connected_threshold.value))
if failure_count>len(vals):
if support.solution_is_integer(instance):
instance_manager.record_feasible_integer_solution(instance)
if debug :
print(support.list_to_string(instance.id)+"----- feasible solution found during column generation is integer!")
print(support.list_to_string(instance.id)+"----- branch is recorded and cut")
print(support.list_to_string(instance.id)+"----- value of objective function for solution is "+str(round(pyo.value(instance.objective),2)))
print()
else:
instance_manager.record_partial_solution(instance)
break
else:
failure_count = 0
results = opt.solve(instance)
if debug:
pprint( support.list_to_string(instance.id)+"----- value of objective function during column generation "+str(loop_count)+" is "+str(round(pyo.value(instance.objective),10))+" and is "+str(support.integer_percent(instance))+"% integer")
loop_count+=1
def remove_inactive_constraints(instance):
#must disable constraints that are inactive being careful of the fact that they may become active again later on...
raise NameError("!!!!! remove_inactive_constraints must be implemented")
def main_loop(instance_manager):
instance = instance_manager.pop()
while instance!=None:
#adding constriants and resolving and verifying if we have, by chance, found an integer and feasible solution
feasible_integer_found = column_generation(instance,instance_manager)
#if we consider that we have done "enough", we also stop (typically : too many iterations)
if instance.depth < max_depth and support.max_time_not_reached() and instance_manager.best_feasible_integer_solution==None:
#branch and apply main_loop to the two new instances
branch(instance,instance_manager)
else :
instance_manager.record_partial_solution(instance)
instance = instance_manager.pop() #will return none if there are no instances left in queue
############
### MAIN ###
############
print()
print("using input file "+file)
instance.file = file
print()
print("++++++++++ starting CVRP solving for "+str(instance.n.value)+" nodes, "+str(instance.number_of_vehicles.value)+" vehicles with capacity of "+str(instance.capacity.value))
print()
#computing lower_bound
instance.lower_bound = cbc.lower_bound(instance)
#initialising instance manager
instance_manager = instance_manager()
instance_manager.add(instance)
#solving the initial instance in order to initialize instance.x values
results = opt.solve(instance)
#printing initial value of objective function
if debug:
print("0----- initial value of objective function "+str(round(pyo.value(instance.objective),2))+" and is "+str(support.integer_percent(instance))+"% integer")
print()
instance.log = ""
instance.id = [id.get_and_increment()]
instance.depth = 0
main_loop(instance_manager)
pprint("++++++++++ finished solving")
if instance_manager.best_feasible_integer_solution==None:
print("no optimal integer solution found")
print("lower bound found :" +str(pyo.value(instance_manager.partial_solution_recorded[0].objective)))
else:
if input("show instance ? (y/n) (yes/no) \n") in ["y","yes","oui","hell","yeah"]:
instance_manager.best_feasible_integer_solution.display()
''' testing '''
''' this works well
results = opt.solve(instance)
instance.display()
success = constraint.add_c_cap(instance)
results = opt.solve(instance)
instance.display()
'''