diff --git a/Readme.md b/Readme.md index b2820bc..52607a2 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,7 @@ ## Tooling -- Python 3.5 +- Python 3.6 - [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 new file mode 100644 index 0000000..2990537 --- /dev/null +++ b/Readme.txt @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..fd1f7b9 --- /dev/null +++ b/doc.md @@ -0,0 +1,36 @@ +## 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/notes.md b/notes.md index a21ba6f..efe7c46 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) +1. Liste von (T, $o_j$) mit $T \in \mathbb{N}$ (Time), $o_j \in O$ (Tasks), j bezeichnet den Job - 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 4612f56..55650c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ mypy -Arpeggio +arpeggio +matplotlib +numpy +tkinter \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..e8d926d --- /dev/null +++ b/shell.nix @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..e69de29 diff --git a/src/Generator/generator.py b/src/Generator/generator.py index 13d3441..4cc4ab3 100644 --- a/src/Generator/generator.py +++ b/src/Generator/generator.py @@ -39,6 +39,7 @@ def accept(solution): random solution. """ return tighten(solution) + #return solution def tighten(solution): diff --git a/src/Output/__init__.py b/src/Output/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/Output/output.py b/src/Output/output.py new file mode 100644 index 0000000..a2d2168 --- /dev/null +++ b/src/Output/output.py @@ -0,0 +1,32 @@ +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 d42e09c..59d2edf 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 new file mode 100644 index 0000000..e69de29 diff --git a/src/example.py b/src/example.py index a56e489..50d2f22 100644 --- a/src/example.py +++ b/src/example.py @@ -1,7 +1,10 @@ -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)]] +import Parser.js1_style as p +#import Parser.js2_style as p +from SchedulingAlgorithms import simanneal as sim +from Output import output as o +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 e1e7dda..44f5448 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,97 @@ #! /usr/bin/env python -def main() -> None: - pass -if "__name__" == "__main__": +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__": main() +