diff --git a/src/main/java/net/berack/upo/valpre/Main.java b/src/main/java/net/berack/upo/valpre/Main.java index f919f20..00df773 100644 --- a/src/main/java/net/berack/upo/valpre/Main.java +++ b/src/main/java/net/berack/upo/valpre/Main.java @@ -29,13 +29,21 @@ public class Main { csv = arguments.get("csv"); var parallel = arguments.containsKey("p"); + // varius distributions + var distrExp = new Distribution.Exponential(lambda); + var distrNorm = new Distribution.NormalBoxMuller(mu, sigma); + var distrUnav = new Distribution.UnavailableTime(0.2, distrNorm); + // Build the network var net = new Net(); - var node1 = ServerNode.createLimitedSource("Source", new Distribution.Exponential(lambda), total); - var node2 = ServerNode.createQueue("Queue", 1, new Distribution.NormalBoxMuller(mu, sigma)); + var node1 = ServerNode.createLimitedSource("Source", distrExp, total); + var node2 = ServerNode.createQueue("Queue", 1, distrNorm); + var node3 = ServerNode.createQueue("Queue Wait", 1, distrNorm, distrUnav); net.addNode(node1); net.addNode(node2); + net.addNode(node3); net.addConnection(node1, node2, 1.0); + net.addConnection(node2, node3, 1.0); net.normalizeWeights(); /// Run multiple simulations diff --git a/src/main/java/net/berack/upo/valpre/rand/Distribution.java b/src/main/java/net/berack/upo/valpre/rand/Distribution.java index 4d06cda..f598477 100644 --- a/src/main/java/net/berack/upo/valpre/rand/Distribution.java +++ b/src/main/java/net/berack/upo/valpre/rand/Distribution.java @@ -4,8 +4,34 @@ package net.berack.upo.valpre.rand; * Represents a probability distribution. */ public interface Distribution { + /** + * Return a sample from the distribution. + * + * @param rng The random number generator to use. + * @return A number given from the distribution. + */ public double sample(Rng rng); + /** + * Gets a positive sample from the distribution. + * This is useful if you need to generate a positive value from a distribution + * that can generate negative values. For example, the normal distribution. + * + * @param distribution The distribution to sample + * @param rng The random number generator to use. + * @return A positive or 0 value from the distribution. + */ + public static double getPositiveSample(Distribution distribution, Rng rng) { + if (distribution == null) + return 0; + + double sample; + do { + sample = distribution.sample(rng); + } while (sample < 0); + return sample; + } + /** * Represents an exponential distribution. */ @@ -172,4 +198,30 @@ public interface Distribution { return -Math.log(rng.random()) / lambdas[i]; } } + + /** + * TODO + */ + public static class UnavailableTime implements Distribution { + public final double probability; + public final Distribution distribution; + + /** + * TODO + * + * @param probability + * @param distribution + */ + public UnavailableTime(double probability, Distribution distribution) { + this.probability = probability; + this.distribution = distribution; + } + + @Override + public double sample(Rng rng) { + if (rng.random() < this.probability) + return Distribution.getPositiveSample(this.distribution, rng); + return 0.0; + } + } } diff --git a/src/main/java/net/berack/upo/valpre/sim/Event.java b/src/main/java/net/berack/upo/valpre/sim/Event.java index 68004e3..b73f867 100644 --- a/src/main/java/net/berack/upo/valpre/sim/Event.java +++ b/src/main/java/net/berack/upo/valpre/sim/Event.java @@ -30,19 +30,6 @@ public class Event implements Comparable { return 1; } - /** - * Create a new event. - * - * @param node The node that the event is associated with. - * @param time The time at which the event occurs. - * @param type The type of event. - * - * @return The new event. - */ - public static Event newType(ServerNode node, double time, Type type) { - return new Event(type, node, time); - } - /** * Create a new arrival event. * @@ -65,11 +52,23 @@ public class Event implements Comparable { return new Event(Type.DEPARTURE, node, time); } + /** + * Create a new available event. + * + * @param node The node that the event is associated with. + * @param time The time at which the event occurs. + * @return The new event. + */ + public static Event newAvailable(ServerNode node, double time) { + return new Event(Type.AVAILABLE, node, time); + } + /** * The type of event. */ public static enum Type { ARRIVAL, DEPARTURE, + AVAILABLE, } } diff --git a/src/main/java/net/berack/upo/valpre/sim/ServerNode.java b/src/main/java/net/berack/upo/valpre/sim/ServerNode.java index 63cb395..3ceff3c 100644 --- a/src/main/java/net/berack/upo/valpre/sim/ServerNode.java +++ b/src/main/java/net/berack/upo/valpre/sim/ServerNode.java @@ -11,7 +11,8 @@ public class ServerNode { public final String name; public final int maxServers; public final int spawnArrivals; - public final Distribution distribution; + public final Distribution service; + public final Distribution unavailable; /** * Creates a source node with the given name and distribution. @@ -23,7 +24,7 @@ public class ServerNode { * @return The created source node. */ public static ServerNode createSource(String name, Distribution distribution) { - return new ServerNode(name, Integer.MAX_VALUE, distribution, Integer.MAX_VALUE); + return new ServerNode(name, Integer.MAX_VALUE, distribution, null, Integer.MAX_VALUE); } /** @@ -31,25 +32,39 @@ public class ServerNode { * arrivals to spawn that are served by infinite servers (Integer.MAX_VALUE). * * @param name The name of the node. - * @param distribution The distribution of the inter-arrival times. + * @param service The distribution of the inter-arrival times. * @param spawnArrivals The number of arrivals to spawn. * @return The created source node. */ - public static ServerNode createLimitedSource(String name, Distribution distribution, int spawnArrivals) { - return new ServerNode(name, Integer.MAX_VALUE, distribution, spawnArrivals); + public static ServerNode createLimitedSource(String name, Distribution service, int spawnArrivals) { + return new ServerNode(name, Integer.MAX_VALUE, service, null, spawnArrivals); } /** * Creates a queue node with the given name, maximum number of servers, and * distribution. * - * @param name The name of the node. - * @param maxServers The maximum number of servers in the queue. - * @param distribution The distribution of the service times. + * @param name The name of the node. + * @param maxServers The maximum number of servers in the queue. + * @param service The distribution of the service times. * @return The created queue node. */ - public static ServerNode createQueue(String name, int maxServers, Distribution distribution) { - return new ServerNode(name, maxServers, distribution, 0); + public static ServerNode createQueue(String name, int maxServers, Distribution service) { + return new ServerNode(name, maxServers, service, null, 0); + } + + /** + * Creates a queue node with the given name, maximum number of servers, and + * distribution. + * + * @param name The name of the node. + * @param maxServers The maximum number of servers in the queue. + * @param service The distribution of the service times. + * @param unavailable The distribution of the unavailable times after service. + * @return The created queue node. + */ + public static ServerNode createQueue(String name, int maxServers, Distribution service, Distribution unavailable) { + return new ServerNode(name, maxServers, service, unavailable, 0); } /** @@ -60,13 +75,14 @@ public class ServerNode { * * @param name The name of the node. * @param maxServers The maximum number of servers in the queue. - * @param distribution The distribution of the service times. + * @param service The distribution of the service times. + * @param unavailable The distribution of the unavailable times after service. * @param spawnArrivals The number of arrivals to spawn. * @throws NullPointerException if the distribution is null */ - public ServerNode(String name, int maxServers, Distribution distribution, int spawnArrivals) { - if (distribution == null) - throw new NullPointerException("Distribution can't be null"); + private ServerNode(String name, int maxServers, Distribution service, Distribution unavailable, int spawnArrivals) { + if (service == null) + throw new NullPointerException("Service distribution can't be null"); if (maxServers <= 0) maxServers = 1; if (spawnArrivals < 0) @@ -74,8 +90,9 @@ public class ServerNode { this.name = name; this.maxServers = maxServers; - this.distribution = distribution; this.spawnArrivals = spawnArrivals; + this.service = service; + this.unavailable = unavailable; } /** @@ -86,12 +103,18 @@ public class ServerNode { * @param rng The random number generator to use. * @return A positive sample from the distribution. */ - public double getPositiveSample(Rng rng) { - double sample; - do { - sample = this.distribution.sample(rng); - } while (sample < 0); - return sample; + public double getServiceTime(Rng rng) { + return Distribution.getPositiveSample(this.service, rng); + } + + /** + * Return the unavailable time after a service + * + * @param rng The random number generator to use. + * @return A positive or 0 value from the distribution. + */ + public double getUnavailableTime(Rng rng) { + return Distribution.getPositiveSample(this.unavailable, rng); } /** @@ -105,6 +128,14 @@ public class ServerNode { return this.spawnArrivals > Math.max(0, numArrivals); } + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ServerNode)) + return false; + var other = (ServerNode) obj; + return obj.hashCode() == other.hashCode(); + } + @Override public int hashCode() { return this.name.hashCode(); 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 31a4c75..b82bbc5 100644 --- a/src/main/java/net/berack/upo/valpre/sim/Simulation.java +++ b/src/main/java/net/berack/upo/valpre/sim/Simulation.java @@ -26,7 +26,7 @@ public final class Simulation { * Creates a new run of the simulation with the given nodes and random number * generator. * - * @param states The nodes in the network. + * @param states The nodes in the network. * @param rng The random number generator to use. * @param criterias when the simulation has to end. */ @@ -73,32 +73,32 @@ public final class Simulation { this.time = event.time; switch (event.type) { + case AVAILABLE -> { + state.stats.updateTimes(this.time, state.numServerBusy, state.numServerUnavailable, node.maxServers); + state.numServerUnavailable--; + this.addDepartureIfPossible(node, state); + } case ARRIVAL -> { state.queue.add(this.time); - state.stats.updateArrival(this.time, state.queue.size(), state.numServerBusy != 0); - - if (state.numServerBusy < node.maxServers) { - state.numServerBusy++; - this.addDeparture(node); - } + state.stats.updateArrival(this.time, state.queue.size()); + state.stats.updateTimes(this.time, state.numServerBusy, state.numServerUnavailable, node.maxServers); + this.addDepartureIfPossible(node, state); } case DEPARTURE -> { - var startService = state.queue.poll(); - state.stats.updateDeparture(this.time, this.time - startService); + var arrivalTime = state.queue.poll(); + state.stats.updateDeparture(this.time, arrivalTime); + state.stats.updateTimes(this.time, state.numServerBusy, state.numServerUnavailable, node.maxServers); + state.numServerBusy--; - if (state.numServerBusy > state.queue.size()) { - state.numServerBusy--; - } else { - this.addDeparture(node); - } + this.addUnavailableIfPossible(node, state); + this.addDepartureIfPossible(node, state); var next = this.net.getChildOf(node, this.rng); - if (next != null) { + if (next != null) this.addArrival(next); - } - if (node.shouldSpawnArrival(state.stats.numArrivals)) { + + if (node.shouldSpawnArrival(state.stats.numArrivals)) this.addArrival(node); - } } } } @@ -149,14 +149,36 @@ public final class Simulation { /** * Adds a departure event to the future event list. The event is created based - * on the given node, and the delay is determined by the node's distribution. + * on the given node, and the delay is determined by the node's service + * distribution. * * @param node The node to create the event for. */ - public void addDeparture(ServerNode node) { - var delay = node.getPositiveSample(this.rng); - var event = Event.newDeparture(node, this.time + delay); - fel.add(event); + public void addDepartureIfPossible(ServerNode node, NodeState state) { + var canServe = node.maxServers > state.numServerBusy + state.numServerUnavailable; + var hasRequests = state.queue.size() > state.numServerBusy; + + if (canServe && hasRequests) { + state.numServerBusy++; + var delay = node.getServiceTime(this.rng); + var event = Event.newDeparture(node, this.time + delay); + fel.add(event); + } + } + + /** + * TODO + * + * @param node + * @param state + */ + public void addUnavailableIfPossible(ServerNode node, NodeState state) { + var delay = node.getUnavailableTime(rng); + if (delay > 0) { + state.numServerUnavailable++; + var event = Event.newAvailable(node, time + delay); + this.fel.add(event); + } } /** @@ -183,7 +205,8 @@ public final class Simulation { */ public static class NodeState { public int numServerBusy = 0; + public int numServerUnavailable = 0; public final Statistics stats = new Statistics(); - private final ArrayDeque queue = new ArrayDeque<>(); + public final ArrayDeque queue = new ArrayDeque<>(); } } \ 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 711e154..fddb4c6 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 @@ -31,7 +31,7 @@ public class Result { this.simulationTime = time; this.timeElapsedMS = elapsed; this.nodes = nodes; - this.size = (int) Math.ceil(Math.log10(this.simulationTime)); + this.size = (int) Math.ceil(Math.max(Math.log10(this.simulationTime), 1)); this.iFormat = "%" + this.size + ".0f"; this.fFormat = "%" + (this.size + 4) + ".3f"; } @@ -57,7 +57,7 @@ public class Result { */ public String getSummary() { String[] h = { "Node", "Departures", "Avg Queue", "Avg Wait", "Avg Response", "Throughput", "Utilization %", - "Last Event" }; + "Unavailable %", "Last Event" }; var table = new ConsoleTable(h); for (var entry : this.nodes.entrySet()) { @@ -70,6 +70,7 @@ public class Result { 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(); @@ -86,7 +87,7 @@ public class Result { if (tableHeader) builder.append( - "Seed,Node,Arrivals,Departures,MaxQueue,AvgQueue,AvgWait,AvgResponse,BusyTime,WaitTime,ResponseTime,LastEventTime,Throughput,Utilization\n"); + "Seed,Node,Arrivals,Departures,MaxQueue,AvgQueue,AvgWait,AvgResponse,BusyTime,WaitTime,UnavailableTime,ResponseTime,LastEventTime,Throughput,Utilization,Unavailable\n"); for (var entry : this.nodes.entrySet()) { var stats = entry.getValue(); builder.append(this.seed); @@ -109,6 +110,8 @@ public class Result { builder.append(','); builder.append(stats.waitTime); builder.append(','); + builder.append(stats.unavailableTime); + builder.append(','); builder.append(stats.responseTime); builder.append(','); builder.append(stats.lastEventTime); @@ -116,6 +119,8 @@ public class Result { builder.append(stats.troughput); builder.append(','); builder.append(stats.utilization); + builder.append(','); + builder.append(stats.unavailable); builder.append('\n'); } return builder.toString(); diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java b/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java index a84286b..9f68097 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/Statistics.java @@ -11,6 +11,7 @@ public class Statistics { public double numDepartures = 0.0d; public double maxQueueLength = 0.0d; public double avgQueueLength = 0.0d; + public double unavailableTime = 0.0d; public double busyTime = 0.0d; public double waitTime = 0.0d; public double responseTime = 0.0d; @@ -21,6 +22,7 @@ public class Statistics { public double avgResponse = 0.0d; public double troughput = 0.0d; public double utilization = 0.0d; + public double unavailable = 0.0d; /** * TODO @@ -29,16 +31,12 @@ public class Statistics { * @param newQueueSize * @param updateBusy */ - public void updateArrival(double time, double newQueueSize, boolean updateBusy) { + public void updateArrival(double time, double newQueueSize) { var total = this.avgQueueLength * this.numArrivals; this.numArrivals++; this.avgQueueLength = (total + newQueueSize) / this.numArrivals; this.maxQueueLength = Math.max(this.maxQueueLength, newQueueSize); - if (updateBusy) - this.busyTime += time - this.lastEventTime; - - this.lastEventTime = time; } /** @@ -49,15 +47,30 @@ public class Statistics { */ public void updateDeparture(double time, double response) { this.numDepartures++; - this.responseTime += response; - this.busyTime += time - this.lastEventTime; - this.lastEventTime = time; - this.waitTime = this.responseTime - this.busyTime; + this.responseTime += time - response; + } + /** + * TODO + * + * @param time + * @param serverBusy + * @param serverUnavailable + */ + public void updateTimes(double time, int serverBusy, int serverUnavailable, int maxServers) { + if (serverBusy > 0) + this.busyTime += time - this.lastEventTime; + else if (serverUnavailable == maxServers) + this.unavailableTime += time - this.lastEventTime; + + this.waitTime = this.responseTime - this.busyTime; this.avgWaitTime = this.waitTime / this.numDepartures; this.avgResponse = this.responseTime / this.numDepartures; - this.troughput = this.numDepartures / this.lastEventTime; - this.utilization = this.busyTime / this.lastEventTime; + this.troughput = this.numDepartures / time; + this.utilization = this.busyTime / time; + this.unavailable = this.unavailableTime / time; + + this.lastEventTime = time; } /** @@ -106,11 +119,13 @@ public class Statistics { save.avgQueueLength = func.apply(val1.avgQueueLength, val2.avgQueueLength); save.busyTime = func.apply(val1.busyTime, val2.busyTime); save.responseTime = func.apply(val1.responseTime, val2.responseTime); + save.unavailableTime = func.apply(val1.unavailableTime, val2.unavailableTime); save.waitTime = func.apply(val1.waitTime, val2.waitTime); save.lastEventTime = func.apply(val1.lastEventTime, val2.lastEventTime); // derived stats save.avgWaitTime = func.apply(val1.avgWaitTime, val2.avgWaitTime); save.avgResponse = func.apply(val1.avgResponse, val2.avgResponse); + save.unavailable = func.apply(val1.unavailable, val2.unavailable); save.troughput = func.apply(val1.troughput, val2.troughput); save.utilization = func.apply(val1.utilization, val2.utilization); } diff --git a/src/test/java/net/berack/upo/valpre/sim/TestSimulation.java b/src/test/java/net/berack/upo/valpre/sim/TestSimulation.java index 2c55f32..82a4517 100644 --- a/src/test/java/net/berack/upo/valpre/sim/TestSimulation.java +++ b/src/test/java/net/berack/upo/valpre/sim/TestSimulation.java @@ -14,7 +14,7 @@ public class TestSimulation { private static double DELTA = 0.0000001; private static Rng rigged = new RiggedRng(); private static Distribution const0 = new Constant(0.0); - private static Distribution const1 = new Constant(0.0); + private static Distribution const1 = new Constant(1.0); private final static class RiggedRng extends Rng { @Override @@ -38,7 +38,7 @@ public class TestSimulation { @Test public void serverNode() { - var node = new ServerNode("Nodo", 0, const1, 0); + var node = ServerNode.createQueue("Nodo", 0, const1); assertEquals("Nodo", node.name); assertEquals(1, node.maxServers); assertFalse(node.shouldSpawnArrival(0)); @@ -46,7 +46,7 @@ public class TestSimulation { assertFalse(node.shouldSpawnArrival(1000)); assertFalse(node.shouldSpawnArrival(Integer.MAX_VALUE)); assertFalse(node.shouldSpawnArrival(-1)); - assertEquals(1.0, node.getPositiveSample(null), DELTA); + assertEquals(1.0, node.getServiceTime(null), DELTA); node = ServerNode.createQueue("Queue", 50, const1); assertEquals("Queue", node.name); @@ -56,7 +56,7 @@ public class TestSimulation { assertFalse(node.shouldSpawnArrival(1000)); assertFalse(node.shouldSpawnArrival(Integer.MAX_VALUE)); assertFalse(node.shouldSpawnArrival(-1)); - assertEquals(1.0, node.getPositiveSample(null), DELTA); + assertEquals(1.0, node.getServiceTime(null), DELTA); node = ServerNode.createSource("Source", const1); assertEquals("Source", node.name); @@ -67,7 +67,7 @@ public class TestSimulation { assertTrue(node.shouldSpawnArrival(Integer.MAX_VALUE - 1)); assertFalse(node.shouldSpawnArrival(Integer.MAX_VALUE)); assertTrue(node.shouldSpawnArrival(-1)); - assertEquals(1.0, node.getPositiveSample(null), DELTA); + assertEquals(1.0, node.getServiceTime(null), DELTA); node = ServerNode.createLimitedSource("Source", const1, 50); assertEquals("Source", node.name); @@ -78,25 +78,25 @@ public class TestSimulation { assertFalse(node.shouldSpawnArrival(1000)); assertFalse(node.shouldSpawnArrival(Integer.MAX_VALUE)); assertTrue(node.shouldSpawnArrival(-1)); - assertEquals(1.0, node.getPositiveSample(null), DELTA); + assertEquals(1.0, node.getServiceTime(null), DELTA); } @Test public void event() { var node = ServerNode.createSource("Source", const0); - var event = Event.newType(node, 0, Event.Type.ARRIVAL); + var event = Event.newAvailable(node, 1.0); assertEquals(node, event.node); - assertEquals(0.0, event.time, 0.000000000001); - assertEquals(Event.Type.ARRIVAL, event.type); + assertEquals(1.0, event.time, 0.000000000001); + assertEquals(Event.Type.AVAILABLE, event.type); - var event2 = Event.newArrival(node, 1.0); + var event2 = Event.newArrival(node, 5.0); assertEquals(node, event2.node); - assertEquals(1.0, event2.time, 0.000000000001); + assertEquals(5.0, event2.time, 0.000000000001); assertEquals(Event.Type.ARRIVAL, event2.type); - var event3 = Event.newDeparture(node, 5.0); + var event3 = Event.newDeparture(node, 8.0); assertEquals(node, event3.node); - assertEquals(5.0, event3.time, 0.000000000001); + assertEquals(8.0, event3.time, 0.000000000001); assertEquals(Event.Type.DEPARTURE, event3.type); assertEquals(0, event2.compareTo(event2));