├── .gitignore ├── LICENSE ├── README.md ├── genome.py ├── job.py ├── main.py ├── operation.py ├── plotter.py └── population.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Irvel Nduva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSSP 2 | =================== 3 | 4 | JSSP is a Job Shop Scheduling problem solver using a Genetic Algorithm implementation. Given a finite set of jobs, each consisting of a series of operations, with each operation being performed by a given machine in a set amount of time. It must also be taken into account that each operation can have other operations as dependencies and some operations can be performed in parallel. The goal of this program is to find an optimal arrangement of operations by minimizing the *makespan*. The makespan can be defined as the total time to perform all of the operations. 5 | 6 | 7 | Jobs and Operations representation 8 | ------------- 9 | An operation is represented as an object attributes: 10 | - Name 11 | - Machine where that operation is supposed to be ran 12 | - The time it takes to be carried out 13 | - The radiator model to which it belongs 14 | - ID of the job instance to which it belongs 15 | - List of operations that it depends on 16 | - Assigned starting time 17 | 18 | Each operation belongs to a job instance, while each job has a list of operations that integrate it. 19 | 20 | Operations can only be performed in their assigned machine, however multiple machines can be ran in parallel. 21 | 22 | Evolutionary aspect 23 | ------------- 24 | Each series of operations is represented by a *genome*, which in JSSP is a list of Operation objects, each object being an individual *gene*. In this genome, the order of elements indicates which operation should be performed first 25 | 26 | ###Initial Population 27 | The initial population is generated by randomly selecting the operations that have no dependencies. Then operations that have had their dependencies satisfied are selected randomly. It is considered that if an operation has already been selected, it counts as a satisfied dependency. This process is repeated for each genome until the set population size is reached. 28 | 29 | ###Mating 30 | To mate, the algorithm selects two genomes as parents randomly from the existing population. It then selects a fixed amount of genes from one parent randomly, and then selects the remaining genes from the second parent to produce the first child. The second child is then produced with the remaining unused genes from each parent. 31 | Special care is taken to ensure that the selected genes from both parents are not duplicated, and to preserve the order of each gene in the offspring relative to their original position in their parents. 32 | 33 | ###Mutation 34 | Each offspring has a set probability of mutating after it has been generated. This mutation consists in taking a fixed percentage of the trailing genes in the genome, and rearranging the genes randomly while taking care in not violating dependencies. (Using a similar algorithm to the one used in the generation of the initial population) 35 | 36 | System requirements 37 | ------------- 38 | 39 | - Python 3.5.x 40 | - matplotlib 41 | 42 | How to run it 43 | ------------- 44 | Simply run 45 | 46 | $ python main.py 47 | 48 | -------------------------------------------------------------------------------- /genome.py: -------------------------------------------------------------------------------- 1 | """ 2 | genome.py 3 | ~~~~~~~~~~~~~ 4 | This class stores the set of operations that compose an individual in the 5 | population. 6 | """ 7 | class Genome: 8 | def __init__(self, operations): 9 | self.operations = operations 10 | self.score = 999999 11 | 12 | def __str__(self): 13 | # A string representation of the genome takes the sum of the 14 | # Job, Order, Machine and Duration in numeric value 15 | genome_string = "" 16 | numeric_value = 0 17 | ascii_value = "" 18 | for op in self.operations: 19 | numeric_value += op.duration 20 | ascii_value = str(int(numeric_value) % 10) 21 | genome_string += ascii_value 22 | from population import is_valid_permutation 23 | return genome_string[:12] + " " + str(is_valid_permutation(self.operations)) + " " + str(self.score) 24 | 25 | -------------------------------------------------------------------------------- /job.py: -------------------------------------------------------------------------------- 1 | """ 2 | job.py 3 | ~~~~~~~~~~~~~ 4 | Stores the information of each job. 5 | - start_date is the date when the job was received 6 | - goal_date is the date when the job must be delivered 7 | - model is the radiator model 8 | - operations is the list of operations that comprise the job 9 | - job_id is a unique ID for each job 10 | """ 11 | from operation import Operation 12 | 13 | class Job: 14 | def __init__(self, start_date, goal_date, model, job_id): 15 | self.start_date = start_date 16 | self.goal_date = goal_date 17 | self.model = model 18 | self.operations = [] 19 | self.job_id = job_id 20 | 21 | if model == "5967": 22 | o1 = Operation("Corte de tubo", "VT1", 4.58, "5967", job_id) 23 | o2 = Operation("Fab. de aleta", "VT2", 2.99, "5967", job_id) 24 | o3 = Operation("Operacion PUN", "PUN", 2.55, "5967", job_id) 25 | o4 = Operation("Operacion TROQ1", "TROQ1", 1.56, "5967", job_id) 26 | o5 = Operation("Operacion TROQ2", "TROQ2", 0.32, "5967", job_id) 27 | o6 = Operation("Operacion VT3", "VT3", 17.46, "5967", job_id) 28 | o7 = Operation("Operacion HYR", "HYR", 14.32, "5967", job_id) 29 | 30 | self.operations.extend([o1, o2, o3, o4, o5, o6, o7]) 31 | o6.dependencies = [o1, o2, o3, o4, o5] 32 | o7.dependencies = [o6] 33 | elif model == "8047": 34 | o1 = Operation("Corte de tubo", "VT1", 5.1, "8047", job_id) 35 | o2 = Operation("Fab. de aleta", "VT2", 2.8, "8047", job_id) 36 | o3 = Operation("Operacion PUN", "PUN", 2.236, "8047", job_id) 37 | o4 = Operation("Operacion TROQ1", "TROQ1", 5.13, "8047", job_id) 38 | o5 = Operation("Operacion TROQ2", "TROQ2", 1.58, "8047", job_id) 39 | o6 = Operation("Operacion VT3", "VT3", 19.28, "8047", job_id) 40 | o7 = Operation("Operacion HYR", "HYR", 16.59, "8047", job_id) 41 | 42 | self.operations.extend([o1, o2, o3, o4, o5, o6, o7]) 43 | o6.dependencies = [o1, o2, o3, o4, o5] 44 | o7.dependencies = [o6] 45 | elif model == "4025": 46 | o1 = Operation("Corte de tubo", "VT1", 2.44, "4025", job_id) 47 | o2 = Operation("Fab. de aleta", "AP1", 7.5, "4025", job_id) 48 | o3 = Operation("Operacion TROQ1", "TROQ1", 1.35, "4025", job_id) 49 | o4 = Operation("Operacion AP2", "AP2", 23.94, "4025", job_id) 50 | o5 = Operation("Operacion HYR", "HYR", 8.1, "4025", job_id) 51 | 52 | self.operations.extend([o1, o2, o3, o4, o5]) 53 | o4.dependencies = [o1, o2] 54 | o5.dependencies = [o3, o4] 55 | 56 | def print_operations(self): 57 | for op in self.operations: 58 | print(str(op) + "\n") -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import random 2 | import collections 3 | from datetime import datetime, timedelta 4 | 5 | from termcolor import cprint 6 | # import tkinter.simpledialog 7 | 8 | import plotter 9 | from population import Population 10 | from job import Job 11 | from operation import Operation 12 | from population import is_valid_permutation, calculate_makespan, calculate_fitness 13 | 14 | if __name__ == "__main__": 15 | 16 | # root = tkinter.Tk() 17 | # root.withdraw() 18 | 19 | products = collections.OrderedDict() 20 | # products['5967'] = tkinter.simpledialog.askinteger("5967", "Quantity of model 5967?", minvalue=0, initialvalue=1) 21 | # products['8047'] = tkinter.simpledialog.askinteger("8047", "Quantity of model 8047?", minvalue=0, initialvalue=1) 22 | # products['4025'] = tkinter.simpledialog.askinteger("4025", "Quantity of model 4025?", minvalue=0, initialvalue=1) 23 | # iterations = tkinter.simpledialog.askinteger("Iterations", "Iterations?", minvalue=1, initialvalue=50000) 24 | products['5967'] = 4 25 | products['8047'] = 5 26 | products['4025'] = 3 27 | iterations = 12000 28 | 29 | all_jobs = [] 30 | all_operations = [] 31 | job_id = 0 32 | for key, value in products.items(): 33 | for i in range(value): 34 | all_jobs.append(Job(datetime.now(), datetime.now() + timedelta(hours=170), key, job_id)) 35 | job_id += 1 36 | for job in all_jobs: 37 | all_operations.extend(job.operations) 38 | 39 | var1, var2 = calculate_makespan(all_operations) 40 | 41 | cprint("********** JSSP Genetic Solver **********", "yellow") 42 | 43 | population_size = int(4.9 * len(all_operations)) 44 | population = Population(all_operations, population_size) 45 | 46 | current_best = population.genomes[1] # So that the first best is always printed 47 | makespans = [] 48 | iteration_numbers = [] 49 | 50 | print("Cantidad de modelos ingresados:") 51 | print(str(products['5967'])+ " modelos 5967") 52 | print(str(products['8047'])+ " modelos 8047") 53 | print(str(products['4025'])+ " modelos 4025") 54 | print("Population size: " + str(population_size)) 55 | cprint("\n\nBase configuration:", "blue") 56 | for operation in all_operations: 57 | print(str(operation)) 58 | cprint("Total makespan: " + str(var2) + "\n", "grey") 59 | 60 | print("\nReproducing population " + str(iterations) + " times...\n") 61 | p = 0 62 | for i in range(iterations): 63 | makespans.extend([calculate_fitness(x.operations) for x in population.genomes]) 64 | for _ in range(len(population.genomes)): 65 | iteration_numbers.append(i) 66 | if current_best is not population.genomes[0]: 67 | current_makespan = calculate_makespan(current_best.operations)[1] 68 | 69 | current_best = population.genomes[0] 70 | print("#" + str(i) + ". The current best is: " + str(calculate_makespan(current_best.operations)[1])[:7], end=" ") 71 | print(" Improvement over base: ", end="") 72 | cprint(str(100 - calculate_makespan(current_best.operations)[1]/ var2 * 100)[:6] + "%", "cyan") 73 | population.reproduce_population() 74 | if i%(iterations/10) == 0: 75 | p += 10 76 | cprint("- Completion amount: " + str(p) + "%", 'green') 77 | 78 | population.reap_population() 79 | 80 | dummy, best_makespan = calculate_makespan(current_best.operations) 81 | sorted_operations = sorted(current_best.operations, key = lambda x: x.start_time, reverse = False) 82 | cprint("\n\nOptimized configuration:", "blue") 83 | for operation in sorted_operations: 84 | print(str(operation)) 85 | cprint("Total makespan: " + str(best_makespan) + "\n", "grey") 86 | 87 | 88 | # plotter.plot(iteration_numbers, makespans) 89 | -------------------------------------------------------------------------------- /operation.py: -------------------------------------------------------------------------------- 1 | """ 2 | operation.py 3 | ~~~~~~~~~~~~~ 4 | This stores the information of each individual operation in the 5 | production line. 6 | - name improves readability when printing 7 | - machine is the machine in which that operation will be executed 8 | - duration is the amount of time in which the operation will be completed 9 | - job_model is the radiator model that this operation belongs to 10 | - job_id is the job to which this operation belongs 11 | - dependencies is a list containing the operations that this operation 12 | depends on 13 | """ 14 | class Operation: 15 | 16 | def __init__(self, name, machine, duration, job_model, job_id): 17 | self.name = name 18 | self.machine = machine 19 | self.duration = duration 20 | self.job_model = job_model 21 | self.job_id = job_id 22 | self.dependencies = [] 23 | self.start_time = 0 24 | 25 | def __str__(self): 26 | return ("Job ID: " + str(self.job_id) + " Stage: " + str(self.machine) 27 | + "\t Model: " + str(self.job_model)+ "\t Start time: " + str(format(round(self.start_time,2))) + "\t Duration: " + str(format(round(self.duration,2)))) 28 | 29 | def print_dependencies(self): 30 | if len(self.dependencies) > 0: 31 | print(str(self) + " depends on ") 32 | for operation in self.dependencies: 33 | print(str(operation)) 34 | -------------------------------------------------------------------------------- /plotter.py: -------------------------------------------------------------------------------- 1 | import plotly.graph_objs as go 2 | import plotly.plotly as py 3 | from plotly.tools import FigureFactory as FF 4 | import matplotlib.pyplot as plt 5 | 6 | 7 | def plot(x_data, y_data): 8 | # x_data = iteration_numbers 9 | # y_data = makespans 10 | # colorscale = ['#7A4579', '#D56073', 'rgb(236,158,105)', (1, 1, 0.2), (0.98,0.98,0.98)] 11 | # fig = FF.create_2D_density( 12 | # x_data, y_data, colorscale=colorscale, 13 | # hist_color='rgb(255, 237, 222)', point_size=3 14 | # ) 15 | 16 | # py.plot(fig, filename='histogram_subplots') 17 | 18 | # trace = go.Scattergl( 19 | # x = x_data, 20 | # y = y_data, 21 | # mode = 'lines', 22 | # marker = dict( 23 | # color = 'rgb(152, 0, 0)', 24 | # line = dict( 25 | # width = 1, 26 | # color = 'rgb(0,0,0)') 27 | # ) 28 | # ) 29 | # data = [trace] 30 | # py.plot(data, filename='goodthick') 31 | plt.plot(x_data, y_data, 'ro') 32 | plt.title("Initial population: " + str(100) + " Iteration Numbers: " + str(len(x_data))) 33 | plt.ylabel("Makespan") 34 | plt.xlabel("Generation") 35 | plt.show() -------------------------------------------------------------------------------- /population.py: -------------------------------------------------------------------------------- 1 | """ 2 | population.py 3 | ~~~~~~~~~~~~~ 4 | This stores a fixed set of genomes, and provides methods to 5 | create new generations based on the existing one. 6 | """ 7 | import random 8 | import collections 9 | from operator import attrgetter 10 | 11 | from genome import Genome 12 | 13 | SIZE = 270 # The static size that the population will be kept at 14 | MATE_DIST = 50 # How much genetic info from each parent to take 15 | MUTATE_PROB = 0.4 # How likely is a newborn to mutate 16 | bad_score = 6300 # The penalization for each genome that violates the date 17 | 18 | class Population: 19 | def __init__(self, operations, size): 20 | self.genomes = [] 21 | self.population_size = size 22 | self.create_new_population(operations) 23 | self.sort_population() 24 | self.reap_population() 25 | _, base_span = calculate_makespan(operations) 26 | bad_score = int(base_span * 1.1) 27 | 28 | def __str__(self): 29 | genomes_string = "" 30 | for genome in self.genomes: 31 | genomes_string += str(genome) 32 | return genomes_string + "\n# of valid genomes: " + str(self.num_trues()) 33 | 34 | def create_new_population(self, operations): 35 | #print("\nCreating initial population...") 36 | genome = Genome(operations[:]) 37 | genome.score = calculate_fitness(genome.operations) 38 | self.genomes.append(genome) 39 | for _ in range(self.population_size): 40 | genome = Genome(shuffle_valid_genome(operations[:])) 41 | genome.score = calculate_fitness(genome.operations) 42 | self.genomes.append(genome) 43 | 44 | def sort_population(self): 45 | """ 46 | Sorts the population based on the fitness score 47 | """ 48 | self.genomes.sort(key = attrgetter('score'), reverse = False) 49 | 50 | def reap_population(self): 51 | """ 52 | Keeps only the first SIZE individuals 53 | """ 54 | self.genomes = self.genomes[:self.population_size] 55 | 56 | def reproduce_population(self): 57 | """ 58 | The miracle of life 59 | This method will take two random parents and create two children from 60 | them. 61 | """ 62 | first_child, second_child = self.mate() 63 | mutate_genome(first_child) 64 | mutate_genome(second_child) 65 | 66 | first_genome = Genome(first_child) 67 | second_genome = Genome(second_child) 68 | first_genome.score = calculate_fitness(first_child) 69 | second_genome.score = calculate_fitness(second_child) 70 | 71 | self.genomes.append(first_genome) 72 | self.genomes.append(second_genome) 73 | self.sort_population() 74 | self.reap_population() 75 | 76 | def mate(self): 77 | # Choose two parents from the existing population pseudo-randomly 78 | parent1, parent2 = random.sample(self.genomes, 2) 79 | genes_num = len(parent1.operations) 80 | 81 | # Choose MATE_DIST% of the genes from each parent 82 | p1_amount= int((genes_num) * (MATE_DIST / 100)) 83 | 84 | idx_from_p1 = random.sample(range(genes_num), p1_amount) 85 | idx_from_p1.sort() # Preserve the genome relative order 86 | 87 | # Get the actual genes to be used for the first child 88 | genes_from_p1 = [] 89 | for idx in idx_from_p1: 90 | genes_from_p1.append(parent1.operations[idx]) 91 | 92 | # Get the indices of the unused genes by p1 from p2 93 | idx_from_p2 = [] 94 | for idx, gene in enumerate(parent2.operations): 95 | if gene not in genes_from_p1: 96 | idx_from_p2.append(idx) 97 | idx_from_p2.sort() 98 | first_child = merge_genomes(parent1, parent2, idx_from_p1, idx_from_p2) 99 | 100 | # The genes for the second child are the ones unused by the first child 101 | idx_from_p1 = [x for x in range(genes_num) if x not in idx_from_p1] 102 | idx_from_p1.sort() 103 | idx_from_p2 = [x for x in range(genes_num) if x not in idx_from_p2] 104 | idx_from_p2.sort() 105 | second_child = merge_genomes(parent1, parent2, idx_from_p1, idx_from_p2) 106 | 107 | return first_child, second_child 108 | 109 | 110 | def merge_genomes(parent1, parent2, idx_from_p1, idx_from_p2): 111 | idx1 = 0 112 | idx2 = 0 113 | child = [] 114 | # Merge the selected genes from each parent while keeping their relative 115 | # order. The lists of indexes indicates which genes have been selected. 116 | while idx1 < len(idx_from_p1) and idx2 < len(idx_from_p2): 117 | if idx_from_p1[idx1] <= idx_from_p2[idx2]: 118 | child.append(parent1.operations[idx_from_p1[idx1]]) 119 | idx1 += 1 120 | else: 121 | child.append(parent2.operations[idx_from_p2[idx2]]) 122 | idx2 += 1 123 | 124 | while idx1 < len(idx_from_p1): 125 | child.append(parent1.operations[idx_from_p1[idx1]]) 126 | idx1 += 1 127 | while idx2 < len(idx_from_p2): 128 | child.append(parent2.operations[idx_from_p2[idx2]]) 129 | idx2 += 1 130 | return child 131 | 132 | 133 | def shuffle_valid_genome(operations, shuffle_amount=100): 134 | operations_dict = collections.OrderedDict() # Constant access time 135 | for i in range(len(operations) * (100 - shuffle_amount) // 100): 136 | operations_dict[operations[i]] = True 137 | 138 | while len(operations_dict) < len(operations): 139 | for op in random.sample(operations, len(operations)): 140 | if op not in operations_dict: 141 | dependency_satisfied = True 142 | for dep in op.dependencies: 143 | # If its dependency is not yet satisfied, then can't add it 144 | if dep not in operations_dict: 145 | dependency_satisfied = False 146 | break 147 | if dependency_satisfied: 148 | operations_dict[op] = True 149 | return [gene for gene in operations_dict.keys()] 150 | 151 | 152 | def mutate_genome(operations): 153 | if random.random() < MUTATE_PROB: 154 | return shuffle_valid_genome(operations, 90) 155 | 156 | 157 | def calculate_fitness(permutation): 158 | penalization = 0 159 | if not is_valid_permutation(permutation): 160 | penalization = bad_score 161 | make_span = 0 162 | else: 163 | _, make_span = calculate_makespan(permutation) 164 | score = make_span + penalization 165 | return score 166 | 167 | 168 | def is_valid_permutation(permutation): 169 | operation_done = {} 170 | for op in permutation: 171 | operation_done[op] = 0 172 | 173 | for op in permutation: 174 | for dep in op.dependencies: 175 | if operation_done[dep] == 0: 176 | return False 177 | operation_done[op] = 1 178 | return True 179 | 180 | 181 | def calculate_makespan(permutation): 182 | cummulative_machine_times = {} 183 | operations_end_time = {} 184 | jobs_end_time = {} 185 | 186 | for operation in permutation: 187 | #initialize variables with 0 if does not exist 188 | if not operation in operations_end_time: 189 | operations_end_time[operation] = 0 190 | 191 | if not operation.machine in cummulative_machine_times: 192 | cummulative_machine_times[operation.machine] = 0 193 | 194 | #Check if the operation has dependencies 195 | if operation.dependencies: 196 | 197 | #initialize time of the operation to the max value of dependencies 198 | max_time_dependencies = operations_end_time[operation.dependencies[0]] 199 | for dependent_operation in operation.dependencies: 200 | if operations_end_time[dependent_operation] > max_time_dependencies: 201 | max_time_dependencies = operations_end_time[dependent_operation] 202 | operations_end_time[operation] = max_time_dependencies 203 | 204 | #Calculate time 205 | if operations_end_time[operation] < cummulative_machine_times[operation.machine]: 206 | 207 | operation.start_time = cummulative_machine_times[operation.machine] 208 | 209 | cummulative_machine_times[operation.machine] += operation.duration 210 | operations_end_time[operation] = cummulative_machine_times[operation.machine] 211 | else: 212 | 213 | operation.start_time = operations_end_time[operation] 214 | 215 | operations_end_time[operation] += operation.duration 216 | cummulative_machine_times[operation.machine] = operations_end_time[operation] 217 | 218 | """Save the time of each operation. 219 | So, at the end you have the last one is the one saved """ 220 | jobs_end_time[operation.job_id] = operations_end_time[operation] 221 | 222 | """Return a map mapping each job with the corresponding end time, 223 | and the biggest time of the jobs""" 224 | #print(operations_end_time) 225 | return jobs_end_time, jobs_end_time[max(jobs_end_time, key=jobs_end_time.get)] 226 | --------------------------------------------------------------------------------