package net.berack.upo.valpre.sim; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; import net.berack.upo.valpre.rand.Rng; /** * A class that represents a network of queues, each with its own servers. * The network in question is created by adding a node and then establishing * connections between nodes. In order to start a simulation, at least one node * must be a Source or must generate at least one event to be processed. */ public final class Net { private final List servers = new ArrayList<>(); private final HashMap indices = new HashMap<>(); private final List> connections = new ArrayList<>(); /** * Adds a new server node to the network. * The unique identifier for the nodes is the name and, if you try to add a node * that has the same name of another, then the method will return an exception * * @param node The server node to add. * @throws IllegalArgumentException if the node already exist * @return the index of the created node */ public int addNode(ServerNode node) { if (this.indices.containsKey(node)) throw new IllegalArgumentException("Node already exist"); var index = this.servers.size(); this.servers.add(node); this.indices.put(node, index); this.connections.add(new ArrayList<>()); return index; } /** * Adds a connection between the nodes with the given weight to select it. * The weight must be > 0 and the nodes must be already added to the net. * If the connection is already present then the new weight is used. * * @param parent The parent node. * @param child The child node to add. * @param weight The probability of the child node. * @throws NullPointerException if one of the two nodes are not in the net * @throws IllegalArgumentException if the weight is negative or zero */ public void addConnection(ServerNode parent, ServerNode child, double weight) { var nodeP = this.indices.get(parent); var nodeC = this.indices.get(child); this.addConnection(nodeP, nodeC, weight); } /** * Adds a connection between the nodes with the given weight to select it. * The weight must be > 0 and the nodes must be already added to the net. * If the connection is already present then the new weight is used. * * @param parent The parent node index. * @param child The child node index to add. * @param weight The probability of the child node. * @throws IndexOutOfBoundsException if one of the two nodes are not in the net * @throws IllegalArgumentException if the weight is negative or zero */ public void addConnection(int parent, int child, double weight) { if (weight <= 0) throw new IllegalArgumentException("Weight must be > 0"); var max = this.servers.size() - 1; if (parent < 0 || child < 0 || parent > max || child > max) throw new IndexOutOfBoundsException("One of the nodes does not exist"); var list = this.connections.get(parent); for (var conn : list) { if (conn.index == child) { conn.weight = weight; return; } } list.add(new Connection(child, weight)); } /** * Get the total number of the nodes in the net * * @return the size of the net */ public int size() { return this.servers.size(); } /** * Return the index of the node based on the name passed as input. * Note that this will iterate over all the nodes. * * @param name the name of the node * @return the node */ public int getNodeIndex(String name) { for (var entry : this.indices.entrySet()) { if (entry.getKey().name.equals(name)) return entry.getValue(); } return -1; } /** * Return a node based on the hash of the string name passed as input * * @param name the name of the node * @return the node */ public ServerNode getNode(String name) { return this.servers.get(this.getNodeIndex(name)); } /** * Return a node based on the index, faster than recovering it by the name * * @param index the index of the node * @return the node */ public ServerNode getNode(int index) { return this.servers.get(index); } /** * Get one of the child nodes from the parent specified. * If the node has no child then null is returned. * * @param parent the parent node * @param rng the random number generator used for getting one of the child * @return the resultig node */ public ServerNode getChildOf(ServerNode parent, Rng rng) { var index = this.indices.get(parent); return this.getChildOf(index, rng); } /** * Get one of the child nodes from the parent specified. If the index is out of * bounds then an exception is thrown. If the node has no child then null is * returned; * * @param parent the parent node * @param rng the random number generator used for getting one of the child * @throws IndexOutOfBoundsException If the index is not in the range * @return the resultig node */ public ServerNode getChildOf(int parent, Rng rng) { var random = rng.random(); for (var conn : this.connections.get(parent)) { random -= conn.weight; if (random <= 0) { return this.servers.get(conn.index); } } return null; } /** * Get a list of all the children of the parent. * In the list there is the node and the weight associated with. * * @param parent the parent node * @return the list of children */ public List getChildren(ServerNode parent) { var index = this.indices.get(parent); return this.getChildren(index); } /** * Get a list of all the children of the parent. * In the list there is the node and the weight associated with. * * @param parent the parent node * @throws IndexOutOfBoundsException If the index is not in the range * @return the resultig node */ public List getChildren(int parent) { var children = new ArrayList(); for (var conn : this.connections.get(parent)) { var child = this.servers.get(conn.index); var listEntry = new NetChild(child, conn.weight); children.add(listEntry); } return children; } /** * Normalizes the weights in each connections so that their sum equals 1. * This method should be called by the user if they have inserted weights that * are not summing to 1 or are unsure. */ public void normalizeWeights() { for (var list : this.connections) { var sum = 0.0d; for (var conn : list) sum += conn.weight; for (var conn : list) conn.weight /= sum; } } /** * Apply a consumer to all the nodes. The implementation uses a stream and for * this reason you should consider to make thread safe the consumer. * * @param consumer a function that takes in input a ServerNode */ public void forEachNode(Consumer consumer) { this.servers.stream().forEach(consumer); } /** * A static inner class used to represent the connection between two nodes */ public static class Connection { public final int index; public double weight; private Connection(int index, double weight) { this.index = index; this.weight = weight; } } /** * A Static inner class used to represent the connection of a node */ public static class NetChild { public final ServerNode child; public final double weight; private NetChild(ServerNode child, double weight) { this.child = child; this.weight = weight; } } }