├── .gitignore ├── FullReport.pdf ├── Integrated.py ├── Makespans.xlsx ├── README.md ├── Run.py ├── genetic.py ├── solve_problem.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | *.pyc 4 | -------------------------------------------------------------------------------- /FullReport.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Proyag/FlowShopProject/1a05a920d82850dc8692ad0b9bb016834b9b3b55/FullReport.pdf -------------------------------------------------------------------------------- /Integrated.py: -------------------------------------------------------------------------------- 1 | # Use this for GUI version 2 | import numpy as np 3 | import openpyxl as ox 4 | import sys 5 | 6 | from tkinter import ttk, font 7 | from tkinter import Tk, N, E, W, S, IntVar, StringVar 8 | 9 | from solve_problem import read_problem, solve_benchmark_problem 10 | 11 | benchmark_file = 'Makespans.xlsx' 12 | taillard_file = ox.load_workbook(benchmark_file) # Load workbook 13 | taillard = taillard_file.get_sheet_by_name('Sheet1') # Load sheet 14 | 15 | 16 | def calculate_optimal_makespan(*args): 17 | # To make command line print complete nd arrays 18 | np.set_printoptions(threshold=sys.maxsize) 19 | 20 | problem = problem_num.get() 21 | 22 | jobs, machines, timeseed, _ = read_problem(taillard, problem) 23 | 24 | # Write these to GUI 25 | jobs_val.set(jobs) 26 | macs_val.set(machines) 27 | time_val.set(timeseed) 28 | 29 | best_sequence, best_makespan = \ 30 | solve_benchmark_problem(problem, jobs, machines, timeseed) 31 | 32 | print("\nBest Sequence:\n", best_sequence, "\n\n") 33 | makespan_val.set(best_makespan) 34 | 35 | 36 | def increment(*args): 37 | problem_num.set(problem_num.get() + 1) 38 | calculate_optimal_makespan() 39 | 40 | 41 | def decrement(*args): 42 | problem_num.set(problem_num.get() - 1) 43 | calculate_optimal_makespan() 44 | 45 | 46 | if __name__ == '__main__': 47 | root = Tk() 48 | # Format window size 49 | # root.geometry('{}x{}'.format(1920, 1080)) 50 | # Give the window a name 51 | root.title("Scheduling Optimization") 52 | 53 | # Creating frame inside window for ttk stuff 54 | mainframe = ttk.Frame(root, padding="3 3 12 12") 55 | mainframe.grid(column=0, row=0, sticky=(N, S, E, W)) 56 | root.columnconfigure(0, weight=1) 57 | root.rowconfigure(0, weight=1) 58 | mainframe.columnconfigure(1, weight=1) 59 | mainframe.columnconfigure(2, weight=3) 60 | mainframe.columnconfigure(3, weight=1) 61 | mainframe.rowconfigure(1, weight=1) 62 | mainframe.rowconfigure(2, weight=1) 63 | mainframe.rowconfigure(3, weight=1) 64 | mainframe.rowconfigure(4, weight=1) 65 | mainframe.rowconfigure(5, weight=1) 66 | mainframe.rowconfigure(6, weight=1) 67 | mainframe.rowconfigure(7, weight=1) 68 | 69 | MyFont = font.Font(family='Helvetica', size=20) 70 | 71 | problem_num = IntVar() 72 | makespan_val = IntVar() 73 | jobs_val = IntVar() 74 | macs_val = IntVar() 75 | time_val = IntVar() 76 | 77 | problem_entry = ttk.Entry(mainframe, width=20, font=MyFont, textvariable=problem_num) 78 | problem_entry.grid(column=2, row=1, sticky=(W, E)) 79 | ttk.Label(mainframe, textvariable=makespan_val, font=MyFont).grid(column=2, row=6, sticky=(W, E)) 80 | ttk.Button(mainframe, text="Calculate", width=30, command=calculate_optimal_makespan).grid(column=3, row=1, sticky=(E)) 81 | ttk.Label(mainframe, text="Problem No.", font=MyFont).grid(column=1, row=1, sticky=(W, E)) 82 | ttk.Label(mainframe, text="Optimal makespan", font=MyFont).grid(column=1, row=6, sticky=(W, E)) 83 | 84 | ttk.Label(mainframe, text="Jobs", font=MyFont).grid(column=1, row=3, sticky=(W, E)) 85 | ttk.Label(mainframe, textvariable=jobs_val, font=MyFont).grid(column=2, row=3, sticky=(W, E)) 86 | ttk.Label(mainframe, text="Machines", font=MyFont).grid(column=1, row=4, sticky=(W, E)) 87 | ttk.Label(mainframe, textvariable=macs_val, font=MyFont).grid(column=2, row=4, sticky=(W, E)) 88 | ttk.Label(mainframe, text="Timeseed", font=MyFont).grid(column=1, row=5, sticky=(W, E)) 89 | ttk.Label(mainframe, textvariable=time_val, font=MyFont).grid(column=2, row=5, sticky=(W, E)) 90 | 91 | calculating = StringVar() 92 | calculating.set("") 93 | ttk.Label(mainframe, textvariable=calculating).grid(column=3, row=5, sticky=(W, E)) 94 | 95 | ttk.Button(mainframe, text="Next", width=30, command=increment).grid(column=3, row=7, sticky=E) 96 | ttk.Button(mainframe, text="Previous", width=30, command=decrement).grid(column=1, row=7, sticky=W) 97 | 98 | # Padding around every widget in the frame 99 | for child in mainframe.winfo_children(): 100 | child.grid_configure(padx=40, pady=40) 101 | # Focus on the entry field at first 102 | problem_entry.focus() 103 | # Executes function when you press Enter 104 | root.bind('', calculate_optimal_makespan) 105 | 106 | # Changing theme 107 | s = ttk.Style() 108 | s.theme_use('clam') 109 | s.configure('TButton', font=MyFont) 110 | 111 | # Start the infinite loop 112 | root.mainloop() 113 | -------------------------------------------------------------------------------- /Makespans.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Proyag/FlowShopProject/1a05a920d82850dc8692ad0b9bb016834b9b3b55/Makespans.xlsx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlowShopProject 2 | ## Optimization of permutation flow shop scheduling on the basis of makespan computation using natural algorithms. 3 | Done as a master's degree project at St. Xavier's College, Kolkata, under the supervision of Prof. Siladitya Mukherjee. 4 | 5 | Team members: Proyag Pal, Kaustav Basu and Triparna Mukherjee. 6 | 7 | A special type of [flow shop scheduling problem](https://en.wikipedia.org/wiki/Flow_shop_scheduling) is the permutation flow shop scheduling problem in which the processing order of the jobs on the resources is the same for each subsequent step of processing. 8 | 9 | We used [genetic algorithms](https://en.wikipedia.org/wiki/Genetic_algorithm) to optimize permutation flow shop scheduling problems on the basis of their makespan values. 10 | 11 | Evaluated our method by comparing our results against the [benchmarks published by Eric Taillard](http://mistic.heig-vd.ch/taillard/articles.dir/Taillard1993EJOR.pdf). 12 | 13 | ## Please see [v1.0](https://github.com/Proyag/FlowShopProject/releases/tag/v1.0) for the original submitted version. Newer commits maintain identical funtionality, but the code has been through a major re-factoring to make it much less terrible to look at. 14 | 15 |
16 | 17 | ### Contents: 18 | 19 | `Makespans.xlsx`: Contains Eric Taillard's benchmarks and our results along with the average relative percentage difference (ARPD). 20 | 21 | `genetic.py`: Contains the genetic algorithms used - inverse mutation, pairwise swap mutation, ordered crossover. 22 | 23 | `solve_problem.py`: Contains the main optimisation logic. 24 | 25 | `Integrated.py`: Runs a GUI to explore the results on our method on the benchmark problems one at a time. 26 | 27 | `Run.py ARG`: Runs the optimization algorithm on benchmark number ARG. (v1.0 takes no argument, and runs on all the 120 problems in the benchmarks. Takes ages to run.) 28 | 29 | 30 | 31 | ### Outline of the algorithm: 32 | 33 | 1. Initial sequence of n jobs in ascending order. [0 1 2 ... n] 34 | 2. Start with 4 jobs [0 1 2 3]. Permute 4! = 24 sequences. Set i=4. 35 | 3. Calculate makespan for each of the 24 sequences (using only the corresponding rows of the matrix to calculate). Arrange in ascending order of makespan. 36 | 4. Select best 20 sequences. 37 | 5. Clone using RWS to have 3 * 20 = 60 sequences. 38 | 6. Select best 20 sequences. 39 | 7. For each of 20 sequences, apply Ordered Crossover with the other 19 sequences. => 20 * 19 = 380 sequences. 40 | 8. Select best 20 sequences. 41 | 9. Apply Mutation (tried different types) on each of the 20 sequences to get 20 more sequences => 40 sequences. 42 | 10. Select best 20 sequences. 43 | 11. Increment i. If i>n goto Step 14. 44 | 12. Add ith job to each of the 20 sequences to each possible position. 45 | 13. Goto step 4. 46 | 14. Select the best sequence. 47 | -------------------------------------------------------------------------------- /Run.py: -------------------------------------------------------------------------------- 1 | # Use this program to write to Excel 2 | import numpy as np 3 | import openpyxl as ox 4 | import sys 5 | 6 | from solve_problem import read_problem, solve_benchmark_problem, write_new_best 7 | 8 | benchmark_file = 'Makespans.xlsx' 9 | taillard_file = ox.load_workbook(benchmark_file) # Load workbook 10 | taillard = taillard_file.get_sheet_by_name('Sheet1') # Load sheet 11 | 12 | 13 | def run_main(problem): 14 | jobs, machines, timeseed, prev_best_makespan = read_problem(taillard, problem) 15 | 16 | best_sequence, best_makespan = \ 17 | solve_benchmark_problem(problem, jobs, machines, timeseed, show_matrix=False) 18 | 19 | best_sequence = list(map(int, best_sequence)) 20 | best_sequence = list(map(str, best_sequence)) 21 | best_sequence = ', '.join(best_sequence) 22 | 23 | print("Jobs: ", jobs, "\nMachines: ", machines, "\nTimeseed: ", timeseed, "\nOptimal sequence: ", best_sequence, "\nOptimal makespan: ", best_makespan) 24 | 25 | if best_makespan < prev_best_makespan: 26 | write_new_best(problem, best_sequence, best_makespan, 27 | taillard, taillard_file, benchmark_file=benchmark_file) 28 | 29 | if __name__ == '__main__': 30 | # To make command line print complete nd arrays. 31 | np.set_printoptions(threshold=sys.maxsize) 32 | 33 | if len(sys.argv) == 1: 34 | # Run for entire 120 problem range 35 | print("No argument given; running all benchmarks in sequence") 36 | for problem in range(0, 120): 37 | run_main(problem) 38 | else: 39 | try: 40 | problem = int(sys.argv[1]) 41 | except ValueError: 42 | print("Invalid argument - only argument should be a problem number (0-119)") 43 | exit(1) 44 | if not 0 <= problem < 120: 45 | print("There are only 120 problems (numbered 0-119)") 46 | exit(1) 47 | run_main(problem) 48 | -------------------------------------------------------------------------------- /genetic.py: -------------------------------------------------------------------------------- 1 | from random import randint, uniform 2 | import numpy as np 3 | 4 | 5 | def inverse_mutation(sequence): 6 | # Inverts slice between start and end 7 | jobs = len(sequence) 8 | # Generating start and end indices randomly 9 | start = randint(0, jobs - 2) 10 | end = randint(start + 1, jobs - 1) 11 | 12 | sequence[start:end] = np.fliplr([sequence[start:end]])[0] 13 | 14 | return sequence 15 | 16 | 17 | def pairwise_swap_mutation(sequence): 18 | # Swaps start and end positions 19 | jobs = len(sequence) 20 | # Generating start and end indices randomly 21 | start = randint(0, jobs - 2) 22 | end = randint(start + 1, jobs - 1) 23 | 24 | sequence[start], sequence[end] = sequence[end], sequence[start] 25 | 26 | return sequence 27 | 28 | 29 | def ordered_crossover(parent1, parent2): 30 | jobs = len(parent1) 31 | # Generating start and end indices randomly 32 | start = randint(0, jobs - 2) 33 | end = randint(start + 1, jobs - 1) 34 | 35 | # Initialize child list 36 | child = [-1] * jobs 37 | 38 | # Copy the alleles between start-end from parent1 39 | child[start:end + 1] = parent1[start:end + 1] 40 | 41 | # Start from 2nd crossover point and 42 | # copy the rest of the elements in order, cyclically 43 | parent_index = (end + 1) % jobs 44 | child_index = (end + 1) % jobs 45 | while child_index != start: 46 | if parent2[parent_index] not in child: 47 | child[child_index] = parent2[parent_index] 48 | child_index = (child_index + 1) % jobs 49 | parent_index = (parent_index + 1) % jobs 50 | 51 | return child 52 | 53 | 54 | def roulette_wheel(sequence, makespan): 55 | # Store inverses of makespan values in a list 56 | inverse = [] 57 | for i in makespan: 58 | j = 1 / i 59 | inverse.append(j) 60 | 61 | total_sum = 0 62 | # Calculating sum of all the inverted values 63 | for i in inverse: 64 | total_sum = total_sum + i 65 | 66 | # Generate arrays of newsize = 3 * previous size, according to RWS 67 | newsize = 3 * len(makespan) 68 | seq = np.ndarray((newsize, sequence.shape[1]), dtype=int) 69 | mks = np.empty(newsize) 70 | 71 | # Generate 'newsize' number of sequences 72 | for r in range(newsize): 73 | # Generating random value between 0 and total_sum 74 | x = uniform(0, total_sum) 75 | partial_sum = 0 76 | 77 | for i in range(len(inverse)): 78 | partial_sum = partial_sum + inverse[i] 79 | if partial_sum >= x: 80 | mks[r] = makespan[i] 81 | seq[r] = sequence[i] 82 | break 83 | 84 | return seq, mks 85 | -------------------------------------------------------------------------------- /solve_problem.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy as np 3 | from itertools import permutations 4 | 5 | from utils import * 6 | import genetic 7 | 8 | 9 | def read_problem(sheet, prob_num): 10 | jobs = sheet.cell(row=prob_num+3, column=1).value 11 | machines = sheet.cell(row=prob_num+3, column=2).value 12 | timeseed = sheet.cell(row=prob_num+3, column=3).value 13 | prev_makespan = sheet.cell(row=prob_num+3, column=6).value 14 | 15 | return jobs, machines, timeseed, prev_makespan 16 | 17 | 18 | def write_new_best(prob_num, sequence, makespan, sheet, workbook, benchmark_file='Makespans.xlsx'): 19 | print("Writing new best for Problem #{:d}".format(prob_num)) 20 | sheet.cell(row=prob_num + 3, column=6).value = makespan 21 | sheet.cell(row=prob_num + 3, column=7).value = sequence 22 | workbook.save(benchmark_file) 23 | 24 | 25 | def solve_benchmark_problem(problem, jobs, machines, timeseed, show_matrix=True): 26 | # Generate matrix of times 27 | a = random_matrix(machines, jobs, timeseed) 28 | print("\nProblem No.:", problem) 29 | if show_matrix: 30 | print("Matrix:\n", a) 31 | # Transpose matrix to calulate makespan 32 | at = a.transpose() 33 | 34 | # Start with all possible permutations of first 4 jobs 35 | init_jobs = 4 36 | init_job_list = list(range(init_jobs)) 37 | # Generate all 24 permutations 38 | sequence_list = np.array(list(permutations(init_job_list))) 39 | 40 | while init_jobs <= jobs: 41 | makespan_list = np.array([]) 42 | for seq in sequence_list: 43 | seq = list(seq) 44 | makespan_list = np.append(makespan_list, calculate_makespan(at[init_job_list], seq)) 45 | 46 | # Sort both arrays in ascending order of makespan 47 | # and reduce to 20 best sequences 48 | sequence_list, makespan_list = sort_and_reduce(sequence_list, makespan_list) 49 | 50 | # Roulette wheel .. 3 x 20 output 51 | sequence_list, makespan_list = genetic.roulette_wheel(sequence_list, makespan_list) 52 | 53 | # Again, sort and reduce to 20 54 | sequence_list, makespan_list = sort_and_reduce(sequence_list, makespan_list) 55 | 56 | # Now ordered crossover each of the 20 sequences with each other 57 | # and add to lists 58 | for i in range(20): 59 | for j in range(20): 60 | if i != j: 61 | child = genetic.ordered_crossover(sequence_list[i], sequence_list[j]) 62 | child = np.array(child) 63 | sequence_list = np.vstack((sequence_list, child)) 64 | makespan_list = np.append(makespan_list, calculate_makespan(at[init_job_list], list(child))) 65 | 66 | # Sort both arrays in ascending order of makespan 67 | # and reduce to 20 best sequences 68 | sequence_list, makespan_list = sort_and_reduce(sequence_list, makespan_list) 69 | 70 | # Applying mutation. Inverse Mutation. 71 | for i in sequence_list: 72 | mutated = genetic.inverse_mutation(i) 73 | mutated = np.array(mutated) 74 | sequence_list = np.vstack((sequence_list, mutated)) 75 | makespan_list = np.append(makespan_list, calculate_makespan(at[init_job_list], list(mutated))) 76 | 77 | # Sort both arrays in ascending order of makespan 78 | # and reduce to 20 best sequences 79 | sequence_list, makespan_list = sort_and_reduce(sequence_list, makespan_list) 80 | 81 | # Applying another mutation. Pairwise Swap Mutation. 82 | for i in sequence_list: 83 | mutated = genetic.pairwise_swap_mutation(i) 84 | mutated = np.array(mutated) 85 | sequence_list = np.vstack((sequence_list, mutated)) 86 | makespan_list = np.append(makespan_list, calculate_makespan(at[init_job_list], list(mutated))) 87 | 88 | # Sort both arrays in ascending order of makespan 89 | # and reduce to 20 best sequences 90 | sequence_list, makespan_list = sort_and_reduce(sequence_list, makespan_list) 91 | 92 | # Ordered Crossover again 93 | for i in range(20): 94 | for j in range(20): 95 | if i != j: 96 | child = genetic.ordered_crossover(sequence_list[i], sequence_list[j]) 97 | child = np.array(child) 98 | sequence_list = np.vstack((sequence_list, child)) 99 | makespan_list = np.append(makespan_list, calculate_makespan(at[init_job_list], list(child))) 100 | 101 | # Sort both arrays in ascending order of makespan 102 | # and reduce to 20 best sequences 103 | sequence_list, makespan_list = sort_and_reduce(sequence_list, makespan_list) 104 | 105 | # Set first element of sorted list as best makespan. 106 | best_sequence = sequence_list[0] 107 | best_makespan = makespan_list[0] 108 | 109 | # Bring in next job into every position in sequence_list 110 | init_jobs += 1 111 | init_job_list = list(range(init_jobs)) 112 | new_sequence_list = np.array([], dtype=int).reshape(0, init_jobs) 113 | for s in sequence_list: 114 | for pos in range(s.size + 1): 115 | new_sequence_list = np.vstack((new_sequence_list, np.insert(s, pos, init_jobs - 1))) 116 | sequence_list = new_sequence_list 117 | 118 | return best_sequence, best_makespan 119 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def rng(seed, low, high): 4 | # Random number generator, as in Taillard's benchmarks paper 5 | m = 2147483647 # 2^31 - 1 6 | a = 16807 7 | b = 127773 8 | c = 2836 9 | k = int(seed / b) 10 | # Update seed 11 | seed = a * (seed % b) - k * c 12 | if (seed < 0): 13 | seed = seed + m 14 | # Random number between 0 and 1 15 | val = float(seed) / m 16 | # Between low and high 17 | randnum = low + int(val * (high - low + 1)) 18 | return randnum, seed 19 | 20 | 21 | def random_matrix(mc, jb, seed_value): 22 | # Generate random mc x jb matrix with rng 23 | a = np.ndarray((mc, jb)) # Initialize matrix 24 | 25 | # Generate random matrix of times for machines x jobs matrix 26 | for i in range(mc): 27 | for j in range(jb): 28 | a[i, j], seed_value = rng(seed_value, 1, 99) 29 | 30 | return a 31 | 32 | 33 | def calculate_makespan(a, seq): 34 | # Order the jobs (rows) in order of the sequence 35 | a = a[seq] 36 | 37 | b = np.zeros(a.shape) 38 | jobs = a.shape[0] 39 | macs = a.shape[1] 40 | 41 | b[0, 0] = a[0, 0] 42 | # Build first row 43 | for i in range(1, macs): 44 | b[0, i] = a[0, i] + b[0, i - 1] 45 | # Build first column 46 | for i in range(1, jobs): 47 | b[i, 0] = a[i, 0] + b[i - 1, 0] 48 | # Build the rest 49 | for i in range(1, jobs): 50 | for j in range(1, macs): 51 | b[i, j] = a[i, j] + (b[i - 1, j] if b[i - 1, j] > b[i, j - 1] else b[i, j - 1]) 52 | 53 | return int(b[-1, -1]) 54 | 55 | 56 | def sort_one_list_by_another(s, m): 57 | # Sort elements in s and m according to m 58 | indices = m.argsort() 59 | s = s[indices] 60 | m = m[indices] 61 | return s, m 62 | 63 | 64 | def sort_and_reduce(s, m): 65 | s, m = sort_one_list_by_another(s, m) 66 | s = s[:20] 67 | m = m[:20] 68 | return s, m --------------------------------------------------------------------------------