Update simulation result handling and improve result processing

This commit is contained in:
2025-02-05 21:19:19 +01:00
parent 1e6fea8af7
commit 14063300f2
10 changed files with 311 additions and 351 deletions

View File

@@ -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. - **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. - **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: - [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. - **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 controparte **StatisticsSummary** che contiene molteplici indici statistici 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 - **ConsoleTable** utile per mostrare i risultati in console sottoforma di tabella
- **CsvResult** utile per la lettura/scrittura dei risultati in formato csv - **CsvResult** utile per la lettura/scrittura dei risultati in formato csv

View File

@@ -21,15 +21,15 @@ import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.data.category.DefaultCategoryDataset;
import net.berack.upo.valpre.sim.stats.CsvResult; 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.NodeStats;
import net.berack.upo.valpre.sim.stats.Result;
/** /**
* This class is used to plot the results of the simulation. * 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. * The results are saved in a CSV file and then loaded to be plotted.
*/ */
public class Plot { public class Plot {
public final ResultSummary summary; public final Result.Summary summary;
private final ChartPanel panelBarChart; private final ChartPanel panelBarChart;
private final JComboBox<String> nodeComboBox; private final JComboBox<String> nodeComboBox;
private final JList<JListEntry> statList; private final JList<JListEntry> statList;
@@ -47,7 +47,7 @@ public class Plot {
var results = CsvResult.loadResults(stream); var results = CsvResult.loadResults(stream);
stream.close(); stream.close();
this.summary = new ResultSummary(results); this.summary = new Result.Summary(results);
var nodes = this.summary.getNodes().toArray(new String[0]); var nodes = this.summary.getNodes().toArray(new String[0]);
this.panelBarChart = new ChartPanel(null); this.panelBarChart = new ChartPanel(null);
@@ -132,13 +132,14 @@ public class Plot {
var stat = this.statList.getSelectedValue().name.getText(); var stat = this.statList.getSelectedValue().name.getText();
var summary = this.summary.getSummaryOf(node); var summary = this.summary.getSummaryOf(node);
var statSummary = summary.get(stat); var frequency = summary.getFrequency(15, n -> n.of(stat));
var frequency = statSummary.getFrequency(15); var min = summary.min.of(stat);
var max = summary.max.of(stat);
var dataset = new DefaultCategoryDataset(); 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++) { 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); var columnKey = String.format("%.3f", columnVal);
dataset.addValue(frequency[i], "Frequency", columnKey); dataset.addValue(frequency[i], "Frequency", columnKey);
} }
@@ -147,10 +148,14 @@ public class Plot {
chart.setTitle(stat + " distribution"); chart.setTitle(stat + " distribution");
var model = this.statList.getModel(); var model = this.statList.getModel();
var avg = summary.average;
var err = summary.calcError(0.95);
for (int i = 0; i < model.getSize(); i++) { for (int i = 0; i < model.getSize(); i++) {
var entry = model.getElementAt(i); var entry = model.getElementAt(i);
var value = summary.get(entry.name.getText()); var value = entry.name.getText();
entry.value.setText(String.format("%8.3f ±% 9.3f", value.average, value.calcError(0.95)));
entry.value.setText(String.format("%8.3f ±% 9.3f", avg.of(value), err.of(value)));
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();

View File

@@ -138,7 +138,7 @@ public class SimulationBuilder {
System.out.println("Final time " + nano / 1e6 + "ms"); System.out.println("Final time " + nano / 1e6 + "ms");
if (csv != null) { 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); System.out.println("Data saved to " + this.csv);
} }
} }

View File

@@ -130,7 +130,7 @@ public final class Simulation {
for (var entry : this.states.entrySet()) for (var entry : this.states.entrySet())
nodes.put(entry.getKey(), entry.getValue().stats); 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);
} }
/** /**

View File

@@ -6,7 +6,6 @@ import java.util.concurrent.Future;
import net.berack.upo.valpre.rand.Rng; import net.berack.upo.valpre.rand.Rng;
import net.berack.upo.valpre.sim.stats.Result; 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 * A network simulation that uses a discrete event simulation to model the
@@ -36,15 +35,16 @@ public class SimulationMultiple {
* events. * events.
* @return The statistics the network. * @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 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++) { for (int i = 0; i < runs; i++) {
var sim = new Simulation(this.net, rngs[i], criterias); 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 InterruptedException If the threads are interrupted.
* @throws ExecutionException If the one of the threads has been aborted. * @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 { throws InterruptedException, ExecutionException {
var rngs = Rng.getMultipleStreams(seed, runs); var rngs = Rng.getMultipleStreams(seed, runs);
var results = new Result[runs];
var futures = new Future[runs]; var futures = new Future[runs];
var numThreads = Math.min(runs, Runtime.getRuntime().availableProcessors()); var numThreads = Math.min(runs, Runtime.getRuntime().availableProcessors());
try (var threads = Executors.newFixedThreadPool(numThreads)) { try (var threads = Executors.newFixedThreadPool(numThreads)) {
var results = new Result.Summary(rngs[0].getSeed());
for (int i = 0; i < runs; i++) { for (int i = 0; i < runs; i++) {
final var id = i; final var id = i;
futures[i] = threads.submit(() -> { futures[i] = threads.submit(() -> {
var sim = new Simulation(this.net, rngs[id], criterias); var sim = new Simulation(this.net, rngs[id], criterias);
results[id] = sim.run(); return sim.run();
}); });
} }
for (var i = 0; i < runs; i++) { 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;
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -35,7 +36,7 @@ public class CsvResult {
* *
* @throws IOException if anything happens wile wriiting to the file * @throws IOException if anything happens wile wriiting to the file
*/ */
public void saveResults(Result[] results) throws IOException { public void saveResults(List<Result> results) throws IOException {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.append("seed,node,"); builder.append("seed,node,");
builder.append(String.join(",", NodeStats.getOrderOfApply())); builder.append(String.join(",", NodeStats.getOrderOfApply()));
@@ -59,7 +60,7 @@ public class CsvResult {
* @return the results loaded from the file * @return the results loaded from the file
* @throws IOException if anything happens while reading the file * @throws IOException if anything happens while reading the file
*/ */
public Result[] loadResults() throws IOException { public List<Result> loadResults() throws IOException {
try (var stream = new FileInputStream(this.file)) { try (var stream = new FileInputStream(this.file)) {
return CsvResult.loadResults(stream); return CsvResult.loadResults(stream);
} }
@@ -71,7 +72,7 @@ public class CsvResult {
* @param input the input stream to read * @param input the input stream to read
* @return the results loaded from the stream * @return the results loaded from the stream
*/ */
public static Result[] loadResults(InputStream input) { public static List<Result> loadResults(InputStream input) {
var results = new ArrayList<Result>(); var results = new ArrayList<Result>();
try (var scan = new Scanner(input)) { try (var scan = new Scanner(input)) {
var _ = scan.nextLine(); var _ = scan.nextLine();
@@ -85,7 +86,7 @@ public class CsvResult {
var node = line[1]; var node = line[1];
if (currentSeed != seed && seed != 0) { 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<>(); nodes = new HashMap<>();
} }
seed = currentSeed; seed = currentSeed;
@@ -95,9 +96,9 @@ public class CsvResult {
nodes.put(node, stats); 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;
} }
/** /**

View File

@@ -1,8 +1,12 @@
package net.berack.upo.valpre.sim.stats; package net.berack.upo.valpre.sim.stats;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import org.apache.commons.math3.distribution.TDistribution;
/** /**
* This class keeps track of various statistical metrics related to the net * This class keeps track of various statistical metrics related to the net
* performance, such as the number of arrivals and departures, queue lengths, * 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 * statistics are updated during simulation events, such as arrivals and
* departures, and can be used to analyze the net's behavior and performance. * 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 numArrivals = 0.0d;
public double numDepartures = 0.0d; public double numDepartures = 0.0d;
public double maxQueueLength = 0.0d; public double maxQueueLength = 0.0d;
@@ -97,8 +101,8 @@ public class NodeStats {
* *
* @param func a function to apply * @param func a function to apply
*/ */
public void apply(Function<Double, Double> func) { public NodeStats apply(Function<Double, Double> func) {
NodeStats.operation(this, this, this, (val1, _) -> func.apply(val1)); return NodeStats.operation(this, this, this, (val1, _) -> func.apply(val1));
} }
/** /**
@@ -109,8 +113,44 @@ public class NodeStats {
* @param other * @param other
* @param func * @param func
*/ */
public void merge(NodeStats other, BiFunction<Double, Double, Double> func) { public NodeStats merge(NodeStats other, BiFunction<Double, Double, Double> func) {
NodeStats.operation(this, this, other, 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" }; "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<Double, Double, Double> 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. * 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 * 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 * @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` * from `val1` and `val2`. It takes two `Double` values (from `val1`
* and `val2`) and returns a new `Double` value. * 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<Double, Double, Double> func) { BiFunction<Double, Double, Double> func) {
save.numArrivals = func.apply(val1.numArrivals, val2.numArrivals); save.numArrivals = func.apply(val1.numArrivals, val2.numArrivals);
save.numDepartures = func.apply(val1.numDepartures, val2.numDepartures); 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.avgQueueLength = func.apply(val1.avgQueueLength, val2.avgQueueLength);
save.avgWaitTime = func.apply(val1.avgWaitTime, val2.avgWaitTime); save.avgWaitTime = func.apply(val1.avgWaitTime, val2.avgWaitTime);
save.avgResponse = func.apply(val1.avgResponse, val2.avgResponse); save.avgResponse = func.apply(val1.avgResponse, val2.avgResponse);
@@ -176,5 +197,102 @@ public class NodeStats {
save.throughput = func.apply(val1.throughput, val2.throughput); save.throughput = func.apply(val1.throughput, val2.throughput);
save.utilization = func.apply(val1.utilization, val2.utilization); save.utilization = func.apply(val1.utilization, val2.utilization);
save.unavailable = func.apply(val1.unavailable, val2.unavailable); 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<NodeStats> 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<NodeStats, Double> 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;
}
} }
} }

View File

@@ -1,5 +1,9 @@
package net.berack.upo.valpre.sim.stats; 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; import java.util.Map;
/** /**
@@ -32,21 +36,24 @@ public class Result {
@Override @Override
public String toString() { 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<String, NodeStats> nodes) {
var size = (int) Math.ceil(Math.max(Math.log10(simTime), 1));
var iFormat = "%" + size + ".0f"; var iFormat = "%" + size + ".0f";
var fFormat = "%" + (size + 4) + ".3f"; var fFormat = "%" + (size + 4) + ".3f";
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.append("===== Net Stats =====\n"); builder.append("===== Net Stats =====\n");
builder.append(String.format("Seed: \t%d\n", this.seed)); builder.append(String.format("Seed: \t%d\n", seed));
builder.append(String.format("Simulation: \t" + fFormat + "\n", this.simulationTime)); builder.append(String.format("Simulation: \t" + fFormat + "\n", simTime));
builder.append(String.format("Elapsed: \t" + fFormat + "ms\n", this.timeElapsedMS / 1e6)); builder.append(String.format("Elapsed: \t" + fFormat + "ms\n", timeMS));
// return builder.toString();
var table = new ConsoleTable("Node", "Departures", "Avg Queue", "Avg Wait", "Avg Response", "Throughput", var table = new ConsoleTable("Node", "Departures", "Avg Queue", "Avg Wait", "Avg Response", "Throughput",
"Utilization %", "Unavailable %", "Last Event"); "Utilization %", "Unavailable %", "Last Event");
for (var entry : this.nodes.entrySet()) { for (var entry : nodes.entrySet()) {
var stats = entry.getValue(); var stats = entry.getValue();
table.addRow( table.addRow(
entry.getKey(), entry.getKey(),
@@ -63,4 +70,120 @@ public class Result {
builder.append(table); builder.append(table);
return builder.toString(); 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<String, NodeStats.Summary> stats = new HashMap<>();
private List<Result> 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<Result> 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<String> 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<Result> getRuns() {
return List.copyOf(this.runs);
}
@Override
public String toString() {
var stats = new HashMap<String, NodeStats>();
for (var entry : this.stats.entrySet())
stats.put(entry.getKey(), entry.getValue().average);
return buildPrintable(this.seed, this.avgSimulationTime, this.avgTimeElapsedMS, stats);
}
}
} }

View File

@@ -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<String, Map<String, StatisticsSummary>> 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<String, StatisticsSummary> 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<String> 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<String, Map<String, StatisticsSummary>> getSummary(Result[] runs) {
// Get the statistics of the nodes
var nodeStats = new HashMap<String, NodeStats[]>();
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<String, Map<String, StatisticsSummary>>();
for (var entry : nodeStats.entrySet()) {
var node = entry.getKey();
var summary = StatisticsSummary.getSummary(entry.getValue());
stats.put(node, summary);
}
return stats;
}
}

View File

@@ -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<String, StatisticsSummary> getSummary(NodeStats[] stats) throws IllegalArgumentException {
try {
var map = new HashMap<String, StatisticsSummary>();
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.");
}
}
}