- added simple plot
- refactored summary of runs
This commit is contained in:
2025-01-31 11:15:55 +01:00
parent 8c349d6eec
commit 7a5b2cc078
8 changed files with 365 additions and 198 deletions

View File

@@ -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();
}
}

View File

@@ -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<String, Statistics>();
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<String, Statistics>();
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;
}
}

View File

@@ -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<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;
// Get the statistics of the nodes
var nodeStats = new HashMap<String, Statistics[]>();
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<String, StatisticsSummary> getSummaryOf(String node) {
return this.stats.get(node);
}
/**
* 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("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();
}
}

View File

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