diff --git a/pom.xml b/pom.xml index 1ea74b4..67884ff 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 net.berack @@ -13,6 +13,30 @@ 23 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 23 + 23 + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + net.berack.upo.valpre.Main + + + + + org.junit.jupiter @@ -36,5 +60,10 @@ kryo 5.6.2 + + org.jfree + jfreechart + 1.5.5 + \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/Plot.java b/src/main/java/net/berack/upo/valpre/Plot.java index e24ee28..efa0c83 100644 --- a/src/main/java/net/berack/upo/valpre/Plot.java +++ b/src/main/java/net/berack/upo/valpre/Plot.java @@ -1,18 +1,35 @@ package net.berack.upo.valpre; +import java.awt.BorderLayout; +import java.awt.GridLayout; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.data.category.DefaultCategoryDataset; + import net.berack.upo.valpre.sim.stats.CsvResult; -import net.berack.upo.valpre.sim.stats.ResultMultiple; +import net.berack.upo.valpre.sim.stats.ResultSummary; +import net.berack.upo.valpre.sim.stats.Statistics; /** * 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; + public final ResultSummary summary; + private final ChartPanel chartPanel; + private final JComboBox nodeComboBox; + private final JComboBox statComboBox; /** * Create a new plot object. @@ -27,14 +44,78 @@ public class Plot { throw new IllegalArgumentException("CSV file needed! Use -csv "); var results = new CsvResult(file).loadResults(); - this.results = new ResultMultiple(results); + this.summary = new ResultSummary(results); + + var nodes = this.summary.getNodes().toArray(new String[0]); + this.chartPanel = new ChartPanel(null); + this.nodeComboBox = new JComboBox<>(nodes); + this.statComboBox = new JComboBox<>(Statistics.getOrderOfApply()); } /** * Show the plot of the results. */ public void show() { - // TODO: Use JavaFX to show the plot + SwingUtilities.invokeLater(() -> { + var nodeLabel = new JLabel("Node: "); + var statLabel = new JLabel("Stat: "); + + var filterPanel = new JPanel(); + filterPanel.setLayout(new GridLayout(2, 2)); + filterPanel.add(nodeLabel); + filterPanel.add(nodeComboBox); + filterPanel.add(statLabel); + filterPanel.add(statComboBox); + + nodeComboBox.addActionListener(_ -> updateChart()); + statComboBox.addActionListener(_ -> updateChart()); + + var rootPane = new JPanel(); + rootPane.setLayout(new BorderLayout()); + rootPane.add(filterPanel, BorderLayout.NORTH); + rootPane.add(chartPanel, BorderLayout.CENTER); + + chartPanel.setChart(ChartFactory.createBarChart( + "Title", + "Run", + "Value", + null, + PlotOrientation.VERTICAL, + true, + true, + false)); + updateChart(); + + var frame = new JFrame("Graph of the Simulation"); + frame.add(rootPane); + frame.setSize(800, 600); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + }); + } + + /** + * Update the chart with the selected node and stat. + */ + private void updateChart() { + try { + var node = this.nodeComboBox.getSelectedItem().toString(); + var stat = this.statComboBox.getSelectedItem().toString(); + + var summary = this.summary.getSummaryOf(node, stat); + var frequency = summary.getFrequency(20); + + var dataset = new DefaultCategoryDataset(); + for (int i = 0; i < frequency.length; i++) { + dataset.addValue(frequency[i], "Frequency", Integer.valueOf(i)); + } + + var chart = chartPanel.getChart(); + chart.getCategoryPlot().setDataset(dataset); + chart.setTitle(String.format("Avg %.3f", summary.average)); + } catch (Exception e) { + e.printStackTrace(); + } } /** diff --git a/src/main/java/net/berack/upo/valpre/Simulation.java b/src/main/java/net/berack/upo/valpre/Simulation.java index 6cfaa4e..bd7b8f2 100644 --- a/src/main/java/net/berack/upo/valpre/Simulation.java +++ b/src/main/java/net/berack/upo/valpre/Simulation.java @@ -54,15 +54,14 @@ public class Simulation { 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); + var summary = 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.print(summary); System.out.println("Final time " + nano / 1e6 + "ms"); if (csv != null) { - new CsvResult(this.csv).saveResults(results.runs); + new CsvResult(this.csv).saveResults(summary.runs); System.out.println("Data saved to " + this.csv); } } diff --git a/src/main/java/net/berack/upo/valpre/sim/SimulationMultiple.java b/src/main/java/net/berack/upo/valpre/sim/SimulationMultiple.java index 73b0f08..2a1367a 100644 --- a/src/main/java/net/berack/upo/valpre/sim/SimulationMultiple.java +++ b/src/main/java/net/berack/upo/valpre/sim/SimulationMultiple.java @@ -5,8 +5,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import net.berack.upo.valpre.rand.Rng; -import net.berack.upo.valpre.sim.stats.ResultMultiple; import net.berack.upo.valpre.sim.stats.Result; +import net.berack.upo.valpre.sim.stats.ResultSummary; /** * A network simulation that uses a discrete event simulation to model the @@ -36,7 +36,7 @@ public class SimulationMultiple { * events. * @return The statistics the network. */ - public ResultMultiple run(long seed, int runs, EndCriteria... criterias) { + public ResultSummary run(long seed, int runs, EndCriteria... criterias) { var rngs = Rng.getMultipleStreams(seed, runs); var stats = new Result[runs]; @@ -44,7 +44,7 @@ public class SimulationMultiple { var sim = new Simulation(this.net, rngs[i], criterias); stats[i] = sim.run(); } - return new ResultMultiple(stats); + return new ResultSummary(stats); } /** @@ -62,7 +62,7 @@ public class SimulationMultiple { * @throws InterruptedException If the threads are interrupted. * @throws ExecutionException If the one of the threads has been aborted. */ - public ResultMultiple runParallel(long seed, int runs, EndCriteria... criterias) + public ResultSummary runParallel(long seed, int runs, EndCriteria... criterias) throws InterruptedException, ExecutionException { var rngs = Rng.getMultipleStreams(seed, runs); var results = new Result[runs]; @@ -82,7 +82,7 @@ public class SimulationMultiple { futures[i].get(); } - return new ResultMultiple(results); + return new ResultSummary(results); } } 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 9ecd6a2..6daab48 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 @@ -13,9 +13,6 @@ public class Result { public final long seed; public final double simulationTime; public final double timeElapsedMS; - private int size; - private String iFormat; - private String fFormat; /** * Creates a new result object for the given parameters obtained by the @@ -31,53 +28,5 @@ public class Result { this.simulationTime = time; this.timeElapsedMS = elapsed; this.nodes = nodes; - this.size = (int) Math.ceil(Math.max(Math.log10(this.simulationTime), 1)); - this.iFormat = "%" + this.size + ".0f"; - this.fFormat = "%" + (this.size + 4) + ".3f"; - } - - /** - * Get the global information of the simulation. In particular this method build - * a string that contains the seed and the time elapsed in the simulation and in - * real time - * - * @return a string with the info - */ - public String getHeader() { - var builder = new StringBuilder(); - builder.append("===== Net Stats =====\n"); - builder.append(String.format("Seed: \t%d\n", this.seed)); - builder.append(String.format("Simulation: \t" + fFormat + "\n", this.simulationTime)); - builder.append(String.format("Elapsed: \t" + fFormat + "ms\n", this.timeElapsedMS / 1e6)); - return builder.toString(); - } - - /** - * Print a summary of the statistics to the console. - * The summary includes all the statistics of nodes and for each it displays the - * departures, queue, wait, response, throughput, utilization, unavailability - * and the last event time. - * - * @return a string with all the stats - */ - public String getSummary() { - String[] h = { "Node", "Departures", "Avg Queue", "Avg Wait", "Avg Response", "Throughput", "Utilization %", - "Unavailable %", "Last Event" }; - var table = new ConsoleTable(h); - - for (var entry : this.nodes.entrySet()) { - var stats = entry.getValue(); - table.addRow( - entry.getKey(), - iFormat.formatted(stats.numDepartures), - fFormat.formatted(stats.avgQueueLength), - fFormat.formatted(stats.avgWaitTime), - fFormat.formatted(stats.avgResponse), - fFormat.formatted(stats.troughput), - fFormat.formatted(stats.utilization * 100), - fFormat.formatted(stats.unavailable * 100), - fFormat.formatted(stats.lastEventTime)); - } - return table.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 deleted file mode 100644 index 69559f2..0000000 --- a/src/main/java/net/berack/upo/valpre/sim/stats/ResultMultiple.java +++ /dev/null @@ -1,132 +0,0 @@ -package net.berack.upo.valpre.sim.stats; - -import java.util.HashMap; - -/** - * This class represent the result of multiple runs of simulation. - */ -public class ResultMultiple { - public final Result[] runs; - public final Result average; - public final Result variance; - public final Result error95; - - /** - * This has all the result and give some statistics about the runs. - * The object created has the average, the variance, and the error95. - * The runs must be an array of at least 2 run result otherwise an exception is - * thrown. - * - * @param runs an array of run result - * @throws IllegalArgumentException if the runs is null or if has a len <= 1 - */ - public ResultMultiple(Result... runs) { - if (runs == null || runs.length <= 1) - throw new IllegalArgumentException("Sample size must be > 1"); - - this.runs = runs; - this.average = ResultMultiple.calcAvg(runs); - this.variance = ResultMultiple.calcStdDev(this.average, runs); - this.error95 = calcError(this.average, this.variance, runs.length, 0.95); - } - - /** - * This method calculate the average of the runs result. - * The average is calculated for each node. - * - * @param runs the run to calculate - * @return the average of the runs - */ - public static Result calcAvg(Result... runs) { - var avgTime = 0.0d; - var avgElapsed = 0L; - var nodes = new HashMap(); - - for (var run : runs) { - avgTime += run.simulationTime; - avgElapsed += run.timeElapsedMS; - - for (var entry : run.nodes.entrySet()) { - var stats = nodes.computeIfAbsent(entry.getKey(), _ -> new Statistics()); - stats.merge(entry.getValue(), (val1, val2) -> val1 + val2); - } - } - - avgTime /= runs.length; - avgElapsed /= runs.length; - for (var stat : nodes.values()) - stat.apply(val -> val / runs.length); - return new Result(runs[0].seed, avgTime, avgElapsed, nodes); - } - - /** - * This method calculate the standard deviation of the runs result. - * The standard deviation is calculated for each node. - * - * @param avg the average of the runs. {@link #calcAvg(Result...)} - * @param runs the run to calculate - * @return the standard deviation of the runs - */ - public static Result calcStdDev(Result avg, Result... runs) { - var time = 0.0d; - var elapsed = 0.0d; - var nodes = new HashMap(); - - for (var run : runs) { - time += Math.pow(run.simulationTime - avg.simulationTime, 2); - elapsed += Math.pow(run.timeElapsedMS - avg.simulationTime, 2); - - for (var entry : run.nodes.entrySet()) { - var stat = nodes.computeIfAbsent(entry.getKey(), _ -> new Statistics()); - var average = avg.nodes.get(entry.getKey()); - var other = entry.getValue(); - var temp = new Statistics(); - Statistics.apply(temp, other, average, (o, a) -> Math.pow(o - a, 2)); - stat.merge(temp, (var1, var2) -> var1 + var2); - } - } - - time = Math.sqrt(time / runs.length - 1); - elapsed = Math.sqrt(elapsed / runs.length - 1); - for (var stat : nodes.values()) - stat.apply(val -> Math.sqrt(val / (runs.length - 1))); - - return new Result(runs[0].seed, time, elapsed, nodes); - } - - /** - * Calculates the error at the selected alpha level. - * This method computes the error margin for the provided average and standard - * deviation values, - * considering the sample size and the confidence level (alpha). - * The result is adjusted using a t-distribution to account for the variability - * in smaller sample sizes. - * - * @param avg The average of the results, typically computed using - * {@link #calcAvg(Result...)}. - * @param stdDev The standard deviation of the results, typically computed - * using {@link #calcVar(Result, Result...)}. - * @param sampleSize The number of runs or samples used. - * @param alpha The significance level (probability) used for the - * t-distribution. A value of 0.95 for a 95% confidence level. - * @return The calculated error. - */ - public static Result calcError(Result avg, Result stdDev, int sampleSize, double alpha) { - // Getting the correct values for the percentile - var distr = new org.apache.commons.math3.distribution.TDistribution(sampleSize - 1); - var percentile = distr.inverseCumulativeProbability(alpha); - - // Calculating the error - var sqrtSample = Math.sqrt(sampleSize); - var error = new Result(avg.seed, - percentile * (stdDev.simulationTime / sqrtSample), - percentile * (stdDev.timeElapsedMS / sqrtSample), - new HashMap<>()); - for (var entry : stdDev.nodes.entrySet()) { - var stat = new Statistics(); - stat.merge(entry.getValue(), (_, val) -> percentile * (val / sqrtSample)); - error.nodes.put(entry.getKey(), stat); - } - return error; - } -} \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/ResultSummary.java b/src/main/java/net/berack/upo/valpre/sim/stats/ResultSummary.java new file mode 100644 index 0000000..06eb4ef --- /dev/null +++ b/src/main/java/net/berack/upo/valpre/sim/stats/ResultSummary.java @@ -0,0 +1,128 @@ +package net.berack.upo.valpre.sim.stats; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * This class represent the summary of the result of multiple runs of + * simulation. It has the average of the simulation time, the average of the + * elapsed time, and the average of the statistics of the nodes. + */ +public class ResultSummary { + + public final long seed; + public final double simulationTime; + public final double timeElapsedMS; + public final Result[] runs; + + private final Map> stats; + + /** + * This has all the result and give some statistics about the runs. + * The object created has the average, the variance, and the error95. + * The runs must be an array of at least 2 run result otherwise an exception is + * thrown. + * + * @param runs an array of run result + * @throws IllegalArgumentException if the runs is null or if has a len <= 1 + */ + public ResultSummary(Result[] runs) { + if (runs == null || runs.length <= 1) + throw new IllegalArgumentException("Sample size must be > 1"); + + // Get the seed, simulation time, and time elapsed + var avgTime = 0.0d; + var avgElapsed = 0L; + for (var run : runs) { + avgTime += run.simulationTime; + avgElapsed += run.timeElapsedMS; + } + this.runs = runs; + this.seed = runs[0].seed; + this.simulationTime = avgTime / runs.length; + this.timeElapsedMS = avgElapsed / runs.length; + + // Get the statistics of the nodes + var nodeStats = new HashMap(); + for (var i = 0; i < runs.length; i++) { + for (var entry : runs[i].nodes.entrySet()) { + var node = entry.getKey(); + var stats = nodeStats.computeIfAbsent(node, _ -> new Statistics[runs.length]); + stats[i] = entry.getValue(); + } + } + + // Get the summary of the statistics of the nodes + this.stats = new HashMap<>(); + for (var entry : nodeStats.entrySet()) { + var node = entry.getKey(); + var summary = StatisticsSummary.getSummary(entry.getValue()); + this.stats.put(node, summary); + } + } + + /** + * Get the summary of the statistics of a node. + * + * @param node the node to get the summary + * @param stat the statistic to get the summary + * @return the summary of the statistics of the node + */ + public StatisticsSummary getSummaryOf(String node, String stat) { + return this.stats.get(node).get(stat); + } + + /** + * Get all the summary of the statistics of a node. + * + * @param node the node to get the summary + * @return the summary of the statistics of the node + */ + public Map getSummaryOf(String node) { + return this.stats.get(node); + } + + /** + * Get the nodes of the simulation. + * + * @return the nodes of the simulation + */ + public Collection getNodes() { + return this.stats.keySet(); + } + + @Override + public String toString() { + var size = (int) Math.ceil(Math.max(Math.log10(this.simulationTime), 1)); + var iFormat = "%" + size + ".0f"; + var fFormat = "%" + (size + 4) + ".3f"; + + var builder = new StringBuilder(); + builder.append("===== Net Stats =====\n"); + builder.append(String.format("Seed: \t%d\n", this.seed)); + builder.append(String.format("Simulation: \t" + fFormat + "\n", this.simulationTime)); + builder.append(String.format("Elapsed: \t" + fFormat + "ms\n", this.timeElapsedMS / 1e6)); + // return builder.toString(); + + var table = new ConsoleTable("Node", "Departures", "Avg Queue", "Avg Wait", "Avg Response", "Throughput", + "Utilization %", "Unavailable %", "Last Event"); + + for (var entry : this.stats.entrySet()) { + var stats = entry.getValue(); + table.addRow( + entry.getKey(), + iFormat.formatted(stats.get("numDepartures").average), + fFormat.formatted(stats.get("avgQueueLength").average), + fFormat.formatted(stats.get("avgWaitTime").average), + fFormat.formatted(stats.get("avgResponse").average), + fFormat.formatted(stats.get("troughput").average), + fFormat.formatted(stats.get("utilization").average * 100), + fFormat.formatted(stats.get("unavailable").average * 100), + fFormat.formatted(stats.get("lastEventTime").average)); + } + + builder.append(table); + return builder.toString(); + } +} diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java b/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java new file mode 100644 index 0000000..48d9202 --- /dev/null +++ b/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java @@ -0,0 +1,113 @@ +package net.berack.upo.valpre.sim.stats; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A summary of the values. + */ +public class StatisticsSummary { + public final String name; + public final double average; + public final double median; + public final double min; + public final double max; + public final double stdDev; + public final double error95; + public final double[] values; + + /** + * Create a summary of the values. + * This method calculates the average, median, minimum, maximum, standard + * deviation, and error at the 95% confidence level of the provided values. + * The values are sorted before calculating the summary. + * + * @param values the values to summarize + */ + public StatisticsSummary(String name, double[] values) { + if (values == null || values.length < 2) + throw new IllegalArgumentException("The values array must have at least two elements."); + + Arrays.sort(values); + var sum = Arrays.stream(values).sum(); + var avg = sum / values.length; + var median = values.length / 2; + var varianceSum = Arrays.stream(values).map(value -> Math.pow(value - avg, 2)).sum(); + + this.name = name; + this.values = values; + this.average = avg; + this.stdDev = Math.sqrt(varianceSum / values.length); + this.median = values.length % 2 == 0 ? (values[median - 1] + values[median]) / 2.0 : values[median]; + this.min = values[0]; + this.max = values[values.length - 1]; + this.error95 = this.calcError(0.95); + } + + /** + * Calculates the error at the selected alpha level. + * This method computes the error for the average and standard deviation values, + * considering the sample size and the confidence level (alpha). + * The result is adjusted using a t-distribution to account for the variability + * in smaller sample sizes. + * + * @param alpha the alpha value + * @return the error of the values + */ + public double calcError(double alpha) { + var sampleSize = this.values.length; + var distr = new org.apache.commons.math3.distribution.TDistribution(sampleSize - 1); + var percentile = distr.inverseCumulativeProbability(alpha); + + return percentile * (this.stdDev / Math.sqrt(sampleSize)); + } + + /** + * Get the frequency of the values in the array. + * + * @param numBins the number of bins to use + * @return an array with the frequency of the values + */ + public int[] getFrequency(int numBins) { + var buckets = new int[numBins]; + var range = this.max - this.min; + var step = numBins / range; + + for (var value : this.values) { + var index = (int) Math.floor((value - this.min) * step); + index = Math.min(index, numBins - 1); + buckets[index] += 1; + } + return buckets; + } + + /** + * Get a summary of the statistics. + * + * @param stats the statistics to summarize + * @return a map with the summary of the statistics + * @throws IllegalArgumentException if the fields of the statistics cannot be + * accessed + */ + public static Map getSummary(Statistics[] stats) throws IllegalArgumentException { + try { + var map = new HashMap(); + + for (var field : Statistics.class.getFields()) { + field.setAccessible(true); + + var values = new double[stats.length]; + for (var i = 0; i < stats.length; i++) + values[i] = field.getDouble(stats[i]); + + var name = field.getName(); + map.put(name, new StatisticsSummary(name, values)); + } + return map; + } catch (IllegalAccessException e) { // This should not happen normally, but it is better to catch it + e.printStackTrace(); + throw new IllegalArgumentException("Cannot access the fields of the statistics."); + } + } +} \ No newline at end of file