├── GAS v4.00.py ├── GASv1.00.py ├── GASv2.00.py ├── GASv3.00.py ├── GASv5.00.py ├── automated test results.xlsx └── examples.xlsx /GAS v4.00.py: -------------------------------------------------------------------------------- 1 | # created by svinec (2019) - use freely but please reference my original work, thanks :) 2 | 3 | """ 4 | SHORT INTRO 5 | 6 | Operation scheduling is a classical problem in Operations Research. The problem arises when there are some number of operations that need to be 7 | assigned to different resources for execution, and we don't know what the optimal way of assinging is. 8 | 9 | Operations in this context can be anything from simple everyday tasks to complex manufacturing operations in a production facility. 10 | Resources in this context is any entity or unit that can do work, for example a machine can be a resource, a human worker can be a resources, 11 | or a whole team of people can be a resource - this is just an abstraction layer. 12 | Optimal solution can have different meanings depending on the objective of the problem (i.e. it can be most constraints respected, least amount 13 | of total time needed, etc.) 14 | 15 | The scheduling problem easily becomes very complex when we have to respect different types of constraints and different given parameters of the 16 | model. There are simply overwhelmingly too many possible ways of assigning a given set of operations to a given set of resources, so it is not 17 | easy to tell which is the best or optimal way. 18 | 19 | The complexity of this and other similar problems, means that there is no manageable mathematical formula or procedure that can be used to 20 | reliably calculate an optimal solution to any given problem. People have tried various methods of tackling this problem, one of which is a Genetic 21 | Algorithm. Generally speaking a GA is a form of a brute force search, i.e. it can search the whole space of solutions, but it won't do it one 22 | by one. It will use the solutions it already has and take their good traits in order to predict the next better solution. It will keep doing so 23 | until a good-enough solutuon is found. In doing so it mimicks the natural process of biological evolution, hence the name Genetic Algorithm. 24 | Some more details can be found in this video https://www.youtube.com/watch?v=e84aLKGWtW4 25 | """ 26 | 27 | """ 28 | CHANGE LOG 29 | 30 | v4.00 31 | - reset function 32 | - automated testing and dumping script 33 | - average score calculation now caclulates average score within populations and then across populations (it used to take the best member of each population and then average it across populations) 34 | 35 | v3.00 36 | - two new complex examples 37 | - printRandom and printAllScores 38 | - mutation probability and mutation size 39 | - average score calculation based on a sample of the latest populations 40 | - specify crossMinStep 41 | - specify crossMinStep and crossMaxStep in relative terms or absolute 42 | 43 | v2.00 added features 44 | - resource dependent operation duration and default duration for operation, plus scoring for fastest resources used 45 | - can view separate scores for each scoring method 46 | - improved performance - all scoring done in one iteration and not separate 47 | 48 | v1.00 added features 49 | - number of resources and resource succession weights 50 | - operation time independent on resource 51 | - operation relations with min and max offset and weights for each relation 52 | - normal / asap / alap mode 53 | - history keeping and retry count 54 | - infuse random members to the population 55 | - cross mode - max step 56 | """ 57 | 58 | import time, datetime 59 | from random import randint 60 | dtnow = datetime.datetime.now # a shortcut for logging messages 61 | 62 | 63 | class GAS(): 64 | """ This is the main class. It is self-sufficient, meaning that every instance of the class has its own set of parameters, operations, resource, etc. 65 | and can function on its own. Each instance of the class can capture only one problem and solve it.""" 66 | 67 | def __init__( self, _parameters ): 68 | # When an instance is created, we take the input parameters and store them inside the instance. We also do some calculations (further below). 69 | self.resourceCount = int( _parameters[ "resourceCount" ] ) # The number of resources [1 <= integer < inf] 70 | self.populationSize = int( _parameters[ "populationSize" ] ) # The size of the population [1 <= integer < inf ] (a population is a collection of solutions, the number of solutions is the population size) 71 | self.population = [] # A container for the population [list of dictionaries {'start_times':[] , 'resources':[], 'score':int, 'genome':str}] 72 | self.survivalRate = float( _parameters[ "survivalRate" ] ) # What percent of the population survives on each breeding cycle [0.0 <= float <= 1.0] 73 | self.infuseRandomToPopulation = int( _parameters[ "infuseRandomToPopulation" ] ) # How many random solutions to add to the population on each breeding cycle [0 <= integer < inf] 74 | self.mutationProbability = float( _parameters[ "mutationProbability" ] ) # The probability of mutating the new genome after crossing the two genomes [0.0000 <= float <= 1.0000]. For example, a probability of 0.33 means that about one third of the new genomes generated on each breeding cycle will be mutated. 75 | self.mutationSize = float( _parameters[ "mutationSize" ] ) if type( _parameters[ "mutationSize" ] ) is float else int( _parameters[ "mutationSize" ] ) 76 | # Mutation size controlls how many bits in the genome will have an attempted mutation. "Attempted" because there is a 50/50 chance to change from 0 to 1 or from 1 to 0. 77 | # If expressed as [0.0 <= float <= 1.0] then represents the relative size of the genome and an integer will be calculated later. 78 | # If expressed as [0 <= integer < inf] then it is the exact number of attempted mutations on bits from the genome. 79 | self.asapAlapMode = str( _parameters[ "asapAlapMode" ] ) 80 | # Controlls how time constraints are scored [string]. Three modes are possible: 81 | # 'normal' - An operation relation will get a negative score only if the relation is outside of the Min and Max offsets defined for that relation 82 | # 'asap' - As soon as possible. Solutions that complete faster are scored higher. 83 | # 'alap' - As late as possible. Solutions that complete as late as possible are scored higher. 84 | self.weightResourceSuccession = int( _parameters[ "weightResourceSuccession" ] ) # Resource Succession means that each resource should be working on no more than one operation at any given time. Generated solutions might violate this constraint. If a constraint is violated then the solution is scored negatively with weightResourceSuccession [0 <= integer < inf]. It is a simple substraction from the total score therefore must be used wisely in conjunction with other scoring. For example, if you choose one unit of time to be one minute, and a solution violates an operation relation by 2 hours, e.g. 120, you might be okay with that if it's not critical, but if the Resource Succession is more critical for you then the weight should be something like 3000. 85 | self.historyKeep = bool( _parameters[ "historyKeep" ] ) # This option will force the algorithm to keep breeding new solutions until the new population has only unique solutions (the uniqueness is across all previous solutions) [boolean]. This can be incredibly slow and is generally discouraged. It's much better to cycle through a few repetitve solutions that to search a log of thousand previous solutions. 86 | self.historyRetryCount = int( _parameters[ "historyRetryCount" ] ) # Because finding a unique solution can sometime be very slow, this option tells the algoritm how many times to try before accepting a duplicate solution and adding to the new population [0 <= integer < inf] 87 | self.history = [] # A container for the history log [list of tuples (Start Times, Resources)] 88 | self.averageScoreSampleSize = int( _parameters[ "averageScoreSampleSize" ] ) # The average score is based on the best solutions from the last N generations [0 for disabled, else 1 <= integer < inf]. This can be a useful indicator if the solver is improving the solution over time or not. 89 | self.averageScoreSample = [] # A container for the best scores of the last N generations [list of integers] 90 | self.averageScore = None # The average score of the current solver 91 | 92 | self.operationDurations = {} 93 | # A definition of how much time each operation takes to complete. [dictionary] This is a unitless definition using integers. The meaning is assigned by the user, e.g. 1 can be one minute, one hour, one day, one 15-minute chunk, etc. Each operation duration can be defined in one of two different ways: 94 | # 1) If all resources take the same amount of time to complete the operation then [1 <= integer < inf] 95 | # 2) If different resources complete the operation in different amount of time then [list of integers, where the index matches the resource index] 96 | for i in _parameters[ "operationDurations" ]: 97 | if type( _parameters[ "operationDurations" ][ i ] ) is int: 98 | self.operationDurations[ i ] = int( _parameters[ "operationDurations" ][ i ] ) 99 | elif type( _parameters[ "operationDurations" ][ i ] ) is list: 100 | self.operationDurations[ i ] = list( _parameters[ "operationDurations" ][ i ] ) # copy by value not by reference 101 | else: 102 | print( "{}\tInvalid operation duration: {}, type: {}\nTerminating".format( dtnow(), _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 103 | return False 104 | 105 | self.operationCount = len( self.operationDurations ) # The number of operations [1 <= integer < inf] 106 | 107 | self.operationRelations = {} 108 | # A dictionary of two more nested dictionaries that stores operation relations. The structure is operationRelations[ op2 ][ op1 ][ parameter ], where: 109 | # 'op2' is the second operation in the relation 110 | # 'op1' is the first operation in the relation 111 | # 'parameter' can be either of four types of parameters: 112 | # - 'type' - available types or relations are: 113 | # - 'SS' - start-to-start - the start of the first operation relates to the start of the second operation 114 | # - 'SE' - start-to-end - the start of the first operation relates to the end of the second operation 115 | # - 'ES' - end-to-start - the end of the first operation relates to the start of the second operation 116 | # - 'EE' - end-to-end - the end of the first operation relates to the end of the second operation 117 | # - 'min' - the minimum time for the relation (for example, if the relation type is 'ES' and the min time is 10, it means that the second operation should start 10 units of time after the end of the first operation or later, but not sooner) 118 | # - 'max' - the maximum time for the relation (for example, if the relation type is 'ES' and the min time is 30, it means that the second operation should start 30 units of time after the end of the first operation or sooner, but not later) 119 | # - 'weight' - a custom weight used to fine-tune the scoring of schedules, default is 1 120 | 121 | for op2 in _parameters[ "operationRelations" ]: # copy by value not by reference 122 | self.operationRelations[ op2 ] = {} 123 | for op1 in _parameters[ "operationRelations" ][ op2 ]: 124 | self.operationRelations[ op2 ][ op1 ] = dict( _parameters[ "operationRelations" ][ op2 ][ op1 ] ) 125 | 126 | # Evaluate the asapAlapMode 127 | for op2 in self.operationRelations: 128 | for op1 in self.operationRelations[ op2 ]: 129 | if self.asapAlapMode == "normal": 130 | if self.operationRelations[ op2 ][ op1 ][ "min" ] == None and self.operationRelations[ op2 ][ op1 ][ "max" ] == None: 131 | # If the mode is 'normal' and min and max are not specified, then we need at leas a min definition 132 | self.operationRelations[ op2 ][ op1 ][ "min" ] = 0 133 | elif self.asapAlapMode == "asap": 134 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 135 | # If the mode is 'asap' then we want to score solutions as if there is no later execution allowed 136 | self.operationRelations[ op2 ][ op1 ][ "max" ] = int( self.operationRelations[ op2 ][ op1 ][ "min" ] ) 137 | elif self.asapAlapMode == "alap": 138 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 139 | # If the mode is 'alap' then we want to score solutions as if there is no early execution allowed 140 | self.operationRelations[ op2 ][ op1 ][ "min" ] = int( self.operationRelations[ op2 ][ op1 ][ "max" ] ) 141 | 142 | self.operationMaxTime = 0 # The longest possible solution [1 <= integer < inf]. This is used later to find what the minimum lenght of the genome is in order to allow to represent all possible solutions 143 | for op in range( self.operationCount ): # It is a sum of all operation durations... 144 | if type( self.operationDurations[ op ] ) is int: 145 | self.operationMaxTime += self.operationDurations[ op ] 146 | else: 147 | self.operationMaxTime += max( self.operationDurations[ op ] ) 148 | for op2 in self.operationRelations: # ...plus the sum of the largest relation offsets 149 | for op1 in self.operationRelations[ op2 ]: 150 | rel_min = self.operationRelations[ op2 ][ op1 ][ "min" ] if self.operationRelations[ op2 ][ op1 ][ "min" ] != None else 0 151 | rel_max = self.operationRelations[ op2 ][ op1 ][ "max" ] if self.operationRelations[ op2 ][ op1 ][ "max" ] != None else 0 152 | self.operationMaxTime += max( abs( rel_min ), abs( rel_max ) ) 153 | 154 | # When two genomes are combined into a new one, this is done by splitting both genomes in steps. crossMinStep defines the minimum length of the step and crossMaxStep defines the maximum lenght of the step. crossMinStep must be less than or equal to crossMaxStep. They can be defined in one of two ways: 155 | # If expressed as [0.0 <= float <= 1.0] then it represents the size of the step relative to the genome length 156 | # If expressed as [0 <= integer < inf] then it is an exact number of characters (zeroes or ones) 157 | if type( _parameters[ "crossMinStep" ] ) is float: 158 | self.crossMinStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * _parameters[ "crossMinStep" ] ) ) 159 | else: 160 | self.crossMinStep = int( _parameters[ "crossMinStep" ] ) 161 | 162 | if type( _parameters[ "crossMaxStep" ] ) is float: 163 | self.crossMaxStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * _parameters[ "crossMaxStep" ] ) ) 164 | else: 165 | self.crossMaxStep = int( _parameters[ "crossMaxStep" ] ) 166 | 167 | # only reset runtime data so the model can be run again, but keep the parameters 168 | def reset( self ): 169 | self.population = [] 170 | self.history = [] 171 | self.averageScoreSample = [] 172 | self.averageScore = None 173 | 174 | # for a given operation (and resource) return the duration of the operation 175 | def getOperationDuration( self, _op, _r = 0 ): 176 | if type( self.operationDurations[ _op ] ) is int: 177 | return int( self.operationDurations[ _op ] ) 178 | elif type( self.operationDurations[ _op ] ) is list: 179 | return int( self.operationDurations[ _op ][ _r ] ) 180 | else: 181 | print( "{}\tInvalid operation duration: {}, type: {}\nTerminating".format( dtnow(), _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 182 | return False 183 | 184 | # add n number of random individuals to the population 185 | def addRandomToPopulation( self, _n ): 186 | for n in range( _n ): 187 | start_times = [ randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 188 | resources = [ randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 189 | 190 | if self.historyKeep == True: 191 | for i in range( self.historyRetryCount ): 192 | if ( start_times, resources ) not in self.history: 193 | self.history.append( ( list( start_times ), list( resources ) ) ) 194 | break 195 | start_times = [ randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 196 | resources = [ randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 197 | 198 | self.population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": "" } ) 199 | 200 | return True 201 | 202 | def scorePopulation( self ): 203 | for p in self.population: # for every member of the population do the below: 204 | p[ "score" ] = 0 # set the score to zero to clear previous scoring 205 | 206 | # Operation Relations - This section will score members based on whether the operation relations are violated or not 207 | # all operations are iterated... it looks a bit messy, but this it is actually well structured and this is what you get in order to check and score all different combinations 208 | for op2 in self.operationRelations: # as you can see here, it is important that the model is defined accurately otherwise can run into IndexErrors and KeyErrors 209 | start2 = p[ "start_times" ][ op2 ] 210 | end2 = p[ "start_times" ][ op2 ] + self.getOperationDuration( op2, p[ "resources" ][ op2 ] ) 211 | 212 | for op1 in self.operationRelations[ op2 ]: 213 | start1 = p[ "start_times" ][ op1 ] 214 | end1 = p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ) 215 | 216 | if self.operationRelations[ op2 ][ op1 ][ "type" ] == "SS": 217 | 218 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 219 | threshold_min = start2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 220 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 221 | elif self.asapAlapMode == "asap": 222 | p[ "score" ] -= start2 223 | 224 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 225 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 226 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 227 | elif self.asapAlapMode == "alap": 228 | p[ "score" ] += start2 229 | 230 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "SE": 231 | 232 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 233 | threshold_min = end2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 234 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 235 | elif self.asapAlapMode == "asap": 236 | p[ "score" ] -= start2 237 | 238 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 239 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 240 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 241 | elif self.asapAlapMode == "alap": 242 | p[ "score" ] += start2 243 | 244 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "ES": # if the relation between op1 and op2 is End-to-Start, meaning op2 cannot start until op1 has ended (one of the most common type of relations): 245 | 246 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: # (a) If there is a min offset specified then we take it into account by adjusting the score... 247 | threshold_min = start2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) # (b) The start of op2 should be greater than the end of op1 plus the min offset... 248 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] # (b) ... otherwise, subtract (it is already negative) the difference from the score adjusted by the specific weight for this relation. In this way the smaller the violation of the min offset, the better the score. 249 | elif self.asapAlapMode == "asap": # (a) ... Otherwise, check if the mode is 'asap'. This is mutually exclusive with a min offset that's why it is in an 'elif' statement. In this case subtract the start time of op2 from the score - the sooner all operations start the better the score will be. 250 | p[ "score" ] -= start2 251 | 252 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: # (c) If there is a max offset specified then we take it into account by adjusting the score... 253 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 # (d) The start of op2 should be no greater than the end of op1 plus the max offset... 254 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] # (d) ... otherwise, subtract (it is already negative) the difference from the score adjusted by the specific weight for this relation. In this way the smaller the violation of the max offset, the better the score. 255 | elif self.asapAlapMode == "alap": # (c) ... Otherwise, check if the mode is 'alap'. This is mutually exclusive with a max offset that's why it is in an 'elif' statement. In this case add the start time of op2 to the score - the later all operations start the better the score will be. 256 | p[ "score" ] += start2 257 | 258 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "EE": 259 | 260 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 261 | threshold_min = end2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 262 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 263 | elif self.asapAlapMode == "asap": 264 | p[ "score" ] -= start2 265 | 266 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 267 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 268 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 269 | elif self.asapAlapMode == "alap": 270 | p[ "score" ] += start2 271 | else: 272 | print( "{}\tInvalid relation type {} at self.operationRelations[ {} ][ {} ][ 'type' ]".format( dtnow(), self.operationRelations[ op2 ][ op1 ][ "type" ], op2, op1 ) ) 273 | return False 274 | 275 | p[ "score_operationRelations" ] = int( p[ "score" ] ) # 'score' is the main score used, 'score_operationRelations' is just to store this score separately 276 | 277 | 278 | # Resource Succession - This section will score members based on whether resources have been assigned one operation at a time or not 279 | p[ "score_resourceSuccession" ] = int( p[ "score" ] ) 280 | 281 | sorted_operations = [] # build a list of all operations and resources which will be sorted, the list is composed of tuples: ( operations id, start time, resource id ) 282 | for i in range( self.operationCount ): 283 | sorted_operations.append( ( i, int( p[ "start_times" ][ i ] ), int( p[ "resources" ][ i ] ) ) ) 284 | sorted_operations.sort( key = lambda x: ( x[ 2 ], x[ 1 ] ) ) # first sort by resource id, then by operation start time 285 | 286 | for i in range( 1, self.operationCount ): # iterate from the second operation to the end 287 | op1 = sorted_operations[ i-1 ][ 0 ] 288 | op2 = sorted_operations[ i ][ 0 ] 289 | r1 = sorted_operations[ i-1 ][ 2 ] 290 | r2 = sorted_operations[ i ][ 2 ] 291 | 292 | if r1 == r2: # if the resource id is the same between two entries then we need to check if there is overlap of operations on that resource 293 | # we do this by checking if one operation starts before the other one has finished 294 | if p[ "start_times" ][ op2 ] < p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ): 295 | p[ "score" ] -= self.weightResourceSuccession # and if yes, reduce the total score by weightResourceSuccession 296 | 297 | p[ "score_resourceSuccession" ] = int( p[ "score" ] - p[ "score_resourceSuccession" ] ) # 'score' is the main score used, 'score_resourceSuccession' is just to store this score separately 298 | 299 | 300 | # Fastest Resource - This section will score members based on whether operations are being assigned to the resources that will execute them the fastest 301 | p[ "score_fastestResource" ] = int( p[ "score" ] ) 302 | 303 | for op in range( self.operationCount ): 304 | # It simply means subtracting the operation duration of the currently assigned resource from the total score. Thus, schedules where fastest resources are used will have higher scores overall. 305 | p[ "score" ] -= self.getOperationDuration( op, p[ "resources" ][ op ] ) 306 | 307 | p[ "score_fastestResource" ] = int( p[ "score" ] - p[ "score_fastestResource" ] ) # 'score' is the main score used, 'score_fastestResource' is just to store this score separately 308 | 309 | return True 310 | 311 | # for every member of the population, calculate a genome by taking start times and resource ids and convering to a string of zeroes and ones 312 | def calculatePopulationGenome( self ): 313 | resourceCount = self.resourceCount - 1 if self.resourceCount > 1 else 1 314 | 315 | for p in self.population: 316 | p[ "genome" ] = "" # first, clear existing genome 317 | for i in range( self.operationCount ): # then, for every operation in the model convert start time and resource id to string and append to the genome in the same order 318 | p[ "genome" ] += self.numberToString( p[ "start_times" ][ i ], self.operationMaxTime ) 319 | p[ "genome" ] += self.numberToString( p[ "resources" ][ i ], resourceCount ) 320 | return True 321 | 322 | # A generic function handles both start time and resource id conversion. This is possible because numbers are encoded as the number of 1s in a string, thus 0010111011 is the number 6 because there are six ones 323 | def numberToString( self, _number, _length ): # the functions needs to know the number and the maximum number possible, which is eiher operationMaxTime or resourceCount 324 | number = int( _number ) # the number itself, or also the number of ones 325 | padding = int( _length - _number ) # padding is the number of zeroes 326 | probability = int( round( 100 * ( padding / _length ) ) ) # We want to space out ones and zeroes evenly and we can do this using a probability. For example, we don't want to have 1111110000, instead we want something like 0010111011 327 | string = "" 328 | while number + padding > 0: # the loop works by consuming the number and the padding, once these are consumed our job is done and the loop stops 329 | if number == 0: # if have no more 1s left to assign, then we assign a 0... 330 | string += "0" 331 | padding -= 1 332 | continue # ... and continue because there might be more 0s to assign 333 | if padding == 0: # if have no more 0s left to assign, then we assign a 1... 334 | string += "1" 335 | number -= 1 336 | continue # ... and continue because there might be more 1s to assign 337 | if randint( 0, 100 ) < probability: # otherwise, there are still both 1s and 0s to assign, so the probability helps us pick which one to assign next in order to space them evenly 338 | string += "0" 339 | padding -= 1 340 | else: 341 | string += "1" 342 | number -= 1 343 | return string 344 | 345 | # This is the heart of everything. When this method is called it drives all the logic and processing. One call of the method is equal to one cycle of evolutiom, meaning we start with one population and end up with a different one which s derived from the first one. Needless to say, the order of actions below matters. 346 | def breedPopulation( self, do_print = False ): 347 | self.scorePopulation() # first, whatever population we have, we want to score it 348 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) # then sort it by descending score, meaning highest score first 349 | 350 | if do_print: self.printBestNormalized() 351 | 352 | # this is where we capture information about the average score calculation 353 | if self.averageScoreSampleSize > 0: 354 | average = sum( [ p[ "score" ] for p in self.population ] ) # calculate the average score for the whole population 355 | average = int( average / self.populationSize ) 356 | self.averageScoreSample.append( average ) # append it to the list that tracks average score across populations 357 | if len( self.averageScoreSample ) > self.averageScoreSampleSize: # if we have more samples than what is defined... 358 | del self.averageScoreSample[ 0 ] # ... remove the earliest one and leave the rest 359 | self.averageScore = sum( self.averageScoreSample ) / self.averageScoreSampleSize # calculate the average score across populations and save it as current - this can later be printed 360 | 361 | # we are now in a position where we can discard members from the current population 362 | survivors = int( round( self.survivalRate * self.populationSize ) ) # the number of members to keep / survive 363 | for i in range( survivors, self.populationSize ): # the rest are deleted 364 | del self.population[ -1 ] # [-1] means last element and since this is sorted by descending score, we are laywas discarding the worst members 365 | 366 | # now is the time to add random members to the population if the model specifies so 367 | if self.infuseRandomToPopulation > 0: 368 | self.addRandomToPopulation( self.infuseRandomToPopulation ) 369 | #self.calculatePopulationGenome() 370 | self.scorePopulation() 371 | 372 | # create genomes for all members of the current population so we can start breeding the population 373 | self.calculatePopulationGenome() 374 | 375 | new_population = [] # first we build the new population and then we assign it to the model 376 | for n in range( self.populationSize ): # we generate the same number of members for the new population 377 | p1 = randint( 0, len( self.population ) - 1 ) # pick two random members from the current population 378 | p2 = randint( 0, len( self.population ) - 1 ) 379 | 380 | genome1 = str( self.population[ p1 ][ "genome" ] ) # take their genomes 381 | genome2 = str( self.population[ p2 ][ "genome" ] ) 382 | 383 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) # and combine them into a new genome 384 | start_times, resources = self.genomeToValues( new_genome ) # then convert the new genome back to start times and resource ids 385 | 386 | if self.historyKeep == True: # if history tracking is switched on, we need to save the new members to the history log 387 | for i in range( self.historyRetryCount ): # 388 | if ( start_times, resources ) not in self.history: # if the new member is not in the history log, then add it, otherwise keep trying to generate a new member until a unique one is found or until the maximum number of tries is exhausted 389 | self.history.append( ( list( start_times ), list( resources ) ) ) 390 | break 391 | genome1 = str( self.population[ randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 392 | genome2 = str( self.population[ randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 393 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 394 | start_times, resources = self.genomeToValues( new_genome ) 395 | 396 | # add the new member to the new population 397 | new_population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": str( new_genome ) } ) 398 | 399 | self.population.clear() # clear the existing population 400 | self.population = list( new_population ) # and assign the new population 401 | 402 | return True 403 | 404 | # take two genomes, combine them randomly and return a new one 405 | def crossTwoGenomes( self, _genome1, _genome2 ): 406 | genome_length = len( _genome1 ) 407 | index = 0 408 | result_genome = ""; 409 | 410 | while True: 411 | step = randint( self.crossMinStep, self.crossMaxStep ) # each time define a new random step between the min and max limit 412 | if step > genome_length - ( index + 1 ): # if the step goes beyond the end of the genome, then we only need to take what's left from the genome 413 | if randint( 0, 99 ) < 50: # randomly choose which genome to copy data from 414 | result_genome += _genome1[ index : ] 415 | else: 416 | result_genome += _genome2[ index : ] 417 | break 418 | if randint( 0, 99 ) < 50: # otherwise, the step is short from the end of the genome so take the step and again randomly choose which genome to copy data from 419 | result_genome += _genome1[ index : index + step ] 420 | else: 421 | result_genome += _genome2[ index : index + step ] 422 | index += step 423 | 424 | if self.mutationProbability > 0: # here we also implement the mutation feature 425 | if randint( 1, 10000 ) < self.mutationProbability * 10000: 426 | result_genome = list( result_genome ) # convert the string to a list so we can access and change individual letters 427 | 428 | if type( self.mutationSize ) is float: 429 | number_of_mutations = int( round( len( result_genome ) * self.mutationSize ) ) # if the parameter is float then it represents relative size of the genome and find what number that translates to 430 | else: 431 | number_of_mutations = int( self.mutationSize ) # else, we take the value, not the reference 432 | 433 | for i in range( number_of_mutations ): 434 | p = randint( 0, len( result_genome ) - 1 ) 435 | result_genome[ p ] = "0" if randint( 0, 99 ) < 50 else "1" # randomize that many random bits in the genome 436 | 437 | result_genome = "".join( result_genome ) # and convert back to a single string 438 | 439 | return result_genome 440 | 441 | # This function is the opposite of numberToString. It takes a genome as an input and converts it to start times and resource ids 442 | def genomeToValues( self, _genome ): 443 | start_times = [] 444 | resources = [] 445 | segment = self.operationMaxTime + ( self.resourceCount - 1 if self.resourceCount > 1 else 1 ) # A segment contains the number of bits needed to represent one operation - the highest possible start time plus the highest possible resource ids. 446 | 447 | for i in range( self.operationCount ): # The number of segments in a genome is equal to the number of operations. 448 | st_from = i * segment # the beginning of the start time string 449 | st_to = i * segment + self.operationMaxTime # the end of the start time string 450 | r_from = i * segment + self.operationMaxTime # the beginning of the resource id string 451 | r_to = ( i + 1 ) * segment # the end of the resource id string 452 | 453 | start_times.append( _genome[ st_from : st_to ].count( "1" ) ) # as previously mentioned, numbers are encoded as the number of ocurrences of 1s 454 | resources.append( _genome[ r_from : r_to ].count( "1" ) ) 455 | 456 | return start_times, resources 457 | 458 | 459 | # A function that prints the current state of the model. 'Normalized' means to shift the whole schedule earlier so it begins at time 0. For example, a start times [ 3, 7, 2, 10 ] is normalized to [ 1, 5, 0, 8 ] because in essence it is the same schedule. 460 | def printBestNormalized( self, _text = '' ): 461 | min_start_time = min( self.population[ 0 ][ "start_times" ] ) # find the lowest start time... 462 | start_times = [] 463 | for i in self.population[ 0 ][ "start_times" ]: 464 | start_times.append( i - min_start_time ) # ... and subtract it from every start time 465 | # then print some information 466 | print( _text + " avg: {}\tscore: {}\ts_opRel: {}\ts_resSucc: {}\ts_fastRes: {}\tstart_times: {}\tresources: {}".format( 467 | self.averageScore, 468 | self.population[ 0 ][ "score" ], 469 | self.population[ 0 ][ "score_operationRelations" ], 470 | self.population[ 0 ][ "score_resourceSuccession" ], 471 | self.population[ 0 ][ "score_fastestResource" ], 472 | start_times, 473 | self.population[ 0 ][ "resources" ] 474 | ) 475 | ) 476 | 477 | # prints a random member of the current population 478 | def printRandom( self, _text = '' ): 479 | i = randint( 0, len( self.population ) - 1 ) 480 | print( _text + " avg: {}\tscore: {}\ts_opRel: {}\ts_resSucc: {}\ts_fastRes: {}\tstart_times: {}\tresources: {}".format( 481 | self.averageScore, 482 | self.population[ i ][ "score" ], 483 | self.population[ i ][ "score_operationRelations" ], 484 | self.population[ i ][ "score_resourceSuccession" ], 485 | self.population[ i ][ "score_fastestResource" ], 486 | self.population[ i ][ "start_times" ], 487 | self.population[ i ][ "resources" ] 488 | ) 489 | ) 490 | 491 | # print all scores from the current population in descending order 492 | def printAllScores( self, _text = '' ): 493 | all_scores = [ p[ "score" ] for p in self.population ] 494 | all_scores.sort( reverse = True ) 495 | print( _text + " " + str( all_scores ) ) 496 | 497 | # This is a function that automates the testing of the model. The same problem definition can be solved using several different combinations of parameters in order to find out which combination works best. Then you can use the best combination to do some more solving and hopefully find an even better solution. 498 | # For example, it can help you answer questions such as: Is it better to have many generations with a small population size or rather have fewer generations with a large population size, or does it not matter overall? 499 | def automatedTest( self ): 500 | # Below are the input parameters to the function. Change these to suit your needs. 501 | # **************************************** 502 | filename = "automatedTest_results.txt" # the file which to write the results to 503 | at_generations = [ 50, 150, 300 ] # the number of generations (or cycles) in each run, i.e. how many times the population will breed and create new solutions 504 | at_runs = [ 5 ] # the number of runs to carry out for each combination of parameters; remember that on each new run the population is totally randomizd initially 505 | at_cross_min_step = [ 0.05, 0.15, 0.35 ] 506 | at_cross_max_step = [ 0.1, 0.3, 0.5 ] 507 | at_population_size = [ 50, 200, 600 ] 508 | at_survival_rate = [ 0.05, 0.15, 0.5 ] 509 | at_mutate_prob = [ 0, 0.05, 0.15, 0.5 ] 510 | at_mutate_size = [ 0.05, 0.15, 0.25 ] 511 | at_infuse_random = [ 0, 5, 15, 30 ] 512 | # **************************************** 513 | 514 | number_of_combinations = len( at_generations ) * len( at_cross_min_step ) * len( at_cross_max_step ) * len( at_population_size ) * len( at_survival_rate ) * len( at_mutate_prob ) * len( at_mutate_size ) * len( at_infuse_random ) 515 | current_combination = 0 516 | 517 | line_template = "{}\t" * 25 + "\n" 518 | with open( filename, "at", encoding = "utf-8" ) as f: 519 | f.write( line_template.format( 520 | "Combination #", 521 | "Run #", 522 | "Time", 523 | 524 | "Best Score", 525 | "Average Score", 526 | "Worst Score", 527 | 528 | "Operation Relations score - Best", 529 | "Operation Relations score - Average", 530 | "Operation Relations score - Worst", 531 | 532 | "Resource Succession score - Best", 533 | "Resource Succession score - Average", 534 | "Resource Succession score - Worst", 535 | 536 | "Fastest Resource score - Best", 537 | "Fastest Resource score - Average", 538 | "Fastest Resource score - Worst", 539 | 540 | "Generations", 541 | "Cross Min Step", 542 | "Cross Max Step", 543 | "Population Size", 544 | "Survival Rate", 545 | "Mutation Probability", 546 | "Mutation Size", 547 | "Infuse Random", 548 | 549 | "Best Solution Start Times", 550 | "Best Solution Resource IDs" 551 | ) ) 552 | 553 | for cross_min in at_cross_min_step: 554 | for cross_max in at_cross_max_step: 555 | if cross_min > cross_max: 556 | # if Cross Min Step is greater than Cross Max Step then skip all these combinations and continue to next step values 557 | current_combination += len( at_generations ) * len( at_population_size ) * len( at_survival_rate ) * len( at_mutate_prob ) * len( at_mutate_size ) * len( at_infuse_random ) 558 | continue 559 | 560 | for pop_size in at_population_size: 561 | for sur_rate in at_survival_rate: 562 | for mut_prob in at_mutate_prob: 563 | for mut_size in at_mutate_size: 564 | for inf_rand in at_infuse_random: 565 | 566 | if type( cross_min ) is float: self.crossMinStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * cross_min ) ) 567 | else: self.crossMinStep = int( cross_min ) 568 | if type( cross_max ) is float: self.crossMaxStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * cross_max ) ) 569 | else: self.crossMaxStep = int( cross_max ) 570 | 571 | self.populationSize = int( pop_size ) 572 | self.survivalRate = float( sur_rate ) 573 | self.mutationProbability = float( mut_prob ) 574 | self.mutationSize = float( mut_size ) if type( mut_size ) is float else int( mut_size ) 575 | self.infuseRandomToPopulation = int( inf_rand ) 576 | 577 | for gen in at_generations: 578 | current_combination += 1 579 | for runs in at_runs: 580 | for r in range( runs ): 581 | 582 | print( "{}\tRunning combination {} of {}, run number {} of {}".format( dtnow(), current_combination, number_of_combinations, r+1, runs ) ) 583 | # just before running reset and initialize the model 584 | self.reset() 585 | self.addRandomToPopulation( self.populationSize ) 586 | 587 | time_start = time.time() 588 | for g in range( gen ): 589 | self.breedPopulation() 590 | time_end = time.time() 591 | 592 | self.scorePopulation() 593 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) 594 | 595 | score_operationRelations = tuple( p[ "score_operationRelations" ] for p in self.population ) 596 | score_resourceSuccession = tuple( p[ "score_resourceSuccession" ] for p in self.population ) 597 | score_fastestResource = tuple( p[ "score_fastestResource" ] for p in self.population ) 598 | 599 | output_line = line_template.format( 600 | current_combination, 601 | r+1, 602 | round( time_end - time_start, 2 ), 603 | 604 | self.population[ 0 ][ "score" ], 605 | self.averageScore, 606 | self.population[ -1 ][ "score" ], 607 | 608 | max( score_operationRelations ), 609 | sum( score_operationRelations ) / len( score_operationRelations ), 610 | min( score_operationRelations ), 611 | 612 | max( score_resourceSuccession ), 613 | sum( score_resourceSuccession ) / len( score_resourceSuccession ), 614 | min( score_resourceSuccession ), 615 | 616 | max( score_fastestResource ), 617 | sum( score_fastestResource ) / len( score_fastestResource ), 618 | min( score_fastestResource ), 619 | 620 | gen, 621 | cross_min, 622 | cross_max, 623 | pop_size, 624 | sur_rate, 625 | mut_prob, 626 | mut_size, 627 | inf_rand, 628 | 629 | self.population[ 0 ][ "start_times" ], 630 | self.population[ 0 ][ "resources" ] 631 | ) 632 | 633 | with open( filename, "at", encoding = "utf-8" ) as f: 634 | f.write( output_line ) 635 | 636 | 637 | 638 | # ideal solution 639 | # start_times = [ 0, 4, 8, 12, 16 ] 640 | # resources = [ 0, 1, 0, 1, 0 ] 641 | operation_durations_simple_1 = { 642 | 0: [ 4, 10 ], 643 | 1: [ 10, 4 ], 644 | 2: [ 4, 10 ], 645 | 3: [ 10, 4 ], 646 | 4: [ 4, 10 ] 647 | } 648 | operation_relations_simple_1 = { 649 | 1: { 650 | 0: { "type":"ES", "min":0, "max":0, "weight":1 } 651 | }, 652 | 2: { 653 | 1: { "type":"ES", "min":0, "max":0, "weight":1 } 654 | }, 655 | 3: { 656 | 2: { "type":"ES", "min":0, "max":0, "weight":1 } 657 | }, 658 | 4: { 659 | 3: { "type":"ES", "min":0, "max":0, "weight":1 } 660 | } 661 | } 662 | 663 | 664 | 665 | # ideal solution? 666 | # start_times = [ 5, 6, 17, 0, 14 ] 667 | # resources = [ 0, 1, 0, 1, 1 ] 668 | operation_durations_simple_2 = { 669 | 0: [ 7, 5 ], 670 | 1: [ 7, 5 ], 671 | 2: [ 7, 5 ], 672 | 3: [ 7, 5 ], 673 | 4: [ 7, 5 ] 674 | } 675 | operation_relations_simple_2 = { 676 | 0: { 677 | 3: { "type":"ES", "min":0, "max":0, "weight":1 } 678 | }, 679 | 4: { 680 | 0: { "type":"ES", "min":2, "max":2, "weight":1 }, 681 | 1: { "type":"EE", "min":8, "max":8, "weight":1 } 682 | }, 683 | 2: { 684 | 1: { "type":"SS", "min":11, "max":11, "weight":1 } 685 | } 686 | } 687 | 688 | 689 | 690 | # ideal solution 691 | # start_times = [ 0, 4, 4, 8, 12, 16, 20, 20, 24, 28, 32, 32, 36, 40, 44, 48, 52, 52, 56, 60 ] 692 | # resources = [ 0, 1, 2, 0, 2, 1, 0, 2, 1, 2, 0, 1, 2, 0, 2, 1, 0, 2, 1, 0 ] 693 | operation_durations_complex_1 = { 694 | 0: [ 4, 10, 10 ], 695 | 1: [ 10, 4, 10 ], 696 | 2: [ 10, 10, 4 ], 697 | 3: [ 4, 10, 10 ], 698 | 4: [ 10, 10, 4 ], 699 | 5: [ 10, 4, 10 ], 700 | 6: [ 4, 10, 10 ], 701 | 7: [ 10, 10, 4 ], 702 | 8: [ 10, 4, 10 ], 703 | 9: [ 10, 10, 4 ], 704 | 10: [ 4, 10, 10 ], 705 | 11: [ 10, 4, 10 ], 706 | 12: [ 10, 10, 4 ], 707 | 13: [ 4, 10, 10 ], 708 | 14: [ 10, 10, 4 ], 709 | 15: [ 10, 4, 10 ], 710 | 16: [ 4, 10, 10 ], 711 | 17: [ 10, 10, 4 ], 712 | 18: [ 10, 4, 10 ], 713 | 19: [ 4, 10, 10 ], 714 | } 715 | operation_relations_complex_1 = { 716 | 1: { 717 | 0: { "type":"ES", "min":0, "max":0, "weight":1 } 718 | }, 719 | 2: { 720 | 0: { "type":"ES", "min":0, "max":0, "weight":1 } 721 | }, 722 | 3: { 723 | 1: { "type":"ES", "min":0, "max":0, "weight":1 } 724 | }, 725 | 3: { 726 | 2: { "type":"ES", "min":0, "max":0, "weight":1 } 727 | }, 728 | 4: { 729 | 3: { "type":"ES", "min":0, "max":0, "weight":1 } 730 | }, 731 | 5: { 732 | 4: { "type":"ES", "min":0, "max":0, "weight":1 } 733 | }, 734 | 6: { 735 | 5: { "type":"ES", "min":0, "max":0, "weight":1 } 736 | }, 737 | 7: { 738 | 5: { "type":"ES", "min":0, "max":0, "weight":1 } 739 | }, 740 | 8: { 741 | 6: { "type":"ES", "min":0, "max":0, "weight":1 } 742 | }, 743 | 8: { 744 | 7: { "type":"ES", "min":0, "max":0, "weight":1 } 745 | }, 746 | 9: { 747 | 8: { "type":"ES", "min":0, "max":0, "weight":1 } 748 | }, 749 | 10: { 750 | 9: { "type":"ES", "min":0, "max":0, "weight":1 } 751 | }, 752 | 11: { 753 | 9: { "type":"ES", "min":0, "max":0, "weight":1 } 754 | }, 755 | 12: { 756 | 10: { "type":"ES", "min":0, "max":0, "weight":1 } 757 | }, 758 | 12: { 759 | 11: { "type":"ES", "min":0, "max":0, "weight":1 } 760 | }, 761 | 13: { 762 | 12: { "type":"ES", "min":0, "max":0, "weight":1 } 763 | }, 764 | 14: { 765 | 13: { "type":"ES", "min":0, "max":0, "weight":1 } 766 | }, 767 | 15: { 768 | 14: { "type":"ES", "min":0, "max":0, "weight":1 } 769 | }, 770 | 16: { 771 | 15: { "type":"ES", "min":0, "max":0, "weight":1 } 772 | }, 773 | 17: { 774 | 15: { "type":"ES", "min":0, "max":0, "weight":1 } 775 | }, 776 | 18: { 777 | 16: { "type":"ES", "min":0, "max":0, "weight":1 } 778 | }, 779 | 18: { 780 | 17: { "type":"ES", "min":0, "max":0, "weight":1 } 781 | }, 782 | 19: { 783 | 18: { "type":"ES", "min":0, "max":0, "weight":1 } 784 | } 785 | } 786 | 787 | 788 | 789 | # ideal solution 790 | # start_times = [ 25, 36, 35, 33, 23, 14, 25, 4, 8, 11, 1 ] (non-normalized form) 791 | # resources = [ 1, 2, 0, 1, 2, 0, 0, 1, 2, 1, 0 ] 792 | operation_durations_complex_2 = { 793 | 0: [ 7, 6, 7 ], 794 | 1: [ 12, 12, 8 ], 795 | 2: [ 5, 8, 6 ], 796 | 3: [ 7, 3, 5 ], 797 | 4: [ 13, 8, 5 ], 798 | 5: [ 7, 9, 10 ], 799 | 6: [ 6, 10, 10 ], 800 | 7: [ 7, 4, 6 ], 801 | 8: [ 14, 10, 8 ], 802 | 9: [ 7, 5, 6 ], 803 | 10: [ 10, 12, 14 ] 804 | } 805 | operation_relations_complex_2 = { 806 | 7: { 807 | 10: { "type":"ES", "min":-7, "max":-7, "weight":1 } 808 | }, 809 | 9: { 810 | 7: { "type":"ES", "min":3, "max":3, "weight":1 }, 811 | 10: { "type":"EE", "min":5, "max":5, "weight":1 } 812 | }, 813 | 8: { 814 | 9: { "type":"SS", "min":-3, "max":-3, "weight":1 } 815 | }, 816 | 6: { 817 | 8: { "type":"EE", "min":15, "max":15, "weight":1 } 818 | }, 819 | 5: { 820 | 6: { "type":"SE", "min":-4, "max":-4, "weight":1 } 821 | }, 822 | 4: { 823 | 5: { "type":"SS", "min":9, "max":9, "weight":1 } 824 | }, 825 | 0: { 826 | 4: { "type":"EE", "min":3, "max":3, "weight":1 } 827 | }, 828 | 2: { 829 | 0: { "type":"SE", "min":15, "max":15, "weight":1 } 830 | }, 831 | 3: { 832 | 2: { "type":"EE", "min":-4, "max":-4, "weight":1 } 833 | }, 834 | 1: { 835 | 3: { "type":"SS", "min":3, "max":3, "weight":1 } 836 | } 837 | } 838 | 839 | 840 | 841 | parameters_simple_1 = { 842 | "resourceCount": 2, 843 | "populationSize": 100, 844 | "survivalRate": 0.2, 845 | "infuseRandomToPopulation": 0, 846 | "crossMinStep": 0.1, 847 | "crossMaxStep": 0.18, 848 | "mutationProbability": 0.1, 849 | "mutationSize": 0.1, 850 | "asapAlapMode": "normal", 851 | "weightResourceSuccession": 5, 852 | "historyKeep": False, 853 | "historyRetryCount": 0, 854 | "averageScoreSampleSize": 10, 855 | "operationDurations": operation_durations_simple_1, 856 | "operationRelations": operation_relations_simple_1 857 | } 858 | 859 | parameters_simple_2 = { 860 | "resourceCount": 2, 861 | "populationSize": 100, 862 | "survivalRate": 0.2, 863 | "infuseRandomToPopulation": 0, 864 | "crossMinStep": 0.1, 865 | "crossMaxStep": 0.18, 866 | "mutationProbability": 0.1, 867 | "mutationSize": 0.1, 868 | "asapAlapMode": "normal", 869 | "weightResourceSuccession": 5, 870 | "historyKeep": False, 871 | "historyRetryCount": 0, 872 | "averageScoreSampleSize": 10, 873 | "operationDurations": operation_durations_simple_2, 874 | "operationRelations": operation_relations_simple_2 875 | } 876 | 877 | parameters_complex_1 = { 878 | "resourceCount": 3, 879 | "populationSize": 300, 880 | "survivalRate": 0.15, 881 | "infuseRandomToPopulation": 0, 882 | "crossMinStep": 0.1, 883 | "crossMaxStep": 0.15, 884 | "mutationProbability": 0.1, 885 | "mutationSize": 0.05, 886 | "asapAlapMode": "normal", 887 | "weightResourceSuccession": 5, 888 | "historyKeep": False, 889 | "historyRetryCount": 0, 890 | "averageScoreSampleSize": 70, 891 | "operationDurations": operation_durations_complex_1, 892 | "operationRelations": operation_relations_complex_1 893 | } 894 | 895 | parameters_complex_2 = { 896 | "resourceCount": 3, 897 | "populationSize": 300, 898 | "survivalRate": 0.15, 899 | "infuseRandomToPopulation": 0, 900 | "crossMinStep": 0.1, 901 | "crossMaxStep": 0.15, 902 | "mutationProbability": 0.1, 903 | "mutationSize": 0.05, 904 | "asapAlapMode": "normal", 905 | "weightResourceSuccession": 5, 906 | "historyKeep": False, 907 | "historyRetryCount": 0, 908 | "averageScoreSampleSize": 70, 909 | "operationDurations": operation_durations_complex_2, 910 | "operationRelations": operation_relations_complex_2 911 | } 912 | 913 | 914 | 915 | # in order to test in real time, do something like: 916 | """GAS_simple_1 = GAS( parameters_simple_1 ) 917 | GAS_simple_1.addRandomToPopulation( GAS_simple_1.populationSize ) 918 | for generation in range( 100 ): 919 | GAS_simple_1.breedPopulation( True )""" 920 | 921 | 922 | # in order to do automated test, do something like: 923 | #GAS_complex_1 = GAS( parameters_complex_1 ) 924 | #GAS_complex_1.automatedTest() 925 | GAS_complex_2 = GAS( parameters_complex_2 ) 926 | GAS_complex_2.automatedTest() 927 | 928 | """ 929 | The above automated test ran for about 21 days or about 509.5 hours on a laptop. Runtime specs are: 930 | - Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:57:36) [MSC v.1900 64 bit (AMD64)] on win32 931 | - Intel Core i7-4712MQ @ 2.3GHz , 4 core/8 logical processors Hyper Threading, utilising 2 cores and 12% total CPU 932 | - Windows 7 Ultimate x64 bit, 8GB RAM 933 | - 38,880 runs of 7,776 combinations (total number of combinations is 11,664 but some are skipped as per the code above) 934 | 935 | 936 | Some statistics and summary: 937 | 938 | Best Average Worst 939 | Runtime 0.45 s 47.17 s 40,154.31 s 940 | Best Score for run -67 -146 -559 941 | Average Score for run -83.31 -323.14 -744.46 942 | Worst Score for run -107 -805 -1,519 943 | 944 | - Optimal solution with score of -67 was reached 27 times, which is 0.0694 % success rate. Run times are (best/average/worst): 21.84 / 82.15 / 159.72 945 | - Excellent solutions with score >= -70 were reached 343 times, which is 0.8822 % success rate. Run times are: 7.35 / 74.42 / 278.23 946 | - Very good solutions with score >= -76 were reached 2082 times, whic is 5.3549 % success rate. Run times are: 3.53 / 83.61 / 10,676.38 947 | - Good solutins with score >= -86 were reached 7299 times, which is 18.7732 % success rate. Run times are: 1.40 / 80.88 / 10,676.38 948 | 949 | 950 | Impact Analysis 951 | 952 | - Generations 50 150 300 953 | Avg Time 16.56 43.54 81.42 high impact 954 | Avg Best Score for run -151 -144 -143 low impact 955 | 956 | - Cross Min Step 0.05 0.15 0.35 957 | Avg Time 44.77 52.48 43.77 low impact 958 | Avg Best Score for run -146 -143 -151 low impact 959 | 960 | - Cross Max Step 0.10 0.30 0.50 961 | Avg Time 45.75 51.32 44.89 low impact 962 | Avg Best Score for run -152 -145 -144 low impact 963 | 964 | - Population Size 50 200 600 965 | Avg Time 12.03 38.92 90.58 high impact 966 | Avg Best Score for run -228 -116 -94 high impact 967 | 968 | - Survival Rate 0.05 0.15 0.50 969 | Avg Time 23.07 35.67 82.79 high impact 970 | Avg Best Score for run -157 -135 -145 low impact 971 | 972 | - Mutation Probability 0.00 0.05 0.15 0.50 973 | Avg Time 38.12 47.68 45.32 57.58 low impact 974 | Avg Best Score for run -156 -145 -140 -142 low impact 975 | 976 | - Mutation Size 0.05 0.15 0.25 977 | Avg Time 41.30 45.76 54.47 low impact 978 | Avg Best Score for run -147 -146 -145 low impact 979 | 980 | - Infuse Random 0 5 15 30 981 | Avg Time 40.10 49.33 46.86 52.40 low impact 982 | Avg Best Score for run -108 -116 -162 -197 high impact 983 | 984 | 985 | Best Overal Choices 986 | 987 | In order to achieve excellent solutions in under a minute the following can be considered: 988 | - Generations: It is an interesting mix of bell shaped curve and diminishing returns. The value should not be too high otherwise impacts run time. 989 | - Cross Min Step: Doesn't seem to have noticable impact, however it's probably best to choose a relatively low value. 990 | - Cross Max Step: Doesn't seem to have noticable impact either, however it's probably best to choose a middle-ish value between 0.35-0.50 991 | - Population Size: It clearly has high impact on both run time and score. A classical example of a trade off between speed and result. Choose a value of a few hundred. It seems that diversity is a very important factor in finding solutions. 992 | - Survival Rate: It has a high impact on run time, but not as much impact on score. It's best to choose a relatively low value, but not a minimal one. 993 | - Mutation Probability: Doesn't seem to have a huge impact on either run time or score. But it does have a small contribution, so it is best to choose a modest value. 994 | - Mutatation Size: Same as above. 995 | - Infuse Random: This actually turns out to be not a very good feature of any model. It adversely affects the score, so set to 0 and do not use it, or use with a very modest value. 996 | 997 | As a good model maybe use the following: 998 | Generations: 120 999 | "crossMinStep": 0.1 1000 | "crossMaxStep": 0.4 1001 | "populationSize": 500 1002 | "survivalRate": 0.15 1003 | "mutationProbability": 0.1 1004 | "mutationSize": 0.12 1005 | "infuseRandomToPopulation": 1 1006 | """ -------------------------------------------------------------------------------- /GASv1.00.py: -------------------------------------------------------------------------------- 1 | """ 2 | v1.00 added features 3 | - number of resources and resource succession weights 4 | - operation time independent on resource 5 | - operation relations with min and max offset and weights for each relation 6 | - normal / asap / alap mode 7 | - history keeping and retry count 8 | - infuse random members to the population 9 | - cross mode - max step 10 | """ 11 | 12 | import random 13 | 14 | class GAS(): 15 | def __init__( self, _parameters ): 16 | self.operationDurations = dict( _parameters[ "operationDurations" ] ) 17 | self.operationCount = len( self.operationDurations ) 18 | self.resourceCount = int( _parameters[ "resourceCount" ] ) 19 | self.populationSize = int( _parameters[ "populationSize" ] ) 20 | self.population = [] 21 | self.survivalRate = float( _parameters[ "survivalRate" ] ) 22 | self.infuseRandomToPopulation = int( _parameters[ "infuseRandomToPopulation" ] ) 23 | self.crossMaxStep = int( _parameters[ "crossMaxStep" ] ) 24 | self.asapAlapMode = str( _parameters[ "asapAlapMode" ] ) 25 | self.weightResourceSuccession = int( _parameters[ "weightResourceSuccession" ] ) 26 | self.historyKeep = bool( _parameters[ "historyKeep" ] ) 27 | self.historyRetryCount = int( _parameters[ "historyRetryCount" ] ) 28 | self.history = [] 29 | 30 | self.operationRelations = {} 31 | # A dictionary of two more nested dictionaries. The structure is operationRelations[ operation2 ][ operation1 ][ parameter ], where: 32 | # - "operation2" is the second operation in the relation 33 | # - "operation1" is the first operation in the relation 34 | # - "parameter" can be either of: 35 | # - "type" - for the type of relation, available types are: 36 | # - SS - start-to-start - the start of the first operation relates to the start of the second operation 37 | # - SE - start-to-end - ... 38 | # - ES - end-to-start - ... 39 | # - EE - end-to-end - ... 40 | # - "min" - the minimum time for the relation (for example, if the relation is ES and the min time is 1, that means that the second operation can start no sooner that 1 unit of time after the end of the first operation) 41 | # - "max" - the maximum time 42 | # - "weight" - a custom weight used to fine-tune the scoring of schedules, default is 1 43 | 44 | for op2 in _parameters[ "operationRelations" ]: # copy by value 45 | self.operationRelations[ op2 ] = {} 46 | for op1 in _parameters[ "operationRelations" ][ op2 ]: 47 | self.operationRelations[ op2 ][ op1 ] = dict( _parameters[ "operationRelations" ][ op2 ][ op1 ] ) 48 | 49 | for op2 in self.operationRelations: 50 | for op1 in self.operationRelations[ op2 ]: 51 | if self.asapAlapMode == "normal": 52 | if self.operationRelations[ op2 ][ op1 ][ "min" ] == None and self.operationRelations[ op2 ][ op1 ][ "max" ] == None: 53 | self.operationRelations[ op2 ][ op1 ][ "min" ] = 0 54 | elif self.asapAlapMode == "asap": 55 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 56 | self.operationRelations[ op2 ][ op1 ][ "max" ] = self.operationRelations[ op2 ][ op1 ][ "min" ] 57 | elif self.asapAlapMode == "alap": 58 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 59 | self.operationRelations[ op2 ][ op1 ][ "min" ] = self.operationRelations[ op2 ][ op1 ][ "max" ] 60 | 61 | self.operationMaxTime = sum( self.operationDurations.values() ) 62 | for op2 in self.operationRelations: 63 | for op1 in self.operationRelations[ op2 ]: 64 | rel_min = self.operationRelations[ op2 ][ op1 ][ "min" ] if self.operationRelations[ op2 ][ op1 ][ "min" ] != None else 0 65 | rel_max = self.operationRelations[ op2 ][ op1 ][ "max" ] if self.operationRelations[ op2 ][ op1 ][ "max" ] != None else 0 66 | self.operationMaxTime += max( abs( rel_min ), abs( rel_max ) ) 67 | 68 | def addRandomToPopulation( self, _n ): 69 | for n in range( _n ): 70 | start_times = [ random.randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 71 | resources = [ random.randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 72 | 73 | if self.historyKeep == True: 74 | for i in range( self.historyRetryCount ): 75 | if ( start_times, resources ) not in self.history: 76 | self.history.append( ( list( start_times ), list( resources ) ) ) 77 | break 78 | start_times = [ random.randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 79 | resources = [ random.randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 80 | 81 | self.population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": "" } ) 82 | 83 | return True 84 | 85 | def scorePopulation( self ): 86 | for p in self.population: 87 | p[ "score" ] = 0 88 | #self.scorePopulation_totalDuration() 89 | self.scorePopulation_operationRelations() 90 | self.scorePopulation_resourceSuccession() 91 | return True 92 | 93 | def scorePopulation_totalDuration( self ): 94 | for p in self.population: 95 | max_duration = 0 96 | for i in range( self.operationCount ): 97 | duration = p[ "start_times" ][ i ] + self.operationDurations[ i ] 98 | if duration > max_duration: max_duration = duration 99 | p[ "score" ] -= max_duration 100 | return True 101 | 102 | def scorePopulation_operationRelations( self ): 103 | for p in self.population: 104 | for op2 in self.operationRelations: 105 | start2 = p[ "start_times" ][ op2 ] 106 | end2 = p[ "start_times" ][ op2 ] + self.operationDurations[ op2 ] 107 | 108 | for op1 in self.operationRelations[ op2 ]: 109 | start1 = p[ "start_times" ][ op1 ] 110 | end1 = p[ "start_times" ][ op1 ] + self.operationDurations[ op1 ] 111 | 112 | if self.operationRelations[ op2 ][ op1 ][ "type" ] == "SS": 113 | 114 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 115 | threshold_min = start2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 116 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 117 | elif self.asapAlapMode == "asap": 118 | p[ "score" ] -= start2 119 | 120 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 121 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 122 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 123 | elif self.asapAlapMode == "alap": 124 | p[ "score" ] += start2 125 | 126 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "SE": 127 | 128 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 129 | threshold_min = end2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 130 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 131 | elif self.asapAlapMode == "asap": 132 | p[ "score" ] -= start2 133 | 134 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 135 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 136 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 137 | elif self.asapAlapMode == "alap": 138 | p[ "score" ] += start2 139 | 140 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "ES": 141 | 142 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 143 | threshold_min = start2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 144 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 145 | elif self.asapAlapMode == "asap": 146 | p[ "score" ] -= start2 147 | 148 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 149 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 150 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 151 | elif self.asapAlapMode == "alap": 152 | p[ "score" ] += start2 153 | 154 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "EE": 155 | 156 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 157 | threshold_min = end2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 158 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 159 | elif self.asapAlapMode == "asap": 160 | p[ "score" ] -= start2 161 | 162 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 163 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 164 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 165 | elif self.asapAlapMode == "alap": 166 | p[ "score" ] += start2 167 | else: 168 | print( "Invalid relation type {} at self.operationRelations[ {} ][ {} ][ 'type' ]".format( self.operationRelations[ op2 ][ op1 ][ "type" ], op2, op1 ) ) 169 | return False 170 | return True 171 | 172 | def scorePopulation_resourceSuccession( self ): 173 | for p in self.population: 174 | sorted_operations = [] 175 | for i in range( self.operationCount ): 176 | sorted_operations.append( ( i, int( p[ "start_times" ][ i ] ), int( p[ "resources" ][ i ] ) ) ) 177 | sorted_operations.sort( key = lambda x: ( x[ 2 ], x[ 1 ] ) ) 178 | 179 | for i in range( 1, self.operationCount ): 180 | op1 = sorted_operations[ i-1 ][ 0 ] 181 | op2 = sorted_operations[ i ][ 0 ] 182 | r1 = sorted_operations[ i-1 ][ 2 ] 183 | r2 = sorted_operations[ i ][ 2 ] 184 | 185 | if r1 == r2: 186 | if p[ "start_times" ][ op2 ] < p[ "start_times" ][ op1 ] + self.operationDurations[ op1 ]: 187 | p[ "score" ] -= self.weightResourceSuccession 188 | 189 | def calculatePopulationGenome( self ): 190 | resourceCount = self.resourceCount - 1 if self.resourceCount > 1 else 1 191 | 192 | for p in self.population: 193 | p[ "genome" ] = "" 194 | for i in range( self.operationCount ): 195 | p[ "genome" ] += self.numberToString( p[ "start_times" ][ i ], self.operationMaxTime ) 196 | p[ "genome" ] += self.numberToString( p[ "resources" ][ i ], resourceCount ) 197 | return True 198 | 199 | def numberToString( self, _number, _length ): 200 | number = int( _number ) 201 | padding = int( _length - _number ) 202 | probability = int( round( 100 * ( padding / _length ) ) ) 203 | string = "" 204 | while number + padding > 0: 205 | if number == 0: 206 | string += "0" 207 | padding -= 1 208 | continue 209 | if padding == 0: 210 | string += "1" 211 | number -= 1 212 | continue 213 | if random.randint( 0, 100 ) < probability: 214 | string += "0" 215 | padding -= 1 216 | else: 217 | string += "1" 218 | number -= 1 219 | return string 220 | 221 | def breedPopulation( self ): 222 | self.scorePopulation() 223 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) 224 | 225 | print( "score: {}, start_times: {}, resources: {}".format( self.population[ 0 ][ "score" ], self.population[ 0 ][ "start_times" ], self.population[ 0 ][ "resources" ] ) ) 226 | if self.population[ 0 ][ "score" ] == 0: 227 | self.printBestNormalized() 228 | 229 | survivors = int( round( self.survivalRate * self.populationSize ) ) 230 | for i in range( survivors, self.populationSize ): 231 | del self.population[ -1 ] 232 | 233 | if self.infuseRandomToPopulation > 0: 234 | self.addRandomToPopulation( self.infuseRandomToPopulation ) 235 | self.calculatePopulationGenome() 236 | self.scorePopulation() 237 | 238 | self.calculatePopulationGenome() 239 | 240 | new_population = [] 241 | for n in range( self.populationSize ): 242 | p1 = random.randint( 0, len( self.population ) - 1 ) 243 | p2 = random.randint( 0, len( self.population ) - 1 ) 244 | 245 | genome1 = str( self.population[ p1 ][ "genome" ] ) 246 | genome2 = str( self.population[ p2 ][ "genome" ] ) 247 | 248 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 249 | start_times, resources = self.genomeToValues( new_genome ) 250 | 251 | if self.historyKeep == True: 252 | for i in range( self.historyRetryCount ): 253 | if ( start_times, resources ) not in self.history: 254 | self.history.append( ( list( start_times ), list( resources ) ) ) 255 | break 256 | genome1 = str( self.population[ random.randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 257 | genome2 = str( self.population[ random.randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 258 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 259 | start_times, resources = self.genomeToValues( new_genome ) 260 | 261 | new_population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": str( new_genome ) } ) 262 | 263 | self.population.clear() 264 | self.population = list( new_population ) 265 | 266 | return True 267 | 268 | def crossTwoGenomes( self, _genome1, _genome2 ): 269 | genome_length = len( _genome1 ) 270 | index = 0 271 | result_genome = ""; 272 | 273 | while True: 274 | step = random.randint( 1, self.crossMaxStep ) 275 | if step > genome_length - ( index + 1 ): 276 | if random.randint( 0, 99 ) < 50: 277 | result_genome += _genome1[ index : ] 278 | else: 279 | result_genome += _genome2[ index : ] 280 | break 281 | if random.randint( 0, 99 ) < 50: 282 | result_genome += _genome1[ index : index + step ] 283 | else: 284 | result_genome += _genome2[ index : index + step ] 285 | index += step 286 | 287 | return result_genome 288 | 289 | def genomeToValues( self, _genome ): 290 | start_times = [] 291 | resources = [] 292 | segment = self.operationMaxTime + ( self.resourceCount - 1 if self.resourceCount > 1 else 1 ) 293 | 294 | for i in range( self.operationCount ): 295 | st_from = i * segment 296 | st_to = i * segment + self.operationMaxTime 297 | r_from = i * segment + self.operationMaxTime 298 | r_to = ( i + 1 ) * segment 299 | 300 | start_times.append( _genome[ st_from : st_to ].count( "1" ) ) 301 | resources.append( _genome[ r_from : r_to ].count( "1" ) ) 302 | 303 | return start_times, resources 304 | 305 | 306 | 307 | def printBestNormalized( self ): 308 | min_start_time = min( self.population[ 0 ][ "start_times" ] ) 309 | start_times = [] 310 | for i in self.population[ 0 ][ "start_times" ]: 311 | start_times.append( i - min_start_time ) 312 | print( "normalized: {}, start_times: {}, resources: {}".format( self.population[ 0 ][ "score" ], start_times, self.population[ 0 ][ "resources" ] ) ) 313 | 314 | def dump( self, message ): 315 | for p in self.population: 316 | message += "{}, ".format( p[ "score" ] ) 317 | print( message ) 318 | 319 | def printSchedule( self ): 320 | for p in self.population: 321 | if p[ "score" ] == 0: 322 | print( "score: {}, start_times: {}, resources: {}".format( p["score"], p["start_times"],p["resources"] ) ) 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 | myOperationRelations = { 351 | 0:{ 352 | 3:{ "type":"ES", "min":0, "max":0, "weight":1 } 353 | }, 354 | 4:{ 355 | 0:{ "type":"ES", "min":5, "max":5, "weight":1 }, 356 | 1:{ "type":"EE", "min":5, "max":5, "weight":1 } 357 | }, 358 | 2:{ 359 | 1:{ "type":"SS", "min":11, "max":11, "weight":1 } 360 | } 361 | } 362 | myParameters = { 363 | "resourceCount": 1, 364 | "populationSize": 200, 365 | "survivalRate": 0.1, 366 | "infuseRandomToPopulation": 0, 367 | "crossMaxStep": 20, 368 | "asapAlapMode": "normal", 369 | "weightResourceSuccession": 10, 370 | "historyKeep": False, 371 | "historyRetryCount": 30, 372 | "operationDurations": { 0:5, 1:5, 2:5, 3:5, 4:5 }, 373 | "operationRelations": myOperationRelations 374 | } 375 | myGAS = GAS( myParameters ) 376 | myGAS.addRandomToPopulation( myGAS.populationSize ) 377 | 378 | 379 | for i in range( 50 ): 380 | myGAS.breedPopulation() -------------------------------------------------------------------------------- /GASv2.00.py: -------------------------------------------------------------------------------- 1 | """ 2 | v2.00 added features 3 | - resource dependent operation duration and default duration for operation, plus scoring for fastest resources used 4 | - can view separate scores for each scoring method 5 | - improved performance - all scoring done in one iteration and not separate 6 | 7 | v1.00 added features 8 | - number of resources and resource succession weights 9 | - operation time independent on resource 10 | - operation relations with min and max offset and weights for each relation 11 | - normal / asap / alap mode 12 | - history keeping and retry count 13 | - infuse random members to the population 14 | - cross mode - max step 15 | """ 16 | 17 | import random 18 | 19 | class GAS(): 20 | def __init__( self, _parameters ): 21 | self.resourceCount = int( _parameters[ "resourceCount" ] ) 22 | self.populationSize = int( _parameters[ "populationSize" ] ) 23 | self.population = [] 24 | self.survivalRate = float( _parameters[ "survivalRate" ] ) 25 | self.infuseRandomToPopulation = int( _parameters[ "infuseRandomToPopulation" ] ) 26 | self.crossMaxStep = int( _parameters[ "crossMaxStep" ] ) 27 | self.asapAlapMode = str( _parameters[ "asapAlapMode" ] ) 28 | self.weightResourceSuccession = int( _parameters[ "weightResourceSuccession" ] ) 29 | self.historyKeep = bool( _parameters[ "historyKeep" ] ) 30 | self.historyRetryCount = int( _parameters[ "historyRetryCount" ] ) 31 | self.history = [] 32 | 33 | self.operationDurations = {} 34 | for i in _parameters[ "operationDurations" ]: 35 | if type( _parameters[ "operationDurations" ][ i ] ) is int: 36 | self.operationDurations[ i ] = int( _parameters[ "operationDurations" ][ i ] ) 37 | elif type( _parameters[ "operationDurations" ][ i ] ) is list: 38 | self.operationDurations[ i ] = list( _parameters[ "operationDurations" ][ i ] ) 39 | else: 40 | print( "Invalid operation duration: {}, type: {}\nTerminating".format( _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 41 | return False 42 | 43 | self.operationCount = len( self.operationDurations ) 44 | 45 | self.operationRelations = {} 46 | # A dictionary of two more nested dictionaries. The structure is operationRelations[ operation2 ][ operation1 ][ parameter ], where: 47 | # - "operation2" is the second operation in the relation 48 | # - "operation1" is the first operation in the relation 49 | # - "parameter" can be either of: 50 | # - "type" - for the type of relation, available types are: 51 | # - SS - start-to-start - the start of the first operation relates to the start of the second operation 52 | # - SE - start-to-end - ... 53 | # - ES - end-to-start - ... 54 | # - EE - end-to-end - ... 55 | # - "min" - the minimum time for the relation (for example, if the relation is ES and the min time is 1, that means that the second operation can start no sooner that 1 unit of time after the end of the first operation) 56 | # - "max" - the maximum time 57 | # - "weight" - a custom weight used to fine-tune the scoring of schedules, default is 1 58 | 59 | for op2 in _parameters[ "operationRelations" ]: # copy by value 60 | self.operationRelations[ op2 ] = {} 61 | for op1 in _parameters[ "operationRelations" ][ op2 ]: 62 | self.operationRelations[ op2 ][ op1 ] = dict( _parameters[ "operationRelations" ][ op2 ][ op1 ] ) 63 | 64 | for op2 in self.operationRelations: 65 | for op1 in self.operationRelations[ op2 ]: 66 | if self.asapAlapMode == "normal": 67 | if self.operationRelations[ op2 ][ op1 ][ "min" ] == None and self.operationRelations[ op2 ][ op1 ][ "max" ] == None: 68 | self.operationRelations[ op2 ][ op1 ][ "min" ] = 0 69 | elif self.asapAlapMode == "asap": 70 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 71 | self.operationRelations[ op2 ][ op1 ][ "max" ] = self.operationRelations[ op2 ][ op1 ][ "min" ] 72 | elif self.asapAlapMode == "alap": 73 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 74 | self.operationRelations[ op2 ][ op1 ][ "min" ] = self.operationRelations[ op2 ][ op1 ][ "max" ] 75 | 76 | self.operationMaxTime = 0 77 | for op in range( self.operationCount ): 78 | self.operationMaxTime += max( self.operationDurations[ op ] ) 79 | for op2 in self.operationRelations: 80 | for op1 in self.operationRelations[ op2 ]: 81 | rel_min = self.operationRelations[ op2 ][ op1 ][ "min" ] if self.operationRelations[ op2 ][ op1 ][ "min" ] != None else 0 82 | rel_max = self.operationRelations[ op2 ][ op1 ][ "max" ] if self.operationRelations[ op2 ][ op1 ][ "max" ] != None else 0 83 | self.operationMaxTime += max( abs( rel_min ), abs( rel_max ) ) 84 | 85 | # calculate theoretical minimum and maximum scores - take into account all scoring methods! 86 | #!!! 87 | 88 | def getOperationDuration( self, _op, _r ): 89 | if type( self.operationDurations[ _op ] ) is int: 90 | return int( self.operationDurations[ _op ] ) 91 | elif type( self.operationDurations[ _op ] ) is list: 92 | return int( self.operationDurations[ _op ][ _r ] ) 93 | else: 94 | print( "Invalid operation duration: {}, type: {}\nTerminating".format( _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 95 | return False 96 | 97 | def addRandomToPopulation( self, _n ): 98 | for n in range( _n ): 99 | start_times = [ random.randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 100 | resources = [ random.randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 101 | 102 | if self.historyKeep == True: 103 | for i in range( self.historyRetryCount ): 104 | if ( start_times, resources ) not in self.history: 105 | self.history.append( ( list( start_times ), list( resources ) ) ) 106 | break 107 | start_times = [ random.randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 108 | resources = [ random.randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 109 | 110 | self.population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": "" } ) 111 | 112 | return True 113 | 114 | def scorePopulation( self ): 115 | for p in self.population: 116 | p[ "score" ] = 0 117 | 118 | # Operation Relations 119 | for op2 in self.operationRelations: 120 | start2 = p[ "start_times" ][ op2 ] 121 | end2 = p[ "start_times" ][ op2 ] + self.getOperationDuration( op2, p[ "resources" ][ op2 ] ) 122 | 123 | for op1 in self.operationRelations[ op2 ]: 124 | start1 = p[ "start_times" ][ op1 ] 125 | end1 = p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ) 126 | 127 | if self.operationRelations[ op2 ][ op1 ][ "type" ] == "SS": 128 | 129 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 130 | threshold_min = start2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 131 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 132 | elif self.asapAlapMode == "asap": 133 | p[ "score" ] -= start2 134 | 135 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 136 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 137 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 138 | elif self.asapAlapMode == "alap": 139 | p[ "score" ] += start2 140 | 141 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "SE": 142 | 143 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 144 | threshold_min = end2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 145 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 146 | elif self.asapAlapMode == "asap": 147 | p[ "score" ] -= start2 148 | 149 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 150 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 151 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 152 | elif self.asapAlapMode == "alap": 153 | p[ "score" ] += start2 154 | 155 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "ES": 156 | 157 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 158 | threshold_min = start2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 159 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 160 | elif self.asapAlapMode == "asap": 161 | p[ "score" ] -= start2 162 | 163 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 164 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 165 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 166 | elif self.asapAlapMode == "alap": 167 | p[ "score" ] += start2 168 | 169 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "EE": 170 | 171 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 172 | threshold_min = end2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 173 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 174 | elif self.asapAlapMode == "asap": 175 | p[ "score" ] -= start2 176 | 177 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 178 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 179 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 180 | elif self.asapAlapMode == "alap": 181 | p[ "score" ] += start2 182 | else: 183 | print( "Invalid relation type {} at self.operationRelations[ {} ][ {} ][ 'type' ]".format( self.operationRelations[ op2 ][ op1 ][ "type" ], op2, op1 ) ) 184 | return False 185 | 186 | p[ "score_operationRelations" ] = int( p[ "score" ] ) 187 | 188 | 189 | # Resource Succession 190 | p[ "score_resourceSuccession" ] = int( p[ "score" ] ) 191 | 192 | sorted_operations = [] 193 | for i in range( self.operationCount ): 194 | sorted_operations.append( ( i, int( p[ "start_times" ][ i ] ), int( p[ "resources" ][ i ] ) ) ) 195 | sorted_operations.sort( key = lambda x: ( x[ 2 ], x[ 1 ] ) ) 196 | 197 | for i in range( 1, self.operationCount ): 198 | op1 = sorted_operations[ i-1 ][ 0 ] 199 | op2 = sorted_operations[ i ][ 0 ] 200 | r1 = sorted_operations[ i-1 ][ 2 ] 201 | r2 = sorted_operations[ i ][ 2 ] 202 | 203 | if r1 == r2: 204 | if p[ "start_times" ][ op2 ] < p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ): 205 | p[ "score" ] -= self.weightResourceSuccession 206 | 207 | p[ "score_resourceSuccession" ] = int( p[ "score" ] - p[ "score_resourceSuccession" ] ) 208 | 209 | 210 | # Fastest Resource (resource dependent operation durations) 211 | p[ "score_fastestResource" ] = int( p[ "score" ] ) 212 | 213 | for op in range( self.operationCount ): 214 | p[ "score" ] -= self.getOperationDuration( op, p[ "resources" ][ op ] ) 215 | 216 | p[ "score_fastestResource" ] = int( p[ "score" ] - p[ "score_fastestResource" ] ) 217 | 218 | return True 219 | 220 | def calculatePopulationGenome( self ): 221 | resourceCount = self.resourceCount - 1 if self.resourceCount > 1 else 1 222 | 223 | for p in self.population: 224 | p[ "genome" ] = "" 225 | for i in range( self.operationCount ): 226 | p[ "genome" ] += self.numberToString( p[ "start_times" ][ i ], self.operationMaxTime ) 227 | p[ "genome" ] += self.numberToString( p[ "resources" ][ i ], resourceCount ) 228 | return True 229 | 230 | def numberToString( self, _number, _length ): 231 | number = int( _number ) 232 | padding = int( _length - _number ) 233 | probability = int( round( 100 * ( padding / _length ) ) ) 234 | string = "" 235 | while number + padding > 0: 236 | if number == 0: 237 | string += "0" 238 | padding -= 1 239 | continue 240 | if padding == 0: 241 | string += "1" 242 | number -= 1 243 | continue 244 | if random.randint( 0, 100 ) < probability: 245 | string += "0" 246 | padding -= 1 247 | else: 248 | string += "1" 249 | number -= 1 250 | return string 251 | 252 | def breedPopulation( self ): 253 | self.scorePopulation() 254 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) 255 | 256 | #print( "score: {}, start_times: {}, resources: {}".format( self.population[ 0 ][ "score" ], self.population[ 0 ][ "start_times" ], self.population[ 0 ][ "resources" ] ) ) 257 | #if self.population[ 0 ][ "score" ] == 0: 258 | self.printBestNormalized() 259 | 260 | survivors = int( round( self.survivalRate * self.populationSize ) ) 261 | for i in range( survivors, self.populationSize ): 262 | del self.population[ -1 ] 263 | 264 | if self.infuseRandomToPopulation > 0: 265 | self.addRandomToPopulation( self.infuseRandomToPopulation ) 266 | self.calculatePopulationGenome() 267 | self.scorePopulation() 268 | 269 | self.calculatePopulationGenome() 270 | 271 | new_population = [] 272 | for n in range( self.populationSize ): 273 | p1 = random.randint( 0, len( self.population ) - 1 ) 274 | p2 = random.randint( 0, len( self.population ) - 1 ) 275 | 276 | genome1 = str( self.population[ p1 ][ "genome" ] ) 277 | genome2 = str( self.population[ p2 ][ "genome" ] ) 278 | 279 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 280 | start_times, resources = self.genomeToValues( new_genome ) 281 | 282 | if self.historyKeep == True: 283 | for i in range( self.historyRetryCount ): 284 | if ( start_times, resources ) not in self.history: 285 | self.history.append( ( list( start_times ), list( resources ) ) ) 286 | break 287 | genome1 = str( self.population[ random.randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 288 | genome2 = str( self.population[ random.randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 289 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 290 | start_times, resources = self.genomeToValues( new_genome ) 291 | 292 | new_population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": str( new_genome ) } ) 293 | 294 | self.population.clear() 295 | self.population = list( new_population ) 296 | 297 | return True 298 | 299 | def crossTwoGenomes( self, _genome1, _genome2 ): 300 | genome_length = len( _genome1 ) 301 | index = 0 302 | result_genome = ""; 303 | 304 | while True: 305 | step = random.randint( 1, self.crossMaxStep ) 306 | if step > genome_length - ( index + 1 ): 307 | if random.randint( 0, 99 ) < 50: 308 | result_genome += _genome1[ index : ] 309 | else: 310 | result_genome += _genome2[ index : ] 311 | break 312 | if random.randint( 0, 99 ) < 50: 313 | result_genome += _genome1[ index : index + step ] 314 | else: 315 | result_genome += _genome2[ index : index + step ] 316 | index += step 317 | 318 | return result_genome 319 | 320 | def genomeToValues( self, _genome ): 321 | start_times = [] 322 | resources = [] 323 | segment = self.operationMaxTime + ( self.resourceCount - 1 if self.resourceCount > 1 else 1 ) 324 | 325 | for i in range( self.operationCount ): 326 | st_from = i * segment 327 | st_to = i * segment + self.operationMaxTime 328 | r_from = i * segment + self.operationMaxTime 329 | r_to = ( i + 1 ) * segment 330 | 331 | start_times.append( _genome[ st_from : st_to ].count( "1" ) ) 332 | resources.append( _genome[ r_from : r_to ].count( "1" ) ) 333 | 334 | return start_times, resources 335 | 336 | 337 | 338 | def printBestNormalized( self ): 339 | min_start_time = min( self.population[ 0 ][ "start_times" ] ) 340 | start_times = [] 341 | for i in self.population[ 0 ][ "start_times" ]: 342 | start_times.append( i - min_start_time ) 343 | print( "score: {}, s_opRel: {}, s_resSucc: {}, s_fastRes: {}, start_times: {}, resources: {}".format( 344 | self.population[ 0 ][ "score" ], 345 | self.population[ 0 ][ "score_operationRelations" ], 346 | self.population[ 0 ][ "score_resourceSuccession" ], 347 | self.population[ 0 ][ "score_fastestResource" ], 348 | start_times, 349 | self.population[ 0 ][ "resources" ] 350 | ) 351 | ) 352 | 353 | def dump( self, message ): 354 | for p in self.population: 355 | message += "{}, ".format( p[ "score" ] ) 356 | print( message ) 357 | 358 | def printSchedule( self ): 359 | for p in self.population: 360 | if p[ "score" ] == 0: 361 | print( "score: {}, start_times: {}, resources: {}".format( p["score"], p["start_times"],p["resources"] ) ) 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 | myOperationRelations = { 390 | 0:{ 391 | 3:{ "type":"ES", "min":0, "max":0, "weight":1 } 392 | }, 393 | 4:{ 394 | 0:{ "type":"ES", "min":2, "max":2, "weight":1 }, 395 | 1:{ "type":"EE", "min":2, "max":2, "weight":1 } 396 | }, 397 | 2:{ 398 | 1:{ "type":"SS", "min":11, "max":11, "weight":1 } 399 | } 400 | } 401 | 402 | myOperationDurations = { 403 | 0: [ 7, 5 ], 404 | 1: [ 7, 5 ], 405 | 2: [ 7, 5 ], 406 | 3: [ 7, 5 ], 407 | 4: [ 7, 5 ] 408 | } 409 | 410 | myParameters = { 411 | "resourceCount": 2, 412 | "populationSize": 200, 413 | "survivalRate": 0.2, 414 | "infuseRandomToPopulation": 0, 415 | "crossMaxStep": 20, 416 | "asapAlapMode": "normal", 417 | "weightResourceSuccession": 10, 418 | "historyKeep": False, 419 | "historyRetryCount": 30, 420 | "operationDurations": myOperationDurations, 421 | "operationRelations": myOperationRelations 422 | } 423 | myGAS = GAS( myParameters ) 424 | myGAS.addRandomToPopulation( myGAS.populationSize ) 425 | 426 | 427 | for i in range( 20 ): 428 | myGAS.breedPopulation() -------------------------------------------------------------------------------- /GASv3.00.py: -------------------------------------------------------------------------------- 1 | """ 2 | v3.00 3 | - two new complex examples 4 | - printRandom and printAllScores 5 | - mutation probability and mutation size 6 | - average score calculation based on a sample of the latest populations 7 | - specify crossMinStep 8 | - specify crossMinStep and crossMaxStep in relative terms or absolute 9 | 10 | v2.00 added features 11 | - resource dependent operation duration and default duration for operation, plus scoring for fastest resources used 12 | - can view separate scores for each scoring method 13 | - improved performance - all scoring done in one iteration and not separate 14 | 15 | v1.00 added features 16 | - number of resources and resource succession weights 17 | - operation time independent on resource 18 | - operation relations with min and max offset and weights for each relation 19 | - normal / asap / alap mode 20 | - history keeping and retry count 21 | - infuse random members to the population 22 | - cross mode - max step 23 | """ 24 | 25 | import random 26 | 27 | class GAS(): 28 | def __init__( self, _parameters ): 29 | self.resourceCount = int( _parameters[ "resourceCount" ] ) 30 | self.populationSize = int( _parameters[ "populationSize" ] ) 31 | self.population = [] 32 | self.survivalRate = float( _parameters[ "survivalRate" ] ) 33 | self.infuseRandomToPopulation = int( _parameters[ "infuseRandomToPopulation" ] ) 34 | self.mutationProbability = float( _parameters[ "mutationProbability" ] ) 35 | self.mutationSize = float( _parameters[ "mutationSize" ] ) if type( _parameters[ "mutationSize" ] ) is float else int( _parameters[ "mutationSize" ] ) 36 | self.asapAlapMode = str( _parameters[ "asapAlapMode" ] ) 37 | self.weightResourceSuccession = int( _parameters[ "weightResourceSuccession" ] ) 38 | self.historyKeep = bool( _parameters[ "historyKeep" ] ) 39 | self.historyRetryCount = int( _parameters[ "historyRetryCount" ] ) 40 | self.history = [] 41 | self.averageScoreSampleSize = int( _parameters[ "averageScoreSampleSize" ] ) 42 | self.averageScoreSample = [] 43 | self.averageScore = None 44 | 45 | self.operationDurations = {} 46 | for i in _parameters[ "operationDurations" ]: 47 | if type( _parameters[ "operationDurations" ][ i ] ) is int: 48 | self.operationDurations[ i ] = int( _parameters[ "operationDurations" ][ i ] ) 49 | elif type( _parameters[ "operationDurations" ][ i ] ) is list: 50 | self.operationDurations[ i ] = list( _parameters[ "operationDurations" ][ i ] ) 51 | else: 52 | print( "Invalid operation duration: {}, type: {}\nTerminating".format( _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 53 | return False 54 | 55 | self.operationCount = len( self.operationDurations ) 56 | 57 | self.operationRelations = {} 58 | # A dictionary of two more nested dictionaries. The structure is operationRelations[ operation2 ][ operation1 ][ parameter ], where: 59 | # - "operation2" is the second operation in the relation 60 | # - "operation1" is the first operation in the relation 61 | # - "parameter" can be either of: 62 | # - "type" - for the type of relation, available types are: 63 | # - SS - start-to-start - the start of the first operation relates to the start of the second operation 64 | # - SE - start-to-end - ... 65 | # - ES - end-to-start - ... 66 | # - EE - end-to-end - ... 67 | # - "min" - the minimum time for the relation (for example, if the relation is ES and the min time is 1, that means that the second operation can start no sooner that 1 unit of time after the end of the first operation) 68 | # - "max" - the maximum time 69 | # - "weight" - a custom weight used to fine-tune the scoring of schedules, default is 1 70 | 71 | for op2 in _parameters[ "operationRelations" ]: # copy by value 72 | self.operationRelations[ op2 ] = {} 73 | for op1 in _parameters[ "operationRelations" ][ op2 ]: 74 | self.operationRelations[ op2 ][ op1 ] = dict( _parameters[ "operationRelations" ][ op2 ][ op1 ] ) 75 | 76 | for op2 in self.operationRelations: 77 | for op1 in self.operationRelations[ op2 ]: 78 | if self.asapAlapMode == "normal": 79 | if self.operationRelations[ op2 ][ op1 ][ "min" ] == None and self.operationRelations[ op2 ][ op1 ][ "max" ] == None: 80 | self.operationRelations[ op2 ][ op1 ][ "min" ] = 0 81 | elif self.asapAlapMode == "asap": 82 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 83 | self.operationRelations[ op2 ][ op1 ][ "max" ] = self.operationRelations[ op2 ][ op1 ][ "min" ] 84 | elif self.asapAlapMode == "alap": 85 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 86 | self.operationRelations[ op2 ][ op1 ][ "min" ] = self.operationRelations[ op2 ][ op1 ][ "max" ] 87 | 88 | self.operationMaxTime = 0 89 | for op in range( self.operationCount ): 90 | self.operationMaxTime += max( self.operationDurations[ op ] ) 91 | for op2 in self.operationRelations: 92 | for op1 in self.operationRelations[ op2 ]: 93 | rel_min = self.operationRelations[ op2 ][ op1 ][ "min" ] if self.operationRelations[ op2 ][ op1 ][ "min" ] != None else 0 94 | rel_max = self.operationRelations[ op2 ][ op1 ][ "max" ] if self.operationRelations[ op2 ][ op1 ][ "max" ] != None else 0 95 | self.operationMaxTime += max( abs( rel_min ), abs( rel_max ) ) 96 | 97 | if type( _parameters[ "crossMinStep" ] ) is float: 98 | self.crossMinStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * _parameters[ "crossMinStep" ] ) ) 99 | else: 100 | self.crossMinStep = int( _parameters[ "crossMinStep" ] ) 101 | 102 | if type( _parameters[ "crossMaxStep" ] ) is float: 103 | self.crossMaxStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * _parameters[ "crossMaxStep" ] ) ) 104 | else: 105 | self.crossMaxStep = int( _parameters[ "crossMaxStep" ] ) 106 | 107 | 108 | def getOperationDuration( self, _op, _r ): 109 | if type( self.operationDurations[ _op ] ) is int: 110 | return int( self.operationDurations[ _op ] ) 111 | elif type( self.operationDurations[ _op ] ) is list: 112 | return int( self.operationDurations[ _op ][ _r ] ) 113 | else: 114 | print( "Invalid operation duration: {}, type: {}\nTerminating".format( _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 115 | return False 116 | 117 | def addRandomToPopulation( self, _n ): 118 | for n in range( _n ): 119 | start_times = [ random.randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 120 | resources = [ random.randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 121 | 122 | if self.historyKeep == True: 123 | for i in range( self.historyRetryCount ): 124 | if ( start_times, resources ) not in self.history: 125 | self.history.append( ( list( start_times ), list( resources ) ) ) 126 | break 127 | start_times = [ random.randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 128 | resources = [ random.randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 129 | 130 | self.population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": "" } ) 131 | 132 | return True 133 | 134 | def scorePopulation( self ): 135 | for p in self.population: 136 | p[ "score" ] = 0 137 | 138 | # Operation Relations 139 | for op2 in self.operationRelations: 140 | start2 = p[ "start_times" ][ op2 ] 141 | end2 = p[ "start_times" ][ op2 ] + self.getOperationDuration( op2, p[ "resources" ][ op2 ] ) 142 | 143 | for op1 in self.operationRelations[ op2 ]: 144 | start1 = p[ "start_times" ][ op1 ] 145 | end1 = p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ) 146 | 147 | if self.operationRelations[ op2 ][ op1 ][ "type" ] == "SS": 148 | 149 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 150 | threshold_min = start2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 151 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 152 | elif self.asapAlapMode == "asap": 153 | p[ "score" ] -= start2 154 | 155 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 156 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 157 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 158 | elif self.asapAlapMode == "alap": 159 | p[ "score" ] += start2 160 | 161 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "SE": 162 | 163 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 164 | threshold_min = end2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 165 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 166 | elif self.asapAlapMode == "asap": 167 | p[ "score" ] -= start2 168 | 169 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 170 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 171 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 172 | elif self.asapAlapMode == "alap": 173 | p[ "score" ] += start2 174 | 175 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "ES": 176 | 177 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 178 | threshold_min = start2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 179 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 180 | elif self.asapAlapMode == "asap": 181 | p[ "score" ] -= start2 182 | 183 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 184 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 185 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 186 | elif self.asapAlapMode == "alap": 187 | p[ "score" ] += start2 188 | 189 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "EE": 190 | 191 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 192 | threshold_min = end2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 193 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 194 | elif self.asapAlapMode == "asap": 195 | p[ "score" ] -= start2 196 | 197 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 198 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 199 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 200 | elif self.asapAlapMode == "alap": 201 | p[ "score" ] += start2 202 | else: 203 | print( "Invalid relation type {} at self.operationRelations[ {} ][ {} ][ 'type' ]".format( self.operationRelations[ op2 ][ op1 ][ "type" ], op2, op1 ) ) 204 | return False 205 | 206 | p[ "score_operationRelations" ] = int( p[ "score" ] ) 207 | 208 | 209 | # Resource Succession 210 | p[ "score_resourceSuccession" ] = int( p[ "score" ] ) 211 | 212 | sorted_operations = [] 213 | for i in range( self.operationCount ): 214 | sorted_operations.append( ( i, int( p[ "start_times" ][ i ] ), int( p[ "resources" ][ i ] ) ) ) 215 | sorted_operations.sort( key = lambda x: ( x[ 2 ], x[ 1 ] ) ) 216 | 217 | for i in range( 1, self.operationCount ): 218 | op1 = sorted_operations[ i-1 ][ 0 ] 219 | op2 = sorted_operations[ i ][ 0 ] 220 | r1 = sorted_operations[ i-1 ][ 2 ] 221 | r2 = sorted_operations[ i ][ 2 ] 222 | 223 | if r1 == r2: 224 | if p[ "start_times" ][ op2 ] < p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ): 225 | p[ "score" ] -= self.weightResourceSuccession 226 | 227 | p[ "score_resourceSuccession" ] = int( p[ "score" ] - p[ "score_resourceSuccession" ] ) 228 | 229 | 230 | # Fastest Resource (resource dependent operation durations) 231 | p[ "score_fastestResource" ] = int( p[ "score" ] ) 232 | 233 | for op in range( self.operationCount ): 234 | p[ "score" ] -= self.getOperationDuration( op, p[ "resources" ][ op ] ) 235 | 236 | p[ "score_fastestResource" ] = int( p[ "score" ] - p[ "score_fastestResource" ] ) 237 | 238 | return True 239 | 240 | def calculatePopulationGenome( self ): 241 | resourceCount = self.resourceCount - 1 if self.resourceCount > 1 else 1 242 | 243 | for p in self.population: 244 | p[ "genome" ] = "" 245 | for i in range( self.operationCount ): 246 | p[ "genome" ] += self.numberToString( p[ "start_times" ][ i ], self.operationMaxTime ) 247 | p[ "genome" ] += self.numberToString( p[ "resources" ][ i ], resourceCount ) 248 | return True 249 | 250 | def numberToString( self, _number, _length ): 251 | number = int( _number ) 252 | padding = int( _length - _number ) 253 | probability = int( round( 100 * ( padding / _length ) ) ) 254 | string = "" 255 | while number + padding > 0: 256 | if number == 0: 257 | string += "0" 258 | padding -= 1 259 | continue 260 | if padding == 0: 261 | string += "1" 262 | number -= 1 263 | continue 264 | if random.randint( 0, 100 ) < probability: 265 | string += "0" 266 | padding -= 1 267 | else: 268 | string += "1" 269 | number -= 1 270 | return string 271 | 272 | def breedPopulation( self, _text ): 273 | self.scorePopulation() 274 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) 275 | 276 | self.printBestNormalized( _text ) 277 | #self.printRandom( _text ) 278 | #self.printAllScores( _text ) 279 | 280 | if self.averageScoreSampleSize > 0: 281 | self.averageScoreSample.append( int( self.population[ 0 ][ "score" ] ) ) 282 | if len( self.averageScoreSample ) > self.averageScoreSampleSize: 283 | del self.averageScoreSample[ 0 ] 284 | self.averageScore = sum( self.averageScoreSample ) / self.averageScoreSampleSize 285 | 286 | survivors = int( round( self.survivalRate * self.populationSize ) ) 287 | for i in range( survivors, self.populationSize ): 288 | del self.population[ -1 ] 289 | 290 | if self.infuseRandomToPopulation > 0: 291 | self.addRandomToPopulation( self.infuseRandomToPopulation ) 292 | self.calculatePopulationGenome() 293 | self.scorePopulation() 294 | 295 | self.calculatePopulationGenome() 296 | 297 | new_population = [] 298 | for n in range( self.populationSize ): 299 | p1 = random.randint( 0, len( self.population ) - 1 ) 300 | p2 = random.randint( 0, len( self.population ) - 1 ) 301 | 302 | genome1 = str( self.population[ p1 ][ "genome" ] ) 303 | genome2 = str( self.population[ p2 ][ "genome" ] ) 304 | 305 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 306 | start_times, resources = self.genomeToValues( new_genome ) 307 | 308 | if self.historyKeep == True: 309 | for i in range( self.historyRetryCount ): 310 | if ( start_times, resources ) not in self.history: 311 | self.history.append( ( list( start_times ), list( resources ) ) ) 312 | break 313 | genome1 = str( self.population[ random.randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 314 | genome2 = str( self.population[ random.randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 315 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 316 | start_times, resources = self.genomeToValues( new_genome ) 317 | 318 | new_population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": str( new_genome ) } ) 319 | 320 | self.population.clear() 321 | self.population = list( new_population ) 322 | 323 | return True 324 | 325 | def crossTwoGenomes( self, _genome1, _genome2 ): 326 | genome_length = len( _genome1 ) 327 | index = 0 328 | result_genome = ""; 329 | 330 | while True: 331 | step = random.randint( self.crossMinStep, self.crossMaxStep ) 332 | if step > genome_length - ( index + 1 ): 333 | if random.randint( 0, 99 ) < 50: 334 | result_genome += _genome1[ index : ] 335 | else: 336 | result_genome += _genome2[ index : ] 337 | break 338 | if random.randint( 0, 99 ) < 50: 339 | result_genome += _genome1[ index : index + step ] 340 | else: 341 | result_genome += _genome2[ index : index + step ] 342 | index += step 343 | 344 | if self.mutationProbability > 0: 345 | if random.randint( 1, 10000 ) < self.mutationProbability * 10000: 346 | result_genome = list( result_genome ) 347 | 348 | if type( self.mutationSize ) is float: 349 | number_of_mutations = int( round( len( result_genome ) * self.mutationSize ) ) 350 | else: 351 | number_of_mutations = int( self.mutationSize ) 352 | 353 | for i in range( number_of_mutations ): 354 | p = random.randint( 0, len( result_genome ) - 1 ) 355 | result_genome[ p ] = "0" if random.randint( 0, 99 ) < 50 else "1" 356 | 357 | result_genome = "".join( result_genome ) 358 | 359 | return result_genome 360 | 361 | def genomeToValues( self, _genome ): 362 | start_times = [] 363 | resources = [] 364 | segment = self.operationMaxTime + ( self.resourceCount - 1 if self.resourceCount > 1 else 1 ) 365 | 366 | for i in range( self.operationCount ): 367 | st_from = i * segment 368 | st_to = i * segment + self.operationMaxTime 369 | r_from = i * segment + self.operationMaxTime 370 | r_to = ( i + 1 ) * segment 371 | 372 | start_times.append( _genome[ st_from : st_to ].count( "1" ) ) 373 | resources.append( _genome[ r_from : r_to ].count( "1" ) ) 374 | 375 | return start_times, resources 376 | 377 | 378 | 379 | def printBestNormalized( self, _text ): 380 | min_start_time = min( self.population[ 0 ][ "start_times" ] ) 381 | start_times = [] 382 | for i in self.population[ 0 ][ "start_times" ]: 383 | start_times.append( i - min_start_time ) 384 | print( _text + " avg: {}, score: {}, s_opRel: {}, s_resSucc: {}, s_fastRes: {}, start_times: {}, resources: {}".format( 385 | self.averageScore, 386 | self.population[ 0 ][ "score" ], 387 | self.population[ 0 ][ "score_operationRelations" ], 388 | self.population[ 0 ][ "score_resourceSuccession" ], 389 | self.population[ 0 ][ "score_fastestResource" ], 390 | start_times, 391 | self.population[ 0 ][ "resources" ] 392 | ) 393 | ) 394 | 395 | def printRandom( self, _text ): 396 | i = random.randint( 0, len( self.population ) - 1 ) 397 | print( _text + " avg: {}, score: {}, s_opRel: {}, s_resSucc: {}, s_fastRes: {}, start_times: {}, resources: {}".format( 398 | self.averageScore, 399 | self.population[ i ][ "score" ], 400 | self.population[ i ][ "score_operationRelations" ], 401 | self.population[ i ][ "score_resourceSuccession" ], 402 | self.population[ i ][ "score_fastestResource" ], 403 | self.population[ i ][ "start_times" ], 404 | self.population[ i ][ "resources" ] 405 | ) 406 | ) 407 | 408 | def printAllScores( self, _text ): 409 | all_scores = [ p[ "score" ] for p in self.population ] 410 | all_scores.sort( reverse = True ) 411 | print( _text + " " + str( all_scores ) ) 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 | operationRelations_simple = { 440 | 0:{ 441 | 3:{ "type":"ES", "min":0, "max":0, "weight":1 } 442 | }, 443 | 4:{ 444 | 0:{ "type":"ES", "min":2, "max":2, "weight":1 }, 445 | 1:{ "type":"EE", "min":2, "max":2, "weight":1 } 446 | }, 447 | 2:{ 448 | 1:{ "type":"SS", "min":11, "max":11, "weight":1 } 449 | } 450 | } 451 | operationDurations_simple = { 452 | 0: [ 7, 5 ], 453 | 1: [ 7, 5 ], 454 | 2: [ 7, 5 ], 455 | 3: [ 7, 5 ], 456 | 4: [ 7, 5 ] 457 | } 458 | 459 | 460 | 461 | operationRelations_complex1 = { 462 | 7:{ 463 | 10:{ "type":"ES", "min":-7, "max":-7, "weight":1 } 464 | }, 465 | 9:{ 466 | 7:{ "type":"ES", "min":3, "max":3, "weight":1 }, 467 | 10:{ "type":"EE", "min":5, "max":5, "weight":1 } 468 | }, 469 | 8:{ 470 | 9:{ "type":"SS", "min":-3, "max":-3, "weight":1 } 471 | }, 472 | 6:{ 473 | 8:{ "type":"EE", "min":15, "max":15, "weight":1 } 474 | }, 475 | 5:{ 476 | 6:{ "type":"SE", "min":-4, "max":-4, "weight":1 } 477 | }, 478 | 4:{ 479 | 5:{ "type":"SS", "min":9, "max":9, "weight":1 } 480 | }, 481 | 0:{ 482 | 4:{ "type":"EE", "min":3, "max":3, "weight":1 } 483 | }, 484 | 2:{ 485 | 0:{ "type":"SE", "min":15, "max":15, "weight":1 } 486 | }, 487 | 3:{ 488 | 2:{ "type":"EE", "min":-4, "max":-4, "weight":1 } 489 | }, 490 | 1:{ 491 | 3:{ "type":"SS", "min":3, "max":3, "weight":1 } 492 | } 493 | } 494 | operationDurations_complex1 = { 495 | 0: [ 7, 6, 7 ], 496 | 1: [ 12, 12, 8 ], 497 | 2: [ 5, 8, 6 ], 498 | 3: [ 7, 3, 5 ], 499 | 4: [ 13, 8, 5 ], 500 | 5: [ 7, 9, 10 ], 501 | 6: [ 6, 10, 10 ], 502 | 7: [ 7, 4, 6 ], 503 | 8: [ 14, 10, 8 ], 504 | 9: [ 6, 5, 6 ], 505 | 10: [ 10, 12, 14 ] 506 | } 507 | # ideal solution 508 | #start_times_complex1 = [ 25, 36, 35, 33, 23, 14, 25, 4, 8, 11, 1 ] 509 | #resources_complex1 = [ 1, 2, 0, 1, 2, 0, 0, 1, 2, 1, 0 ] 510 | 511 | 512 | 513 | operationRelations_complex2 = { 514 | 1:{ 515 | 0:{ "type":"ES", "min":0, "max":0, "weight":1 } 516 | }, 517 | 2:{ 518 | 0:{ "type":"ES", "min":0, "max":0, "weight":1 } 519 | }, 520 | 3:{ 521 | 1:{ "type":"ES", "min":0, "max":0, "weight":1 }, 522 | 2:{ "type":"ES", "min":0, "max":0, "weight":1 } 523 | }, 524 | 4:{ 525 | 3:{ "type":"ES", "min":0, "max":0, "weight":1 } 526 | }, 527 | 5:{ 528 | 4:{ "type":"ES", "min":0, "max":0, "weight":1 } 529 | }, 530 | 6:{ 531 | 5:{ "type":"ES", "min":0, "max":0, "weight":1 } 532 | }, 533 | 7:{ 534 | 5:{ "type":"ES", "min":0, "max":0, "weight":1 } 535 | }, 536 | 8:{ 537 | 6:{ "type":"ES", "min":0, "max":0, "weight":1 }, 538 | 7:{ "type":"ES", "min":0, "max":0, "weight":1 } 539 | }, 540 | 9:{ 541 | 8:{ "type":"ES", "min":0, "max":0, "weight":1 } 542 | }, 543 | 10:{ 544 | 9:{ "type":"ES", "min":0, "max":0, "weight":1 } 545 | }, 546 | 11:{ 547 | 9:{ "type":"ES", "min":0, "max":0, "weight":1 } 548 | }, 549 | 12:{ 550 | 10:{ "type":"ES", "min":0, "max":0, "weight":1 }, 551 | 11:{ "type":"ES", "min":0, "max":0, "weight":1 } 552 | }, 553 | 13:{ 554 | 12:{ "type":"ES", "min":0, "max":0, "weight":1 } 555 | }, 556 | 14:{ 557 | 13:{ "type":"ES", "min":0, "max":0, "weight":1 } 558 | }, 559 | 15:{ 560 | 14:{ "type":"ES", "min":0, "max":0, "weight":1 } 561 | }, 562 | 16:{ 563 | 15:{ "type":"ES", "min":0, "max":0, "weight":1 } 564 | }, 565 | 17:{ 566 | 15:{ "type":"ES", "min":0, "max":0, "weight":1 } 567 | }, 568 | 18:{ 569 | 16:{ "type":"ES", "min":0, "max":0, "weight":1 }, 570 | 17:{ "type":"ES", "min":0, "max":0, "weight":1 } 571 | }, 572 | 19:{ 573 | 18:{ "type":"ES", "min":0, "max":0, "weight":1 } 574 | } 575 | } 576 | operationDurations_complex2 = { 577 | 0: [ 4, 6, 6 ], 578 | 1: [ 6, 4, 6 ], 579 | 2: [ 6, 6, 4 ], 580 | 3: [ 4, 6, 6 ], 581 | 4: [ 6, 6, 4 ], 582 | 5: [ 6, 4, 6 ], 583 | 6: [ 4, 6, 6 ], 584 | 7: [ 6, 6, 4 ], 585 | 8: [ 6, 4, 6 ], 586 | 9: [ 6, 6, 4 ], 587 | 10: [ 4, 6, 6 ], 588 | 11: [ 6, 4, 6 ], 589 | 12: [ 6, 6, 4 ], 590 | 13: [ 4, 6, 6 ], 591 | 14: [ 6, 6, 4 ], 592 | 15: [ 6, 4, 6 ], 593 | 16: [ 4, 6, 6 ], 594 | 17: [ 6, 6, 4 ], 595 | 18: [ 6, 4, 6 ], 596 | 19: [ 4, 6, 6 ] 597 | } 598 | # ideal solution 599 | #start_times_complex2 = { 0, 4, 4, 8, 12, 16, 20, 20, 24, 28, 32, 32, 36, 40, 44, 48, 52, 52, 56, 60 ] 600 | #resources_complex2 = [ 0, 1, 2, 0, 2, 1, 0, 2, 1, 2, 0, 1, 2, 0, 2, 1, 0, 2, 1, 0 ] 601 | 602 | 603 | 604 | myParameters = { 605 | "resourceCount": 3, # int from 1 to N 606 | "populationSize": 200, # int from 1 to N 607 | "survivalRate": 0.2, # float from min to 1.0 608 | "infuseRandomToPopulation": 0, # int from 1 to N 609 | "crossMinStep": 0.1, # float from min to 1.0 or int from 1 to genome segment length, must be less than or equal to crossMaxStep 610 | "crossMaxStep": 0.12, # float from min to 1.0 or int from 1 to genome segment length, must be greater than or equal to crossMinStep 611 | "mutationProbability": 0, # float from min to 1.0 612 | "mutationSize": 0.05, # float from min to 1.0 or int from 1 to genome length 613 | "asapAlapMode": "normal", # "normal", "asap", "alap" 614 | "weightResourceSuccession": 10, # int from 0 to N 615 | "historyKeep": False, # True or False 616 | "historyRetryCount": 30, # int from 0 to N 617 | "averageScoreSampleSize": 0, # 0 for disabled or int 1 from N for average score sample size 618 | "operationDurations": operationDurations_complex2, # dictionary 619 | "operationRelations": operationRelations_complex2 # dictionary 620 | } 621 | myGAS = GAS( myParameters ) 622 | myGAS.addRandomToPopulation( myGAS.populationSize ) 623 | 624 | 625 | for i in range( 20 ): 626 | myGAS.breedPopulation( str( i ) ) -------------------------------------------------------------------------------- /GASv5.00.py: -------------------------------------------------------------------------------- 1 | # created by svinec (2019) - use freely but please reference my original work, thanks :) 2 | 3 | """ 4 | SHORT INTRO 5 | 6 | Operation scheduling is a classical problem in Operations Research. The problem arises when there are some number of operations that need to be 7 | assigned to different resources for execution, and we don't know what the optimal way of assinging is. 8 | 9 | Operations in this context can be anything from simple everyday tasks to complex manufacturing operations in a production facility. 10 | Resources in this context is any entity or unit that can do work, for example a machine can be a resource, a human worker can be a resources, 11 | or a whole team of people can be a resource - this is just an abstraction layer. 12 | Optimal solution can have different meanings depending on the objective of the problem (i.e. it can be most constraints respected, least amount 13 | of total time needed, etc.) 14 | 15 | The scheduling problem easily becomes very complex when we have to respect different types of constraints and different given parameters of the 16 | model. There are simply overwhelmingly too many possible ways of assigning a given set of operations to a given set of resources, so it is not 17 | easy to tell which is the best or optimal way. 18 | 19 | The complexity of this and other similar problems, means that there is no manageable mathematical formula or procedure that can be used to 20 | reliably calculate an optimal solution to any given problem. People have tried various methods of tackling this problem, one of which is a Genetic 21 | Algorithm. Generally speaking a GA is a form of a brute force search, i.e. it can search the whole space of solutions, but it won't do it one 22 | by one. It will use the solutions it already has and take their good traits in order to predict the next better solution. It will keep doing so 23 | until a good-enough solutuon is found. In doing so it mimicks the natural process of biological evolution, hence the name Genetic Algorithm. 24 | Some more details can be found in this video https://www.youtube.com/watch?v=e84aLKGWtW4 25 | """ 26 | 27 | """ 28 | CHANGE LOG 29 | 30 | v5.00 31 | - Tournament mode 32 | 33 | v4.00 34 | - reset function 35 | - automated testing and dumping script 36 | - average score calculation now caclulates average score within populations and then across populations (it used to take the best member of each population and then average it across populations) 37 | 38 | v3.00 39 | - two new complex examples 40 | - printRandom and printAllScores 41 | - mutation probability and mutation size 42 | - average score calculation based on a sample of the latest populations 43 | - specify crossMinStep 44 | - specify crossMinStep and crossMaxStep in relative terms or absolute 45 | 46 | v2.00 added features 47 | - resource dependent operation duration and default duration for operation, plus scoring for fastest resources used 48 | - can view separate scores for each scoring method 49 | - improved performance - all scoring done in one iteration and not separate 50 | 51 | v1.00 added features 52 | - number of resources and resource succession weights 53 | - operation time independent on resource 54 | - operation relations with min and max offset and weights for each relation 55 | - normal / asap / alap mode 56 | - history keeping and retry count 57 | - infuse random members to the population 58 | - cross mode - max step 59 | """ 60 | 61 | import time, datetime 62 | from random import randint 63 | dtnow = datetime.datetime.now # a shortcut for logging messages 64 | 65 | 66 | class GAS(): 67 | """ This is the main class. It is self-sufficient, meaning that every instance of the class has its own set of parameters, operations, resource, etc. 68 | and can function on its own. Each instance of the class can capture only one problem and solve it.""" 69 | 70 | def __init__( self, _parameters ): 71 | # When an instance is created, we take the input parameters and store them inside the instance. We also do some calculations (further below). 72 | self.resourceCount = int( _parameters[ "resourceCount" ] ) # The number of resources [1 <= integer < inf] 73 | self.populationSize = int( _parameters[ "populationSize" ] ) # The size of the population [1 <= integer < inf ] (a population is a collection of solutions, the number of solutions is the population size) 74 | self.population = [] # A container for the population [list of dictionaries {'start_times':[] , 'resources':[], 'score':int, 'genome':str}] 75 | self.survivalRate = float( _parameters[ "survivalRate" ] ) # What percent of the population survives on each breeding cycle [0.0 <= float <= 1.0] 76 | self.infuseRandomToPopulation = int( _parameters[ "infuseRandomToPopulation" ] ) # How many random solutions to add to the population on each breeding cycle [0 <= integer < inf] 77 | self.mutationProbability = float( _parameters[ "mutationProbability" ] ) # The probability of mutating the new genome after crossing the two genomes [0.0000 <= float <= 1.0000]. For example, a probability of 0.33 means that about one third of the new genomes generated on each breeding cycle will be mutated. 78 | self.mutationSize = float( _parameters[ "mutationSize" ] ) if type( _parameters[ "mutationSize" ] ) is float else int( _parameters[ "mutationSize" ] ) 79 | # Mutation size controlls how many bits in the genome will have an attempted mutation. "Attempted" because there is a 50/50 chance to change from 0 to 1 or from 1 to 0. 80 | # If expressed as [0.0 <= float <= 1.0] then represents the relative size of the genome and an integer will be calculated later. 81 | # If expressed as [0 <= integer < inf] then it is the exact number of attempted mutations on bits from the genome. 82 | self.asapAlapMode = str( _parameters[ "asapAlapMode" ] ) 83 | # Controlls how time constraints are scored [string]. Three modes are possible: 84 | # 'normal' - An operation relation will get a negative score only if the relation is outside of the Min and Max offsets defined for that relation 85 | # 'asap' - As soon as possible. Solutions that complete faster are scored higher. 86 | # 'alap' - As late as possible. Solutions that complete as late as possible are scored higher. 87 | self.weightResourceSuccession = int( _parameters[ "weightResourceSuccession" ] ) # Resource Succession means that each resource should be working on no more than one operation at any given time. Generated solutions might violate this constraint. If a constraint is violated then the solution is scored negatively with weightResourceSuccession [0 <= integer < inf]. It is a simple substraction from the total score therefore must be used wisely in conjunction with other scoring. For example, if you choose one unit of time to be one minute, and a solution violates an operation relation by 2 hours, e.g. 120, you might be okay with that if it's not critical, but if the Resource Succession is more critical for you then the weight should be something like 3000. 88 | self.historyKeep = bool( _parameters[ "historyKeep" ] ) # This option will force the algorithm to keep breeding new solutions until the new population has only unique solutions (the uniqueness is across all previous solutions) [boolean]. This can be incredibly slow and is generally discouraged. It's much better to cycle through a few repetitve solutions that to search a log of thousand previous solutions. 89 | self.historyRetryCount = int( _parameters[ "historyRetryCount" ] ) # Because finding a unique solution can sometime be very slow, this option tells the algoritm how many times to try before accepting a duplicate solution and adding to the new population [0 <= integer < inf] 90 | self.history = [] # A container for the history log [list of tuples (Start Times, Resources)] 91 | self.averageScoreSampleSize = int( _parameters[ "averageScoreSampleSize" ] ) # The average score is based on the best solutions from the last N generations [0 for disabled, else 1 <= integer < inf]. This can be a useful indicator if the solver is improving the solution over time or not. 92 | self.averageScoreSample = [] # A container for the best scores of the last N generations [list of integers] 93 | self.averageScore = None # The average score of the current solver 94 | self.tournamentPopulationSize = int( _parameters[ "tournamentPopulationSize" ] ) # When running in Tournament mode, this is the number of individuals to sample in total [1 <= integer < inf] 95 | self.tournamentPopulation = [] # A list that will hold the best individuals from each run 96 | self.tournamentSample = int( _parameters[ "tournamentSample" ] ) # The number of best individuals to collect from each run and save into the Tournamen population [1 <= integer < inf] 97 | self.tournamentGenerations = int( _parameters[ "tournamentGenerations" ] ) # The number of generations within each run of the solver before the best individuals are saved [1 <= integer < inf] 98 | 99 | self.operationDurations = {} 100 | # A definition of how much time each operation takes to complete. [dictionary] This is a unitless definition using integers. The meaning is assigned by the user, e.g. 1 can be one minute, one hour, one day, one 15-minute chunk, etc. Each operation duration can be defined in one of two different ways: 101 | # 1) If all resources take the same amount of time to complete the operation then [1 <= integer < inf] 102 | # 2) If different resources complete the operation in different amount of time then [list of integers, where the index matches the resource index] 103 | for i in _parameters[ "operationDurations" ]: 104 | if type( _parameters[ "operationDurations" ][ i ] ) is int: 105 | self.operationDurations[ i ] = int( _parameters[ "operationDurations" ][ i ] ) 106 | elif type( _parameters[ "operationDurations" ][ i ] ) is list: 107 | self.operationDurations[ i ] = list( _parameters[ "operationDurations" ][ i ] ) # copy by value not by reference 108 | else: 109 | print( "{}\tInvalid operation duration: {}, type: {}\nTerminating".format( dtnow(), _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 110 | return False 111 | 112 | self.operationCount = len( self.operationDurations ) # The number of operations [1 <= integer < inf] 113 | 114 | self.operationRelations = {} 115 | # A dictionary of two more nested dictionaries that stores operation relations. The structure is operationRelations[ op2 ][ op1 ][ parameter ], where: 116 | # 'op2' is the second operation in the relation 117 | # 'op1' is the first operation in the relation 118 | # 'parameter' can be either of four types of parameters: 119 | # - 'type' - available types or relations are: 120 | # - 'SS' - start-to-start - the start of the first operation relates to the start of the second operation 121 | # - 'SE' - start-to-end - the start of the first operation relates to the end of the second operation 122 | # - 'ES' - end-to-start - the end of the first operation relates to the start of the second operation 123 | # - 'EE' - end-to-end - the end of the first operation relates to the end of the second operation 124 | # - 'min' - the minimum time for the relation (for example, if the relation type is 'ES' and the min time is 10, it means that the second operation should start 10 units of time after the end of the first operation or later, but not sooner) 125 | # - 'max' - the maximum time for the relation (for example, if the relation type is 'ES' and the min time is 30, it means that the second operation should start 30 units of time after the end of the first operation or sooner, but not later) 126 | # - 'weight' - a custom weight used to fine-tune the scoring of schedules, default is 1 127 | 128 | for op2 in _parameters[ "operationRelations" ]: # copy by value not by reference 129 | self.operationRelations[ op2 ] = {} 130 | for op1 in _parameters[ "operationRelations" ][ op2 ]: 131 | self.operationRelations[ op2 ][ op1 ] = dict( _parameters[ "operationRelations" ][ op2 ][ op1 ] ) 132 | 133 | # Evaluate the asapAlapMode 134 | for op2 in self.operationRelations: 135 | for op1 in self.operationRelations[ op2 ]: 136 | if self.asapAlapMode == "normal": 137 | if self.operationRelations[ op2 ][ op1 ][ "min" ] == None and self.operationRelations[ op2 ][ op1 ][ "max" ] == None: 138 | # If the mode is 'normal' and min and max are not specified, then we need at leas a min definition 139 | self.operationRelations[ op2 ][ op1 ][ "min" ] = 0 140 | elif self.asapAlapMode == "asap": 141 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 142 | # If the mode is 'asap' then we want to score solutions as if there is no later execution allowed 143 | self.operationRelations[ op2 ][ op1 ][ "max" ] = int( self.operationRelations[ op2 ][ op1 ][ "min" ] ) 144 | elif self.asapAlapMode == "alap": 145 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 146 | # If the mode is 'alap' then we want to score solutions as if there is no early execution allowed 147 | self.operationRelations[ op2 ][ op1 ][ "min" ] = int( self.operationRelations[ op2 ][ op1 ][ "max" ] ) 148 | 149 | self.operationMaxTime = 0 # The longest possible solution [1 <= integer < inf]. This is used later to find what the minimum lenght of the genome is in order to allow to represent all possible solutions 150 | for op in range( self.operationCount ): # It is a sum of all operation durations... 151 | if type( self.operationDurations[ op ] ) is int: 152 | self.operationMaxTime += self.operationDurations[ op ] 153 | else: 154 | self.operationMaxTime += max( self.operationDurations[ op ] ) 155 | for op2 in self.operationRelations: # ...plus the sum of the largest relation offsets 156 | for op1 in self.operationRelations[ op2 ]: 157 | rel_min = self.operationRelations[ op2 ][ op1 ][ "min" ] if self.operationRelations[ op2 ][ op1 ][ "min" ] != None else 0 158 | rel_max = self.operationRelations[ op2 ][ op1 ][ "max" ] if self.operationRelations[ op2 ][ op1 ][ "max" ] != None else 0 159 | self.operationMaxTime += max( abs( rel_min ), abs( rel_max ) ) 160 | 161 | # When two genomes are combined into a new one, this is done by splitting both genomes in steps. crossMinStep defines the minimum length of the step and crossMaxStep defines the maximum lenght of the step. crossMinStep must be less than or equal to crossMaxStep. They can be defined in one of two ways: 162 | # If expressed as [0.0 <= float <= 1.0] then it represents the size of the step relative to the genome length 163 | # If expressed as [0 <= integer < inf] then it is an exact number of characters (zeroes or ones) 164 | if type( _parameters[ "crossMinStep" ] ) is float: 165 | self.crossMinStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * _parameters[ "crossMinStep" ] ) ) 166 | else: 167 | self.crossMinStep = int( _parameters[ "crossMinStep" ] ) 168 | 169 | if type( _parameters[ "crossMaxStep" ] ) is float: 170 | self.crossMaxStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * _parameters[ "crossMaxStep" ] ) ) 171 | else: 172 | self.crossMaxStep = int( _parameters[ "crossMaxStep" ] ) 173 | 174 | # only reset runtime data so the model can be run again, but keep the parameters 175 | def reset( self ): 176 | self.population = [] 177 | self.history = [] 178 | self.averageScoreSample = [] 179 | self.averageScore = None 180 | 181 | # for a given operation (and resource) return the duration of the operation 182 | def getOperationDuration( self, _op, _r = 0 ): 183 | if type( self.operationDurations[ _op ] ) is int: 184 | return int( self.operationDurations[ _op ] ) 185 | elif type( self.operationDurations[ _op ] ) is list: 186 | return int( self.operationDurations[ _op ][ _r ] ) 187 | else: 188 | print( "{}\tInvalid operation duration: {}, type: {}\nTerminating".format( dtnow(), _parameters[ "operationDurations" ][ i ], type( _parameters[ "operationDurations" ][ i ] ) ) ) 189 | return False 190 | 191 | # add n number of random individuals to the population 192 | def addRandomToPopulation( self, _n ): 193 | for n in range( _n ): 194 | start_times = [ randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 195 | resources = [ randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 196 | 197 | if self.historyKeep == True: 198 | for i in range( self.historyRetryCount ): 199 | if ( start_times, resources ) not in self.history: 200 | self.history.append( ( list( start_times ), list( resources ) ) ) 201 | break 202 | start_times = [ randint( 0, self.operationMaxTime ) for o in range( self.operationCount ) ] 203 | resources = [ randint( 0, self.resourceCount - 1 ) for o in range( self.operationCount ) ] 204 | 205 | self.population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": "" } ) 206 | 207 | return True 208 | 209 | def scorePopulation( self ): 210 | for p in self.population: # for every member of the population do the below: 211 | p[ "score" ] = 0 # set the score to zero to clear previous scoring 212 | 213 | # Operation Relations - This section will score members based on whether the operation relations are violated or not 214 | # all operations are iterated... it looks a bit messy, but this it is actually well structured and this is what you get in order to check and score all different combinations 215 | for op2 in self.operationRelations: # as you can see here, it is important that the model is defined accurately otherwise can run into IndexErrors and KeyErrors 216 | start2 = p[ "start_times" ][ op2 ] 217 | end2 = p[ "start_times" ][ op2 ] + self.getOperationDuration( op2, p[ "resources" ][ op2 ] ) 218 | 219 | for op1 in self.operationRelations[ op2 ]: 220 | start1 = p[ "start_times" ][ op1 ] 221 | end1 = p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ) 222 | 223 | if self.operationRelations[ op2 ][ op1 ][ "type" ] == "SS": 224 | 225 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 226 | threshold_min = start2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 227 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 228 | elif self.asapAlapMode == "asap": 229 | p[ "score" ] -= start2 230 | 231 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 232 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 233 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 234 | elif self.asapAlapMode == "alap": 235 | p[ "score" ] += start2 236 | 237 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "SE": 238 | 239 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 240 | threshold_min = end2 - ( start1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 241 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 242 | elif self.asapAlapMode == "asap": 243 | p[ "score" ] -= start2 244 | 245 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 246 | threshold_max = ( start1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 247 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 248 | elif self.asapAlapMode == "alap": 249 | p[ "score" ] += start2 250 | 251 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "ES": # if the relation between op1 and op2 is End-to-Start, meaning op2 cannot start until op1 has ended (one of the most common type of relations): 252 | 253 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: # (a) If there is a min offset specified then we take it into account by adjusting the score... 254 | threshold_min = start2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) # (b) The start of op2 should be greater than the end of op1 plus the min offset... 255 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] # (b) ... otherwise, subtract (it is already negative) the difference from the score adjusted by the specific weight for this relation. In this way the smaller the violation of the min offset, the better the score. 256 | elif self.asapAlapMode == "asap": # (a) ... Otherwise, check if the mode is 'asap'. This is mutually exclusive with a min offset that's why it is in an 'elif' statement. In this case subtract the start time of op2 from the score - the sooner all operations start the better the score will be. 257 | p[ "score" ] -= start2 258 | 259 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: # (c) If there is a max offset specified then we take it into account by adjusting the score... 260 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - start2 # (d) The start of op2 should be no greater than the end of op1 plus the max offset... 261 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] # (d) ... otherwise, subtract (it is already negative) the difference from the score adjusted by the specific weight for this relation. In this way the smaller the violation of the max offset, the better the score. 262 | elif self.asapAlapMode == "alap": # (c) ... Otherwise, check if the mode is 'alap'. This is mutually exclusive with a max offset that's why it is in an 'elif' statement. In this case add the start time of op2 to the score - the later all operations start the better the score will be. 263 | p[ "score" ] += start2 264 | 265 | elif self.operationRelations[ op2 ][ op1 ][ "type" ] == "EE": 266 | 267 | if self.operationRelations[ op2 ][ op1 ][ "min" ] != None: 268 | threshold_min = end2 - ( end1 + self.operationRelations[ op2 ][ op1 ][ "min" ] ) 269 | if threshold_min < 0: p[ "score" ] += threshold_min * self.operationRelations[ op2 ][ op1 ][ "weight" ] 270 | elif self.asapAlapMode == "asap": 271 | p[ "score" ] -= start2 272 | 273 | if self.operationRelations[ op2 ][ op1 ][ "max" ] != None: 274 | threshold_max = ( end1 + self.operationRelations[ op2 ][ op1 ][ "max" ] ) - end2 275 | if threshold_max < 0: p[ "score" ] += threshold_max * self.operationRelations[ op2 ][ op1 ][ "weight" ] 276 | elif self.asapAlapMode == "alap": 277 | p[ "score" ] += start2 278 | else: 279 | print( "{}\tInvalid relation type {} at self.operationRelations[ {} ][ {} ][ 'type' ]".format( dtnow(), self.operationRelations[ op2 ][ op1 ][ "type" ], op2, op1 ) ) 280 | return False 281 | 282 | p[ "score_operationRelations" ] = int( p[ "score" ] ) # 'score' is the main score used, 'score_operationRelations' is just to store this score separately 283 | 284 | 285 | # Resource Succession - This section will score members based on whether resources have been assigned one operation at a time or not 286 | p[ "score_resourceSuccession" ] = int( p[ "score" ] ) 287 | 288 | sorted_operations = [] # build a list of all operations and resources which will be sorted, the list is composed of tuples: ( operations id, start time, resource id ) 289 | for i in range( self.operationCount ): 290 | sorted_operations.append( ( i, int( p[ "start_times" ][ i ] ), int( p[ "resources" ][ i ] ) ) ) 291 | sorted_operations.sort( key = lambda x: ( x[ 2 ], x[ 1 ] ) ) # first sort by resource id, then by operation start time 292 | 293 | for i in range( 1, self.operationCount ): # iterate from the second operation to the end 294 | op1 = sorted_operations[ i-1 ][ 0 ] 295 | op2 = sorted_operations[ i ][ 0 ] 296 | r1 = sorted_operations[ i-1 ][ 2 ] 297 | r2 = sorted_operations[ i ][ 2 ] 298 | 299 | if r1 == r2: # if the resource id is the same between two entries then we need to check if there is overlap of operations on that resource 300 | # we do this by checking if one operation starts before the other one has finished 301 | if p[ "start_times" ][ op2 ] < p[ "start_times" ][ op1 ] + self.getOperationDuration( op1, p[ "resources" ][ op1 ] ): 302 | p[ "score" ] -= self.weightResourceSuccession # and if yes, reduce the total score by weightResourceSuccession 303 | 304 | p[ "score_resourceSuccession" ] = int( p[ "score" ] - p[ "score_resourceSuccession" ] ) # 'score' is the main score used, 'score_resourceSuccession' is just to store this score separately 305 | 306 | 307 | # Fastest Resource - This section will score members based on whether operations are being assigned to the resources that will execute them the fastest 308 | p[ "score_fastestResource" ] = int( p[ "score" ] ) 309 | 310 | for op in range( self.operationCount ): 311 | # It simply means subtracting the operation duration of the currently assigned resource from the total score. Thus, schedules where fastest resources are used will have higher scores overall. 312 | p[ "score" ] -= self.getOperationDuration( op, p[ "resources" ][ op ] ) 313 | 314 | p[ "score_fastestResource" ] = int( p[ "score" ] - p[ "score_fastestResource" ] ) # 'score' is the main score used, 'score_fastestResource' is just to store this score separately 315 | 316 | return True 317 | 318 | # for every member of the population, calculate a genome by taking start times and resource ids and convering to a string of zeroes and ones 319 | def calculatePopulationGenome( self ): 320 | resourceCount = self.resourceCount - 1 if self.resourceCount > 1 else 1 321 | 322 | for p in self.population: 323 | p[ "genome" ] = "" # first, clear existing genome 324 | for i in range( self.operationCount ): # then, for every operation in the model convert start time and resource id to string and append to the genome in the same order 325 | p[ "genome" ] += self.numberToString( p[ "start_times" ][ i ], self.operationMaxTime ) 326 | p[ "genome" ] += self.numberToString( p[ "resources" ][ i ], resourceCount ) 327 | return True 328 | 329 | # A generic function handles both start time and resource id conversion. This is possible because numbers are encoded as the number of 1s in a string, thus 0010111011 is the number 6 because there are six ones 330 | def numberToString( self, _number, _length ): # the functions needs to know the number and the maximum number possible, which is eiher operationMaxTime or resourceCount 331 | number = int( _number ) # the number itself, or also the number of ones 332 | padding = int( _length - _number ) # padding is the number of zeroes 333 | probability = int( round( 100 * ( padding / _length ) ) ) # We want to space out ones and zeroes evenly and we can do this using a probability. For example, we don't want to have 1111110000, instead we want something like 0010111011 334 | string = "" 335 | while number + padding > 0: # the loop works by consuming the number and the padding, once these are consumed our job is done and the loop stops 336 | if number == 0: # if have no more 1s left to assign, then we assign a 0... 337 | string += "0" 338 | padding -= 1 339 | continue # ... and continue because there might be more 0s to assign 340 | if padding == 0: # if have no more 0s left to assign, then we assign a 1... 341 | string += "1" 342 | number -= 1 343 | continue # ... and continue because there might be more 1s to assign 344 | if randint( 0, 100 ) < probability: # otherwise, there are still both 1s and 0s to assign, so the probability helps us pick which one to assign next in order to space them evenly 345 | string += "0" 346 | padding -= 1 347 | else: 348 | string += "1" 349 | number -= 1 350 | return string 351 | 352 | # This is the heart of everything. When this method is called it drives all the logic and processing. One call of the method is equal to one cycle of evolutiom, meaning we start with one population and end up with a different one which s derived from the first one. Needless to say, the order of actions below matters. 353 | def breedPopulation( self, do_print = False, print_text = "" ): 354 | self.scorePopulation() # first, whatever population we have, we want to score it 355 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) # then sort it by descending score, meaning highest score first 356 | 357 | if do_print: self.printBestNormalized( print_text ) 358 | 359 | # this is where we capture information about the average score calculation 360 | if self.averageScoreSampleSize > 0: 361 | average = sum( [ p[ "score" ] for p in self.population ] ) # calculate the average score for the whole population 362 | average = int( average / self.populationSize ) 363 | self.averageScoreSample.append( average ) # append it to the list that tracks average score across populations 364 | if len( self.averageScoreSample ) > self.averageScoreSampleSize: # if we have more samples than what is defined... 365 | del self.averageScoreSample[ 0 ] # ... remove the earliest one and leave the rest 366 | self.averageScore = sum( self.averageScoreSample ) / self.averageScoreSampleSize # calculate the average score across populations and save it as current - this can later be printed 367 | 368 | # we are now in a position where we can discard members from the current population 369 | survivors = int( round( self.survivalRate * self.populationSize ) ) # the number of members to keep / survive 370 | for i in range( survivors, self.populationSize ): # the rest are deleted 371 | del self.population[ -1 ] # [-1] means last element and since this is sorted by descending score, we are laywas discarding the worst members 372 | 373 | # now is the time to add random members to the population if the model specifies so 374 | if self.infuseRandomToPopulation > 0: 375 | self.addRandomToPopulation( self.infuseRandomToPopulation ) 376 | #self.calculatePopulationGenome() 377 | self.scorePopulation() 378 | 379 | # create genomes for all members of the current population so we can start breeding the population 380 | self.calculatePopulationGenome() 381 | 382 | new_population = [] # first we build the new population and then we assign it to the model 383 | for n in range( self.populationSize ): # we generate the same number of members for the new population 384 | p1 = randint( 0, len( self.population ) - 1 ) # pick two random members from the current population 385 | p2 = randint( 0, len( self.population ) - 1 ) 386 | 387 | genome1 = str( self.population[ p1 ][ "genome" ] ) # take their genomes 388 | genome2 = str( self.population[ p2 ][ "genome" ] ) 389 | 390 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) # and combine them into a new genome 391 | start_times, resources = self.genomeToValues( new_genome ) # then convert the new genome back to start times and resource ids 392 | 393 | if self.historyKeep == True: # if history tracking is switched on, we need to save the new members to the history log 394 | for i in range( self.historyRetryCount ): # 395 | if ( start_times, resources ) not in self.history: # if the new member is not in the history log, then add it, otherwise keep trying to generate a new member until a unique one is found or until the maximum number of tries is exhausted 396 | self.history.append( ( list( start_times ), list( resources ) ) ) 397 | break 398 | genome1 = str( self.population[ randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 399 | genome2 = str( self.population[ randint( 0, len( self.population ) - 1 ) ][ "genome" ] ) 400 | new_genome = str( self.crossTwoGenomes( genome1, genome2 ) ) 401 | start_times, resources = self.genomeToValues( new_genome ) 402 | 403 | # add the new member to the new population 404 | new_population.append( { "start_times": list( start_times ), "resources": list( resources ), "score": 0, "genome": str( new_genome ) } ) 405 | 406 | self.population.clear() # clear the existing population 407 | self.population = list( new_population ) # and assign the new population 408 | 409 | return True 410 | 411 | # take two genomes, combine them randomly and return a new one 412 | def crossTwoGenomes( self, _genome1, _genome2 ): 413 | genome_length = len( _genome1 ) 414 | index = 0 415 | result_genome = ""; 416 | 417 | while True: 418 | step = randint( self.crossMinStep, self.crossMaxStep ) # each time define a new random step between the min and max limit 419 | if step > genome_length - ( index + 1 ): # if the step goes beyond the end of the genome, then we only need to take what's left from the genome 420 | if randint( 0, 99 ) < 50: # randomly choose which genome to copy data from 421 | result_genome += _genome1[ index : ] 422 | else: 423 | result_genome += _genome2[ index : ] 424 | break 425 | if randint( 0, 99 ) < 50: # otherwise, the step is short from the end of the genome so take the step and again randomly choose which genome to copy data from 426 | result_genome += _genome1[ index : index + step ] 427 | else: 428 | result_genome += _genome2[ index : index + step ] 429 | index += step 430 | 431 | if self.mutationProbability > 0: # here we also implement the mutation feature 432 | if randint( 1, 10000 ) < self.mutationProbability * 10000: 433 | result_genome = list( result_genome ) # convert the string to a list so we can access and change individual letters 434 | 435 | if type( self.mutationSize ) is float: 436 | number_of_mutations = int( round( len( result_genome ) * self.mutationSize ) ) # if the parameter is float then it represents relative size of the genome and find what number that translates to 437 | else: 438 | number_of_mutations = int( self.mutationSize ) # else, we take the value, not the reference 439 | 440 | for i in range( number_of_mutations ): 441 | p = randint( 0, len( result_genome ) - 1 ) 442 | result_genome[ p ] = "0" if randint( 0, 99 ) < 50 else "1" # randomize that many random bits in the genome 443 | 444 | result_genome = "".join( result_genome ) # and convert back to a single string 445 | 446 | return result_genome 447 | 448 | # This function is the opposite of numberToString. It takes a genome as an input and converts it to start times and resource ids 449 | def genomeToValues( self, _genome ): 450 | start_times = [] 451 | resources = [] 452 | segment = self.operationMaxTime + ( self.resourceCount - 1 if self.resourceCount > 1 else 1 ) # A segment contains the number of bits needed to represent one operation - the highest possible start time plus the highest possible resource ids. 453 | 454 | for i in range( self.operationCount ): # The number of segments in a genome is equal to the number of operations. 455 | st_from = i * segment # the beginning of the start time string 456 | st_to = i * segment + self.operationMaxTime # the end of the start time string 457 | r_from = i * segment + self.operationMaxTime # the beginning of the resource id string 458 | r_to = ( i + 1 ) * segment # the end of the resource id string 459 | 460 | start_times.append( _genome[ st_from : st_to ].count( "1" ) ) # as previously mentioned, numbers are encoded as the number of ocurrences of 1s 461 | resources.append( _genome[ r_from : r_to ].count( "1" ) ) 462 | 463 | return start_times, resources 464 | 465 | """ Beginning with version 5.00, Tournament is a new mode that accumulates best individuals from each run until a new population is formed, called the Tournament population. 466 | The new population then becomes the starting point of a run of the solver and continues indefinitely. 467 | The run can be interrupted with Ctrl+C which will prompt for a value: 468 | - Providing no value and hiting enter will start a new run with the same Tournament population. 469 | - Providing any other value will exit the script. Also, doing Ctrl+C during the prompt will exit the script. 470 | """ 471 | def tournament( self ): 472 | keepbreeding = True 473 | while keepbreeding: # keep looping until the Tournament population has been filled with individuals; each cycle of the loop is refer to as "run of the solver" or "run of the model" 474 | self.reset() 475 | self.addRandomToPopulation( self.populationSize ) # every run starts with a random population 476 | for g in range( self.tournamentGenerations ): 477 | self.breedPopulation( do_print=True, print_text="TrnmPop{}of{}".format( len( self.tournamentPopulation ), self.tournamentPopulationSize ) ) 478 | for i in range( self.tournamentSample ): # how many best individuals to take from the current population... 479 | self.tournamentPopulation.append( self.getIndividualAsACopy( self.population, i ) ) # ...and add to the Tournament population 480 | if len( self.tournamentPopulation ) == self.tournamentPopulationSize: 481 | keepbreeding = False # make sure the parent loop will break, too 482 | break 483 | 484 | while True: # now that we have a Tournament population, we start breeding the population 485 | try: 486 | self.reset() 487 | self.populationSize = self.tournamentPopulationSize 488 | self.population = [ self.getIndividualAsACopy( self.tournamentPopulation, i ) for i in range( self.tournamentPopulationSize ) ] # the Tournament population is not consumed, but copied, so can be resued 489 | while True: # the breeding will continue indefinitely... 490 | self.breedPopulation( do_print=True, print_text="Trnmnt" ) 491 | except KeyboardInterrupt: # ...until you press Ctrl+C... 492 | reply = input( "Press Enter to start a new run with the tournament population..." ) 493 | if reply != "": # ...and here you have a choice to start again with the same Tournament population or exit the script 494 | break 495 | 496 | def getIndividualAsACopy( self, source, i ): 497 | return_dict = {} 498 | return_dict[ "start_times" ] = list( source[ i ][ "start_times" ] ) 499 | return_dict[ "resources" ] = list( source[ i ][ "resources" ] ) 500 | return_dict[ "score" ] = int( source[ i ][ "score" ] ) 501 | return_dict[ "genome" ] = str( source[ i ][ "genome" ] ) 502 | return return_dict 503 | 504 | # A function that prints the current state of the model. 'Normalized' means to shift the whole schedule earlier so it begins at time 0. For example, a start times [ 3, 7, 2, 10 ] is normalized to [ 1, 5, 0, 8 ] because in essence it is the same schedule. 505 | def printBestNormalized( self, _text = '' ): 506 | min_start_time = min( self.population[ 0 ][ "start_times" ] ) # find the lowest start time... 507 | start_times = [] 508 | for i in self.population[ 0 ][ "start_times" ]: 509 | start_times.append( i - min_start_time ) # ... and subtract it from every start time 510 | # then print some information 511 | print( "{} avg: {}, score: {}, s_opRel: {}, s_resSucc: {}, s_fastRes: {}".format( 512 | _text, 513 | round( self.averageScore, 1 ) if self.averageScore else self.averageScore, 514 | self.population[ 0 ][ "score" ], 515 | self.population[ 0 ][ "score_operationRelations" ], 516 | self.population[ 0 ][ "score_resourceSuccession" ], 517 | self.population[ 0 ][ "score_fastestResource" ], 518 | #start_times 519 | #self.population[ 0 ][ "resources" ] 520 | ) 521 | ) 522 | 523 | # prints a random member of the current population 524 | def printRandom( self, _text = '' ): 525 | i = randint( 0, len( self.population ) - 1 ) 526 | print( _text + " avg: {}\tscore: {}\ts_opRel: {}\ts_resSucc: {}\ts_fastRes: {}\tstart_times: {}\tresources: {}".format( 527 | self.averageScore, 528 | self.population[ i ][ "score" ], 529 | self.population[ i ][ "score_operationRelations" ], 530 | self.population[ i ][ "score_resourceSuccession" ], 531 | self.population[ i ][ "score_fastestResource" ], 532 | self.population[ i ][ "start_times" ], 533 | self.population[ i ][ "resources" ] 534 | ) 535 | ) 536 | 537 | # print all scores from the current population in descending order 538 | def printAllScores( self, _text = '' ): 539 | all_scores = [ p[ "score" ] for p in self.population ] 540 | all_scores.sort( reverse = True ) 541 | print( _text + " " + str( all_scores ) ) 542 | 543 | # This is a function that automates the testing of the model. The same problem definition can be solved using several different combinations of parameters in order to find out which combination works best. Then you can use the best combination to do some more solving and hopefully find an even better solution. 544 | # For example, it can help you answer questions such as: Is it better to have many generations with a small population size or rather have fewer generations with a large population size, or does it not matter overall? 545 | def automatedTest( self ): 546 | # Below are the input parameters to the function. Change these to suit your needs. 547 | # **************************************** 548 | filename = "automatedTest_results.txt" # the file which to write the results to 549 | at_generations = [ 50, 150, 300 ] # the number of generations (or cycles) in each run, i.e. how many times the population will breed and create new solutions 550 | at_runs = [ 5 ] # the number of runs to carry out for each combination of parameters; remember that on each new run the population is totally randomizd initially 551 | at_cross_min_step = [ 0.05, 0.15, 0.35 ] 552 | at_cross_max_step = [ 0.1, 0.3, 0.5 ] 553 | at_population_size = [ 50, 200, 600 ] 554 | at_survival_rate = [ 0.05, 0.15, 0.5 ] 555 | at_mutate_prob = [ 0, 0.05, 0.15, 0.5 ] 556 | at_mutate_size = [ 0.05, 0.15, 0.25 ] 557 | at_infuse_random = [ 0, 5, 15, 30 ] 558 | # **************************************** 559 | 560 | number_of_combinations = len( at_generations ) * len( at_cross_min_step ) * len( at_cross_max_step ) * len( at_population_size ) * len( at_survival_rate ) * len( at_mutate_prob ) * len( at_mutate_size ) * len( at_infuse_random ) 561 | current_combination = 0 562 | 563 | line_template = "{}\t" * 25 + "\n" 564 | with open( filename, "at", encoding = "utf-8" ) as f: 565 | f.write( line_template.format( 566 | "Combination #", 567 | "Run #", 568 | "Time", 569 | 570 | "Best Score", 571 | "Average Score", 572 | "Worst Score", 573 | 574 | "Operation Relations score - Best", 575 | "Operation Relations score - Average", 576 | "Operation Relations score - Worst", 577 | 578 | "Resource Succession score - Best", 579 | "Resource Succession score - Average", 580 | "Resource Succession score - Worst", 581 | 582 | "Fastest Resource score - Best", 583 | "Fastest Resource score - Average", 584 | "Fastest Resource score - Worst", 585 | 586 | "Generations", 587 | "Cross Min Step", 588 | "Cross Max Step", 589 | "Population Size", 590 | "Survival Rate", 591 | "Mutation Probability", 592 | "Mutation Size", 593 | "Infuse Random", 594 | 595 | "Best Solution Start Times", 596 | "Best Solution Resource IDs" 597 | ) ) 598 | 599 | for cross_min in at_cross_min_step: 600 | for cross_max in at_cross_max_step: 601 | if cross_min > cross_max: 602 | # if Cross Min Step is greater than Cross Max Step then skip all these combinations and continue to next step values 603 | current_combination += len( at_generations ) * len( at_population_size ) * len( at_survival_rate ) * len( at_mutate_prob ) * len( at_mutate_size ) * len( at_infuse_random ) 604 | continue 605 | 606 | for pop_size in at_population_size: 607 | for sur_rate in at_survival_rate: 608 | for mut_prob in at_mutate_prob: 609 | for mut_size in at_mutate_size: 610 | for inf_rand in at_infuse_random: 611 | 612 | if type( cross_min ) is float: self.crossMinStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * cross_min ) ) 613 | else: self.crossMinStep = int( cross_min ) 614 | if type( cross_max ) is float: self.crossMaxStep = int( round( ( self.operationMaxTime + self.resourceCount - 1 ) * self.operationCount * cross_max ) ) 615 | else: self.crossMaxStep = int( cross_max ) 616 | 617 | self.populationSize = int( pop_size ) 618 | self.survivalRate = float( sur_rate ) 619 | self.mutationProbability = float( mut_prob ) 620 | self.mutationSize = float( mut_size ) if type( mut_size ) is float else int( mut_size ) 621 | self.infuseRandomToPopulation = int( inf_rand ) 622 | 623 | for gen in at_generations: 624 | current_combination += 1 625 | for runs in at_runs: 626 | for r in range( runs ): 627 | 628 | print( "{}\tRunning combination {} of {}, run number {} of {}".format( dtnow(), current_combination, number_of_combinations, r+1, runs ) ) 629 | # just before running reset and initialize the model 630 | self.reset() 631 | self.addRandomToPopulation( self.populationSize ) 632 | 633 | time_start = time.time() 634 | for g in range( gen ): 635 | self.breedPopulation() 636 | time_end = time.time() 637 | 638 | self.scorePopulation() 639 | self.population.sort( key = lambda x: x[ "score" ], reverse = True ) 640 | 641 | score_operationRelations = tuple( p[ "score_operationRelations" ] for p in self.population ) 642 | score_resourceSuccession = tuple( p[ "score_resourceSuccession" ] for p in self.population ) 643 | score_fastestResource = tuple( p[ "score_fastestResource" ] for p in self.population ) 644 | 645 | output_line = line_template.format( 646 | current_combination, 647 | r+1, 648 | round( time_end - time_start, 2 ), 649 | 650 | self.population[ 0 ][ "score" ], 651 | self.averageScore, 652 | self.population[ -1 ][ "score" ], 653 | 654 | max( score_operationRelations ), 655 | sum( score_operationRelations ) / len( score_operationRelations ), 656 | min( score_operationRelations ), 657 | 658 | max( score_resourceSuccession ), 659 | sum( score_resourceSuccession ) / len( score_resourceSuccession ), 660 | min( score_resourceSuccession ), 661 | 662 | max( score_fastestResource ), 663 | sum( score_fastestResource ) / len( score_fastestResource ), 664 | min( score_fastestResource ), 665 | 666 | gen, 667 | cross_min, 668 | cross_max, 669 | pop_size, 670 | sur_rate, 671 | mut_prob, 672 | mut_size, 673 | inf_rand, 674 | 675 | self.population[ 0 ][ "start_times" ], 676 | self.population[ 0 ][ "resources" ] 677 | ) 678 | 679 | with open( filename, "at", encoding = "utf-8" ) as f: 680 | f.write( output_line ) 681 | 682 | 683 | 684 | # ideal solution 685 | # start_times = [ 0, 4, 8, 12, 16 ] 686 | # resources = [ 0, 1, 0, 1, 0 ] 687 | operation_durations_simple_1 = { 688 | 0: [ 4, 10 ], 689 | 1: [ 10, 4 ], 690 | 2: [ 4, 10 ], 691 | 3: [ 10, 4 ], 692 | 4: [ 4, 10 ] 693 | } 694 | operation_relations_simple_1 = { 695 | 1: { 696 | 0: { "type":"ES", "min":0, "max":0, "weight":1 } 697 | }, 698 | 2: { 699 | 1: { "type":"ES", "min":0, "max":0, "weight":1 } 700 | }, 701 | 3: { 702 | 2: { "type":"ES", "min":0, "max":0, "weight":1 } 703 | }, 704 | 4: { 705 | 3: { "type":"ES", "min":0, "max":0, "weight":1 } 706 | } 707 | } 708 | 709 | 710 | 711 | # ideal solution? 712 | # start_times = [ 5, 6, 17, 0, 14 ] 713 | # resources = [ 0, 1, 0, 1, 1 ] 714 | operation_durations_simple_2 = { 715 | 0: [ 7, 5 ], 716 | 1: [ 7, 5 ], 717 | 2: [ 7, 5 ], 718 | 3: [ 7, 5 ], 719 | 4: [ 7, 5 ] 720 | } 721 | operation_relations_simple_2 = { 722 | 0: { 723 | 3: { "type":"ES", "min":0, "max":0, "weight":1 } 724 | }, 725 | 4: { 726 | 0: { "type":"ES", "min":2, "max":2, "weight":1 }, 727 | 1: { "type":"EE", "min":8, "max":8, "weight":1 } 728 | }, 729 | 2: { 730 | 1: { "type":"SS", "min":11, "max":11, "weight":1 } 731 | } 732 | } 733 | 734 | 735 | 736 | # ideal solution 737 | # start_times = [ 0, 4, 4, 8, 12, 16, 20, 20, 24, 28, 32, 32, 36, 40, 44, 48, 52, 52, 56, 60 ] 738 | # resources = [ 0, 1, 2, 0, 2, 1, 0, 2, 1, 2, 0, 1, 2, 0, 2, 1, 0, 2, 1, 0 ] 739 | operation_durations_complex_1 = { 740 | 0: [ 4, 10, 10 ], 741 | 1: [ 10, 4, 10 ], 742 | 2: [ 10, 10, 4 ], 743 | 3: [ 4, 10, 10 ], 744 | 4: [ 10, 10, 4 ], 745 | 5: [ 10, 4, 10 ], 746 | 6: [ 4, 10, 10 ], 747 | 7: [ 10, 10, 4 ], 748 | 8: [ 10, 4, 10 ], 749 | 9: [ 10, 10, 4 ], 750 | 10: [ 4, 10, 10 ], 751 | 11: [ 10, 4, 10 ], 752 | 12: [ 10, 10, 4 ], 753 | 13: [ 4, 10, 10 ], 754 | 14: [ 10, 10, 4 ], 755 | 15: [ 10, 4, 10 ], 756 | 16: [ 4, 10, 10 ], 757 | 17: [ 10, 10, 4 ], 758 | 18: [ 10, 4, 10 ], 759 | 19: [ 4, 10, 10 ], 760 | } 761 | operation_relations_complex_1 = { 762 | 1: { 763 | 0: { "type":"ES", "min":0, "max":0, "weight":1 } 764 | }, 765 | 2: { 766 | 0: { "type":"ES", "min":0, "max":0, "weight":1 } 767 | }, 768 | 3: { 769 | 1: { "type":"ES", "min":0, "max":0, "weight":1 } 770 | }, 771 | 3: { 772 | 2: { "type":"ES", "min":0, "max":0, "weight":1 } 773 | }, 774 | 4: { 775 | 3: { "type":"ES", "min":0, "max":0, "weight":1 } 776 | }, 777 | 5: { 778 | 4: { "type":"ES", "min":0, "max":0, "weight":1 } 779 | }, 780 | 6: { 781 | 5: { "type":"ES", "min":0, "max":0, "weight":1 } 782 | }, 783 | 7: { 784 | 5: { "type":"ES", "min":0, "max":0, "weight":1 } 785 | }, 786 | 8: { 787 | 6: { "type":"ES", "min":0, "max":0, "weight":1 } 788 | }, 789 | 8: { 790 | 7: { "type":"ES", "min":0, "max":0, "weight":1 } 791 | }, 792 | 9: { 793 | 8: { "type":"ES", "min":0, "max":0, "weight":1 } 794 | }, 795 | 10: { 796 | 9: { "type":"ES", "min":0, "max":0, "weight":1 } 797 | }, 798 | 11: { 799 | 9: { "type":"ES", "min":0, "max":0, "weight":1 } 800 | }, 801 | 12: { 802 | 10: { "type":"ES", "min":0, "max":0, "weight":1 } 803 | }, 804 | 12: { 805 | 11: { "type":"ES", "min":0, "max":0, "weight":1 } 806 | }, 807 | 13: { 808 | 12: { "type":"ES", "min":0, "max":0, "weight":1 } 809 | }, 810 | 14: { 811 | 13: { "type":"ES", "min":0, "max":0, "weight":1 } 812 | }, 813 | 15: { 814 | 14: { "type":"ES", "min":0, "max":0, "weight":1 } 815 | }, 816 | 16: { 817 | 15: { "type":"ES", "min":0, "max":0, "weight":1 } 818 | }, 819 | 17: { 820 | 15: { "type":"ES", "min":0, "max":0, "weight":1 } 821 | }, 822 | 18: { 823 | 16: { "type":"ES", "min":0, "max":0, "weight":1 } 824 | }, 825 | 18: { 826 | 17: { "type":"ES", "min":0, "max":0, "weight":1 } 827 | }, 828 | 19: { 829 | 18: { "type":"ES", "min":0, "max":0, "weight":1 } 830 | } 831 | } 832 | 833 | 834 | 835 | # ideal solution 836 | # start_times = [ 24, 35, 34, 32, 22, 13, 24, 3, 7, 10, 0 ] 837 | # resources = [ 1, 2, 0, 1, 2, 0, 0, 1, 2, 1, 0 ] 838 | operation_durations_complex_2 = { 839 | 0: [ 7, 6, 7 ], 840 | 1: [ 12, 12, 8 ], 841 | 2: [ 5, 8, 6 ], 842 | 3: [ 7, 3, 5 ], 843 | 4: [ 13, 8, 5 ], 844 | 5: [ 7, 9, 10 ], 845 | 6: [ 6, 10, 10 ], 846 | 7: [ 7, 4, 6 ], 847 | 8: [ 14, 10, 8 ], 848 | 9: [ 7, 5, 6 ], 849 | 10: [ 10, 12, 14 ] 850 | } 851 | operation_relations_complex_2 = { 852 | 7: { 853 | 10: { "type":"ES", "min":-7, "max":-7, "weight":1 } 854 | }, 855 | 9: { 856 | 7: { "type":"ES", "min":3, "max":3, "weight":1 }, 857 | 10: { "type":"EE", "min":5, "max":5, "weight":1 } 858 | }, 859 | 8: { 860 | 9: { "type":"SS", "min":-3, "max":-3, "weight":1 } 861 | }, 862 | 6: { 863 | 8: { "type":"EE", "min":15, "max":15, "weight":1 } 864 | }, 865 | 5: { 866 | 6: { "type":"SE", "min":-4, "max":-4, "weight":1 } 867 | }, 868 | 4: { 869 | 5: { "type":"SS", "min":9, "max":9, "weight":1 } 870 | }, 871 | 0: { 872 | 4: { "type":"EE", "min":3, "max":3, "weight":1 } 873 | }, 874 | 2: { 875 | 0: { "type":"SE", "min":15, "max":15, "weight":1 } 876 | }, 877 | 3: { 878 | 2: { "type":"EE", "min":-4, "max":-4, "weight":1 } 879 | }, 880 | 1: { 881 | 3: { "type":"SS", "min":3, "max":3, "weight":1 } 882 | } 883 | } 884 | 885 | 886 | 887 | parameters_simple_1 = { 888 | "resourceCount": 2, 889 | "populationSize": 100, 890 | "survivalRate": 0.2, 891 | "infuseRandomToPopulation": 0, 892 | "crossMinStep": 0.1, 893 | "crossMaxStep": 0.18, 894 | "mutationProbability": 0.1, 895 | "mutationSize": 0.1, 896 | "asapAlapMode": "normal", 897 | "weightResourceSuccession": 5, 898 | "historyKeep": False, 899 | "historyRetryCount": 0, 900 | "averageScoreSampleSize": 10, 901 | "operationDurations": operation_durations_simple_1, 902 | "operationRelations": operation_relations_simple_1 903 | } 904 | 905 | parameters_simple_2 = { 906 | "resourceCount": 2, 907 | "populationSize": 100, 908 | "survivalRate": 0.2, 909 | "infuseRandomToPopulation": 0, 910 | "crossMinStep": 0.1, 911 | "crossMaxStep": 0.18, 912 | "mutationProbability": 0.1, 913 | "mutationSize": 0.1, 914 | "asapAlapMode": "normal", 915 | "weightResourceSuccession": 5, 916 | "historyKeep": False, 917 | "historyRetryCount": 0, 918 | "averageScoreSampleSize": 10, 919 | "operationDurations": operation_durations_simple_2, 920 | "operationRelations": operation_relations_simple_2 921 | } 922 | 923 | parameters_complex_1 = { 924 | "resourceCount": 3, 925 | "populationSize": 300, 926 | "survivalRate": 0.15, 927 | "infuseRandomToPopulation": 0, 928 | "crossMinStep": 0.1, 929 | "crossMaxStep": 0.15, 930 | "mutationProbability": 0.1, 931 | "mutationSize": 0.05, 932 | "asapAlapMode": "normal", 933 | "weightResourceSuccession": 5, 934 | "historyKeep": False, 935 | "historyRetryCount": 0, 936 | "averageScoreSampleSize": 70, 937 | "operationDurations": operation_durations_complex_1, 938 | "operationRelations": operation_relations_complex_1 939 | } 940 | 941 | parameters_complex_2 = { 942 | "resourceCount": 3, 943 | "populationSize": 300, 944 | "survivalRate": 0.15, 945 | "infuseRandomToPopulation": 0, 946 | "crossMinStep": 0.1, 947 | "crossMaxStep": 0.15, 948 | "mutationProbability": 0.1, 949 | "mutationSize": 0.05, 950 | "asapAlapMode": "normal", 951 | "weightResourceSuccession": 5, 952 | "historyKeep": False, 953 | "historyRetryCount": 0, 954 | "averageScoreSampleSize": 70, 955 | "operationDurations": operation_durations_complex_2, 956 | "operationRelations": operation_relations_complex_2 957 | } 958 | 959 | 960 | 961 | parameters_testing = { 962 | "resourceCount": 3, 963 | "populationSize": 600, 964 | "survivalRate": 0.18, 965 | "infuseRandomToPopulation": 1, 966 | "crossMinStep": 0.1, 967 | "crossMaxStep": 0.4, 968 | "mutationProbability": 0.1, 969 | "mutationSize": 0.12, 970 | "asapAlapMode": "normal", 971 | "weightResourceSuccession": 5, 972 | "historyKeep": False, 973 | "historyRetryCount": 0, 974 | "averageScoreSampleSize": 70, 975 | "tournamentPopulationSize": 100, 976 | "tournamentSample": 10, 977 | "tournamentGenerations": 70, 978 | "operationDurations": operation_durations_complex_1, 979 | "operationRelations": operation_relations_complex_1 980 | } 981 | 982 | 983 | 984 | # in order to test in real time, do something like: 985 | GAS_testing = GAS( parameters_testing ) 986 | GAS_testing.addRandomToPopulation( GAS_testing.populationSize ) 987 | for generation in range( 999 ): 988 | GAS_testing.breedPopulation( do_print=True ) 989 | 990 | # in order to test in Tournament mode: 991 | #GAS_testing = GAS( parameters_testing ) 992 | #GAS_testing.tournament() 993 | pass 994 | 995 | # in order to do automated tests, do something like: 996 | #GAS_complex_1 = GAS( parameters_complex_1 ) 997 | #GAS_complex_1.automatedTest() 998 | #GAS_complex_2 = GAS( parameters_complex_2 ) 999 | #GAS_complex_2.automatedTest() 1000 | 1001 | """ 1002 | The above automated test ran for about 21 days or about 509.5 hours on a laptop. Runtime specs are: 1003 | - Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:57:36) [MSC v.1900 64 bit (AMD64)] on win32 1004 | - Intel Core i7-4712MQ @ 2.3GHz , 4 core/8 logical processors Hyper Threading, utilising 2 cores and 12% total CPU 1005 | - Windows 7 Ultimate x64 bit, 8GB RAM 1006 | - 38,880 runs of 7,776 combinations (total number of combinations is 11,664 but some are skipped as per the code above) 1007 | 1008 | 1009 | Some statistics and summary: 1010 | 1011 | Best Average Worst 1012 | Runtime 0.45 s 47.17 s 40,154.31 s 1013 | Best Score for run -67 -146 -559 1014 | Average Score for run -83.31 -323.14 -744.46 1015 | Worst Score for run -107 -805 -1,519 1016 | 1017 | - Optimal solution with score of -67 was reached 27 times, which is 0.0694 % success rate. Run times are (best/average/worst): 21.84 / 82.15 / 159.72 1018 | - Excellent solutions with score >= -70 were reached 343 times, which is 0.8822 % success rate. Run times are: 7.35 / 74.42 / 278.23 1019 | - Very good solutions with score >= -76 were reached 2082 times, whic is 5.3549 % success rate. Run times are: 3.53 / 83.61 / 10,676.38 1020 | - Good solutins with score >= -86 were reached 7299 times, which is 18.7732 % success rate. Run times are: 1.40 / 80.88 / 10,676.38 1021 | 1022 | 1023 | Impact Analysis 1024 | 1025 | - Generations 50 150 300 1026 | Avg Time 16.56 43.54 81.42 high impact 1027 | Avg Best Score for run -151 -144 -143 low impact 1028 | 1029 | - Cross Min Step 0.05 0.15 0.35 1030 | Avg Time 44.77 52.48 43.77 low impact 1031 | Avg Best Score for run -146 -143 -151 low impact 1032 | 1033 | - Cross Max Step 0.10 0.30 0.50 1034 | Avg Time 45.75 51.32 44.89 low impact 1035 | Avg Best Score for run -152 -145 -144 low impact 1036 | 1037 | - Population Size 50 200 600 1038 | Avg Time 12.03 38.92 90.58 high impact 1039 | Avg Best Score for run -228 -116 -94 high impact 1040 | 1041 | - Survival Rate 0.05 0.15 0.50 1042 | Avg Time 23.07 35.67 82.79 high impact 1043 | Avg Best Score for run -157 -135 -145 low impact 1044 | 1045 | - Mutation Probability 0.00 0.05 0.15 0.50 1046 | Avg Time 38.12 47.68 45.32 57.58 low impact 1047 | Avg Best Score for run -156 -145 -140 -142 low impact 1048 | 1049 | - Mutation Size 0.05 0.15 0.25 1050 | Avg Time 41.30 45.76 54.47 low impact 1051 | Avg Best Score for run -147 -146 -145 low impact 1052 | 1053 | - Infuse Random 0 5 15 30 1054 | Avg Time 40.10 49.33 46.86 52.40 low impact 1055 | Avg Best Score for run -108 -116 -162 -197 high impact 1056 | 1057 | 1058 | Best Overal Choices 1059 | 1060 | In order to achieve excellent solutions in under a minute the following can be considered: 1061 | - Generations: It is an interesting mix of bell shaped curve and diminishing returns. The value should not be too high otherwise impacts run time. 1062 | - Cross Min Step: Doesn't seem to have noticable impact, however it's probably best to choose a relatively low value. 1063 | - Cross Max Step: Doesn't seem to have noticable impact either, however it's probably best to choose a middle-ish value between 0.35-0.50 1064 | - Population Size: It clearly has high impact on both run time and score. A classical example of a trade off between speed and result. Choose a value of a few hundred. It seems that diversity is a very important factor in finding solutions. 1065 | - Survival Rate: It has a high impact on run time, but not as much impact on score. It's best to choose a relatively low value, but not a minimal one. 1066 | - Mutation Probability: Doesn't seem to have a huge impact on either run time or score. But it does have a small contribution, so it is best to choose a modest value. 1067 | - Mutatation Size: Same as above. 1068 | - Infuse Random: This actually turns out to be not a very good feature of any model. It adversely affects the score, so set to 0 and do not use it, or use with a very modest value. 1069 | 1070 | As a good model maybe use the following: 1071 | Generations: 120 1072 | "crossMinStep": 0.1 1073 | "crossMaxStep": 0.4 1074 | "populationSize": 500 1075 | "survivalRate": 0.15 1076 | "mutationProbability": 0.1 1077 | "mutationSize": 0.12 1078 | "infuseRandomToPopulation": 1 1079 | """ -------------------------------------------------------------------------------- /automated test results.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svinec/GeneticAlgorithmScheduling/67eaa6480071c1f3815b85319cac66d776c41866/automated test results.xlsx -------------------------------------------------------------------------------- /examples.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svinec/GeneticAlgorithmScheduling/67eaa6480071c1f3815b85319cac66d776c41866/examples.xlsx --------------------------------------------------------------------------------