diff --git a/.vscode/launch.json b/.vscode/launch.json index 30215a4..e983c63 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,21 +9,21 @@ "name": "Run1k Simple", "request": "launch", "mainClass": "net.berack.upo.valpre.Main", - "args": "-net example1.net -runs 1000 -p -seed 0" + "args": "simulation -net example1.net -runs 1000 -p -seed 0" }, { "type": "java", "name": "Run1k Complex", "request": "launch", "mainClass": "net.berack.upo.valpre.Main", - "args": "-net example2.net -runs 1000 -p -seed 0" + "args": "simulation -net example2.net -runs 1000 -p -seed 0" }, { "type": "java", "name": "Run10", "request": "launch", "mainClass": "net.berack.upo.valpre.Main", - "args": "-net example1.net -runs 10" + "args": "simulation -net example1.net -runs 10" }, ] } \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/Main.java b/src/main/java/net/berack/upo/valpre/Main.java index 67e923c..64e0e3b 100644 --- a/src/main/java/net/berack/upo/valpre/Main.java +++ b/src/main/java/net/berack/upo/valpre/Main.java @@ -1,77 +1,33 @@ package net.berack.upo.valpre; -import java.util.HashMap; -import java.util.Map; - -import net.berack.upo.valpre.sim.Net; -import net.berack.upo.valpre.sim.SimulationMultiple; +import java.io.File; +import java.net.URISyntaxException; +import java.util.Arrays; public class Main { public static void main(String[] args) throws Exception { - // Parameters for the simulation - String csv = null; - var runs = 100; - var seed = 2007539552L; + if (args.length == 0) + exit(); - // Evantually change the parameters - var arguments = parseParameters(args); - if (arguments.containsKey("seed")) - seed = Long.parseLong(arguments.get("seed")); - if (arguments.containsKey("runs")) - runs = Integer.parseInt(arguments.get("runs")); - if (arguments.containsKey("csv")) - csv = arguments.get("csv"); - var parallel = arguments.containsKey("p"); - - var file = arguments.get("net"); - if (file == null) - throw new IllegalArgumentException("Net file needed! Use -net "); - if (file.startsWith("example")) - file = Main.class.getClassLoader().getResource(file).getPath(); - - // var maxDepartures = new EndSimulationCriteria.MaxDepartures("Queue", total); - // var maxTime = new EndSimulationCriteria.MaxTime(1000.0); - - /// Run multiple simulations - var net = Net.load(file); - var nano = System.nanoTime(); - var sim = new SimulationMultiple(net); - var results = parallel ? sim.runParallel(seed, runs) : sim.run(seed, runs); - nano = System.nanoTime() - nano; - - System.out.print(results.average.getHeader()); - System.out.print(results.average.getSummary()); - System.out.println("Final time " + nano / 1e6 + "ms"); - - if (csv != null) { - results.saveCSV(csv); - System.out.println("Data saved to " + csv); + var subArgs = Arrays.copyOfRange(args, 1, args.length); + switch (args[0]) { + case "simulation": + var sim = new Simulation(subArgs); + sim.run(); + break; + case "plot": + var plot = new Plot(subArgs); + plot.show(); + break; + default: + exit(); } } - public static Map parseParameters(String[] args) { - var arguments = new HashMap(); - arguments.put("p", false); - arguments.put("seed", true); - arguments.put("runs", true); - arguments.put("net", true); - arguments.put("csv", true); - - var param = new Parameters("-", arguments); - try { - return param.parse(args); - } catch (IllegalArgumentException e) { - var descriptions = new HashMap(); - descriptions.put("p", "Add this if you want the simulation to use threads (one each run)."); - descriptions.put("seed", "The seed of the simulation."); - descriptions.put("runs", "How many runs the simulator should run."); - descriptions.put("net", - "The net to use. It should be a file. Use example1.net or example2.net for the provided ones."); - descriptions.put("csv", "The filename for saving every run statistics."); - - System.out.println(e.getMessage()); - System.out.println(param.helper(descriptions)); - return null; - } + public static void exit() throws URISyntaxException { + var uri = Main.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + var name = new File(uri).getName(); + System.out.println("Usage: java -jar " + name + ".jar [simulation|plot] [args]"); + System.exit(1); } } \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/Parameters.java b/src/main/java/net/berack/upo/valpre/Parameters.java index 73617b5..2a7f9a9 100644 --- a/src/main/java/net/berack/upo/valpre/Parameters.java +++ b/src/main/java/net/berack/upo/valpre/Parameters.java @@ -124,4 +124,31 @@ public class Parameters { throw new IllegalArgumentException("Argument unknown"); return false; } + + /** + * Parse the arguments passed and returns a map of Argument --> Value that can + * be used to retrieve the information. In the case that the arguments are not + * in the correct format then an exception is thrown and the helper is printed. + * + * @param args the arguments in input + * @param prefix the prefix to be used + * @param arguments a map of arguments where the key is a string and if the + * boolean is true then the argument expect a value + * @param descriptions a map of descriptions for the arguments + * @throws IllegalArgumentException if the arguments are not formatted correctly + * or if there is an unknown argument + * @return a map of the values + */ + public static Map getArgsOrHelper(String[] args, String prefix, Map arguments, + Map descriptions) { + + var param = new Parameters(prefix, arguments); + try { + return param.parse(args); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + System.out.println(param.helper(descriptions)); + throw new IllegalArgumentException("Invalid arguments"); + } + } } diff --git a/src/main/java/net/berack/upo/valpre/Plot.java b/src/main/java/net/berack/upo/valpre/Plot.java new file mode 100644 index 0000000..a984ee0 --- /dev/null +++ b/src/main/java/net/berack/upo/valpre/Plot.java @@ -0,0 +1,55 @@ +package net.berack.upo.valpre; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import net.berack.upo.valpre.sim.stats.CsvResult; +import net.berack.upo.valpre.sim.stats.ResultMultiple; + +/** + * This class is used to plot the results of the simulation. + * The results are saved in a CSV file and then loaded to be plotted. + */ +public class Plot { + public final ResultMultiple results; + + /** + * Create a new plot object. + * + * @param args the arguments to create the plot + * @throws IOException if anything happens while reading the file + */ + public Plot(String[] args) throws IOException { + var arguments = Plot.parseParameters(args); + var csv = arguments.get("csv"); + if (csv == null) + throw new IllegalArgumentException("CSV file needed! Use -csv "); + + var results = new CsvResult(csv).loadResults(); + this.results = new ResultMultiple(results); + } + + /** + * Show the plot of the results. + */ + public void show() { + // TODO: Use JavaFX to show the plot + } + + /** + * Parse the arguments to get the CSV file. + * + * @param args the arguments to parse + * @return a map with the arguments + */ + private static Map parseParameters(String[] args) { + var arguments = new HashMap(); + arguments.put("csv", true); + + var descriptions = new HashMap(); + descriptions.put("csv", "The filename that contains the previous saved runs."); + + return Parameters.getArgsOrHelper(args, "-", arguments, descriptions); + } +} diff --git a/src/main/java/net/berack/upo/valpre/Simulation.java b/src/main/java/net/berack/upo/valpre/Simulation.java new file mode 100644 index 0000000..6c46af6 --- /dev/null +++ b/src/main/java/net/berack/upo/valpre/Simulation.java @@ -0,0 +1,113 @@ +package net.berack.upo.valpre; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +import com.esotericsoftware.kryo.KryoException; + +import net.berack.upo.valpre.sim.Net; +import net.berack.upo.valpre.sim.SimulationMultiple; +import net.berack.upo.valpre.sim.stats.CsvResult; + +/** + * This class is responsible for running the simulation. It parses the arguments + * and runs the simulation with the given parameters. + */ +public class Simulation { + public final String csv; + public final int runs; + public final long seed; + public final boolean parallel; + public final String file; + + /** + * Create a new simulation with the given arguments. + * + * @param args The arguments for the simulation. + */ + public Simulation(String[] args) { + // Evantually change the parameters + var arguments = Simulation.parseParameters(args); + this.runs = Simulation.getFromArguments(arguments, "runs", Integer::parseInt, 100); + this.seed = Simulation.getFromArguments(arguments, "seed", Long::parseLong, 2007539552L); + this.csv = arguments.getOrDefault("csv", null); + this.parallel = arguments.containsKey("p"); + + var file = arguments.get("net"); + if (file == null) + throw new IllegalArgumentException("Net file needed! Use -net "); + if (file.startsWith("example")) + file = Main.class.getClassLoader().getResource(file).getPath(); + this.file = file; + } + + /** + * Run the simulation with the given parameters. + * At the end it prints the results and saves them to a CSV file if requested. + * + * @throws InterruptedException If the simulation is interrupted. + * @throws ExecutionException If the simulation fails. + * @throws KryoException If the simulation fails. + * @throws IOException If the simulation fails. + */ + public void run() throws InterruptedException, ExecutionException, KryoException, IOException { + var net = Net.load(this.file); + var nano = System.nanoTime(); + var sim = new SimulationMultiple(net); + var results = this.parallel ? sim.runParallel(this.seed, this.runs) : sim.run(this.seed, this.runs); + nano = System.nanoTime() - nano; + + System.out.print(results.average.getHeader()); + System.out.print(results.average.getSummary()); + System.out.println("Final time " + nano / 1e6 + "ms"); + + if (csv != null) { + new CsvResult(this.csv).saveResults(results.runs); + System.out.println("Data saved to " + this.csv); + } + } + + /** + * Get the value from the arguments or the default value if it is not present. + * + * @param args The arguments for the simulation. + * @param key The key to get the value from. + * @param parse The function to parse the value. + * @param value The default value if the key is not present. + * @return The value from the arguments or the default value if it is not + * present. + */ + private static T getFromArguments(Map args, String key, Function parse, T value) { + if (args.containsKey(key)) + return parse.apply(args.get(key)); + return value; + } + + /** + * Parse the arguments for the simulation. + * + * @param args The arguments for the simulation. + * @return The parsed arguments for the simulation. + */ + private static Map parseParameters(String[] args) { + var arguments = new HashMap(); + arguments.put("p", false); + arguments.put("seed", true); + arguments.put("runs", true); + arguments.put("net", true); + arguments.put("csv", true); + + var descriptions = new HashMap(); + descriptions.put("p", "Add this if you want the simulation to use threads (one each run)."); + descriptions.put("seed", "The seed of the simulation."); + descriptions.put("runs", "How many runs the simulator should run."); + descriptions.put("net", + "The net to use. It should be a file. Use example1.net or example2.net for the provided ones."); + descriptions.put("csv", "The filename for saving every run statistics."); + + return Parameters.getArgsOrHelper(args, "-", arguments, descriptions); + } +} diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/CsvResult.java b/src/main/java/net/berack/upo/valpre/sim/stats/CsvResult.java new file mode 100644 index 0000000..6d45268 --- /dev/null +++ b/src/main/java/net/berack/upo/valpre/sim/stats/CsvResult.java @@ -0,0 +1,116 @@ +package net.berack.upo.valpre.sim.stats; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Scanner; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class is used to save the results of the simulation to a CSV file. + * The CSV file is used to save the results of the simulation in a format that + * can + * be easily read by other programs. + */ +public class CsvResult { + public final String file; + + /** + * Create a new CSV result object. + * + * @param file the file to save/load the results + */ + public CsvResult(String file) { + if (!file.endsWith(".csv")) + file = file + ".csv"; + this.file = file; + } + + /** + * Save all the runs to a csv file. + * + * @throws IOException if anything happens wile wriiting to the file + */ + public void saveResults(Result[] results) throws IOException { + var builder = new StringBuilder(); + builder.append(String.join(",", Statistics.getOrderOfApply())); + + try (var writer = new FileWriter(this.file)) { + for (var result : results) { + for (var entry : result.nodes.entrySet()) { + builder.append(result.seed).append(","); + builder.append(entry.getKey()).append(","); + builder.append(CsvResult.statsToCSV(entry.getValue())).append('\n'); + } + } + writer.write(builder.toString()); + } + } + + /** + * Load the results from the CSV file. + * + * @return the results loaded from the file + * @throws IOException if anything happens while reading the file + */ + public Result[] loadResults() throws IOException { + var results = new ArrayList(); + try (var scan = new Scanner(new File(this.file))) { + var _ = scan.nextLine(); + + var nodes = new HashMap(); + var seed = 0L; + + while (scan.hasNextLine()) { + var line = scan.nextLine().split(","); + var currentSeed = Long.parseLong(line[0]); + var node = line[1]; + + if (currentSeed != seed && seed != 0) { + results.add(new Result(seed, 0.0, 0L, nodes)); + nodes = new HashMap<>(); + } + seed = currentSeed; + + var copy = Arrays.copyOfRange(line, 2, line.length); + var stats = CsvResult.statsFromCSV(copy); + nodes.put(node, stats); + } + + results.add(new Result(seed, 0.0, 0L, nodes)); + } + return results.toArray(new Result[0]); + } + + /** + * Converts the statistics object to a CSV string. + * + * @param stats the statistics to convert + * @return the CSV string + */ + public static String statsToCSV(Statistics stats) { + var builder = new StringBuilder(); + stats.apply(val -> { + builder.append(val).append(","); + return val; + }); + return builder.toString(); + } + + /** + * Converts the CSV string to a statistics object. + * + * @param values the values to convert + * @return the statistics object + */ + public static Statistics statsFromCSV(String[] values) { + var i = new AtomicInteger(0); + var stats = new Statistics(); + stats.apply(_ -> Double.parseDouble(values[i.getAndIncrement()])); + return stats; + } + +} diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/Result.java b/src/main/java/net/berack/upo/valpre/sim/stats/Result.java index 14d2de1..9ecd6a2 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/Result.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/Result.java @@ -80,58 +80,4 @@ public class Result { } return table.toString(); } - - /** - * Return a summary formatted for CSV. - * This meaning that all the stats will be separated by a comma (,) and each row - * is a single statistic of the node. - * Each row it will have all the statistic from the class {@link Statistics}, - * the seed used for obtaining them. - * - * @param tableHeader - * @return a string with all the stats formatted - */ - public String getSummaryCSV(boolean tableHeader) { - var builder = new StringBuilder(); - - if (tableHeader) - builder.append( - "Seed,Node,Arrivals,Departures,MaxQueue,AvgQueue,AvgWait,AvgResponse,BusyTime,WaitTime,UnavailableTime,ResponseTime,LastEventTime,Throughput,Utilization,Unavailable\n"); - for (var entry : this.nodes.entrySet()) { - var stats = entry.getValue(); - builder.append(this.seed); - builder.append(','); - builder.append(entry.getKey().replace(',', ';').replace('"', '\'')); - builder.append(','); - builder.append(stats.numArrivals); - builder.append(','); - builder.append(stats.numDepartures); - builder.append(','); - builder.append(stats.maxQueueLength); - builder.append(','); - builder.append(stats.avgQueueLength); - builder.append(','); - builder.append(stats.avgWaitTime); - builder.append(','); - builder.append(stats.avgResponse); - builder.append(','); - builder.append(stats.busyTime); - builder.append(','); - builder.append(stats.waitTime); - builder.append(','); - builder.append(stats.unavailableTime); - builder.append(','); - builder.append(stats.responseTime); - builder.append(','); - builder.append(stats.lastEventTime); - builder.append(','); - builder.append(stats.troughput); - builder.append(','); - builder.append(stats.utilization); - builder.append(','); - builder.append(stats.unavailable); - builder.append('\n'); - } - return builder.toString(); - } } diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/ResultMultiple.java b/src/main/java/net/berack/upo/valpre/sim/stats/ResultMultiple.java index 973b380..69559f2 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/ResultMultiple.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/ResultMultiple.java @@ -1,7 +1,5 @@ package net.berack.upo.valpre.sim.stats; -import java.io.FileWriter; -import java.io.IOException; import java.util.HashMap; /** @@ -32,27 +30,6 @@ public class ResultMultiple { this.error95 = calcError(this.average, this.variance, runs.length, 0.95); } - /** - * Save all the runs to a csv file. - * - * @param filename the name of the file - * @throws IOException if anything happens wile wriiting to the file - */ - public void saveCSV(String filename) throws IOException { - if (!filename.endsWith(".csv")) - filename = filename + ".csv"; - - try (var file = new FileWriter(filename)) { - var first = true; - var builder = new StringBuilder(); - for (var run : this.runs) { - builder.append(run.getSummaryCSV(first)); - first = false; - } - file.write(builder.toString()); - } - } - /** * This method calculate the average of the runs result. * The average is calculated for each node. diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java b/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java index ef76141..5382a01 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java @@ -113,6 +113,17 @@ public class Statistics { Statistics.apply(this, this, other, func); } + /** + * Get the order of update of the stats in the apply function. + * + * @return the order of the stats + */ + public static String[] getOrderOfApply() { + return new String[] { "numArrivals", "numDepartures", "avgQueueLength", "avgWaitTime", "avgResponse", + "busyTime", "waitTime", "unavailableTime", "responseTime", "lastEventTime", "troughput", "utilization", + "unavailable" }; + } + /** * Applies a binary function to merge two sets of statistics into a third one. * This method combines the statistics from two `Statistics` objects (`val1` and @@ -134,16 +145,15 @@ public class Statistics { save.numArrivals = func.apply(val1.numArrivals, val2.numArrivals); save.numDepartures = func.apply(val1.numDepartures, val2.numDepartures); save.avgQueueLength = func.apply(val1.avgQueueLength, val2.avgQueueLength); - save.busyTime = func.apply(val1.busyTime, val2.busyTime); - save.responseTime = func.apply(val1.responseTime, val2.responseTime); - save.unavailableTime = func.apply(val1.unavailableTime, val2.unavailableTime); - save.waitTime = func.apply(val1.waitTime, val2.waitTime); - save.lastEventTime = func.apply(val1.lastEventTime, val2.lastEventTime); - // derived stats save.avgWaitTime = func.apply(val1.avgWaitTime, val2.avgWaitTime); save.avgResponse = func.apply(val1.avgResponse, val2.avgResponse); - save.unavailable = func.apply(val1.unavailable, val2.unavailable); + save.busyTime = func.apply(val1.busyTime, val2.busyTime); + save.waitTime = func.apply(val1.waitTime, val2.waitTime); + save.unavailableTime = func.apply(val1.unavailableTime, val2.unavailableTime); + save.responseTime = func.apply(val1.responseTime, val2.responseTime); + save.lastEventTime = func.apply(val1.lastEventTime, val2.lastEventTime); save.troughput = func.apply(val1.troughput, val2.troughput); save.utilization = func.apply(val1.utilization, val2.utilization); + save.unavailable = func.apply(val1.unavailable, val2.unavailable); } } \ No newline at end of file