From dce9b786e9b05302d723f1e2a07005e705f22d0f Mon Sep 17 00:00:00 2001 From: Arthur Bit-Monnot Date: Tue, 6 Apr 2021 12:09:56 +0200 Subject: [PATCH] Improve documentation --- README.md | 61 +++++++++------ ...KnownResult.java => BestKnownResults.java} | 29 ++++++- src/main/java/jobshop/DebuggingMain.java | 3 +- src/main/java/jobshop/Instance.java | 15 ++++ src/main/java/jobshop/Main.java | 78 +++++++++++++------ .../java/jobshop/encodings/ResourceOrder.java | 2 +- src/main/java/jobshop/encodings/Task.java | 6 +- 7 files changed, 140 insertions(+), 54 deletions(-) rename src/main/java/jobshop/{BestKnownResult.java => BestKnownResults.java} (83%) diff --git a/README.md b/README.md index 21efe0d..d236477 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,48 @@ This repository contains the starter code for the assignment. +## Working in IntelliJ -## Compile +For working on this project, we recommend using the IntelliJ-IDEA development environment. It is available in INSA's +classrooms as well as on `montp.insa-toulouse.fr`. -Compilation instructions are given for Linux. On windows you can use the `gradlew.bat` script. +To import the project in IntelliJ (once IntelliJ is running): + + - Open a new project : `Open project` or `File > Open` + - Select the `gradle.build` file in the cloned repository. + - Select `Open as project`. + +To run the program in IntelliJ, you can + + - Right click on the `src/main/java/Jobshop/Main` class in the project view. + - Select `Run Main.main()`. It should complain that some arguments are missing. + - Give it the expected command line arguments : `Run > Edit Configuration`, then fill in the `Program arguments` text box. + + +## Working on the command line (Gradle) + +Compilation instructions are given for Linux. On Windows you can use the `gradlew.bat` script (but you are on your own). ``` -❯ ./gradlew build # Compiles the project -❯ ./gradlew jar # Creates a fat-jar in build/libs/JSP.jar +❯ ./gradlew build # Compiles the project ``` -The compiled jar is now `build/libs/JSP.jar` can be executed like so : +The project can be executed directly with `gradle` by specifying the arguments like so : ``` +❯ ./gradlew run --args="--solver basic random --instance aaa1 ft" +``` + +You can also build an executable jar file, and run it with the java command. +This is especially useful if you want to run it on another machine. + +``` + # Create a jar file with all dependencies in build/libs/JSP.jar +❯ ./gradlew jar +# Run the jar file. Only requires a Java Runtime Environment (JRE) ❯ java -jar build/libs/JSP.jar --solver basic --instance ft06 ``` + The command line above indicates that we want to solve the instance named`ft06` with the `basic` solver. It should give an output like the following : ``` basic @@ -48,36 +75,26 @@ AVG - - 0.3 - 31.5 999.0 - 20.4 Here the last line give the average `runtime` and `ecart` for each solver. ``` -usage: jsp-solver [-h] [-t TIMEOUT] --solver SOLVER [SOLVER ...] +sage: jsp-solver [-h] [-t TIMEOUT] --solver SOLVER [SOLVER ...] --instance INSTANCE [INSTANCE ...] Solves jobshop problems. named arguments: -h, --help show this help message and exit + -t TIMEOUT, --timeout TIMEOUT + Solver timeout in seconds for each instance. + (default: 1) --solver SOLVER [SOLVER ...] Solver(s) to use (space separated if more than one) - -t TIMEOUT, --timeout TIMEOUT - Solver timeout in seconds for each instance - (default: 1) --instance INSTANCE [INSTANCE ...] Instance(s) to solve (space separated if more - than one) - - + than one). All instances starting with the given + String will be selected. (e.g. "ft" will select + the instances ft06, ft10 and ft20. ``` -### Running directly from Gradle - -The project can be executed directly with `gradle` by specifying the arguments like so : - -``` -❯ ./gradlew run --args="--solver basic random --instance aaa1 ft06 ft10 ft20" -``` - -This notably ensures that sources have been recompiled whenever necessary. - ## IDE Support diff --git a/src/main/java/jobshop/BestKnownResult.java b/src/main/java/jobshop/BestKnownResults.java similarity index 83% rename from src/main/java/jobshop/BestKnownResult.java rename to src/main/java/jobshop/BestKnownResults.java index 55d5f9b..c1641f6 100644 --- a/src/main/java/jobshop/BestKnownResult.java +++ b/src/main/java/jobshop/BestKnownResults.java @@ -5,12 +5,28 @@ import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; -public class BestKnownResult { +/** + * This class contains the best known results for common jobshop instances. + * Note that the best known result might not have been proven to be the optimal solution + * for the instance. + */ +public class BestKnownResults { + /** + * Checks whether we have data available for the provided instance. + * @param instanceName Name of the instance. + * @return True if the isntance is known, false otherwise. + */ public static boolean isKnown(String instanceName) { return bests.containsKey(instanceName); } + /** + * Returns all instances that start with the given prefix. + * For example, the prefix "ft" should match the instances "ft06", "ft10" and "ft20" + * @param namePrefix Prefix that should be looked-up. + * @return All instances that start with the given prefix, in alphabetical order. + */ public static List instancesMatching(String namePrefix) { return Arrays.stream(instances) .filter(i -> i.startsWith(namePrefix)) @@ -18,6 +34,11 @@ public class BestKnownResult { .collect(Collectors.toList()); } + /** + * Returns the best known result for the given instance name. + * @param instanceName Instance of which we want to retrieve the best result. + * @return Best makespan that has ever been found for this instance. + */ public static int of(String instanceName) { if(!bests.containsKey(instanceName)) { throw new RuntimeException("Unknown best result for "+instanceName); @@ -25,8 +46,12 @@ public class BestKnownResult { return bests.get(instanceName); } - static private HashMap bests; + // all best results. + static final private HashMap bests; + // a sorted array of instance names static String[] instances; + + // initialize the internal data structures. static { bests = new HashMap<>(); bests.put("aaa1", 11); diff --git a/src/main/java/jobshop/DebuggingMain.java b/src/main/java/jobshop/DebuggingMain.java index 9261b73..4487e68 100644 --- a/src/main/java/jobshop/DebuggingMain.java +++ b/src/main/java/jobshop/DebuggingMain.java @@ -27,8 +27,7 @@ public class DebuggingMain { System.out.println("\nENCODING: " + enc); Schedule sched = enc.toSchedule(); - // TODO: make it print something meaningful - // by implementing the toString() method + // TODO: make it print something meaningful by implementing the Schedule.toString() method System.out.println("SCHEDULE: " + sched); System.out.println("VALID: " + sched.isValid()); System.out.println("MAKESPAN: " + sched.makespan()); diff --git a/src/main/java/jobshop/Instance.java b/src/main/java/jobshop/Instance.java index 73bcce7..8824474 100644 --- a/src/main/java/jobshop/Instance.java +++ b/src/main/java/jobshop/Instance.java @@ -20,18 +20,28 @@ public class Instance { /** Number of machines, assumed to be same as number of tasks. */ public final int numMachines; + /** Matrix containing the duration of all tasks. */ final int[][] durations; + + /** Matrix containing the machine on which each task must be scheduled. */ final int[][] machines; + /** Duration of the given task. */ public int duration(int job, int task) { return durations[job][task]; } + + /** Duration of the given task. */ public int duration(Task t) { return duration(t.job, t.task); } + + /** Machine on which the given task must be scheduled. */ public int machine(int job, int task) { return machines[job][task]; } + + /** Machine on which the given task must be scheduled. */ public int machine(Task t) { return this.machine(t.job, t.task); } @@ -45,6 +55,11 @@ public class Instance { throw new RuntimeException("No task targeting machine "+wanted_machine+" on job "+job); } + /** + * Creates a new instance, with uninitialized durations and machines. + * This should no be called directly. Instead, Instance objects should be created with the + * Instance.fromFile() static method. + */ Instance(int numJobs, int numTasks) { this.numJobs = numJobs; this.numTasks = numTasks; diff --git a/src/main/java/jobshop/Main.java b/src/main/java/jobshop/Main.java index 7382124..bf57dd3 100644 --- a/src/main/java/jobshop/Main.java +++ b/src/main/java/jobshop/Main.java @@ -8,82 +8,101 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; - import jobshop.solvers.*; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.inf.ArgumentParser; import net.sourceforge.argparse4j.inf.ArgumentParserException; import net.sourceforge.argparse4j.inf.Namespace; - +/** + * This class is the main entry point for testing solver on instances. + * It provides + */ public class Main { /** All solvers available in this program */ - private static HashMap solvers; + private static final HashMap solvers; static { solvers = new HashMap<>(); solvers.put("basic", new BasicSolver()); solvers.put("random", new RandomSolver()); - // add new solvers here + // TODO: add new solvers here } public static void main(String[] args) { + // configure the argument parser ArgumentParser parser = ArgumentParsers.newFor("jsp-solver").build() .defaultHelp(true) .description("Solves jobshop problems."); - parser.addArgument("-t", "--timeout") .setDefault(1L) .type(Long.class) - .help("Solver timeout in seconds for each instance"); + .help("Solver timeout in seconds for each instance. Default is 1 second."); parser.addArgument("--solver") .nargs("+") .required(true) .help("Solver(s) to use (space separated if more than one)"); - parser.addArgument("--instance") .nargs("+") .required(true) - .help("Instance(s) to solve (space separated if more than one)"); + .help("Instance(s) to solve (space separated if more than one). All instances starting with the given " + + "string will be selected. (e.g. \"ft\" will select the instances ft06, ft10 and ft20."); + // parse command line arguments Namespace ns = null; try { ns = parser.parseArgs(args); } catch (ArgumentParserException e) { + // error while parsing arguments, provide helpful error message and exit. + System.err.println("Invalid arguments provided to the program.\n"); + System.err.println("In IntelliJ, you can provide arguments to the program by opening the dialog,"); + System.err.println("\"Run > Edit Configurations\" and filling in the \"program arguments\" box."); + System.err.println("See the README for a documentation of the expected arguments."); + System.err.println(); parser.handleError(e); - System.exit(1); + System.exit(0); } PrintStream output = System.out; + // convert the timeout from seconds to milliseconds. long solveTimeMs = ns.getLong("timeout") * 1000; + // Get the list of solvers that we should benchmark. + // We also check that we have a solver available for the given name and print an error message otherwise. List solversToTest = ns.getList("solver"); for(String solverName : solversToTest) { if(!solvers.containsKey(solverName)) { System.err.println("ERROR: Solver \"" + solverName + "\" is not avalaible."); System.err.println(" Available solvers: " + solvers.keySet().toString()); System.err.println(" You can provide your own solvers by adding them to the `Main.solvers` HashMap."); - System.exit(1); + System.exit(0); } } - List instancePrefixes = ns.getList("instance"); + + // retrieve all instances on which we should run the solvers. List instances = new ArrayList<>(); + List instancePrefixes = ns.getList("instance"); for(String instancePrefix : instancePrefixes) { - List matches = BestKnownResult.instancesMatching(instancePrefix); + List matches = BestKnownResults.instancesMatching(instancePrefix); if(matches.isEmpty()) { System.err.println("ERROR: instance prefix \"" + instancePrefix + "\" does not match any instance."); - System.err.println(" available instances: " + Arrays.toString(BestKnownResult.instances)); + System.err.println(" available instances: " + Arrays.toString(BestKnownResults.instances)); System.exit(1); } instances.addAll(matches); } - float[] runtimes = new float[solversToTest.size()]; - float[] distances = new float[solversToTest.size()]; + // average runtime of each solver + float[] avg_runtimes = new float[solversToTest.size()]; + // average distance to best known result for each solver + float[] avg_distances = new float[solversToTest.size()]; try { + // header of the result table : + // - solver names (first line) + // - name of each column (second line) output.print( " "); for(String s : solversToTest) output.printf("%-30s", s); @@ -94,51 +113,62 @@ public class Main { } output.println(); - + // for all instances, load it from f for(String instanceName : instances) { - int bestKnown = BestKnownResult.of(instanceName); - + // get the best known result for this instance + int bestKnown = BestKnownResults.of(instanceName); + // load instance from file. Path path = Paths.get("instances/", instanceName); Instance instance = Instance.fromFile(path); + // print some general statistics on the instance output.printf("%-8s %-5s %4d ",instanceName, instance.numJobs +"x"+instance.numTasks, bestKnown); + // run all selected solvers on the instance and print the results for(int solverId = 0 ; solverId < solversToTest.size() ; solverId++) { + // Select the next solver to run. Given the solver name passed on the command line, + // we lookup the `Main.solvers` hash map to get the solver object with the given name. String solverName = solversToTest.get(solverId); Solver solver = solvers.get(solverName); + + // start chronometer and compute deadline for the solver to provide a result. long start = System.currentTimeMillis(); long deadline = System.currentTimeMillis() + solveTimeMs; + // run the solver on the current instance Result result = solver.solve(instance, deadline); + // measure elapsed time (in milliseconds) long runtime = System.currentTimeMillis() - start; + // check that the solver returned a valid solution if(!result.schedule.isValid()) { System.err.println("ERROR: solver returned an invalid schedule"); - System.exit(1); + System.exit(1); // bug in implementation, bail out } - assert result.schedule.isValid(); + // compute some statistics on the solution and print them. int makespan = result.schedule.makespan(); float dist = 100f * (makespan - bestKnown) / (float) bestKnown; - runtimes[solverId] += (float) runtime / (float) instances.size(); - distances[solverId] += dist / (float) instances.size(); + avg_runtimes[solverId] += (float) runtime / (float) instances.size(); + avg_distances[solverId] += dist / (float) instances.size(); output.printf("%7d %8s %5.1f ", runtime, makespan, dist); output.flush(); } output.println(); - } + // we have finished all benchmarks, compute the average solve time and distance of each solver. output.printf("%-8s %-5s %4s ", "AVG", "-", "-"); for(int solverId = 0 ; solverId < solversToTest.size() ; solverId++) { - output.printf("%7.1f %8s %5.1f ", runtimes[solverId], "-", distances[solverId]); + output.printf("%7.1f %8s %5.1f ", avg_runtimes[solverId], "-", avg_distances[solverId]); } } catch (Exception e) { + // there was uncought exception, print the stack trace and exit with error. e.printStackTrace(); System.exit(1); } diff --git a/src/main/java/jobshop/encodings/ResourceOrder.java b/src/main/java/jobshop/encodings/ResourceOrder.java index fdd44a7..e3c35b7 100644 --- a/src/main/java/jobshop/encodings/ResourceOrder.java +++ b/src/main/java/jobshop/encodings/ResourceOrder.java @@ -41,7 +41,7 @@ public class ResourceOrder extends Encoding { for(int m = 0 ; m new Task(j, pb.task_with_machine(j, machine))) // all tasks on this machine (one per job) diff --git a/src/main/java/jobshop/encodings/Task.java b/src/main/java/jobshop/encodings/Task.java index 95fecca..749bbaf 100644 --- a/src/main/java/jobshop/encodings/Task.java +++ b/src/main/java/jobshop/encodings/Task.java @@ -2,10 +2,10 @@ package jobshop.encodings; import java.util.Objects; -/** Represents a task (job,task) of an jobshop problem. +/** Represents a task (job,task) of a jobshop problem. * - * Example : (2, 3) repesents the fourth task of the third job. (remeber that we tart counting at 0) - * */ + * Example : (2, 3) represents the fourth task of the third job. (remember that we start counting at 0) + **/ public final class Task { /** Identifier of the job */