-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSolver.py
1407 lines (1161 loc) · 54.8 KB
/
Solver.py
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
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import random
from VRP_Model import *
from SolutionDrawer import *
class Solution:
"""
Represents a solution to the VRP instance.
Attributes:
-----------
cost : float
The total cost of the solution.
routes : list
A list containing the routes of vehicles in the solution.
Methods:
--------
__init__():
Initializes a Solution object with default attributes (cost = 0.0 and routes= []).
"""
def __init__(self):
self.cost = 0.0
self.routes = []
class RelocationMove(object):
"""
Represents a relocation move between nodes (customers) in a VRP solution.
Attributes:
-----------
originRoutePosition : int or None
Position of the origin route in the solution's routes list.
targetRoutePosition : int or None
Position of the target route in the solution's routes list.
originNodePosition : int or None
Position of the origin node within its route.
targetNodePosition : int or None
Position of the target node within its route.
costChangeOriginRt : float or None
Change in cost if the origin node is relocated.
costChangeTargetRt : float or None
Change in cost if the target node is relocated.
moveCost : float
Cost of the relocation move.
Methods:
--------
__init__():
Initialize a RelocationMove object with default attributes.
Initialize():
Reset all attributes to their initial state. The moveCost attribute is initialized with a high non-logical value (10^9) to signify that it needs to be recalculated during the relocation process.
"""
def __init__(self):
"""
Initialize a RelocationMove object with default attributes.
"""
self.originRoutePosition = None
self.targetRoutePosition = None
self.originNodePosition = None
self.targetNodePosition = None
self.costChangeOriginRt = None
self.costChangeTargetRt = None
self.moveCost = None
def Initialize(self):
"""
Reset all attributes of the RelocationMove object to their initial state.
"""
self.originRoutePosition = None
self.targetRoutePosition = None
self.originNodePosition = None
self.targetNodePosition = None
self.costChangeOriginRt = None
self.costChangeTargetRt = None
self.moveCost = 10 ** 9
class SwapMove(object):
"""
Represents a potential swap move in the VRP.
Attributes:
positionOfFirstRoute (int): Index of the first route involved in the swap.
positionOfSecondRoute (int): Index of the second route involved in the swap.
positionOfFirstNode (int): Index of the node in the first route to be swapped.
positionOfSecondNode (int): Index of the node in the second route to be swapped.
costChangeFirstRt (float): Cost change associated with the swap in the first route.
costChangeSecondRt (float): Cost change associated with the swap in the second route.
moveCost (float): Total cost change resulting from the swap operation.
"""
def __init__(self):
"""
Initializes a SwapMove object.
The moveCost attribute is set to a large value (10^9) to ensure it is initially higher than any potential cost change.
"""
self.positionOfFirstRoute = None
self.positionOfSecondRoute = None
self.positionOfFirstNode = None
self.positionOfSecondNode = None
self.costChangeFirstRt = None
self.costChangeSecondRt = None
self.moveCost = None
def Initialize(self):
"""
Resets the attributes of the SwapMove object to their initial values.
The moveCost attribute is set back to a large value to allow easy replacement with a better cost during calculations.
"""
self.positionOfFirstRoute = None
self.positionOfSecondRoute = None
self.positionOfFirstNode = None
self.positionOfSecondNode = None
self.costChangeFirstRt = None
self.costChangeSecondRt = None
self.moveCost = 10 ** 9
class CustomerInsertion(object):
def __init__(self):
self.customer = None
self.route = None
self.cost = 10 ** 9
class CustomerInsertionAllPositions(object):
"""
Represents a potential customer insertion into all possible positions along a route.
Attributes:
customer (Node): The customer node to be inserted.
route (Route): The route where the customer is to be inserted.
insertionPosition (Position): The position at which the customer is to be inserted in the route.
cost (float): The cost associated with the insertion position,
Initialized to a large value to allow easy replacement with a better cost.
"""
def __init__(self):
"""
Initializes a CustomerInsertionAllPositions object.
The cost attribute is set to a large value (10^9) to ensure it is initially higher than any potential insertion cost.
"""
self.customer = None
self.route = None
self.insertionPosition = None
self.cost = 10 ** 9
class TwoOptMove(object):
"""
Represents a potential 2-opt move in the VRP.
Attributes:
positionOfFirstRoute (int): Index of the first route involved in the 2-opt move.
positionOfSecondRoute (int): Index of the second route involved in the 2-opt move.
positionOfFirstNode (int): Index of the first node to be swapped.
positionOfSecondNode (int): Index of the second node to be swapped.
moveCost (float): Cost of the relocation move.
Methods:
Initialize(): Resets the attributes to their default values, setting moveCost to a large value(10^9).
"""
def __init__(self):
"""
Initializes a TwoOptMove object with default attributes.
"""
self.positionOfFirstRoute = None
self.positionOfSecondRoute = None
self.positionOfFirstNode = None
self.positionOfSecondNode = None
self.moveCost = None
def Initialize(self):
"""
Resets the attributes to their initial values, setting moveCost to a large value(10^9).
"""
self.positionOfFirstRoute = None
self.positionOfSecondRoute = None
self.positionOfFirstNode = None
self.positionOfSecondNode = None
self.moveCost = 10 ** 9
class Solver:
"""
A class that provides methods to solve the Vehicle Routing Problem (VRP).
Attributes:
-----------
allNodes : list
List containing all nodes in the VRP, including the depot and customers.
customers : list
List containing only the customer nodes.
depot : Node
The depot node where all routes begin and end.
distanceMatrix : list of lists
A 2D list representing the distance between each pair of nodes.
capacity : float
The capacity of the vehicles in the VRP.
sol : Solution
An instance of the Solution class representing the current solution.
bestSolution : Solution
An instance of the Solution class representing the best solution found so far.
Methods:
--------
__init__(m):
Initialize the Solver with the given model 'm'.
- m: An instance of the Model class containing problem data.
solve():
Main method to solve the VRP.
Executes a sequence of methods to construct and optimize the solution.
SetRoutedFlagToFalseForAllCustomers():
Set the 'isRouted' flag to False for all customer nodes.
Ensures that all customers are unrouted before constructing a new solution.
ApplyNearestNeighborMethod():
Construct initial routes using the Nearest Neighbor method.
Continues to assign customers to routes until all are assigned or capacity constraints are violated.
Uses the distance matrix to determine the nearest customers every time.
Always_keep_an_empty_route():
Ensure there is always at least one empty route available.
Adds a new route if the last route is partially filled.
MinimumInsertions():
Construct initial routes using the minimum insertions.
Continuously identifies the best customer to insert into the current routes. (prioritizes inserting customers with the least additional distance.)
LocalSearch(operator):
Apply local search techniques to further optimize the solution.
- operator: An integer representing the type of local search move (0: Relocation, 1: Swap, 2: TwoOpt).
Implements the specified local search operator to explore neighboring solutions.
cloneRoute(rt):
Clone a given route 'rt' to create an identical route object.
- rt: The route to be cloned.
Returns the cloned route.
cloneSolution(sol):
Clone a given solution 'sol' to create an identical solution object.
- sol: The solution to be cloned.
Returns the cloned solution.
FindBestRelocationMove(rm):
Find the best relocation move to optimize the solution.
Evaluates all possible relocations and identifies the move with the greatest cost improvement.
- rm: An instance of the RelocationMove class to store the best move details.
Returns the identified best relocation move.
InitializeOperators(rm, sm, top):
Initializes the move operators for local search.
- rm: The relocation move object to be initialized.
- sm: The swap move object to be initialized.
- top: The 2-opt move object to be initialized.
FindBestTwoOptMove(top):
Finds the best 2-opt move among all possible combinations of routes and nodes.
- top: The TwoOptMove object to store the best move information.
CapacityIsViolated(rt1, nodeInd1, rt2, nodeInd2):
Checks whether the capacity of the given routes is violated after a potential 2-opt move.
- rt1: Index of the first route involved in the swap.
- nodeInd1: Index of the node in `rt1` before which the segment load is calculated.
- rt2: Index of the second route involved in the swap.
- nodeInd2: Index of the node in `rt2` before which the segment load is calculated.
Returns True if the capacity is violated. Otherwise, it return false.
StoreBestTwoOptMove(rtInd1, rtInd2, nodeInd1, nodeInd2, moveCost, top):
Stores the information of the best 2-opt move in the provided TwoOptMove object.
- rtInd1: Index of the first route in the solution.
- rtInd2: Index of the second route in the solution.
- nodeInd1: Index of the first node in the first route.
- nodeInd2: Index of the second node in the second route.
- moveCost: The cost associated with the 2-opt move.
- top: TwoOptMove object to be updated with the best move information.
ApplyTwoOptMove(top):
Applies the best 2-opt move to the solution based on the information provided in the TwoOptMove object.
- top: TwoOptMove object which contains the information about the best 2-opt move.
UpdateRouteCostAndLoad(rt: Route):
Updates the cost and load of a given route.
- rt: The route whose cost and load need to be updated.
TestSolution():
Tests the integrity of the solution by checking route costs and loads.
IdentifyMinimumCostInsertion(best_insertion):
Identifies the minimum cost insertion for a customer into existing routes.
- best_insertion: An object to store information about the best insertion.
ApplyCustomerInsertionAllPositions(insertion):
Applies the customer insertion at all possible positions within a route.
- insertion: An object containing information about the customer insertion.
"""
def __init__(self, m):
"""
Initialize the Solver with the given model 'm'.
- m: An instance of the Model class containing problem data.
"""
self.allNodes = m.allNodes
self.customers = m.customers
self.depot = m.allNodes[0]
self.distanceMatrix = m.matrix
self.capacity = m.capacity
self.sol = None
self.bestSolution = None
self.searchTrajectory = []
def solve(self):
"""
Main method to solve the VRP.
Executes a sequence of methods to construct and optimize the solution.
Returns:
--------
Solution
The optimized solution for the VRP.
"""
self.SetRoutedFlagToFalseForAllCustomers()
self.ApplyNearestNeighborMethod()
# self.MinimumInsertions()
# self.LocalSearch(1)
# self.LocalSearch(0)
self.LocalSearch(2)
self.VND()
self.ClownMove(5)
self.LocalSearch(0)
self.VND()
self.reverseRoutes()
self.randomlyPartlyReverseRoutes(5)
return self.sol
def SetRoutedFlagToFalseForAllCustomers(self):
"""
Set the 'isRouted' flag to False for all customer nodes.
Ensures that all customers are unrouted before constructing a new solution.
"""
for i in range(0, len(self.customers)):
self.customers[i].isRouted = False
def ApplyNearestNeighborMethod(self):
"""
Construct initial routes using the Nearest Neighbor method.
Continues to assign customers to routes until all are assigned or capacity constraints are violated.
Uses the distance matrix to determine the nearest customers every time.
"""
modelIsFeasible = True
self.sol = Solution()
insertions = 0
while (insertions < len(self.customers)):
bestInsertion = CustomerInsertion()
lastOpenRoute: Route = self.GetLastOpenRoute()
if lastOpenRoute is not None:
self.IdentifyBestInsertion(bestInsertion, lastOpenRoute)
if (bestInsertion.customer is not None):
self.ApplyCustomerInsertion(bestInsertion)
insertions += 1
else:
# If there is an empty available route
if lastOpenRoute is not None and len(lastOpenRoute.sequenceOfNodes) == 2:
modelIsFeasible = False
break
else:
rt = Route(self.depot, self.capacity)
self.sol.routes.append(rt)
if (modelIsFeasible == False):
print('FeasibilityIssue')
# reportSolution
# def MinimumInsertions(self):
# modelIsFeasible = True
# self.sol = Solution()
# insertions = 0
#
# while (insertions < len(self.customers)):
# bestInsertion = CustomerInsertionAllPositions()
# lastOpenRoute: Route = self.GetLastOpenRoute()
#
# if lastOpenRoute is not None:
# self.IdentifyBestInsertionAllPositions(bestInsertion, lastOpenRoute)
#
# if (bestInsertion.customer is not None):
# self.ApplyCustomerInsertionAllPositions(bestInsertion)
# insertions += 1
# else:
# # If there is an empty available route
# if lastOpenRoute is not None and len(lastOpenRoute.sequenceOfNodes) == 2:
# modelIsFeasible = False
# break
# # If there is no empty available route and no feasible insertion was identified
# else:
# rt = Route(self.depot, self.capacity)
# self.sol.routes.append(rt)
#
# if (modelIsFeasible == False):
# print('FeasibilityIssue')
# # reportSolution
#
# self.TestSolution()
def Always_keep_an_empty_route(self):
"""
Ensure there is always at least one empty route available.
Adds a new route if the last route is partially filled.
"""
if len(self.sol.routes) == 0:
rt = Route(self.depot, self.capacity)
self.sol.routes.append(rt)
else:
rt = self.sol.routes[-1]
if len(rt.sequenceOfNodes) > 2:
rt = Route(self.depot, self.capacity)
self.sol.routes.append(rt)
def MinimumInsertions(self):
"""
Construct initial routes using the minimum insertions.
Continuously identifies the best customer to insert into the current routes. (prioritizes inserting customers with the least additional distance.)
"""
model_is_feasible = True
self.sol = Solution()
insertions = 0
while insertions < len(self.customers):
best_insertion = CustomerInsertionAllPositions()
self.Always_keep_an_empty_route()
self.IdentifyMinimumCostInsertion(best_insertion)
if best_insertion.customer is not None:
self.ApplyCustomerInsertionAllPositions(best_insertion)
insertions += 1
else:
print('FeasibilityIssue')
model_is_feasible = False
break
if model_is_feasible:
self.TestSolution()
def LocalSearch(self, operator):
"""
Apply local search techniques to further optimize the solution.
Parameters:
-----------
operator : int
Type of local search operation to perform (0: Relocation, 1: Swap, 2: TwoOpt).
"""
self.bestSolution = self.cloneSolution(self.sol)
terminationCondition = False
localSearchIterator = 0
rm = RelocationMove()
sm = SwapMove()
top = TwoOptMove()
while terminationCondition is False:
self.InitializeOperators(rm, sm, top)
# SolDrawer.draw(localSearchIterator, self.sol, self.allNodes)
# Relocations
if operator == 0:
self.FindBestRelocationMove(rm)
if rm.originRoutePosition is not None:
if rm.moveCost < 0:
self.ApplyRelocationMove(rm)
else:
terminationCondition = True
# Swaps
elif operator == 1:
self.FindBestSwapMove(sm)
if sm.positionOfFirstRoute is not None:
if sm.moveCost < 0:
self.ApplySwapMove(sm)
else:
terminationCondition = True
elif operator == 2:
self.FindBestTwoOptMove(top)
if top.positionOfFirstRoute is not None:
if top.moveCost < 0:
self.ApplyTwoOptMove(top)
else:
terminationCondition = True
# self.TestSolution()
if (self.sol.cost < self.bestSolution.cost):
self.bestSolution = self.cloneSolution(self.sol)
localSearchIterator = localSearchIterator + 1
print(localSearchIterator, self.sol.cost)
self.sol = self.bestSolution
def VND(self):
"""
Perform Variable Neighborhood Descent (VND) optimization on the current solution.
This method iteratively applies three different types of moves (Relocation, Swap, and Two-Opt)
to improve the solution until convergence or a maximum number of iterations.
:return: None
"""
# Initialize the best solution with the current solution
self.bestSolution = self.cloneSolution(self.sol)
# Initialize iteration parameters
VNDIterator = 0
kmax = 2
rm = RelocationMove()
sm = SwapMove()
top = TwoOptMove()
k = 0
draw = True
# Main loop for VND iterations
while k <= kmax:
self.InitializeOperators(rm, sm, top)
# Apply Relocation Move
if k == 2:
self.FindBestRelocationMove(rm)
if rm.originRoutePosition is not None and rm.moveCost < 0:
self.ApplyRelocationMove(rm)
if draw:
SolDrawer.draw(VNDIterator, self.sol, self.allNodes)
VNDIterator = VNDIterator + 1
self.searchTrajectory.append(self.sol.cost)
k = 0
else:
k += 1
# Apply Swap Move
elif k == 1:
self.FindBestSwapMove(sm)
if sm.positionOfFirstRoute is not None and sm.moveCost < 0:
self.ApplySwapMove(sm)
if draw:
SolDrawer.draw(VNDIterator, self.sol, self.allNodes)
VNDIterator = VNDIterator + 1
self.searchTrajectory.append(self.sol.cost)
k = 0
else:
k += 1
# Apply Two-Opt Move
elif k == 0:
self.FindBestTwoOptMove(top)
if top.positionOfFirstRoute is not None and top.moveCost < 0:
self.ApplyTwoOptMove(top)
if draw:
SolDrawer.draw(VNDIterator, self.sol, self.allNodes)
VNDIterator = VNDIterator + 1
self.searchTrajectory.append(self.sol.cost)
k = 0
else:
k += 1
# Update the best solution if a better solution is found
if self.sol.cost < self.bestSolution.cost:
self.bestSolution = self.cloneSolution(self.sol)
# Draw the final best solution and the search trajectory
SolDrawer.draw('final_vnd', self.bestSolution, self.allNodes)
SolDrawer.drawTrajectory(self.searchTrajectory)
def ClownMove(self, seed: int, iterations: int = 999999):
"""
Its name comes from clowns which usually juggle balls the same way we jugle the nodes!
Randomly picks 2 pairs of nodes and swaps them
:arg seed: Pick a number 1~5
:arg iterations: How many times (999999) recommended
:returns: None
"""
def capacity_is_violated(rt1, rt2):
"""
Checks whether the capacity of the given routes is violated
"""
load_1 = 0
for node in rt1.sequenceOfNodes:
load_1 += node.demand
if load_1 > rt1.capacity:
return True
load_2 = 0
for node in rt2.sequenceOfNodes:
load_2 += node.demand
if load_2 > rt2.capacity:
return True
return False
def flip_nodes():
copy_of_route_1 = self.cloneRoute(route_1)
copy_of_route_2 = self.cloneRoute(route_2)
copy_of_route_1.sequenceOfNodes[node_position_1] = node_2_a
copy_of_route_1.sequenceOfNodes[node_position_1+1] = node_2_b
copy_of_route_2.sequenceOfNodes[node_position_2] = node_1_a
copy_of_route_2.sequenceOfNodes[node_position_2+1] = node_1_b
if capacity_is_violated(copy_of_route_1, copy_of_route_2) or \
len(copy_of_route_1.sequenceOfNodes) < 3 or len(copy_of_route_2.sequenceOfNodes) < 3:
return
else:
tn_km_1_new, _ = copy_of_route_1.calculate_route_details(self)
tn_km_1_old, _ = route_1.calculate_route_details(self)
tn_km_2_new, _ = copy_of_route_2.calculate_route_details(self)
tn_km_2_old, _ = route_2.calculate_route_details(self)
if tn_km_1_new + tn_km_2_new < (tn_km_1_old + tn_km_2_old)*1.01:
self.sol.routes[route_position_1]: Route = copy_of_route_1
self.sol.routes[route_position_2]: Route = copy_of_route_2
random.seed(seed)
iters = iterations
for _ in range(iters):
# random_seed_2 = random.randint(1,5)
# random.seed(random_seed_2)
route_position_1 = random.randint(0, len(self.sol.routes) - 1)
route_position_2 = random.randint(0, len(self.sol.routes) - 1)
route_1: Route = self.sol.routes[route_position_1]
route_2: Route = self.sol.routes[route_position_2]
try:
node_position_1 = random.randint(1, len(route_1.sequenceOfNodes)-3)
node_position_2 = random.randint(1, len(route_2.sequenceOfNodes)-3)
except Exception:
continue
node_1_a = route_1.sequenceOfNodes[node_position_1]
node_1_b = route_1.sequenceOfNodes[node_position_1+1]
node_2_a = route_2.sequenceOfNodes[node_position_2]
node_2_b = route_2.sequenceOfNodes[node_position_2+1]
if route_2 != route_1:
flip_nodes()
def cloneRoute(self, rt: Route):
"""
Create a deep copy of a given route.
Parameters:
-----------
rt : Route
The route object to be cloned.
Returns:
--------
Route
Cloned route object.
"""
cloned = Route(self.depot, self.capacity)
cloned.cost = rt.cost
cloned.load = rt.load
cloned.sequenceOfNodes = rt.sequenceOfNodes.copy()
return cloned
def cloneSolution(self, sol: Solution):
"""
Create a deep copy of a given solution.
Parameters:
-----------
sol : Solution
The solution object to be cloned.
Returns:
--------
Solution
Cloned solution object.
"""
cloned = Solution()
for i in range(0, len(sol.routes)):
rt = sol.routes[i]
clonedRoute = self.cloneRoute(rt)
cloned.routes.append(clonedRoute)
cloned.cost = self.sol.cost
return cloned
def FindBestRelocationMove(self, rm):
"""
Identify the best relocation move to improve the current solution.
Parameters:
-----------
rm : RelocationMove
Object to store the details of the best relocation move.
"""
for originRouteIndex in range(0, len(self.sol.routes)):
rt1: Route = self.sol.routes[originRouteIndex]
for originNodeIndex in range(1, len(rt1.sequenceOfNodes) - 1):
for targetRouteIndex in range(0, len(self.sol.routes)):
rt2: Route = self.sol.routes[targetRouteIndex]
for targetNodeIndex in range(0, len(rt2.sequenceOfNodes) - 1):
if originRouteIndex == targetRouteIndex and (
targetNodeIndex == originNodeIndex or targetNodeIndex == originNodeIndex - 1):
continue
A = rt1.sequenceOfNodes[originNodeIndex - 1]
B = rt1.sequenceOfNodes[originNodeIndex]
C = rt1.sequenceOfNodes[originNodeIndex + 1]
F = rt2.sequenceOfNodes[targetNodeIndex]
G = rt2.sequenceOfNodes[targetNodeIndex + 1]
if rt1 != rt2:
if rt2.load + B.demand > rt2.capacity:
continue
costAdded = self.distanceMatrix[A.ID][C.ID] + self.distanceMatrix[F.ID][B.ID] + \
self.distanceMatrix[B.ID][G.ID]
costRemoved = self.distanceMatrix[A.ID][B.ID] + self.distanceMatrix[B.ID][C.ID] + \
self.distanceMatrix[F.ID][G.ID]
originRtCostChange = self.distanceMatrix[A.ID][C.ID] - self.distanceMatrix[A.ID][B.ID] - \
self.distanceMatrix[B.ID][C.ID]
targetRtCostChange = self.distanceMatrix[F.ID][B.ID] + self.distanceMatrix[B.ID][G.ID] - \
self.distanceMatrix[F.ID][G.ID]
moveCost = costAdded - costRemoved
if (moveCost < rm.moveCost):
self.StoreBestRelocationMove(originRouteIndex, targetRouteIndex, originNodeIndex,
targetNodeIndex, moveCost, originRtCostChange,
targetRtCostChange, rm)
def FindBestSwapMove(self, sm):
"""
Finds the best swap move among all possible combinations of nodes in the solution's routes.
Args:
self: Instance of the VehicleRoutingProblem class.
sm (SwapMove): The SwapMove object to store the best swap move.
Returns:
None
This method iterates through all routes and nodes, evaluating potential swap moves.
It calculates the cost changes for each move and updates the provided SwapMove object
with the details of the best move found.
"""
for firstRouteIndex in range(0, len(self.sol.routes)):
rt1: Route = self.sol.routes[firstRouteIndex]
for secondRouteIndex in range(firstRouteIndex, len(self.sol.routes)):
rt2: Route = self.sol.routes[secondRouteIndex]
for firstNodeIndex in range(1, len(rt1.sequenceOfNodes) - 1):
startOfSecondNodeIndex = 1
if rt1 == rt2:
startOfSecondNodeIndex = firstNodeIndex + 1
for secondNodeIndex in range(startOfSecondNodeIndex, len(rt2.sequenceOfNodes) - 1):
a1 = rt1.sequenceOfNodes[firstNodeIndex - 1]
b1 = rt1.sequenceOfNodes[firstNodeIndex]
c1 = rt1.sequenceOfNodes[firstNodeIndex + 1]
a2 = rt2.sequenceOfNodes[secondNodeIndex - 1]
b2 = rt2.sequenceOfNodes[secondNodeIndex]
c2 = rt2.sequenceOfNodes[secondNodeIndex + 1]
moveCost = None
costChangeFirstRoute = None
costChangeSecondRoute = None
if rt1 == rt2:
if firstNodeIndex == secondNodeIndex - 1:
# case of consecutive nodes swap
costRemoved = self.distanceMatrix[a1.ID][b1.ID] + self.distanceMatrix[b1.ID][b2.ID] + \
self.distanceMatrix[b2.ID][c2.ID]
costAdded = self.distanceMatrix[a1.ID][b2.ID] + self.distanceMatrix[b2.ID][b1.ID] + \
self.distanceMatrix[b1.ID][c2.ID]
moveCost = costAdded - costRemoved
else:
costRemoved1 = self.distanceMatrix[a1.ID][b1.ID] + self.distanceMatrix[b1.ID][c1.ID]
costAdded1 = self.distanceMatrix[a1.ID][b2.ID] + self.distanceMatrix[b2.ID][c1.ID]
costRemoved2 = self.distanceMatrix[a2.ID][b2.ID] + self.distanceMatrix[b2.ID][c2.ID]
costAdded2 = self.distanceMatrix[a2.ID][b1.ID] + self.distanceMatrix[b1.ID][c2.ID]
moveCost = costAdded1 + costAdded2 - (costRemoved1 + costRemoved2)
else:
if rt1.load - b1.demand + b2.demand > self.capacity:
continue
if rt2.load - b2.demand + b1.demand > self.capacity:
continue
costRemoved1 = self.distanceMatrix[a1.ID][b1.ID] + self.distanceMatrix[b1.ID][c1.ID]
costAdded1 = self.distanceMatrix[a1.ID][b2.ID] + self.distanceMatrix[b2.ID][c1.ID]
costRemoved2 = self.distanceMatrix[a2.ID][b2.ID] + self.distanceMatrix[b2.ID][c2.ID]
costAdded2 = self.distanceMatrix[a2.ID][b1.ID] + self.distanceMatrix[b1.ID][c2.ID]
costChangeFirstRoute = costAdded1 - costRemoved1
costChangeSecondRoute = costAdded2 - costRemoved2
moveCost = costAdded1 + costAdded2 - (costRemoved1 + costRemoved2)
if moveCost < sm.moveCost:
self.StoreBestSwapMove(firstRouteIndex, secondRouteIndex, firstNodeIndex, secondNodeIndex,
moveCost, costChangeFirstRoute, costChangeSecondRoute, sm)
def ApplyRelocationMove(self, rm: RelocationMove):
"""
Applies the relocation move to the current solution.
Args:
rm (RelocationMove): The relocation move to be applied.
Returns:
None
This method updates the solution by relocating a node from its current position
to a new position within the same route or a different route, as specified by the
given relocation move. The solution's cost, route costs, and loads are adjusted accordingly.
"""
oldCost = self.CalculateTotalCost(self.sol)
originRt = self.sol.routes[rm.originRoutePosition]
targetRt = self.sol.routes[rm.targetRoutePosition]
B = originRt.sequenceOfNodes[rm.originNodePosition]
if originRt == targetRt:
del originRt.sequenceOfNodes[rm.originNodePosition]
if (rm.originNodePosition < rm.targetNodePosition):
targetRt.sequenceOfNodes.insert(rm.targetNodePosition, B)
else:
targetRt.sequenceOfNodes.insert(rm.targetNodePosition + 1, B)
originRt.cost += rm.moveCost
else:
del originRt.sequenceOfNodes[rm.originNodePosition]
targetRt.sequenceOfNodes.insert(rm.targetNodePosition + 1, B)
originRt.cost += rm.costChangeOriginRt
targetRt.cost += rm.costChangeTargetRt
originRt.load -= B.demand
targetRt.load += B.demand
self.sol.cost += rm.moveCost
newCost = self.CalculateTotalCost(self.sol)
# debuggingOnly
if abs((newCost - oldCost) - rm.moveCost) > 0.0001:
print('Cost Issue')
def ApplySwapMove(self, sm):
"""
Apply a swap move to the solution.
Parameters:
- sm (SwapMove): The swap move containing information about the move.
Returns:
None
"""
oldCost = self.CalculateTotalCost(self.sol)
rt1 = self.sol.routes[sm.positionOfFirstRoute]
rt2 = self.sol.routes[sm.positionOfSecondRoute]
b1 = rt1.sequenceOfNodes[sm.positionOfFirstNode]
b2 = rt2.sequenceOfNodes[sm.positionOfSecondNode]
rt1.sequenceOfNodes[sm.positionOfFirstNode] = b2
rt2.sequenceOfNodes[sm.positionOfSecondNode] = b1
if (rt1 == rt2):
rt1.cost += sm.moveCost
else:
rt1.cost += sm.costChangeFirstRt
rt2.cost += sm.costChangeSecondRt
rt1.load = rt1.load - b1.demand + b2.demand
rt2.load = rt2.load + b1.demand - b2.demand
self.sol.cost += sm.moveCost
newCost = self.CalculateTotalCost(self.sol)
# debuggingOnly
if abs((newCost - oldCost) - sm.moveCost) > 0.0001:
print('Cost Issue')
def ReportSolution(self, sol):
"""
Print a report of the given solution.
Parameters:
- sol (Solution): The solution to be reported.
Returns:
None
"""
for i in range(0, len(sol.routes)):
rt = sol.routes[i]
for j in range(0, len(rt.sequenceOfNodes)):
print(rt.sequenceOfNodes[j].ID, end=' ')
print(rt.cost)
print(self.sol.cost)
def GetLastOpenRoute(self):
"""
Get the last open route in the current solution.
Returns:
- Route or None: The last open route if routes exist, otherwise None.
"""
if len(self.sol.routes) == 0:
return None
else:
return self.sol.routes[-1]
def IdentifyBestInsertion(self, bestInsertion, rt):
"""
Identifies the best customer insertion for a given route, considering capacity constraints.
Parameters:
- bestInsertion (CustomerInsertion): An object to store information about the best insertion.
- rt (Route): The route for which the insertion is being considered.
Returns:
None
Modifies the bestInsertion object with the details of the best customer insertion.
"""
for i in range(1, len(self.customers)):
candidateCust: Node = self.customers[i]
if candidateCust.isRouted is False:
if rt.load + candidateCust.demand <= rt.capacity:
lastNodePresentInTheRoute = rt.sequenceOfNodes[-2]
trialCost = self.distanceMatrix[lastNodePresentInTheRoute.ID][candidateCust.ID]
if trialCost < bestInsertion.cost:
bestInsertion.customer = candidateCust
bestInsertion.route = rt
bestInsertion.cost = trialCost
def ApplyCustomerInsertion(self, insertion):
"""
Applies the customer insertion to the given route in the solution.
Parameters:
- insertion (CustomerInsertion): An object containing information about the customer insertion.
Returns:
None
Modifies the route and solution to reflect the applied customer insertion.
"""
insCustomer = insertion.customer
rt = insertion.route
# before the second depot occurrence
insIndex = len(rt.sequenceOfNodes) - 1
rt.sequenceOfNodes.insert(insIndex, insCustomer)
beforeInserted = rt.sequenceOfNodes[-3]
costAdded = self.distanceMatrix[beforeInserted.ID][insCustomer.ID] + self.distanceMatrix[insCustomer.ID][
self.depot.ID]
costRemoved = self.distanceMatrix[beforeInserted.ID][self.depot.ID]
rt.cost += costAdded - costRemoved
self.sol.cost += costAdded - costRemoved
rt.load += insCustomer.demand
insCustomer.isRouted = True
def StoreBestRelocationMove(self, originRouteIndex, targetRouteIndex, originNodeIndex, targetNodeIndex, moveCost,
originRtCostChange, targetRtCostChange, rm: RelocationMove):
"""
Stores the information about the best relocation move in the given RelocationMove object.
Parameters:
- originRouteIndex (int): Index of the origin route.
- targetRouteIndex (int): Index of the target route.
- originNodeIndex (int): Index of the node in the origin route.
- targetNodeIndex (int): Index of the node in the target route.
- moveCost (float): Cost change due to the relocation move.
- originRtCostChange (float): Cost change in the origin route.