From 14063300f2862f3af09c938e6f6aecc265ebfc0b Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 5 Feb 2025 21:19:19 +0100 Subject: [PATCH] Update simulation result handling and improve result processing --- README.md | 4 +- src/main/java/net/berack/upo/valpre/Plot.java | 23 ++- .../berack/upo/valpre/SimulationBuilder.java | 2 +- .../net/berack/upo/valpre/sim/Simulation.java | 2 +- .../upo/valpre/sim/SimulationMultiple.java | 22 ++- .../upo/valpre/sim/stats/CsvResult.java | 13 +- .../upo/valpre/sim/stats/NodeStats.java | 172 +++++++++++++++--- .../berack/upo/valpre/sim/stats/Result.java | 135 +++++++++++++- .../upo/valpre/sim/stats/ResultSummary.java | 151 --------------- .../valpre/sim/stats/StatisticsSummary.java | 138 -------------- 10 files changed, 311 insertions(+), 351 deletions(-) delete mode 100644 src/main/java/net/berack/upo/valpre/sim/stats/ResultSummary.java delete mode 100644 src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java diff --git a/README.md b/README.md index e860bef..577214d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ I percorsi che invece sono direttamente responsabili per la simulazione sono: - **EndCriteria** interfaccia che viene implementata dalle classi interne usata per controllare se e quando la simulazione debba finire. - **Simulation** e **SimulationMultiple** che vengono usate per far partire la simulazione; la versione multiple serve ad organizzare molteplici simulazioni su più thread o su un singolo core. - [net.berack.upo.valpre.sim.stats](https://github.com/Berack96/upo-valpre/tree/main/src/main/java/net/berack/upo/valpre/sim/stats) Package che contiene tutte le classi utili per la raccolta e l'analisi statistica dei vari valori generati dalla simulazione: - - **Result** il risultato di una run e la sua controparte **ResultSummary** che contiene molteplici risultati di run già analizzati. - - **NodeStats** contiene indici statistici di un nodo e la sua controparte **StatisticsSummary** che contiene molteplici indici statistici già analizzati. + - **Result** il risultato di una run e la sua classe interna **Result.Summary** che contiene molteplici risultati di run già analizzati. + - **NodeStats** contiene indici statistici di un nodo e la sua classe interna **NodeStats.Summary** che contiene molteplici indici statistici già analizzati. - **ConsoleTable** utile per mostrare i risultati in console sottoforma di tabella - **CsvResult** utile per la lettura/scrittura dei risultati in formato csv diff --git a/src/main/java/net/berack/upo/valpre/Plot.java b/src/main/java/net/berack/upo/valpre/Plot.java index 4be490d..3f13a7a 100644 --- a/src/main/java/net/berack/upo/valpre/Plot.java +++ b/src/main/java/net/berack/upo/valpre/Plot.java @@ -21,15 +21,15 @@ 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.ResultSummary; import net.berack.upo.valpre.sim.stats.NodeStats; +import net.berack.upo.valpre.sim.stats.Result; /** * 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 ResultSummary summary; + public final Result.Summary summary; private final ChartPanel panelBarChart; private final JComboBox nodeComboBox; private final JList statList; @@ -47,7 +47,7 @@ public class Plot { var results = CsvResult.loadResults(stream); stream.close(); - this.summary = new ResultSummary(results); + this.summary = new Result.Summary(results); var nodes = this.summary.getNodes().toArray(new String[0]); this.panelBarChart = new ChartPanel(null); @@ -132,13 +132,14 @@ public class Plot { var stat = this.statList.getSelectedValue().name.getText(); var summary = this.summary.getSummaryOf(node); - var statSummary = summary.get(stat); - var frequency = statSummary.getFrequency(15); + var frequency = summary.getFrequency(15, n -> n.of(stat)); + var min = summary.min.of(stat); + var max = summary.max.of(stat); var dataset = new DefaultCategoryDataset(); - var bucket = (statSummary.max - statSummary.min) / frequency.length; + var bucket = (max - min) / frequency.length; for (int i = 0; i < frequency.length; i++) { - var columnVal = statSummary.min + i * bucket; + var columnVal = min + i * bucket; var columnKey = String.format("%.3f", columnVal); dataset.addValue(frequency[i], "Frequency", columnKey); } @@ -147,10 +148,14 @@ public class Plot { chart.setTitle(stat + " distribution"); var model = this.statList.getModel(); + var avg = summary.average; + var err = summary.calcError(0.95); + for (int i = 0; i < model.getSize(); i++) { var entry = model.getElementAt(i); - var value = summary.get(entry.name.getText()); - entry.value.setText(String.format("%8.3f ±% 9.3f", value.average, value.calcError(0.95))); + var value = entry.name.getText(); + + entry.value.setText(String.format("%8.3f ±% 9.3f", avg.of(value), err.of(value))); } } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/net/berack/upo/valpre/SimulationBuilder.java b/src/main/java/net/berack/upo/valpre/SimulationBuilder.java index 3d0fc6e..eee9c90 100644 --- a/src/main/java/net/berack/upo/valpre/SimulationBuilder.java +++ b/src/main/java/net/berack/upo/valpre/SimulationBuilder.java @@ -138,7 +138,7 @@ public class SimulationBuilder { System.out.println("Final time " + nano / 1e6 + "ms"); if (csv != null) { - new CsvResult(this.csv).saveResults(summary.runs); + new CsvResult(this.csv).saveResults(summary.getRuns()); System.out.println("Data saved to " + this.csv); } } diff --git a/src/main/java/net/berack/upo/valpre/sim/Simulation.java b/src/main/java/net/berack/upo/valpre/sim/Simulation.java index eec7bac..dfeb9b7 100644 --- a/src/main/java/net/berack/upo/valpre/sim/Simulation.java +++ b/src/main/java/net/berack/upo/valpre/sim/Simulation.java @@ -130,7 +130,7 @@ public final class Simulation { for (var entry : this.states.entrySet()) nodes.put(entry.getKey(), entry.getValue().stats); - return new Result(this.seed, this.time, elapsed, nodes); + return new Result(this.seed, this.time, elapsed * 1e-6, nodes); } /** 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 9448834..9210f50 100644 --- a/src/main/java/net/berack/upo/valpre/sim/SimulationMultiple.java +++ b/src/main/java/net/berack/upo/valpre/sim/SimulationMultiple.java @@ -6,7 +6,6 @@ import java.util.concurrent.Future; import net.berack.upo.valpre.rand.Rng; 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,15 +35,16 @@ public class SimulationMultiple { * events. * @return The statistics the network. */ - public ResultSummary run(long seed, int runs, EndCriteria... criterias) { + public Result.Summary run(long seed, int runs, EndCriteria... criterias) { var rngs = Rng.getMultipleStreams(seed, runs); - var stats = new Result[runs]; + var result = new Result.Summary(rngs[0].getSeed()); for (int i = 0; i < runs; i++) { var sim = new Simulation(this.net, rngs[i], criterias); - stats[i] = sim.run(); + var res = sim.run(); + result.add(res); } - return new ResultSummary(stats); + return result; } /** @@ -62,27 +62,29 @@ public class SimulationMultiple { * @throws InterruptedException If the threads are interrupted. * @throws ExecutionException If the one of the threads has been aborted. */ - public ResultSummary runParallel(long seed, int runs, EndCriteria... criterias) + public Result.Summary runParallel(long seed, int runs, EndCriteria... criterias) throws InterruptedException, ExecutionException { var rngs = Rng.getMultipleStreams(seed, runs); - var results = new Result[runs]; var futures = new Future[runs]; var numThreads = Math.min(runs, Runtime.getRuntime().availableProcessors()); try (var threads = Executors.newFixedThreadPool(numThreads)) { + var results = new Result.Summary(rngs[0].getSeed()); + for (int i = 0; i < runs; i++) { final var id = i; futures[i] = threads.submit(() -> { var sim = new Simulation(this.net, rngs[id], criterias); - results[id] = sim.run(); + return sim.run(); }); } for (var i = 0; i < runs; i++) { - futures[i].get(); + var res = (Result) futures[i].get(); + results.add(res); } - return new ResultSummary(results); + return results; } } } 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 index 9430099..549014e 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/CsvResult.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/CsvResult.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Scanner; import java.util.concurrent.atomic.AtomicInteger; @@ -35,7 +36,7 @@ public class CsvResult { * * @throws IOException if anything happens wile wriiting to the file */ - public void saveResults(Result[] results) throws IOException { + public void saveResults(List results) throws IOException { var builder = new StringBuilder(); builder.append("seed,node,"); builder.append(String.join(",", NodeStats.getOrderOfApply())); @@ -59,7 +60,7 @@ public class CsvResult { * @return the results loaded from the file * @throws IOException if anything happens while reading the file */ - public Result[] loadResults() throws IOException { + public List loadResults() throws IOException { try (var stream = new FileInputStream(this.file)) { return CsvResult.loadResults(stream); } @@ -71,7 +72,7 @@ public class CsvResult { * @param input the input stream to read * @return the results loaded from the stream */ - public static Result[] loadResults(InputStream input) { + public static List loadResults(InputStream input) { var results = new ArrayList(); try (var scan = new Scanner(input)) { var _ = scan.nextLine(); @@ -85,7 +86,7 @@ public class CsvResult { var node = line[1]; if (currentSeed != seed && seed != 0) { - results.add(new Result(seed, 0.0, 0L, nodes)); + results.add(new Result(seed, 0.0, 0.0, nodes)); nodes = new HashMap<>(); } seed = currentSeed; @@ -95,9 +96,9 @@ public class CsvResult { nodes.put(node, stats); } - results.add(new Result(seed, 0.0, 0L, nodes)); + results.add(new Result(seed, 0.0, 0.0, nodes)); } - return results.toArray(new Result[0]); + return results; } /** diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/NodeStats.java b/src/main/java/net/berack/upo/valpre/sim/stats/NodeStats.java index 0b3b3d0..f369130 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/NodeStats.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/NodeStats.java @@ -1,8 +1,12 @@ package net.berack.upo.valpre.sim.stats; +import java.util.ArrayList; +import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; +import org.apache.commons.math3.distribution.TDistribution; + /** * This class keeps track of various statistical metrics related to the net * performance, such as the number of arrivals and departures, queue lengths, @@ -10,7 +14,7 @@ import java.util.function.Function; * statistics are updated during simulation events, such as arrivals and * departures, and can be used to analyze the net's behavior and performance. */ -public class NodeStats { +public class NodeStats implements Cloneable { public double numArrivals = 0.0d; public double numDepartures = 0.0d; public double maxQueueLength = 0.0d; @@ -97,8 +101,8 @@ public class NodeStats { * * @param func a function to apply */ - public void apply(Function func) { - NodeStats.operation(this, this, this, (val1, _) -> func.apply(val1)); + public NodeStats apply(Function func) { + return NodeStats.operation(this, this, this, (val1, _) -> func.apply(val1)); } /** @@ -109,8 +113,44 @@ public class NodeStats { * @param other * @param func */ - public void merge(NodeStats other, BiFunction func) { - NodeStats.operation(this, this, other, func); + public NodeStats merge(NodeStats other, BiFunction func) { + return NodeStats.operation(this, this, other, func); + } + + @Override + protected NodeStats clone() { + try { + return (NodeStats) super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Get the value of the stat. + * + * @param statName the name of the stat + * @return the value of the stat + */ + public double of(String statName) { + return switch (statName) { + case "numArrivals" -> this.numArrivals; + case "numDepartures" -> this.numDepartures; + case "maxQueueLength" -> this.maxQueueLength; + case "avgQueueLength" -> this.avgQueueLength; + case "avgWaitTime" -> this.avgWaitTime; + case "avgResponse" -> this.avgResponse; + case "busyTime" -> this.busyTime; + case "waitTime" -> this.waitTime; + case "unavailableTime" -> this.unavailableTime; + case "responseTime" -> this.responseTime; + case "lastEventTime" -> this.lastEventTime; + case "throughput" -> this.throughput; + case "utilization" -> this.utilization; + case "unavailable" -> this.unavailable; + default -> throw new IllegalArgumentException("Invalid stat name"); + }; } /** @@ -124,27 +164,6 @@ public class NodeStats { "unavailable" }; } - /** - * Merges two sets of statistics into a new one. - * This method combines the statistics from two `Statistics` objects (`val1` and - * `val2`) and returns a new `Statistics` object with the merged results. The - * provided function is applied to each pair of corresponding statistics from - * `val1` and `val2` to compute the merged value. This is useful for merging or - * combining statistics from different sources (e.g., different simulation - * runs), allowing the creation of aggregated statistics. - * - * @param val1 The first `Statistics` object to merge. - * @param val2 The second `Statistics` object to merge. - * @param func The binary function that defines how to merge each pair of values - * from `val1` and `val2`. It takes two `Double` values (from `val1` - * and `val2`) and returns a new `Double` value. - */ - public static NodeStats merge(NodeStats val1, NodeStats val2, BiFunction func) { - var save = new NodeStats(); - NodeStats.operation(save, val1, val2, func); - return save; - } - /** * 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 @@ -160,11 +179,13 @@ public class NodeStats { * @param func The binary function that defines how to merge each pair of values * from `val1` and `val2`. It takes two `Double` values (from `val1` * and `val2`) and returns a new `Double` value. + * @return The `save` object with the merged statistics. */ - public static void operation(NodeStats save, NodeStats val1, NodeStats val2, + public static NodeStats operation(NodeStats save, NodeStats val1, NodeStats val2, BiFunction func) { save.numArrivals = func.apply(val1.numArrivals, val2.numArrivals); save.numDepartures = func.apply(val1.numDepartures, val2.numDepartures); + // save.maxQueueLength = func.apply(val1.maxQueueLength, val2.maxQueueLength); save.avgQueueLength = func.apply(val1.avgQueueLength, val2.avgQueueLength); save.avgWaitTime = func.apply(val1.avgWaitTime, val2.avgWaitTime); save.avgResponse = func.apply(val1.avgResponse, val2.avgResponse); @@ -176,5 +197,102 @@ public class NodeStats { save.throughput = func.apply(val1.throughput, val2.throughput); save.utilization = func.apply(val1.utilization, val2.utilization); save.unavailable = func.apply(val1.unavailable, val2.unavailable); + return save; + } + + /** + * A class to store incremental statistics. + * This class is used to store incremental statistics for a confidence index. + * It keeps track of the average, variance, minimum, and maximum values of the + * statistics over time. The statistics are updated incrementally as new data + * points are added, allowing for real-time monitoring of the confidence index. + */ + public static class Summary { + public final NodeStats average = new NodeStats(); + public final NodeStats variance = new NodeStats(); + public final NodeStats min = new NodeStats().apply(_ -> Double.MAX_VALUE); + public final NodeStats max = new NodeStats().apply(_ -> Double.MIN_VALUE); + private List stats = new ArrayList<>(); + + /** + * Update the incremental statistics with new data. + * This method updates the incremental statistics with new data points. It + * calculates the average, variance, minimum, and maximum values of the + * statistics over time, based on the new data points. The statistics are + * stored in the `average`, `variance`, `min`, and `max` fields of the + * `Incremental` object, respectively. + * + * @param other The `NodeStats` object containing the new data points to add to + * the incremental statistics. + */ + public void update(NodeStats other) { + var n = this.stats.size() + 1; + this.stats.add(other); + + var delta = this.average.clone().merge(other, (avg, newVal) -> newVal - avg); + this.average.merge(delta, (val1, val2) -> val1 + val2 / n); + + var mergedDelta = this.average.clone() + .merge(other, (avg, newVal) -> newVal - avg) + .merge(delta, (dNew, dOld) -> dNew * dOld); + + var nSampleSize = Math.max(n - 1, 1); + this.variance.merge(mergedDelta, (var, deltas) -> var + deltas / nSampleSize); + this.min.merge(other, Math::min); + this.max.merge(other, Math::max); + } + + /** + * Get the standard deviation of the values in the array. + * + * @return the standard deviation value + */ + public NodeStats stdDev() { + return this.variance.clone().apply(Math::sqrt); + } + + /** + * 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 distribution the t-distribution to use + * @param stdDev the standard deviation of the values + * @param alpha the alpha value + * @return the error of the values + */ + public NodeStats calcError(double alpha) { + var n = this.stats.size(); + var distr = new TDistribution(null, n - 1); + var tValue = distr.inverseCumulativeProbability(alpha); + + return this.stdDev().apply(std -> tValue * (std / Math.sqrt(n))); + } + + /** + * Get the frequency of the values in the array. + * In the function passed you can choose which value to use for the frequency. + * + * @param numBins the number of bins to use + * @param getValue the function to get the value from the stats + * @return an array with the frequency of the values + */ + public int[] getFrequency(int numBins, Function getValue) { + var buckets = new int[numBins]; + var min = getValue.apply(this.min); + var max = getValue.apply(this.max); + var range = max - min; + var step = numBins / range; + + for (var stat : this.stats) { + var value = getValue.apply(stat); + var index = (int) Math.floor((value - min) * step); + index = Math.min(index, numBins - 1); + buckets[index] += 1; + } + return buckets; + } } } \ No newline at end of file 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 fef219a..da07937 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 @@ -1,5 +1,9 @@ package net.berack.upo.valpre.sim.stats; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -32,21 +36,24 @@ public class Result { @Override public String toString() { - var size = (int) Math.ceil(Math.max(Math.log10(this.simulationTime), 1)); + return buildPrintable(this.seed, this.simulationTime, this.timeElapsedMS, this.nodes); + } + + private static String buildPrintable(long seed, double simTime, double timeMS, Map nodes) { + var size = (int) Math.ceil(Math.max(Math.log10(simTime), 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(); + builder.append(String.format("Seed: \t%d\n", seed)); + builder.append(String.format("Simulation: \t" + fFormat + "\n", simTime)); + builder.append(String.format("Elapsed: \t" + fFormat + "ms\n", timeMS)); var table = new ConsoleTable("Node", "Departures", "Avg Queue", "Avg Wait", "Avg Response", "Throughput", "Utilization %", "Unavailable %", "Last Event"); - for (var entry : this.nodes.entrySet()) { + for (var entry : nodes.entrySet()) { var stats = entry.getValue(); table.addRow( entry.getKey(), @@ -63,4 +70,120 @@ public class Result { builder.append(table); return builder.toString(); } + + /** + * Represents the summary of the statistics of a network simulation. + * It is used to save the final results of the network and its nodes, including + * the number of arrivals and departures, the maximum queue length, the busy + * time, and the response time. + */ + public static class Summary { + public final long seed; + private double avgSimulationTime = 0.0d; + private double avgTimeElapsedMS = 0.0d; + private Map stats = new HashMap<>(); + private List runs = new ArrayList<>(); + + /** + * Creates a new summary object for the given seed. + * + * @param seed the initial seed used by the simulation + */ + public Summary(long seed) { + this.seed = seed; + } + + /** + * Creates a new summary object for the given results. + * + * @param results the results to summarize + */ + public Summary(List results) { + this(results.get(0).seed); + for (var result : results) + this.add(result); + } + + /** + * Adds the result to the summary. It updates the average simulation time and + * the average time elapsed. It also updates the statistics of the nodes. + * + * @param result the result to add + */ + public void add(Result result) { + if (result == null) + throw new IllegalArgumentException("Result cannot be null"); + + var n = this.runs.size() + 1; + this.runs.add(result); + this.avgSimulationTime += (result.simulationTime - this.avgSimulationTime) / n; + this.avgTimeElapsedMS += (result.timeElapsedMS - this.avgTimeElapsedMS) / n; + + for (var entry : result.nodes.entrySet()) { + var node = entry.getKey(); + var stats = entry.getValue(); + var summary = this.stats.computeIfAbsent(node, _ -> new NodeStats.Summary()); + summary.update(stats); + } + } + + /** + * Gets the average simulation time of the summary. + * + * @return the average simulation time + */ + public double getAvgSimulationTime() { + return this.avgSimulationTime; + } + + /** + * Gets the average time elapsed of the summary. + * + * @return the average time elapsed + */ + public double getAvgTimeElapsedMS() { + return this.avgTimeElapsedMS; + } + + /** + * Gets the nodes of the summary. + * + * @return the nodes of the summary + */ + public Collection getNodes() { + return this.stats.keySet(); + } + + /** + * Gets 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 + * @throws IllegalArgumentException if the node is not found + */ + public NodeStats.Summary getSummaryOf(String node) { + var stat = this.stats.get(node); + if (stat == null) + throw new IllegalArgumentException("Node not found"); + return stat; + } + + /** + * Gets all the runs of the summary. + * + * @return the runs of the summary + */ + public List getRuns() { + return List.copyOf(this.runs); + } + + @Override + public String toString() { + var stats = new HashMap(); + for (var entry : this.stats.entrySet()) + stats.put(entry.getKey(), entry.getValue().average); + + return buildPrintable(this.seed, this.avgSimulationTime, this.avgTimeElapsedMS, stats); + } + } } 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 deleted file mode 100644 index 32d86b9..0000000 --- a/src/main/java/net/berack/upo/valpre/sim/stats/ResultSummary.java +++ /dev/null @@ -1,151 +0,0 @@ -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; - this.stats = ResultSummary.getSummary(runs); - } - - /** - * 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 - * @throws IllegalArgumentException if the node or the statistic is not found - */ - public StatisticsSummary getSummaryOf(String node, String stat) { - var stats = this.getSummaryOf(node); - var value = stats.get(stat); - if (value == null) { - var arr = String.join(", ", stats.keySet().toArray(new String[0])); - throw new IllegalArgumentException("Statistic [" + stat + "] not found. Available: " + arr); - } - return value; - } - - /** - * 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 - * @throws IllegalArgumentException if the node is not found - */ - public Map getSummaryOf(String node) { - var stat = this.stats.get(node); - if (stat == null) - throw new IllegalArgumentException("Node not found"); - return stat; - } - - /** - * 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("throughput").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(); - } - - /** - * Get the summary of the statistics of the nodes. - * The first map is the node name, the second map is the statistic name. - * - * @param runs the runs to get the summary - * @return the summary of the statistics of the nodes - */ - public static Map> getSummary(Result[] runs) { - // 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 NodeStats[runs.length]); - stats[i] = entry.getValue(); - } - } - - // Get the summary of the statistics of the nodes - var stats = new HashMap>(); - for (var entry : nodeStats.entrySet()) { - var node = entry.getKey(); - var summary = StatisticsSummary.getSummary(entry.getValue()); - stats.put(node, summary); - } - - return stats; - } -} 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 deleted file mode 100644 index 1e83801..0000000 --- a/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.berack.upo.valpre.sim.stats; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import org.apache.commons.math3.distribution.TDistribution; - -/** - * 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[] values; - private final TDistribution distr; - - /** - * 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 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 - 1)); - this.median = this.getPercentile(0.50); - this.min = values[0]; - this.max = values[values.length - 1]; - this.distr = new TDistribution(null, values.length - 1); - } - - /** - * 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 the percentile of the values in the array. - * - * @param percentile the percentile to calculate - * @return the value at the selected percentile - */ - public double getPercentile(double percentile) { - var index = (int) Math.floor(percentile * (this.values.length - 1)); - return this.values[index]; - } - - /** - * 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) { - return StatisticsSummary.calcError(this.distr, this.stdDev, alpha); - } - - /** - * 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 distribution the t-distribution to use - * @param stdDev the standard deviation of the values - * @param alpha the alpha value - * @return the error of the values - */ - public static double calcError(TDistribution distribution, double stdDev, double alpha) { - var percentile = distribution.inverseCumulativeProbability(alpha); - var n = distribution.getDegreesOfFreedom() + 1; - return percentile * (stdDev / Math.sqrt(n)); - } - - /** - * 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(NodeStats[] stats) throws IllegalArgumentException { - try { - var map = new HashMap(); - - for (var field : NodeStats.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