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.0net.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 @@
kryo5.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