- CsvResult and Result classes for improved iteration and CSV output;
- update SimulationBuilder to include confidence index handling
- rename setRuns to setMaxRuns for clarity
This commit is contained in:
2025-02-10 15:19:40 +01:00
parent 82c237bc16
commit 0ac111d94a
8 changed files with 281 additions and 31 deletions

View File

@@ -20,7 +20,7 @@ public class Main {
var param = Main.getParameters(program, subArgs);
new SimulationBuilder(param.get("net"))
.setCsv(param.get("csv"))
.setRuns(param.getOrDefault("runs", Integer::parseInt, 100))
.setMaxRuns(param.getOrDefault("runs", Integer::parseInt, 100))
.setSeed(param.getOrDefault("seed", Long::parseLong, 2007539552L))
.setParallel(param.get("p") != null)
.setEndCriteria(EndCriteria.parse(param.get("end")))

View File

@@ -2,13 +2,16 @@ package net.berack.upo.valpre;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import com.esotericsoftware.kryo.KryoException;
import net.berack.upo.valpre.sim.ConfidenceIndices;
import net.berack.upo.valpre.sim.EndCriteria;
import net.berack.upo.valpre.sim.Net;
import net.berack.upo.valpre.sim.SimulationMultiple;
import net.berack.upo.valpre.sim.stats.CsvResult;
import net.berack.upo.valpre.sim.stats.NodeStats;
/**
* This class is responsible for running the simulation. It parses the arguments
@@ -21,6 +24,7 @@ public class SimulationBuilder {
private boolean parallel;
private Net net;
private EndCriteria[] endCriteria;
private ConfidenceIndices confidences;
/**
* Create a new simulation for the given net.
@@ -32,6 +36,7 @@ public class SimulationBuilder {
try {
var file = Parameters.getFileOrExample(netFile);
this.net = Net.load(file);
this.confidences = new ConfidenceIndices(this.net);
file.close();
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("Net file needed!");
@@ -50,16 +55,17 @@ public class SimulationBuilder {
if (net == null)
throw new IllegalArgumentException("Net needed!");
this.net = net;
this.confidences = new ConfidenceIndices(net);
}
/**
* Set the number of runs for the simulation.
* Set the maximum number of runs for the simulation.
*
* @param runs the number of runs
* @throws IllegalArgumentException if the runs are less than 1
* @return this simulation
*/
public SimulationBuilder setRuns(int runs) {
public SimulationBuilder setMaxRuns(int runs) {
if (runs <= 0)
throw new IllegalArgumentException("Runs must be greater than 0!");
@@ -117,16 +123,46 @@ public class SimulationBuilder {
return this;
}
/**
* Add a confidence index for the given node and stat.
* The confidence index is used to determine when the simulation should stop.
*
*
* @param node the node
* @param stat the stat to calculate the confidence index for
* @param confidence the confidence level expressed as a percentage [0,1]
* @param relError the relative error expressed as a percentage [0,1]
* @return this simulation
* @throws IllegalArgumentException if the node is invalid
* @throws IllegalArgumentException if the stat is invalid
* @throws IllegalArgumentException if the confidence is invalid
* @throws IllegalArgumentException if the relative error is invalid
*/
public SimulationBuilder addConfidenceIndex(String node, String stat, double confidence, double relError) {
if (!List.of(NodeStats.getOrderOfApply()).contains(stat))
throw new IllegalArgumentException("Invalid statistic: " + stat);
if (confidence <= 0 || confidence > 1)
throw new IllegalArgumentException("Confidence must be between 0 and 1");
if (relError <= 0 || relError > 1)
throw new IllegalArgumentException("Relative error must be between 0 and 1");
var index = this.net.getNodeIndex(node);
if (index < 0)
throw new IllegalArgumentException("Invalid node: " + node);
this.confidences.add(index, stat, confidence, relError);
return this;
}
/**
* Run the simulation with the given parameters.
* At the end it prints the results and saves them to a CSV file if requested.
*
* @throws InterruptedException If the simulation is interrupted.
* @throws ExecutionException If the simulation fails.
* @throws KryoException If the simulation fails.
* @throws IOException If the simulation fails.
* @throws ExecutionException If the simulation has an error.
* @throws IOException If the CSV file has a problem.
*/
public void run() throws InterruptedException, ExecutionException, KryoException, IOException {
public void run() throws InterruptedException, ExecutionException, IOException {
var nano = System.nanoTime();
var sim = new SimulationMultiple(this.net);
var summary = this.parallel

View File

@@ -0,0 +1,132 @@
package net.berack.upo.valpre.sim;
import java.lang.reflect.Field;
import java.util.ArrayList;
import net.berack.upo.valpre.sim.stats.NodeStats;
import net.berack.upo.valpre.sim.stats.Result;
/**
* Confidence indices for a simulation.
* This class is used to store the confidence indices for a simulation.
* The confidence indices are used to determine when the simulation has
* reached a certain level of confidence.
*/
public class ConfidenceIndices {
private final String[] nodes;
private final NodeStats[] confidences;
private final NodeStats[] relativeErrors;
/**
* Create a new confidence indices object for the given network.
*
* @param net the network to create the confidence indices for
*/
public ConfidenceIndices(Net net) {
var size = net.size();
this.nodes = new String[size];
this.confidences = new NodeStats[size];
this.relativeErrors = new NodeStats[size];
for (var i = 0; i < size; i++) {
this.nodes[i] = net.getNode(i).name;
this.confidences[i] = new NodeStats();
this.relativeErrors[i] = new NodeStats();
this.relativeErrors[i].apply(_ -> 1.0);
}
}
/**
* Add a confidence index to the simulation. The simulation will stop when the
* relative error of the confidence index is less than the given value.
*
* @param node The node to calculate the confidence index for.
* @param stat The statistic to calculate the confidence index for.
* @param confidence The confidence level of the confidence index.
* @param relError The relative error of the confidence index.
*/
public void add(int node, String stat, double confidence, double relError) {
if (node < 0 || node >= this.nodes.length)
throw new IllegalArgumentException("Invalid node: " + node);
try {
Field field = NodeStats.class.getField(stat);
field.set(this.confidences[node], confidence);
field.set(this.relativeErrors[node], relError);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid statistic: " + stat);
}
}
/**
* Calculate the relative errors of the statistics of the network.
*
* @param summary the summary of the network statistics
* @return the relative errors of the statistics
*/
public NodeStats[] calcRelativeErrors(Result.Summary summary) {
var errors = new NodeStats[this.nodes.length];
for (var i = 0; i < this.confidences.length; i++) {
var node = this.nodes[i];
var stat = summary.getSummaryOf(node);
var confidence = this.confidences[i];
var relativeError = stat.calcError(confidence);
relativeError.merge(stat.average, (err, avg) -> err / avg);
errors[i] = relativeError;
}
return errors;
}
/**
* Check if the errors are within the confidence indices.
* The errors within the confidence indices are calculated using the
* {@link #calcRelativeErrors(Result.Summary)} method.
*
* @param errors the relative errors of the statistics
* @return true if the simulation is ok, false otherwise
*/
public boolean isOk(NodeStats[] errors) {
for (var i = 0; i < this.relativeErrors.length; i++) {
var error = errors[i].clone();
var relError = this.relativeErrors[i];
error.merge(relError, (err, rel) -> err - rel);
for (var value : error)
if (value > 0)
return false;
}
return true;
}
/**
* Get the errors of the statistics of the network.
* The errors are calculated using the
* {@link #calcRelativeErrors(Result.Summary)} method.
* Each error is formatted as a string in the format: "node:stat=value".
*
* @param errors the relative errors of the statistics
* @return the errors of the statistics
*/
public String[] getErrors(NodeStats[] errors) {
var statistics = NodeStats.getOrderOfApply();
var retValues = new ArrayList<String>();
for (var i = 0; i < this.relativeErrors.length; i++) {
var error = errors[i].clone();
var relError = this.relativeErrors[i];
error.merge(relError, (err, rel) -> err - rel);
var j = 0;
for (var value : error) {
if (value > 0)
retValues.add("%s:%s=%0.3f".formatted(this.nodes[i], statistics[j], value));
j += 1;
}
}
return retValues.toArray(new String[0]);
}
}

View File

@@ -168,13 +168,11 @@ public class ServerNodeState {
* otherwise
*/
public Event spawnArrivalToChild(double time, Rng rng) {
if (!this.children.isEmpty()) {
var random = rng.random();
for (var child : this.children) {
random -= child.weight;
if (random <= 0)
return Event.newArrival(child.index, time);
}
var random = rng.random();
for (var child : this.children) {
random -= child.weight;
if (random <= 0)
return Event.newArrival(child.index, time);
}
return null;
}

View File

@@ -87,4 +87,50 @@ public class SimulationMultiple {
return results;
}
}
/**
* Run the simulation multiple times with the given seed and end criteria. The
* simulation runs will stop when the relative error of the confidence index is
* less than the given value.
* The results are printed on the console.
*
* @param seed The seed to use for the random number generator.
* @param criterias The criteria to determine when to end the simulation. If
* null then the simulation will run until there are no more
* events.
* @return The statistics the network.
* @throws IllegalArgumentException If the confidence is not set.
*/
public void runIncremental(long seed, int runs, ConfidenceIndices confidences, EndCriteria... criterias) {
if (confidences == null)
throw new IllegalArgumentException("Confidence must be not null");
var rng = new Rng(seed);
var results = new Result.Summary(rng.getSeed());
var output = new StringBuilder();
var stop = false;
for (int i = 0; !stop; i++) {
var sim = new Simulation(this.net, rng, criterias);
var result = sim.run();
results.add(result);
if (i > 0) {
output.setLength(0);
output.append(String.format("\rSimulation [%6d]: ", i + 1));
var errors = confidences.calcRelativeErrors(results);
stop = confidences.isOk(errors);
var errString = confidences.getErrors(errors);
var oneSting = String.join("], [", errString);
output.append('[').append(oneSting).append("]");
System.out.print(output);
}
}
System.out.println("\nSimulation ended");
System.out.println(results);
}
}

View File

@@ -44,7 +44,7 @@ public class CsvResult {
try (var writer = new FileWriter(this.file)) {
for (var result : results) {
for (var entry : result.nodes.entrySet()) {
for (var entry : result) {
builder.append(result.seed).append(",");
builder.append(entry.getKey()).append(",");
builder.append(CsvResult.statsToCSV(entry.getValue())).append('\n');

View File

@@ -1,6 +1,7 @@
package net.berack.upo.valpre.sim.stats;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -14,7 +15,11 @@ import org.apache.commons.math3.distribution.TDistribution;
* 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 implements Cloneable {
public class NodeStats implements Cloneable, Iterable<Double> {
private static final String[] ORDER_OF_APPLY = { "numArrivals", "numDepartures", "maxQueueLength", "avgQueueLength",
"avgWaitTime", "avgResponse", "busyTime", "waitTime", "unavailableTime", "responseTime", "lastEventTime",
"throughput", "utilization", "unavailable" };
public double numArrivals = 0.0d;
public double numDepartures = 0.0d;
public double maxQueueLength = 0.0d;
@@ -95,8 +100,6 @@ public class NodeStats implements Cloneable {
/**
* Apply a function to ALL the stats in this class.
* The only stats that are not updated with this function are the one that
* starts with max, min (since they are special)
* The input of the function is the current value of the stat.
*
* @param func a function to apply
@@ -106,19 +109,17 @@ public class NodeStats implements Cloneable {
}
/**
* A function used to merge tree stats.
* The only stats that are not updated with this function are the one that
* starts with max, min (since they are special)
* A function used to merge two sets of statistics.
*
* @param other
* @param func
* @param other the other stats to merge
* @param func the function to merge the stats
*/
public NodeStats merge(NodeStats other, BiFunction<Double, Double, Double> func) {
return NodeStats.operation(this, this, other, func);
}
@Override
protected NodeStats clone() {
public NodeStats clone() {
try {
return (NodeStats) super.clone();
} catch (CloneNotSupportedException e) {
@@ -127,6 +128,23 @@ public class NodeStats implements Cloneable {
}
}
@Override
public Iterator<Double> iterator() {
return new Iterator<>() {
private int index = 0;
@Override
public boolean hasNext() {
return this.index < ORDER_OF_APPLY.length;
}
@Override
public Double next() {
return NodeStats.this.of(ORDER_OF_APPLY[this.index++]);
}
};
}
/**
* Get the value of the stat.
*
@@ -159,9 +177,7 @@ public class NodeStats implements Cloneable {
* @return the order of the stats
*/
public static String[] getOrderOfApply() {
return new String[] { "numArrivals", "numDepartures", "avgQueueLength", "avgWaitTime", "avgResponse",
"busyTime", "waitTime", "unavailableTime", "responseTime", "lastEventTime", "throughput", "utilization",
"unavailable" };
return ORDER_OF_APPLY;
}
/**
@@ -185,7 +201,7 @@ public class NodeStats implements Cloneable {
BiFunction<Double, Double, Double> 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.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);
@@ -264,11 +280,27 @@ public class NodeStats implements Cloneable {
* @return the error of the values
*/
public NodeStats calcError(double alpha) {
return this.calcError(new NodeStats().apply(_ -> alpha));
}
/**
* Calculates the error at the selected alpha level for each NodeStats.
* 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 values for each statistics
* @return the error of the values
*/
public NodeStats calcError(NodeStats alpha) {
var n = this.stats.size();
var distr = new TDistribution(null, n - 1);
var tValue = distr.inverseCumulativeProbability(alpha);
var tValue = alpha.clone().apply(a -> distr.inverseCumulativeProbability(a));
return this.stdDev().apply(std -> tValue * (std / Math.sqrt(n)));
return this.stdDev().merge(tValue, (std, t) -> t * (std / Math.sqrt(n)));
}
/**

View File

@@ -5,6 +5,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Represents the statistics of a network simulation.
@@ -12,7 +13,7 @@ import java.util.Map;
* nodes, including the number of arrivals and departures, the maximum queue
* length, the busy time, and the response time.
*/
public class Result {
public class Result implements Iterable<Entry<String, NodeStats>> {
public final Map<String, NodeStats> nodes;
public final long seed;
public final double simulationTime;
@@ -39,6 +40,11 @@ public class Result {
return buildPrintable(this.seed, this.simulationTime, this.timeElapsedMS, this.nodes);
}
@Override
public java.util.Iterator<Entry<String, NodeStats>> iterator() {
return this.nodes.entrySet().iterator();
}
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";