diff --git a/Readme.md b/Readme.md index 52607a2..b2820bc 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,7 @@ ## Tooling -- Python 3.6 +- Python 3.5 - [MyPy](http://www.mypy-lang.org/ ) für statische Typchecks - [Pandoc](https://pandoc.org/ ) für die Dokumentation - Python Module: siehe [requirements.txt](https://pip.pypa.io/en/latest/user_guide/#requirements-files ) diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index 2990537..0000000 --- a/Readme.txt +++ /dev/null @@ -1,23 +0,0 @@ -README ------ - -Für die Ausführung des Algorithmus wird Python 3 (empfohlene Version: 3.6.1) benötigt. -Die Packages, die zusätzlich gebraucht werden, können der requirements.txt entnommen werden. -(Installation kann hier einzeln oder über den Befehl: python -m pip install -r requirements.txt) - -Zur Ausführung bitte im Terminal in den Ordner src gehen und dort das Skript main.py starten. -Parameter, die hierbei möglich sind: - -h zeigt alle Optionen an - -p aktiviert die Ausgabe über den Plotter als Diagramm - -l wird benötigt falls die Eingabe eine Liste von Problemen ist (d.h. für jobshop1.txt) - -i Index des Problems in der Liste (nur relevant bei -l) - -t setzt die Starttemperatur des Simulated Annealings - -s setzt die maximalen Umformungsschritte pro Generierung einer neuen Lösung - -a setzt die Wahrscheinlichkeit, pro Umformungsschritt auch eine Lösung zu akzeptieren, obwohl - noch nicht die maximalen Umformungsschritte erreicht sind - --t -s und -a müssen nicht alle gesetzt sein, dann wird der jeweilige Defaultwert verwendet -Defaultwerte: max_temp = 300, max_steps = 250, accept_prob = 0.01 - -Beispielaufruf: - python .\main.py -p -l -i 2 -t 50 ..\inputdata\jobshop1.txt \ No newline at end of file diff --git a/doc.md b/doc.md deleted file mode 100644 index fd1f7b9..0000000 --- a/doc.md +++ /dev/null @@ -1,36 +0,0 @@ -## scheduling problem defined by: - 1. $m$ specialized machines - 2. tasks $\tau$ of the form $(e, i)$ with $t \in \mathbb{N}$ the execution time and $i \in \{1,2,\dots,m\}$ the machine the task has to run on - 3. $n$ jobs $T_k$ with $\forall T_k:$ linear order of tasks, with $k \in \{1,2,\dots,n\}$ - 4. Additionally, a multiset $\Omega$ of arbitrary but fixed size that contains wait states $\omega := (1, i)$ with $i \in \{1,2,\dots,m\}$ the blocked machine. - -The goal is to find the fastest feasible schedule $\sigma_{min}$. - -## evaluative function - - minimize the execution time of $\sigma$ - - upper bound: largest processing time first - - lower bound: max sum of execution times on one machine - -## solutions - - list of tuples $(t, \tau)$ with $t \in \mathbb{N}$ the scheduled begin of $\tau$ - -## operations - - $\operatorname{ins}(\omega, t)$: block a machine at time $t$ for $w$ time steps. - - $\operatorname{xchg}(\tau_1,\tau_2)$: exchange the position of two tasks. - -Both operations require that the start times are recomputed. - -## neighbourhood of solution - - - $\operatorname{neighbours}(\sigma) = \{x \in \Sigma | \delta(\sigma, x) \leq n\}$ with $\Sigma$ the set of all feasible schedules. - - $\delta$: $\delta ( \sigma )=0$, $\delta ( \operatorname{op}(x)) = \delta (x) + 1$ (ass. ins has the same penalty xchg has), $x$ either op($y$) or $\sigma$ - -## constraints - - only schedule new $\tau$ if another $\tau$ is finished - - only schedule $\tau \in T_k$ that has no unscheduled predecessor in $T_k$ - - only one task on a machine any given time - -## implementation in Python - - translate problem into list of jobs, jobs into lists of tasks, ie problem = [$T_0, T_1,\dots,T_{k-1}$], $T_i$ = [$\tau_1,\tau_2,\dots$] - - address tasks based on their indices, ie [0][1] is the second task of the first job. - - compute only one possible next solution, rate, drop/accept. $\delta$ is computed iteratively during generation diff --git a/inputdata/sample b/inputdata/sample deleted file mode 100644 index 0ace874..0000000 --- a/inputdata/sample +++ /dev/null @@ -1,4 +0,0 @@ -3 3 -0 4 1 6 2 1 -1 3 0 13 2 4 -1 2 2 5 0 3 diff --git a/notes.md b/notes.md index efe7c46..a21ba6f 100644 --- a/notes.md +++ b/notes.md @@ -26,7 +26,7 @@ - $S = \left\{(o_j,t) | o_j \in O \cup \left\{w_n | n \in \mathbb{N} \wedge w_n \text{ v.d.F. } (1, m) \right\} \wedge o_j \text{ v.d.F. } (d, m, j) \wedge t \in T \forall o \in O : \exists (o,t) \in S\right\}$ - indirekt lässt sich durch laufende Operation und Zeitpunkt auch Belegung einer Maschine zu einem Zeitpunkt ermitteln - Optimierung: sparse speichern -1. Liste von (T, $o_j$) mit $T \in \mathbb{N}$ (Time), $o_j \in O$ (Tasks), j bezeichnet den Job +1. Liste von (T, $o_j$) mit $T \in \mathbb{N}$ (Time), $o_j \in O$ (Tasks) - Operationen: - Vertauschen von 2 Jobs auf einer Maschine, selbstinvers - Verzögern von Operationen (keine expliziten Wartezustände nötig) diff --git a/requirements.txt b/requirements.txt index 55650c7..4612f56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ mypy -arpeggio -matplotlib -numpy -tkinter \ No newline at end of file +Arpeggio diff --git a/shell.nix b/shell.nix deleted file mode 100644 index e8d926d..0000000 --- a/shell.nix +++ /dev/null @@ -1,3 +0,0 @@ -with import {}; - - (python3.withPackages (ps: [ps.numpy (ps.matplotlib.override {enableQt=true;}) ps.mypy ps.arpeggio])).env diff --git a/src/Generator/__init__.py b/src/Generator/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/Generator/generator.py b/src/Generator/generator.py deleted file mode 100644 index 4cc4ab3..0000000 --- a/src/Generator/generator.py +++ /dev/null @@ -1,198 +0,0 @@ -from Parser import JobShopProblem as Problem -import random - - - -def pull_fwd(solution): - """ - Pull a task from a pseudo-random position to the position of - a random task forward. If the task directly in front is part - of the same job, pull that instead. The first task can never - be pulled forward. Will not rectify solutions. - - Returns the modified solution and the tasks index. - """ - old_idx = random.randint(1, len(solution)-1) - #print("old_idx" + str(old_idx)) - while(solution[old_idx][1][0] == solution[old_idx-1][1][0]): - old_idx -= 1 - #Catch case of the op to be pulled being 0 - #print("old_idx: " + str(old_idx)) - if(old_idx == 0): - return pull_fwd(solution) - new_idx = random.randint(0, old_idx-1) - for task in solution[new_idx:old_idx]: - if(task[1][0] == solution[old_idx][1][0]): - #break - return pull_fwd(solution) - #else: - task = solution[old_idx] - solution.remove(task) - solution.insert(new_idx, task) - return rectify(solution, new_idx) - - -def accept(solution): - """ - Accept the current generated solution and evaluate it. - Maybe skip this during the first step to generate a more - random solution. - """ - return tighten(solution) - #return solution - - -def tighten(solution): - """ - Try to remove any holes in the schedule. - """ - - global problem - - bound = len(solution) - for i in range(0,bound): - jobs = ( task for task in solution[i-1:bound:-1] if task[1][0] == solution[i][1][0] ) - machs = ( task for task in solution[i-1:bound:-1] if problem[task[1]][1] == problem[solution[i][1]][1] ) - job = next(jobs,None) - mach = next(machs,None) - times = [] - if job: - times += [job[0] + problem[job[1]][0]] - if mach: - times += [mach[0] + problem[job[1]][0]] - if times: - solution[i] = (max(times), solution[i][1]) - solution.sort() - return solution - -def rectify(solution, idx): - """ - Transform solution by adapting the begin times and delaying - tasks on the same machine if affected. - """ - solution[idx] = (solution[idx+1][0],) + solution[idx][1:] - - update_begin(solution, idx) - correct_indices(solution, idx) - for i in range(idx, len(solution)-1): - #print("i: " + str(i)) - correct_machine(solution, i) - #print(solution) - correct_precedence(solution, i) - #print(solution) - return solution - -def update_begin(solution, idx): - """ - Update the start time of the given task wrt machine and job. - """ - - global problem - task = solution[idx] - - if(idx == 0): - solution[idx] = (0,) + solution[idx][1:] - return - - #find the next task with condition=true, if exists - machine = ( x for x in solution[idx-1::-1] if problem[x[1]][1] == problem[task[1]][1] ) - prev_mach = next(machine, None) #returns the task or None - - job = ( x for x in solution[idx-1::-1] if task[1][0] == x[1][0] ) - prev_job = next(job, None) - end_mach = 0 - end_job = 0 - if prev_mach: - end_mach = problem[prev_mach[1]][0] + prev_mach[0] - if prev_job: - end_job = problem[prev_job[1]][0] + prev_job[0] - solution[idx] = (max(end_mach, end_job, task[0]),) + solution[idx][1:] - - -def correct_indices(solution, idx): - """ - Adapt solution to reestablish ascending order of execution times. - """ - task = solution[idx] - tasks = [ x for x in solution[idx:] if x[0] < task[0]] - if tasks: - solution.remove(task) - solution.insert(idx + len(tasks), task) - #[1,3,2] -> idx = 1, len([2])=1 - -def correct_machine(solution, idx): - """ - Check conflicts on machines and correct if needed. - """ - task = solution[idx] - end = problem[task[1]][0] + task[0] - possible_conf = ( x for x in solution[idx+1:] if problem[x[1]][1] == problem[task[1]][1]) - conflict = next(( x for x in possible_conf if x[0] < end ), None) - if(conflict): - idx = solution.index(conflict) - solution[idx] = (end,) + solution[idx][1:] - correct_indices(solution,idx) - - -def correct_precedence(solution, idx): - """ - Check precedence relation and correct if needed. - """ - task = solution[idx] - end = problem[task[1]][0] + task[0] - possible_conf = ( x for x in solution[idx+1:] if x[1][0] == task[1][0] ) - conflict = next(( x for x in possible_conf if x[0] < end or x[1][1] < task[1][1]), None) - if(conflict): - idx = solution.index(conflict) - #print("idx->" + str(idx)) - if(conflict[0] < end): - solution[idx] = (end,) + solution[idx][1:] - correct_indices(solution,idx) - if(conflict[1][1] < task[1][1]): - new_start = solution[idx][0] + solution[idx][1][1] - #print("new_start: " + str(new_start)) - solution.remove(task) - task = (new_start,) + task[1:] - solution.insert(idx, task) - -def generate(old_solution, steps, p=0.01): - """ - Generate a new solution from an existing solution with a - specified number of max steps. - """ - import sys - print("Max steps: " + str(steps)) - print("Accept probability: " + str(p)) - sys.stdout.write("Start generation... ") - solution = old_solution[:] - option = pull_fwd #do at least one pull - for i in range(0, steps): - solution = option(solution) - if(option == accept): - break - option = pull_fwd if p < random.random() else accept - if ((i * 100) % steps == 0): - sys.stdout.write(str(i*100/steps) + "%... ") - sys.stdout.flush() - sys.stdout.write("Done\n") - if option != accept: - accept(solution) - return solution - -def mock(): - """ - Reads a mock problem and creates the corresponding enumerated - solution. Should clean up the namespace afterwards. - """ - global problem - from Parser.js2_style import parse_file as mockload - from SchedulingAlgorithms.enumerate import enumerate as mockenum - problem = mockload('../inputdata/sample') - solution = mockenum(problem) - del mockload - del mockenum - return solution - -def init(in_problem): - global problem - problem = in_problem diff --git a/src/Output/__init__.py b/src/Output/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/Output/output.py b/src/Output/output.py deleted file mode 100644 index a2d2168..0000000 --- a/src/Output/output.py +++ /dev/null @@ -1,32 +0,0 @@ -from matplotlib import pyplot as plt -from matplotlib import colors -from matplotlib import patches -import numpy as np -from SchedulingAlgorithms.simanneal import rate -import random - -def create_plot(problem, solution): - - end = rate(solution) - - with plt.xkcd(): - fig,ax = plt.subplots() - col = colors.XKCD_COLORS - del col['xkcd:white'] - colorlist = list(col.values()) - random.shuffle(colorlist) - for m in range(0, problem.machines): - mach_ops = [ x for x in solution if problem.problem_data[x[1][0]][x[1][1]][1] == m ] - xranges = [ (x[0], problem.problem_data[x[1][0]][x[1][1]][0]) for x in mach_ops ] - ax.broken_barh(xranges, ((problem.machines - m)*10, 9), - facecolors=[colorlist[x[1][0]] for x in mach_ops]) - ax.set_ylim(5, 5 + (problem.machines+1)*10) - ax.set_xlim(0, 1.25 * end) - ax.set_yticks([15 + m * 10 for m in range(0, problem.machines)]) - ax.set_yticklabels([ problem.machines - 1 - m for m in range(0, problem.machines)] ) - handlecolors = colorlist[0:problem.jobs] - handles = [ patches.Patch(color = handlecolors[j], label = "Job "+str(j)) for j in range(0,problem.jobs) ] - labels = ["Job "+str(j) for j in range(0,problem.jobs)] - ax.legend(handles, labels) - - plt.show() diff --git a/src/Parser/__init__.py b/src/Parser/__init__.py index 59d2edf..d42e09c 100644 --- a/src/Parser/__init__.py +++ b/src/Parser/__init__.py @@ -6,33 +6,33 @@ from collections.abc import Mapping __all__ = ["js1_style", "js2_style"] grammar = """ - // starting point for jobshop1 input file + # starting point for jobshop1 input file job_shop1 = skip_preface - // eat away lines of preface, until first problem_instance is - // encountered; then the list of instances start + # eat away lines of preface, until first problem_instance is + # encountered; then the list of instances start skip_preface = (!problem_instance r"[^\n]+" skip_preface) / (eol skip_preface) / instance_list instance_list = problem_instance (sep_line trim_ws eol problem_instance eol?)* eof_sep problem_instance = trim_ws "instance" ' ' instance_name trim_ws eol trim_ws eol sep_line description eol problem_data description = r"[^\n]*" instance_name = r"\w+" sep_line = trim_ws plus_line trim_ws eol - // lines out of multiple + signs + # lines out of multiple + signs plus_line = r"\+\+\++" - // EOF is a builtin rule matching end of file + # EOF is a builtin rule matching end of file eof_sep = trim_ws plus_line " EOF " plus_line trim_ws eol* EOF - // entry point for jobshop2 input files + # entry point for jobshop2 input files job_shop2 = problem_data EOF problem_data = trim_ws num_jobs ' ' num_machines eol job_data+ - // used for skipping arbitrary number of non-breaking whitespace + # used for skipping arbitrary number of non-breaking whitespace trim_ws = r'[ \t]*' - // git may change line-endings on windows, so we have to match on both + # git may change line-endings on windows, so we have to match on both eol = "\n" / "\r\n" nonneg_num = r'\d+' num_jobs = nonneg_num num_machines = nonneg_num machine = nonneg_num duration = nonneg_num - // task data for 1 job + # task data for 1 job job_data = ' '* machine ' '+ duration (' '+ machine ' '+ duration)* trim_ws eol """ diff --git a/src/SchedulingAlgorithms/__init__.py b/src/SchedulingAlgorithms/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/SchedulingAlgorithms/enumerate.py b/src/SchedulingAlgorithms/enumerate.py deleted file mode 100644 index fb291ba..0000000 --- a/src/SchedulingAlgorithms/enumerate.py +++ /dev/null @@ -1,10 +0,0 @@ -from Parser import JobShopProblem as Problem - -def enumerate(problem): - schedule = ( (job, task) for job in range(0, problem.jobs) for task in range(0, len(problem.get_tasks_by_job(job))) ) - begin = 0 - solution = [] - for task in schedule: - solution.append((begin, task)) - begin += problem[task][0] - return solution diff --git a/src/SchedulingAlgorithms/simanneal.py b/src/SchedulingAlgorithms/simanneal.py deleted file mode 100644 index 66d8818..0000000 --- a/src/SchedulingAlgorithms/simanneal.py +++ /dev/null @@ -1,36 +0,0 @@ -from Generator.generator import generate -from Generator.generator import init as gen_init -from SchedulingAlgorithms.enumerate import enumerate as enum -from math import e -from random import random - -def anneal(max_temp = 300, max_steps = 250, accept_prob=0.01): - global problem - gen_init(problem) - temp = max_temp - initial = enum(problem) - current = generate(initial, problem.machines * problem.jobs * 10, 0) #Complete the iteration once fully. - del initial - for step in range(0, max_steps): - new = generate(current, problem.machines * problem.jobs, accept_prob) - new_end = rate(new) - curr_end = rate(current) - p = 1 / ( 1 + (e ** ((curr_end - new_end)/temp))) - if (new_end < curr_end) or (p < random()): - current = new - print("Old: " + str(curr_end) + " New: " + str(new_end)) - temp = ((max_temp-1)/(max_steps**2))*(step-max_steps)**2+1 - print("Iteration: "+ str(step) + " Temperature: " + str(temp)) - return current - -def rate(solution): - global problem - last_tasks = [] - for i in range(0,problem.jobs): - last_tasks += [next(( x for x in solution[::-1] if x[1][0] == i), [])] - end_times = [ problem[x[1]][0] + x[0] for x in last_tasks] - return max(end_times) - -def init(in_problem): - global problem - problem = in_problem diff --git a/src/example.py b/src/example.py index 50d2f22..a56e489 100644 --- a/src/example.py +++ b/src/example.py @@ -1,10 +1,7 @@ -import Parser.js1_style as p -#import Parser.js2_style as p -from SchedulingAlgorithms import simanneal as sim -from Output import output as o +INSTANCES = [(5, 5)] +TASKS = [[(1, 21), (0, 53), (4, 95), (3, 55), (2, 35)], + [(0, 21), (3, 52), (4, 16), (2, 26), (1, 71)], + [(3, 39), (4, 98), (1, 42), (2, 31), (0, 12)], + [(1, 77), (0, 55), (4, 79), (2, 66), (3, 77)], + [(0, 83), (3, 34), (2, 64), (1, 19), (4, 37)]] -problem = p.parse_file("../inputdata/jobshop1.txt")[0] -#problem = p.parse_file("../inputdata/sample") -sim.init(problem) -solution = sim.anneal() -o.create_plot(problem, solution) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 44f5448..e1e7dda 100644 --- a/src/main.py +++ b/src/main.py @@ -1,97 +1,6 @@ #! /usr/bin/env python +def main() -> None: + pass -import sys -import getopt -from SchedulingAlgorithms import simanneal as sim -from Output import output as o - - -def usage(): - s= """ -Command line options: - -h show this help - -p activate pretty output (requires tkinter) - -l assume that a file contains multiple problems, default is only 1 - -i index of the problem you want solved. has no effect without l - -t set parameter max_temp of simulated annealing - -s set parameter max_steps of simulated annealing - -a set parameter accept_prob of simulated annealing - -Invocation: - python [-hlp] file -""" - return s - - -def main(): - js1 = False - plot = False - try: - opts, args = getopt.getopt(sys.argv[1:], 'hpli:t:s:a:') - except getopt.GetoptError as err: - print(err) - sys.exit() - if ('-h', '') in opts: - print(usage()) - if ('-p', '') in opts: - print("Plotting enabled.") - from Output import output as o - plot = True - if('-l', '') in opts: - js1 = True - idx = [int(x[1]) for x in opts if x[0]=='-i'] - idx = idx[0] if idx else -1 - max_temp = [int(x[1]) for x in opts if x[0]=='-t'] - max_temp = max_temp[0] if max_temp else -1 - max_steps = [int(x[1]) for x in opts if x[0]=='-s'] - max_steps = max_steps[0] if max_steps else -1 - accept_prob = [int(x[1]) for x in opts if x[0]=='-a'] - accept_prob = accept_prob[0] if accept_prob else -1 - if not args: - print("No file given.") - sys.exit() - else: - infile = args[0] - if js1: - from Parser import js1_style as parser - else: - from Parser import js2_style as parser - print("Parsing file: " + infile) - problem = parser.parse_file(infile) - if js1: - print("File contains " + str(len(problem)) + " problems.") - if idx == -1: - idx = int(input("Which problem do you want so solve? [0-" + str(len(problem)-1) + "] ")) - problem = problem[idx] - print(problem) - sim.init(problem) - if not max_temp == -1: - if not max_steps == -1: - if not accept_prob == -1: - solution = sim.anneal(max_temp, max_steps, accept_prob) - else: - solution = sim.anneal(max_temp = max_temp, max_steps = max_steps) - else: - if not accept_prob == -1: - solution = sim.anneal(max_temp = max_temp, accept_prob = accept_prob) - else: - solution = sim.anneal(max_temp = max_temp) - else: - if not max_steps == -1: - if not accept_prob == -1: - solution = sim.anneal(max_steps = max_steps, accept_prob = accept_prob) - else: - solution = sim.anneal(max_steps = max_steps) - else: - if not accept_prob == -1: - solution = sim.anneal(accept_prob = accept_prob) - else: - solution = sim.anneal() - print(solution) - print(sim.rate(solution)) - if plot: - o.create_plot(problem, solution) - -if __name__ == "__main__": +if "__name__" == "__main__": main() -