From e9690cd03abe3e70afcc7669cb260049224732b6 Mon Sep 17 00:00:00 2001
From: Dipo Olaitan <oladipupo.olaitan2@mail.dcu.ie>
Date: Fri, 4 Sep 2015 13:24:25 +0200
Subject: [PATCH] ACO Updates and Default Shift Pattern in Exceptions Sheet

---
 dream/plugins/ACO.py                  | 30 ++++++++++++++++++++++-----
 dream/plugins/JobShop/JobShopACO.py   | 13 ++++++++++++
 dream/plugins/JobShop/ReadJSShifts.py | 11 +++++++---
 3 files changed, 46 insertions(+), 8 deletions(-)

diff --git a/dream/plugins/ACO.py b/dream/plugins/ACO.py
index 54cc640a..5d4f6150 100644
--- a/dream/plugins/ACO.py
+++ b/dream/plugins/ACO.py
@@ -23,7 +23,12 @@ class ACO(plugin.ExecutionPlugin):
     """Calculate the score of this ant. Implemented in the Subclass, raises NotImplementedError
     """
     raise NotImplementedError("ACO subclass must define '_calculateAntScore' method")
-
+  
+  def _calculateAntActualDelay(self, ant):
+    """Calculate the score of this ant. Implemented in the Subclass, raises NotImplementedError
+    """
+    raise NotImplementedError("ACO subclass must define '_calculateAntActualDelay' method")
+  
   def createCollatedScenarios(self,data):
     """creates the collated scenarios, i.e. the list of options collated into a dictionary for ease of referencing in ManPy
     to be implemented in the subclass
@@ -51,14 +56,16 @@ class ACO(plugin.ExecutionPlugin):
     tested_ants = set()
     start = time.time()         # start counting execution time
 
-    collated=self.createCollatedScenarios(data)    
+    collated=self.createCollatedScenarios(data) 
+    resetCollated = deepcopy(collated)#starting pheromone level, use for reset after every generation
     assert collated 
 
     max_results = int(data['general'].get('numberOfSolutions',0))
     assert max_results >= 1
 
     ants = [] #list of ants for keeping track of their performance
-
+    solutionConvergence = [] #for monitoring solution convergence
+    
     # Number of times new ants are to be created, i.e. number of generations (a
     # generation can have more than 1 ant)
     seedPlus = 0
@@ -133,6 +140,7 @@ class ACO(plugin.ExecutionPlugin):
 
         for ant in scenario_list:
             ant['score'] = self._calculateAntScore(ant)
+            ant['actualDelays'] = self._calculateAntActualDelay(ant)
 
         ants.extend(scenario_list)
 
@@ -147,9 +155,14 @@ class ACO(plugin.ExecutionPlugin):
 
         # The ants in this generation are ranked based on their scores and the
         # best (max_results) are selected
-        ants = sorted(ants_without_duplicates.values(),
+        if max([ant['score'] for ant in ants]) == 0:#if all ants achieved zero delays, then use their earliness values
+            ants = sorted(ants_without_duplicates.values(),
+          key=operator.itemgetter('actualDelays'))[:max_results]
+        else:
+            ants = sorted(ants_without_duplicates.values(),
           key=operator.itemgetter('score'))[:max_results]
-
+        
+        collated = deepcopy(resetCollated)#reset previous pheromone updates
         for l in ants:
             # update the options list to ensure that good performing queue-rule
             # combinations have increased representation and good chance of
@@ -159,6 +172,13 @@ class ACO(plugin.ExecutionPlugin):
                 # 'EDD' is added to Q1 so there is a higher chance that it is
                 # selected by the next ants.
                 collated[m].append(l[m])
+        
+        #termination criterion: after four generations check if the solution has been improving
+        solutionConvergence.append(max([ant['actualDelays'] for ant in ants])) #add the worst solution after the last generation
+        solutionConvergence.sort()
+        del(solutionConvergence[int(data['general'].get('numberOfSolutions',0)):]) #extract the last 4 worst solutions among the best, if there has been no improvement, terminate the optimisation process
+        if len(solutionConvergence) == int(data['general'].get('numberOfSolutions',0)) and max(solutionConvergence) == min(solutionConvergence):
+            break
 
     data['result']['result_list'] = result_list = []
     for ant in ants:
diff --git a/dream/plugins/JobShop/JobShopACO.py b/dream/plugins/JobShop/JobShopACO.py
index b66e6faf..d5266a8e 100644
--- a/dream/plugins/JobShop/JobShopACO.py
+++ b/dream/plugins/JobShop/JobShopACO.py
@@ -24,6 +24,19 @@ class JobShopACO(ACO):
             # should not be considered better than being on time.
             totalDelay += max(delay, 0)
     return totalDelay
+  
+  def _calculateAntActualDelay(self, ant):
+    antActualDelay = 0 #used to further compare ants with the earliness if they all had zero delay
+    result, = ant['result']['result_list']  #read the result as JSON
+    #loop through the elements
+    for element in result['elementList']:
+        element_family = element.get('family', None)
+        #id the class is Job
+        if element_family == 'Job':
+            results=element['results']
+            delay = float(results.get('delay', "0"))
+            antActualDelay += delay
+    return antActualDelay
 
   # creates the collated scenarios, i.e. the list 
   # of options collated into a dictionary for ease of referencing in ManPy
diff --git a/dream/plugins/JobShop/ReadJSShifts.py b/dream/plugins/JobShop/ReadJSShifts.py
index 2d862a29..1073ab14 100644
--- a/dream/plugins/JobShop/ReadJSShifts.py
+++ b/dream/plugins/JobShop/ReadJSShifts.py
@@ -52,7 +52,12 @@ class ReadJSShifts(plugin.InputPreparationPlugin, TimeSupportMixin):
     exceptionShiftPattern = {} # exceptions for shift pattern dictionary as defined in the spreadsheet
     
     if shiftData:
-      shiftData.pop(0)
+      shiftData.pop(0)#remove headers from the shiftData
+      standardStartTime = shiftData[0][2]
+      standardEndTime = shiftData[0][3]
+      shiftData.pop(0) #remove standard times declared on the first line from the shiftData
+      #iteration through the raw data to structure it into ManPy config
+      lastrec=None
       #iteration through the raw data to structure it into ManPy config
       lastrec=None
       for line in shiftData:
@@ -71,12 +76,12 @@ class ReadJSShifts(plugin.InputPreparationPlugin, TimeSupportMixin):
         #if no shift start was given, assume standard 8:00
         startTime = line[2]
         if startTime == '' or startTime == None:
-          startTime = "08:00"
+          startTime = standardStartTime
         shiftStart = self.convertToSimulationTime(strptime("%s %s" % (line[1], startTime), '%Y/%m/%d %H:%M'))
         #if no shift end was given, assume standard 18:00
         endTime = line[3]
         if endTime == '' or endTime == None:
-          endTime = "18:00"
+          endTime = standardEndTime
         shiftEnd = self.convertToSimulationTime(strptime("%s %s" % (line[1], endTime), '%Y/%m/%d %H:%M'))
         timePair = self.correctTimePair(shiftStart, shiftEnd)
         if not timePair:
-- 
2.30.9