diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b9b1433d..e33662d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index f21fe75d..df75f93a 100644 --- a/README.rst +++ b/README.rst @@ -32,11 +32,11 @@ Install with :code:`pip install ciw`. Current supported version of Python: -- Python 3.7 - Python 3.8 - Python 3.9 - Python 3.10 - Python 3.11 +- Python 3.12 Usage ----- @@ -75,23 +75,25 @@ Features A number of other features are also implemented, including: -+ `Type I blocking `_ ++ `Type I blocking `_ + `A large range of sampling distributions `_ -+ `Phase-Type distributions `_ -+ `Time-dependent and state-dependent distributions `_ -+ `Batch arrivals `_ -+ `Baulking customers `_ -+ `Reneging customers `_ -+ `Processor sharing `_ -+ `Multiple customer classes `_ -+ `Priorities `_ -+ `Server priorities `_ -+ `Service disciplines `_ -+ `Customers changing classes `_ -+ `Server schedules `_ -+ `Slotted services `_ -+ `State tracking `_ -+ `Stopping the simulation after a certain amount of customers `_ -+ `Process-based routing `_ -+ `Deadlock detection `_ ++ `Phase-Type distributions `_ ++ `Time-dependent and state-dependent distributions `_ ++ `Batch arrivals `_ ++ `Baulking customers `_ ++ `Reneging customers `_ ++ `Processor sharing `_ ++ `Multiple customer classes `_ ++ `Priorities `_ ++ `Server priorities `_ ++ `Service disciplines `_ ++ `Customers changing classes while queueing `_ ++ `Customers changing classes after service `_ ++ `Server schedules `_ ++ `Slotted services `_ ++ `State tracking `_ ++ `Stopping the simulation after a certain amount of customers `_ ++ `Process-based routing `_ ++ `Logical routing `_ ++ `Deadlock detection `_ diff --git a/ciw/arrival_node.py b/ciw/arrival_node.py index 52d88809..32104953 100644 --- a/ciw/arrival_node.py +++ b/ciw/arrival_node.py @@ -28,6 +28,7 @@ def __init__(self, simulation): self.number_of_individuals_per_class = {clss: 0 for clss in self.simulation.network.customer_class_names} self.number_accepted_individuals = 0 self.number_accepted_individuals_per_class = {clss: 0 for clss in self.simulation.network.customer_class_names} + self.system_capacity = self.simulation.network.system_capacity self.event_dates_dict = { nd + 1: {clss: False for clss in self.simulation.network.customer_class_names } for nd in range(self.simulation.network.number_of_nodes) @@ -91,9 +92,9 @@ def have_event(self): priority_class, simulation=self.simulation, ) - if self.simulation.network.process_based: - next_individual.route = self.simulation.network.customer_classes[next_individual.customer_class].routing[self.next_node - 1](next_individual) next_node = self.simulation.transitive_nodes[self.next_node - 1] + next_individual.starting_node = next_node.id_number + self.simulation.routers[next_individual.customer_class].initialise_individual(next_individual) self.release_individual(next_node, next_individual) self.event_dates_dict[self.next_node][self.next_class] = self.increment_time( @@ -153,7 +154,7 @@ def release_individual(self, next_node, next_individual): Either rejects the next_individual die to lack of capacity, or sends that individual to baulk or not. """ - if next_node.number_of_individuals >= next_node.node_capacity: + if (next_node.number_of_individuals >= next_node.node_capacity) or (self.simulation.number_of_individuals >= self.system_capacity): self.record_rejection(next_node, next_individual) self.simulation.nodes[-1].accept(next_individual, completed=False) else: diff --git a/ciw/import_params.py b/ciw/import_params.py index 37dce67d..0ed6c8ab 100644 --- a/ciw/import_params.py +++ b/ciw/import_params.py @@ -3,6 +3,7 @@ import ciw.dists from .network import * from .schedules import * +from .routing import * def create_network( @@ -21,6 +22,7 @@ def create_network( reneging_time_distributions=None, reneging_destinations=None, service_disciplines=None, + system_capacity=float('inf') ): """ Takes in kwargs, creates dictionary. @@ -41,6 +43,7 @@ def create_network( "arrival_distributions": arrival_distributions, "number_of_servers": number_of_servers, "service_distributions": service_distributions, + 'system_capacity': system_capacity } if baulking_functions is not None: @@ -115,35 +118,19 @@ def create_network_from_dictionary(params_input): ) ) for clss_name in params['customer_class_names']: - if all(isinstance(f, types.FunctionType) or isinstance(f, types.MethodType) for f in params["routing"]): - classes[clss_name] = CustomerClass( - params['arrival_distributions'][clss_name], - params['service_distributions'][clss_name], - params['routing'], - params["priority_classes"][clss_name], - params["baulking_functions"][clss_name], - params["batching_distributions"][clss_name], - params["reneging_time_distributions"][clss_name], - params["reneging_destinations"][clss_name], - class_change_time_distributions[clss_name], - ) - else: - classes[clss_name] = CustomerClass( - params['arrival_distributions'][clss_name], - params['service_distributions'][clss_name], - params['routing'][clss_name], - params["priority_classes"][clss_name], - params["baulking_functions"][clss_name], - params["batching_distributions"][clss_name], - params["reneging_time_distributions"][clss_name], - params["reneging_destinations"][clss_name], - class_change_time_distributions[clss_name], - ) + classes[clss_name] = CustomerClass( + params['arrival_distributions'][clss_name], + params['service_distributions'][clss_name], + params['routing'][clss_name], + params["priority_classes"][clss_name], + params["baulking_functions"][clss_name], + params["batching_distributions"][clss_name], + params["reneging_time_distributions"][clss_name], + params["reneging_destinations"][clss_name], + class_change_time_distributions[clss_name], + ) n = Network(nodes, classes) - if all(isinstance(f, types.FunctionType) or isinstance(f, types.MethodType) for f in params["routing"]): - n.process_based = True - else: - n.process_based = False + n.system_capacity = params['system_capacity'] return n @@ -159,9 +146,16 @@ def fill_out_dictionary(params): srv_dists = params["service_distributions"] params["service_distributions"] = {"Customer": srv_dists} if "routing" in params: - if all(isinstance(f, list) for f in params["routing"]): - rtng_mat = params["routing"] - params["routing"] = {"Customer": rtng_mat} + if isinstance(params["routing"], list): + transition_matrix = params["routing"] + params["routing"] = {"Customer": routing.TransitionMatrix(transition_matrix=transition_matrix)} + elif isinstance(params["routing"], dict): + for clss in params["routing"]: + if isinstance(params["routing"][clss], list): + transition_matrix = params["routing"][clss] + params["routing"][clss] = routing.TransitionMatrix(transition_matrix=transition_matrix) + else: + params["routing"] = {"Customer": params["routing"]} if "baulking_functions" in params: if isinstance(params["baulking_functions"], list): blk_fncs = params["baulking_functions"] @@ -184,7 +178,7 @@ def fill_out_dictionary(params): default_dict = { "name": "Simulation", - "routing": {class_name: [[0.0]] for class_name in class_names}, + "routing": {class_name: routing.TransitionMatrix(transition_matrix=[[0.0]]) for class_name in class_names}, "number_of_nodes": len(params["number_of_servers"]), "number_of_classes": len(class_names), "queue_capacities": [float("inf") for _ in range(len(params["number_of_servers"]))], @@ -208,6 +202,7 @@ def fill_out_dictionary(params): "service_disciplines": [ ciw.disciplines.FIFO for _ in range(len(params["number_of_servers"])) ], + "system_capacity": float('inf') } for a in default_dict: @@ -220,87 +215,46 @@ def validify_dictionary(params): Raises errors if there is something wrong with the parameters dictionary. """ - if all(isinstance(f, types.FunctionType) or isinstance(f, types.MethodType) for f in params["routing"]): - consistant_num_classes = ( - params["number_of_classes"] - == len(params["arrival_distributions"]) - == len(params["service_distributions"]) - == len(params["batching_distributions"]) - == len(params["reneging_time_distributions"]) - == len(params["reneging_destinations"]) - ) - else: - consistant_num_classes = ( - params["number_of_classes"] - == len(params["arrival_distributions"]) - == len(params["service_distributions"]) - == len(params["routing"]) - == len(params["batching_distributions"]) - == len(params["reneging_time_distributions"]) - == len(params["reneging_destinations"]) - ) + consistant_num_classes = ( + params["number_of_classes"] + == len(params["arrival_distributions"]) + == len(params["service_distributions"]) + == len(params["routing"]) + == len(params["batching_distributions"]) + == len(params["reneging_time_distributions"]) + == len(params["reneging_destinations"]) + ) if not consistant_num_classes: raise ValueError("Ensure consistant number of classes is used throughout.") - if all(isinstance(f, types.FunctionType) or isinstance(f, types.MethodType) for f in params["routing"]): - consistant_class_names = ( - set(params["arrival_distributions"]) - == set(params["service_distributions"]) - == set(params["batching_distributions"]) - == set(params["reneging_time_distributions"]) - == set(params["reneging_destinations"]) - ) - else: - consistant_class_names = ( - set(params["arrival_distributions"]) - == set(params["service_distributions"]) - == set(params["routing"]) - == set(params["batching_distributions"]) - == set(params["reneging_time_distributions"]) - == set(params["reneging_destinations"]) - ) and ( - len(params["arrival_distributions"]) - == len(params["service_distributions"]) - == len(params["batching_distributions"]) - == len(params["reneging_time_distributions"]) - == len(params["reneging_destinations"]) - ) + consistant_class_names = ( + set(params["arrival_distributions"]) + == set(params["service_distributions"]) + == set(params["routing"]) + == set(params["batching_distributions"]) + == set(params["reneging_time_distributions"]) + == set(params["reneging_destinations"]) + ) and ( + len(params["arrival_distributions"]) + == len(params["service_distributions"]) + == len(params["batching_distributions"]) + == len(params["reneging_time_distributions"]) + == len(params["reneging_destinations"]) + ) if not consistant_class_names: raise ValueError("Ensure consistant names for customer classes.") - if all(isinstance(f, types.FunctionType) or isinstance(f, types.MethodType) for f in params["routing"]): - num_nodes_count = ( - [params["number_of_nodes"]] - + [len(obs) for obs in params["arrival_distributions"].values()] - + [len(obs) for obs in params["service_distributions"].values()] - + [len(obs) for obs in params["batching_distributions"].values()] - + [len(obs) for obs in params["reneging_time_distributions"].values()] - + [len(obs) for obs in params["reneging_destinations"].values()] - + [len(params["routing"])] - + [len(params["number_of_servers"])] - + [len(params["server_priority_functions"])] - + [len(params["queue_capacities"])] - ) - else: - num_nodes_count = ( - [params["number_of_nodes"]] - + [len(obs) for obs in params["arrival_distributions"].values()] - + [len(obs) for obs in params["service_distributions"].values()] - + [len(obs) for obs in params["routing"].values()] - + [len(obs) for obs in params["batching_distributions"].values()] - + [len(obs) for obs in params["reneging_time_distributions"].values()] - + [len(obs) for obs in params["reneging_destinations"].values()] - + [len(row) for row in [obs for obs in params["routing"].values()][0]] - + [len(params["number_of_servers"])] - + [len(params["server_priority_functions"])] - + [len(params["queue_capacities"])] - + [len(params["service_disciplines"])] - ) + num_nodes_count = ( + [params["number_of_nodes"]] + + [len(obs) for obs in params["arrival_distributions"].values()] + + [len(obs) for obs in params["service_distributions"].values()] + + [len(obs) for obs in params["batching_distributions"].values()] + + [len(obs) for obs in params["reneging_time_distributions"].values()] + + [len(obs) for obs in params["reneging_destinations"].values()] + + [len(params["number_of_servers"])] + + [len(params["server_priority_functions"])] + + [len(params["queue_capacities"])] + ) if len(set(num_nodes_count)) != 1: raise ValueError("Ensure consistant number of nodes is used throughout.") - if not all(isinstance(f, types.FunctionType) or isinstance(f, types.MethodType) for f in params["routing"]): - for clss in params["routing"].values(): - for row in clss: - if sum(row) > 1.0 or min(row) < 0.0 or max(row) > 1.0: - raise ValueError("Ensure that routing matrix is valid.") neg_numservers = any( [(isinstance(obs, int) and obs < 0) for obs in params["number_of_servers"]] ) @@ -352,3 +306,8 @@ def validify_dictionary(params): ) if not correct_destinations: raise ValueError("Ensure all reneging destinations are possible.") + + if not isinstance(params['system_capacity'], int) and params['system_capacity'] != float('inf'): + raise ValueError("Ensure system capacity is a positive integer.") + if params['system_capacity'] <= 0: + raise ValueError("Ensure system capacity is a positive integer.") diff --git a/ciw/node.py b/ciw/node.py index e735ecee..3bfd6ddc 100644 --- a/ciw/node.py +++ b/ciw/node.py @@ -41,11 +41,6 @@ def __init__(self, id_, simulation): self.next_event_date = float("Inf") self.next_shift_change = float("Inf") self.node_capacity = node.queueing_capacity + self.c - if not self.simulation.network.process_based: - self.transition_row = { - clss: self.simulation.network.customer_classes[clss].routing[id_ - 1] + [1.0 - sum(self.simulation.network.customer_classes[clss].routing[id_ - 1])] - for clss in self.simulation.network.customer_class_names - } self.class_change = node.class_change_matrix self.individuals = [[] for _ in range(simulation.number_of_priority_classes)] self.number_of_individuals = 0 @@ -534,25 +529,14 @@ def kill_server(self, srvr): def next_node(self, ind): """ Finds the next node according the routing method: - - if not process-based then sample from transition matrix - - if process-based then take the next value from the predefined route, - removing the current node from the route - """ - if not self.simulation.network.process_based: - customer_class = ind.customer_class - return random_choice( - self.simulation.nodes[1:], - self.transition_row[customer_class], - ) - else: - if ind.route == [] or ind.route[0] != self.id_number: - raise ValueError("Individual process route sent to wrong node") - ind.route.pop(0) - if len(ind.route) == 0: - next_node_number = -1 - else: - next_node_number = ind.route[0] - return self.simulation.nodes[next_node_number] + """ + return self.simulation.routers[ind.customer_class].next_node(ind, self.id_number) + + def next_node_for_rerouting(self, ind): + """ + Finds the next node (for rerouting) according the routing method: + """ + return self.simulation.routers[ind.customer_class].next_node_for_rerouting(ind, self.id_number) def preempt(self, individual_to_preempt, next_individual): """ @@ -560,13 +544,16 @@ def preempt(self, individual_to_preempt, next_individual): """ server = individual_to_preempt.server individual_to_preempt.original_service_time = individual_to_preempt.service_time - self.write_interruption_record(individual_to_preempt) - individual_to_preempt.service_start_date = False - individual_to_preempt.time_left = individual_to_preempt.service_end_date - self.now - individual_to_preempt.service_time = self.priority_preempt - individual_to_preempt.service_end_date = False - self.detatch_server(server, individual_to_preempt) - self.decide_class_change(individual_to_preempt) + if self.priority_preempt == 'reroute': + self.reroute(individual_to_preempt) + else: + self.write_interruption_record(individual_to_preempt) + individual_to_preempt.service_start_date = False + individual_to_preempt.time_left = individual_to_preempt.service_end_date - self.now + individual_to_preempt.service_time = self.priority_preempt + individual_to_preempt.service_end_date = False + self.detatch_server(server, individual_to_preempt) + self.decide_class_change(individual_to_preempt) self.attach_server(server, next_individual) next_individual.service_start_date = self.now next_individual.service_time = self.get_service_time(next_individual) @@ -574,7 +561,7 @@ def preempt(self, individual_to_preempt, next_individual): self.reset_class_change(next_individual) server.next_end_service_date = next_individual.service_end_date - def release(self, next_individual, next_node): + def release(self, next_individual, next_node, reroute=False): """ Update node when an individual is released: - find the individual to release @@ -592,7 +579,8 @@ def release(self, next_individual, next_node): self.number_in_service -= 1 next_individual.queue_size_at_departure = self.number_of_individuals next_individual.exit_date = self.now - self.write_individual_record(next_individual) + if not reroute: + self.write_individual_record(next_individual) newly_free_server = None if not isinf(self.c) and not self.slotted: newly_free_server = next_individual.server @@ -603,9 +591,11 @@ def release(self, next_individual, next_node): self.simulation.statetracker.change_state_release( self, next_node, next_individual, next_individual.is_blocked ) - self.begin_service_if_possible_release(next_individual, newly_free_server) + if not reroute: + self.begin_service_if_possible_release(next_individual, newly_free_server) next_node.accept(next_individual) - self.release_blocked_individual() + if not reroute: + self.release_blocked_individual() def release_blocked_individual(self): """ @@ -706,17 +696,20 @@ def interrupt_service(self, individual): Interrupts the service of an individual and places them in an interrupted queue, and writes an interruption record for them. """ - self.interrupted_individuals.append(individual) - individual.interrupted = True - self.number_interrupted_individuals += 1 individual.original_service_time = individual.service_time - self.write_interruption_record(individual) - individual.original_service_start_date = individual.service_start_date - individual.service_start_date = False - individual.time_left = individual.service_end_date - self.now - individual.service_time = self.schedule.preemption - individual.service_end_date = False - self.number_in_service -= 1 + if self.schedule.preemption == 'reroute': + self.reroute(individual) + else: + self.interrupted_individuals.append(individual) + individual.interrupted = True + self.number_interrupted_individuals += 1 + self.write_interruption_record(individual) + individual.original_service_start_date = individual.service_start_date + individual.service_start_date = False + individual.time_left = individual.service_end_date - self.now + individual.service_time = self.schedule.preemption + individual.service_end_date = False + self.number_in_service -= 1 def sort_interrupted_individuals(self): """ @@ -724,6 +717,14 @@ def sort_interrupted_individuals(self): """ self.interrupted_individuals.sort(key=lambda x: (x.priority_class, x.arrival_date)) + def reroute(self, individual): + """ + Rerouts a preempted individual + """ + next_node = self.next_node_for_rerouting(individual) + self.write_interruption_record(individual, destination=next_node.id_number) + self.release(individual, next_node, reroute=True) + def update_next_end_service_without_server(self): """ Updates the next end of a slotted service in the `possible_next_events` dictionary. @@ -867,7 +868,7 @@ def write_individual_record(self, individual): ) individual.data_records.append(record) - def write_interruption_record(self, individual): + def write_interruption_record(self, individual, destination=nan): """ Write a data record for an individual when being interrupted. """ @@ -888,7 +889,7 @@ def write_interruption_record(self, individual): service_end_date=nan, time_blocked=nan, exit_date=self.now, - destination=nan, + destination=destination, queue_size_at_arrival=individual.queue_size_at_arrival, queue_size_at_departure=individual.queue_size_at_departure, server_id=server_id, diff --git a/ciw/routing/__init__.py b/ciw/routing/__init__.py new file mode 100644 index 00000000..94cee786 --- /dev/null +++ b/ciw/routing/__init__.py @@ -0,0 +1 @@ +from .routing import * diff --git a/ciw/routing/routing.py b/ciw/routing/routing.py new file mode 100644 index 00000000..15a19aca --- /dev/null +++ b/ciw/routing/routing.py @@ -0,0 +1,338 @@ +import ciw +import itertools + +class NetworkRouting: + """ + A class to hold a number of routing objects for each node. + """ + def __init__(self, routers): + """ + Sets up the router objects for each node. + """ + self.routers = routers + + def initialise(self, simulation): + """ + Gives the simulation and node attributes to the routing objects. + """ + self.simulation = simulation + for router, node in zip(self.routers, self.simulation.transitive_nodes): + router.initialise(self.simulation, node) + + def initialise_individual(self, ind): + """ + A method that is called at the arrival node when the individual is spawned. + """ + pass + + def next_node(self, ind, node_id): + """ + Chooses the next node. + """ + return self.routers[node_id - 1].next_node(ind) + + def next_node_for_rerouting(self, ind, node_id): + """ + Chooses the next node when rerouting preempted customer. + """ + return self.routers[node_id - 1].next_node_for_rerouting(ind) + + +class TransitionMatrix(NetworkRouting): + """ + A class to hold a number of probabilistic routing objects. + """ + def __init__(self, transition_matrix): + """ + Sets up the relevant probabilistic router objects for each node. + """ + n_destinations = len(transition_matrix) + self.routers = [ + Probabilistic( + destinations=[i for i in range(1, n_destinations + 1)], + probs=row + ) for row in transition_matrix + ] + + def initialise(self, simulation): + super().initialise(simulation) + if len(self.routers) != simulation.network.number_of_nodes: + raise ValueError("Ensure a transition matrix is given, and that the number of rows is equal to the number of nodes in the network.") + + +class ProcessBased(NetworkRouting): + """ + A class to route an individual based on a pre-defined process. + """ + def __init__(self, route_function): + """ + Initialises the routing object. + + Takes: + - route_function: a function that returns a pre-defined route + """ + self.route_function = route_function + + def initialise(self, simulation): + """ + Gives the simulation and node attributes to the routing objects. + """ + self.simulation = simulation + + def initialise_individual(self, ind): + """ + A method that is called at the arrival node when the individual is spawned. + """ + ind.route = self.route_function(ind, self.simulation) + + def next_node(self, ind, node_id): + """ + Chooses the next node from the process-based pre-defined route. + """ + if len(ind.route) == 0: + node_index = -1 + else: + node_index = ind.route.pop(0) + return self.simulation.nodes[node_index] + + def next_node_for_rerouting(self, ind, node_id): + """ + Chooses the next node when rerouting preempted customer. + """ + return self.next_node(ind, node_id) + +class FlexibleProcessBased(ProcessBased): + """ + A class to route an individual based on a pre-defined process. + """ + def __init__(self, route_function, rule, choice): + """ + Initialises the routing object. + + Takes: + - route_function: a function that returns a pre-defined route + """ + self.route_function = route_function + if rule not in ['any', 'all']: + raise ValueError("Flexible routing rules must be one of 'any' or 'all'.") + if choice not in ['random', 'jsq', 'lb']: + raise ValueError("Flexible routing choices must be one of 'random', 'jsq', or 'lb'.") + self.rule = rule + self.choice = choice + + def find_next_node_from_subset(self, subset, ind): + """ + Finds the next node within a subset of nodes + according to the 'choice' parameter + """ + if self.choice == 'random': + return ciw.random_choice(subset) + if self.choice == 'jsq': + temp_router = JoinShortestQueue(destinations=subset) + temp_router.initialise(self.simulation, None) + nd = temp_router.next_node(ind) + return nd.id_number + if self.choice == 'lb': + temp_router = LoadBalancing(destinations=subset) + temp_router.initialise(self.simulation, None) + nd = temp_router.next_node(ind) + return nd.id_number + + def update_individual_route(self, ind, next_node_id): + """ + Updates the individual route by removing chosen nodes + along the route, according to the 'rule' parameter + """ + if self.rule == 'any': + ind.route = ind.route[1:] + if self.rule == 'all': + ind.route[0].remove(next_node_id) + if len(ind.route[0]) == 0: + ind.route = ind.route[1:] + + def next_node(self, ind, node_id): + """ + Chooses the next node from the process-based pre-defined route. + """ + if len(ind.route) == 0: + node_index = -1 + else: + node_index = self.find_next_node_from_subset(ind.route[0], ind) + self.update_individual_route(ind, node_index) + return self.simulation.nodes[node_index] + + + +class NodeRouting: + """ + A generic routing class to determine next sampled node. + """ + def initialise(self, simulation, node): + """ + Gives the simulation and node attributes to the routing object. + """ + self.simulation = simulation + self.node = node + self.error_check_at_initialise() + + def error_check_at_initialise(self): + pass + + def next_node_for_rerouting(self, ind): + """ + By default, the next node for rerouting uses the same method as next_node. + """ + return self.next_node(ind) + + +class Probabilistic(NodeRouting): + """ + A router that probabilistically chooses the next node. + """ + def __init__(self, destinations, probs): + """ + Initialises the routing object. + + Takes: + - destinations: a list of node indices + - probs: a list of probabilities associated with each destination + """ + for p in probs: + if not isinstance(p, float): + raise ValueError("Routing probabilities must be between 0 and 1, and sum to less than 1.") + if p < 0 or p > 1: + raise ValueError("Routing probabilities must be between 0 and 1, and sum to less than 1.") + if sum(probs) > 1.0: + raise ValueError("Routing probabilities must be between 0 and 1, and sum to less than 1.") + self.destinations = destinations + [-1] + self.probs = probs + [1 - sum(probs)] + + def error_check_at_initialise(self): + if len(self.probs) != len(self.destinations): + raise ValueError("Routing probabilities should correspond to destinations, and so should be lists of the same length.") + if not set(self.destinations).issubset(set([nd.id_number for nd in self.simulation.nodes[1:]])): + raise ValueError("Routing destinations should be a subset of the nodes in the network.") + + def next_node(self, ind): + """ + Probabilistically chooses the next node from the destinations. + """ + node_index = ciw.random_choice(self.destinations, self.probs) + return self.simulation.nodes[node_index] + + +class Direct(NodeRouting): + """ + A router that sends the individual directly to another node. + """ + def __init__(self, to): + """ + Initialises the routing object. + + Takes: + - to: a the node index to send to. + """ + self.to = to + + def next_node(self, ind): + """ + Chooses the node 'to' with probability 1. + """ + return self.simulation.nodes[self.to] + + +class Leave(NodeRouting): + """ + A router that sends the individual directly to the exit node. + """ + def next_node(self, ind): + """ + Chooses the exit node with probability 1. + """ + return self.simulation.nodes[-1] + + +class JoinShortestQueue(NodeRouting): + """ + A router that sends the individual to the node + with the shortest queue from a list of destinations. + """ + def __init__(self, destinations, tie_break='random'): + """ + Initialises the routing object. + + Takes: + - destinations: a list of node indices + - tie_break: the method to deal with ties. + + "random" - randomly choose between ties + + "order" - prioritise nodes in the order given by the destinations + """ + self.destinations = destinations + self.tie_break = tie_break + + def error_check_at_initialise(self): + if not set(self.destinations).issubset(set([nd.id_number for nd in self.simulation.nodes[1:]])): + raise ValueError("Routing destinations should be a subset of the nodes in the network.") + + def get_queue_size(self, node_index): + """ + Gets the size of the queue at the node_index. + """ + return self.simulation.nodes[node_index].number_of_individuals - self.simulation.nodes[node_index].number_in_service + + def next_node(self, ind): + """ + Chooses the node from the destinations with the shortest queue. + """ + shortest_queues = [] + shortest_queue_size = float('inf') + for node_index in self.destinations: + queue_size = self.get_queue_size(node_index) + if queue_size == shortest_queue_size: + shortest_queues.append(node_index) + if queue_size < shortest_queue_size: + shortest_queues = [node_index] + shortest_queue_size = queue_size + if self.tie_break == 'random': + next_node_index = ciw.random_choice(shortest_queues) + return self.simulation.nodes[next_node_index] + if self.tie_break == 'order': + next_node_index = shortest_queues[0] + return self.simulation.nodes[next_node_index] + + +class LoadBalancing(JoinShortestQueue): + """ + A version of JoinShortestQueue that also counts customers in service. + """ + def get_queue_size(self, node_index): + """ + Gets the size of the queue at the node_index. + """ + return self.simulation.nodes[node_index].number_of_individuals + + +class Cycle(NodeRouting): + """ + A router that cycles through destinations, repeating the cycle once ended. + """ + def __init__(self, cycle): + """ + Initialises the routing object. + + Takes: + - cycle: an ordered sequence of nodes. + """ + self.cycle = cycle + self.generator = itertools.cycle(self.cycle) + + def error_check_at_initialise(self): + if not set(self.cycle).issubset(set([nd.id_number for nd in self.simulation.nodes[1:]])): + raise ValueError("Routing destinations should be a subset of the nodes in the network.") + + def next_node(self, ind): + """ + Chooses the node 'to' with probability 1. + """ + next_node_index = next(self.generator) + return self.simulation.nodes[next_node_index] diff --git a/ciw/schedules.py b/ciw/schedules.py index a72a52e0..855948c6 100644 --- a/ciw/schedules.py +++ b/ciw/schedules.py @@ -18,9 +18,9 @@ class Schedule: ---------- schedule_type : str Type of the schedule. - schedule_dates : List[float] + shift_end_dates : List[float] List of shift end dates. - schedule_servers : List[int] + numbers_of_servers : List[int] List of corresponding server numbers. preemption : Union[bool, str] Pre-emption option. @@ -37,37 +37,44 @@ class Schedule: get_next_shift() Updates the next shifts from the generator. """ - def __init__(self, schedule: List[Tuple[int, float]], preemption: Union[bool, str] = False) -> NoReturn: + def __init__(self, numbers_of_servers: List[int], shift_end_dates: List[float], preemption: Union[bool, str] = False, offset: float = 0.0) -> NoReturn: """ Initializes the instance of the Schedule object. Parameters ---------- - schedule : List[Tuple[int, float]] - A list of tuples representing shifts, where each tuple contains - the number of servers and the shift date. + numbers_of_servers : List[int] + A list containing the number of servers working at each shift + shift_end_dates : List[float] + A list containing the end dates of each shift. preemption : Union[bool, str], optional Pre-emption option, should be either 'resume', 'restart', 'resample', or False. Default is False. """ - if preemption not in [False, 'resume', 'restart', 'resample']: - raise ValueError("Pre-emption options should be either 'resume', 'restart', 'resample', or False.") + if preemption not in [False, 'resume', 'restart', 'resample', 'reroute']: + raise ValueError("Pre-emption options should be either 'resume', 'restart', 'resample', 'reroute', or False.") + if not isinstance(offset, float): + raise ValueError("Offset should be a positive float.") + if offset < 0.0: + raise ValueError("Offset should be a positive float.") self.schedule_type = 'schedule' - self.schedule_dates = [shift[1] for shift in schedule] - self.schedule_servers = [shift[0] for shift in schedule] + self.shift_end_dates = shift_end_dates + self.numbers_of_servers = numbers_of_servers self.preemption = preemption - self.cyclelength = self.schedule_dates[-1] + self.cyclelength = self.shift_end_dates[-1] + self.offset = offset def initialise(self) -> NoReturn: """ Initializes the generator object at the beginning of a simulation. """ - self.next_c = self.schedule_servers[0] - self.schedule_generator = self.get_schedule_generator(self.schedule_dates, self.schedule_servers) - self.get_next_shift() + self.c = 0 + self.next_shift_change_date = self.offset + self.next_c = self.numbers_of_servers[0] + self.schedule_generator = self.get_schedule_generator(self.shift_end_dates, self.numbers_of_servers, self.offset) - def get_schedule_generator(self, boundaries: List[float], values:List[int]) -> Generator[Tuple[float, int], None, None]: + def get_schedule_generator(self, boundaries:List[float], values:List[int], offset:float) -> Generator[Tuple[float, int], None, None]: """ A generator that yields the next time and number of servers according to a given schedule. @@ -88,7 +95,7 @@ def get_schedule_generator(self, boundaries: List[float], values:List[int]) -> G index = 0 date = 0 while True: - date = boundaries[index % num_boundaries] + ((index) // num_boundaries * self.cyclelength) + date = offset + boundaries[index % num_boundaries] + ((index) // num_boundaries * self.cyclelength) index += 1 yield date, values[index % num_boundaries] @@ -103,7 +110,7 @@ def get_next_shift(self) -> NoReturn: class Slotted(Schedule): - def __init__(self, slots, slot_sizes, capacitated=False, preemption=False): + def __init__(self, slots, slot_sizes, capacitated=False, preemption=False, offset=0.0): """ Initialises the instance of the Slotted Schedule object """ @@ -112,20 +119,25 @@ def __init__(self, slots, slot_sizes, capacitated=False, preemption=False): raise ValueError("Pre-emption options not availale for non-capacitated slots.") if preemption not in [False, 'resume', 'restart', 'resample']: raise ValueError("Pre-emption options should be either 'resume', 'restart', 'resample', or False.") + if not isinstance(offset, float): + raise ValueError("Offset should be a positive float.") + if offset < 0.0: + raise ValueError("Offset should be a positive float.") self.schedule_type = 'slotted' + self.offset = offset self.slots = slots self.slot_sizes = slot_sizes + self.next_slot_sizes = [self.slot_sizes[-1]] + self.slot_sizes[:-1] self.capacitated = capacitated self.preemption = preemption self.cyclelength = self.slots[-1] self.c = 0 - def initialise(self): """ Initialises the generator object at the beginning of a simulation """ - self.schedule_generator = self.get_schedule_generator(self.slots, [self.slot_sizes[-1]] + self.slot_sizes[:-1]) + self.schedule_generator = self.get_schedule_generator(self.slots, self.next_slot_sizes, self.offset) self.get_next_slot() def get_next_slot(self): diff --git a/ciw/simulation.py b/ciw/simulation.py index 52795507..90921bfc 100644 --- a/ciw/simulation.py +++ b/ciw/simulation.py @@ -50,6 +50,7 @@ def __init__( self.transitive_nodes = [node_type(i + 1, self) for i, node_type in enumerate(self.NodeTypes)] self.nodes = [self.ArrivalNodeType(self)] + self.transitive_nodes + [ExitNode()] self.active_nodes = self.nodes[:-1] + self.routers = self.find_and_initialise_routers() self.nodes[0].initialise() if tracker is None: self.statetracker = trackers.StateTracker() @@ -66,6 +67,13 @@ def __repr__(self): """ return self.name + @property + def number_of_individuals(self): + """ + The number of individuals currently in the system. + """ + return (self.nodes[0].number_of_individuals - 1) - self.nodes[-1].number_of_individuals + def find_arrival_dists(self): """ Create the dictionary of arrival time distribution @@ -113,6 +121,16 @@ def show_simulation_to_distributions(self): self.service_times[nd + 1][clss].simulation = self self.batch_sizes[nd + 1][clss].simulation = self + def find_and_initialise_routers(self): + """ + Initialises the routing objects. + """ + routers_dict = {} + for clss in self.network.customer_class_names: + routers_dict[clss] = self.network.customer_classes[clss].routing + routers_dict[clss].initialise(self) + return routers_dict + def find_next_active_node(self): """ Returns the next active node, the node whose next_event_date is next: diff --git a/ciw/tests/test_network.py b/ciw/tests/test_network.py index 1fddbf5f..2f34890e 100644 --- a/ciw/tests/test_network.py +++ b/ciw/tests/test_network.py @@ -191,7 +191,10 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + + router0 = N.customer_classes['Class 0'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Class 0': 0}) @@ -204,7 +207,7 @@ def test_create_network_from_dictionary(self): ciw.dists.Exponential(7.0), ciw.dists.Deterministic(0.7), ], - "number_of_servers": [ciw.Schedule(schedule=[[1, 20], [4, 50]]), 3], + "number_of_servers": [ciw.Schedule(numbers_of_servers=[1, 4], shift_end_dates=[20, 50]), 3], "routing": [[0.5, 0.2], [0.0, 0.0]], "queue_capacities": [10, float("inf")], } @@ -214,8 +217,8 @@ def test_create_network_from_dictionary(self): self.assertEqual(N.service_centres[0].queueing_capacity, 10) self.assertTrue(type(N.service_centres[0].number_of_servers), ciw.schedules.Schedule) self.assertEqual(N.service_centres[0].class_change_matrix, None) - self.assertEqual(N.service_centres[0].number_of_servers.schedule_dates, [20, 50]) - self.assertEqual(N.service_centres[0].number_of_servers.schedule_servers, [1, 4]) + self.assertEqual(N.service_centres[0].number_of_servers.shift_end_dates, [20, 50]) + self.assertEqual(N.service_centres[0].number_of_servers.numbers_of_servers, [1, 4]) self.assertEqual(N.service_centres[1].queueing_capacity, float("inf")) self.assertEqual(N.service_centres[1].number_of_servers, 3) self.assertEqual(N.service_centres[1].class_change_matrix, None) @@ -227,7 +230,12 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Customer'].service_distributions], ["Exponential(rate=7.0)", "Deterministic(value=0.7)"], ) - self.assertEqual(N.customer_classes['Customer'].routing, [[0.5, 0.2], [0.0, 0.0]]) + router = N.customer_classes['Customer'].routing + self.assertEqual(router.routers[0].destinations, [1, 2, -1]) + self.assertEqual([round(p, 2) for p in router.routers[0].probs], [0.5, 0.2, 0.3]) + self.assertEqual(router.routers[1].destinations, [1, 2, -1]) + self.assertEqual([round(p, 2) for p in router.routers[1].probs], [0.0, 0.0, 1.0]) + self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Customer': 0}) @@ -262,7 +270,12 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) self.assertEqual( [str(d) for d in N.customer_classes['Class 1'].arrival_distributions], ["Exponential(rate=4.0)"], @@ -271,7 +284,6 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 1'].service_distributions], ["Uniform(lower=0.4, upper=1.2)"], ) - self.assertEqual(N.customer_classes['Class 1'].routing, [[0.0]]) self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Class 0': 0, 'Class 1': 0}) @@ -313,7 +325,12 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) self.assertEqual( [str(d) for d in N.customer_classes['Class 1'].arrival_distributions], ["Exponential(rate=4.0)"], @@ -322,7 +339,6 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 1'].service_distributions], ["Uniform(lower=0.4, upper=1.2)"], ) - self.assertEqual(N.customer_classes['Class 1'].routing, [[0.0]]) self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Class 0': 0, 'Class 1': 0}) @@ -354,7 +370,12 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) self.assertEqual( [str(d) for d in N.customer_classes['Class 1'].arrival_distributions], ["Exponential(rate=4.0)"], @@ -363,7 +384,6 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Class 1'].service_distributions], ["Uniform(lower=0.4, upper=1.2)"], ) - self.assertEqual(N.customer_classes['Class 1'].routing, [[0.0]]) self.assertEqual(N.customer_classes['Class 0'].priority_class, 1) self.assertEqual(N.customer_classes['Class 1'].priority_class, 0) self.assertEqual(N.number_of_priority_classes, 2) @@ -403,10 +423,13 @@ def test_create_network_from_dictionary(self): [str(d) for d in N.customer_classes['Customer'].service_distributions], ["Exponential(rate=7.0)", "Uniform(lower=0.4, upper=1.2)", "Deterministic(value=5.33)"], ) - self.assertEqual( - N.customer_classes['Customer'].routing, - [[0.5, 0.0, 0.1], [0.2, 0.1, 0.0], [0.0, 0.0, 0.0]], - ) + router = N.customer_classes['Customer'].routing + self.assertEqual(router.routers[0].destinations, [1, 2, 3, -1]) + self.assertEqual([round(p, 2) for p in router.routers[0].probs], [0.5, 0.0, 0.1, 0.4]) + self.assertEqual(router.routers[1].destinations, [1, 2, 3, -1]) + self.assertEqual([round(p, 2) for p in router.routers[1].probs], [0.2, 0.1, 0.0, 0.7]) + self.assertEqual(router.routers[2].destinations, [1, 2, 3, -1]) + self.assertEqual([round(p, 2) for p in router.routers[2].probs], [0.0, 0.0, 0.0, 1.0]) self.assertEqual( N.customer_classes['Customer'].baulking_functions, [None, None, example_baulking_function], @@ -424,7 +447,7 @@ def test_raising_errors(self): "number_of_nodes": 1, "queue_capacities": [float("inf")], } - params_list = [copy.deepcopy(params) for i in range(27)] + params_list = [copy.deepcopy(params) for i in range(28)] params_list[0]["number_of_classes"] = -2 self.assertRaises( @@ -531,16 +554,18 @@ def test_raising_errors(self): params_list[14] ) params_list[15]["routing"]["Class 0"] = [[0.2], [0.1]] + N = ciw.create_network_from_dictionary(params_list[15]) self.assertRaises( ValueError, - ciw.create_network_from_dictionary, - params_list[15] + ciw.Simulation, + N ) params_list[16]["routing"]["Class 0"] = [[0.2, 0.1]] + N = ciw.create_network_from_dictionary(params_list[16]) self.assertRaises( ValueError, - ciw.create_network_from_dictionary, - params_list[16] + ciw.Simulation, + N ) params_list[17]["routing"]["Class 0"] = [[-0.6]] self.assertRaises( @@ -602,6 +627,52 @@ def test_raising_errors(self): ciw.create_network_from_dictionary, params_list[26] ) + params_list[27]["routing"]["Class 0"] = [['0.5']] + self.assertRaises( + ValueError, + ciw.create_network_from_dictionary, + params_list[27] + ) + + def test_raising_errors_routing(self): + params = { + 'arrival_distributions': [ciw.dists.Exponential(1.0), ciw.dists.Exponential(1.0)], + 'service_distributions': [ciw.dists.Exponential(2.0), ciw.dists.Exponential(2.0)], + 'number_of_servers': [2, 1], + 'routing': [[0.5, 0.6], [0.0, 0.3]] + } + self.assertRaises( + ValueError, + ciw.create_network_from_dictionary, + params + ) + + N = ciw.create_network( + arrival_distributions=[ciw.dists.Exponential(1.0), ciw.dists.Exponential(1.0)], + service_distributions=[ciw.dists.Exponential(2.0), ciw.dists.Exponential(2.0)], + number_of_servers=[2, 1], + routing=[[0.5, 0.2, 0.0], [0.0, 0.3, 0.0], [0.1, 0.1, 0.3]] + ) + self.assertRaises( + ValueError, + ciw.Simulation, + N + ) + + N = ciw.create_network( + arrival_distributions=[ciw.dists.Exponential(1.0), ciw.dists.Exponential(1.0)], + service_distributions=[ciw.dists.Exponential(2.0), ciw.dists.Exponential(2.0)], + number_of_servers=[2, 1], + routing=ciw.routing.NetworkRouting(routers=[ + ciw.routing.Probabilistic(destinations=[0, 1], probs=[0.5, 0.2]), + ciw.routing.Probabilistic(destinations=[0, 2], probs=[0.5, 0.2]) + ]) + ) + self.assertRaises( + ValueError, + ciw.Simulation, + N + ) class TestImportNoMatrix(unittest.TestCase): @@ -611,7 +682,9 @@ def test_optional_transition_matrix(self): service_distributions=[ciw.dists.Exponential(2.0)], number_of_servers=[1], ) - self.assertEqual([c.routing for c in N.customer_classes.values()], [[[0.0]]]) + router = N.customer_classes['Customer'].routing + self.assertEqual(router.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router.routers[0].probs], [0.0, 1.0]) N = ciw.create_network( arrival_distributions={ @@ -625,7 +698,12 @@ def test_optional_transition_matrix(self): number_of_servers=[1], ) - self.assertEqual([c.routing for c in N.customer_classes.values()], [[[0.0]], [[0.0]]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.0, 1.0]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) params = { "arrival_distributions": [ @@ -638,10 +716,11 @@ def test_optional_transition_matrix(self): ], "number_of_servers": [1, 2], } + N = ciw.create_network(**params) self.assertRaises( ValueError, - ciw.create_network_from_dictionary, - params + ciw.Simulation, + N ) @@ -668,7 +747,9 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Class 0': 0}) @@ -681,7 +762,7 @@ def test_network_from_kwargs(self): ciw.dists.Exponential(7.0), ciw.dists.Deterministic(0.7), ], - number_of_servers=[ciw.Schedule(schedule=[[1, 20], [4, 50]]), 3], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 4], shift_end_dates=[20, 50]), 3], routing=[[0.5, 0.2], [0.0, 0.0]], queue_capacities=[10, float("inf")], ) @@ -691,8 +772,8 @@ def test_network_from_kwargs(self): self.assertEqual(N.service_centres[0].queueing_capacity, 10) self.assertEqual(type(N.service_centres[0].number_of_servers), ciw.schedules.Schedule) self.assertEqual(N.service_centres[0].class_change_matrix, None) - self.assertEqual(N.service_centres[0].number_of_servers.schedule_dates, [20, 50]) - self.assertEqual(N.service_centres[0].number_of_servers.schedule_servers, [1, 4]) + self.assertEqual(N.service_centres[0].number_of_servers.shift_end_dates, [20, 50]) + self.assertEqual(N.service_centres[0].number_of_servers.numbers_of_servers, [1, 4]) self.assertFalse(N.service_centres[0].number_of_servers.preemption) self.assertEqual(N.service_centres[1].queueing_capacity, float("inf")) self.assertEqual(N.service_centres[1].number_of_servers, 3) @@ -705,7 +786,12 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Customer'].service_distributions], ["Exponential(rate=7.0)", "Deterministic(value=0.7)"], ) - self.assertEqual(N.customer_classes['Customer'].routing, [[0.5, 0.2], [0.0, 0.0]]) + router = N.customer_classes['Customer'].routing + self.assertEqual(router.routers[0].destinations, [1, 2, -1]) + self.assertEqual([round(p, 2) for p in router.routers[0].probs], [0.5, 0.2, 0.3]) + self.assertEqual(router.routers[1].destinations, [1, 2, -1]) + self.assertEqual([round(p, 2) for p in router.routers[1].probs], [0.0, 0.0, 1.0]) + self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Customer': 0}) @@ -739,7 +825,12 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) self.assertEqual( [str(d) for d in N.customer_classes['Class 1'].arrival_distributions], ["Exponential(rate=4.0)"], @@ -748,7 +839,6 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 1'].service_distributions], ["Uniform(lower=0.4, upper=1.2)"], ) - self.assertEqual(N.customer_classes['Class 1'].routing, [[0.0]]) self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Class 0': 0, 'Class 1': 0}) @@ -779,7 +869,12 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) self.assertEqual( [str(d) for d in N.customer_classes['Class 1'].arrival_distributions], ["Exponential(rate=4.0)"], @@ -788,7 +883,6 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 1'].service_distributions], ["Uniform(lower=0.4, upper=1.2)"], ) - self.assertEqual(N.customer_classes['Class 1'].routing, [[0.0]]) self.assertEqual(N.customer_classes['Class 0'].priority_class, 1) self.assertEqual(N.customer_classes['Class 1'].priority_class, 0) self.assertEqual(N.number_of_priority_classes, 2) @@ -828,10 +922,14 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Customer'].service_distributions], ["Exponential(rate=7.0)", "Uniform(lower=0.4, upper=1.2)", "Deterministic(value=5.33)"], ) - self.assertEqual( - N.customer_classes['Customer'].routing, - [[0.5, 0.0, 0.1], [0.2, 0.1, 0.0], [0.0, 0.0, 0.0]], - ) + router = N.customer_classes['Customer'].routing + self.assertEqual(router.routers[0].destinations, [1, 2, 3, -1]) + self.assertEqual([round(p, 2) for p in router.routers[0].probs], [0.5, 0.0, 0.1, 0.4]) + self.assertEqual(router.routers[1].destinations, [1, 2, 3, -1]) + self.assertEqual([round(p, 2) for p in router.routers[1].probs], [0.2, 0.1, 0.0, 0.7]) + self.assertEqual(router.routers[2].destinations, [1, 2, 3, -1]) + self.assertEqual([round(p, 2) for p in router.routers[2].probs], [0.0, 0.0, 0.0, 1.0]) + self.assertEqual( N.customer_classes['Customer'].baulking_functions, [None, None, example_baulking_function], @@ -898,7 +996,13 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 0'].service_distributions], ["Exponential(rate=7.0)"], ) - self.assertEqual(N.customer_classes['Class 0'].routing, [[0.5]]) + router0 = N.customer_classes['Class 0'].routing + router1 = N.customer_classes['Class 1'].routing + self.assertEqual(router0.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.5, 0.5]) + self.assertEqual(router1.routers[0].destinations, [1, -1]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.0, 1.0]) + self.assertEqual( [str(d) for d in N.customer_classes['Class 1'].arrival_distributions], ["Exponential(rate=4.0)"], @@ -907,7 +1011,6 @@ def test_network_from_kwargs(self): [str(d) for d in N.customer_classes['Class 1'].service_distributions], ["Uniform(lower=0.4, upper=1.2)"], ) - self.assertEqual(N.customer_classes['Class 1'].routing, [[0.0]]) self.assertEqual(N.number_of_priority_classes, 1) self.assertEqual(N.priority_class_mapping, {'Class 0': 0, 'Class 1': 0}) @@ -1024,3 +1127,23 @@ def test_raise_error_invalid_class_change_matrix(self): ciw.create_network(**params_geq1) with self.assertRaises(ValueError): ciw.create_network(**params_neg) + + def test_raising_errors_system_capacity(self): + params_neg = { + 'arrival_distributions': [ciw.dists.Exponential(10), ciw.dists.Exponential(10)], + 'service_distributions': [ciw.dists.Exponential(30), ciw.dists.Exponential(20)], + 'number_of_servers': [2, 2], + 'routing': [[0.0, 0.0], [0.0, 0.0]], + 'system_capacity': -4 + } + params_str = { + 'arrival_distributions': [ciw.dists.Exponential(10), ciw.dists.Exponential(10)], + 'service_distributions': [ciw.dists.Exponential(30), ciw.dists.Exponential(20)], + 'number_of_servers': [2, 2], + 'routing': [[0.0, 0.0], [0.0, 0.0]], + 'system_capacity': '77' + } + with self.assertRaises(ValueError): + ciw.create_network(**params_neg) + with self.assertRaises(ValueError): + ciw.create_network(**params_str) diff --git a/ciw/tests/test_node.py b/ciw/tests/test_node.py index aaf131f2..c4d0780f 100644 --- a/ciw/tests/test_node.py +++ b/ciw/tests/test_node.py @@ -98,10 +98,6 @@ def test_init_method(self): Q = ciw.Simulation(N_params) N = ciw.Node(1, Q) self.assertEqual(N.c, 9) - self.assertEqual( - [[round(p, 10) for p in row] for row in N.transition_row.values()], - [[0.1, 0.2, 0.1, 0.4, 0.2], [0.6, 0.0, 0.0, 0.2, 0.2], [0.0, 0.0, 0.4, 0.3, 0.3]], - ) self.assertEqual(N.next_event_date, float("inf")) self.assertEqual(N.all_individuals, []) self.assertEqual(N.id_number, 1) @@ -143,7 +139,7 @@ def test_init_method(self): "Class 0": [ciw.dists.Exponential(0.05), ciw.dists.Exponential(0.04)], "Class 1": [ciw.dists.Exponential(0.04), ciw.dists.Exponential(0.06)], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 30], [2, 60], [1, 90], [3, 100]]), 3], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2, 1, 3], shift_end_dates=[30, 60, 90, 100]), 3], queue_capacities=[float("Inf"), 10], service_distributions={ "Class 0": [ciw.dists.Deterministic(5.0), ciw.dists.Exponential(0.2)], @@ -156,16 +152,20 @@ def test_init_method(self): ) Q = ciw.Simulation(N_schedule) N = Q.transitive_nodes[0] + N.have_event() + N.update_next_event_date() self.assertEqual(N.schedule.cyclelength, 100) self.assertEqual(N.c, 1) - self.assertEqual(N.schedule.schedule_dates, [30, 60, 90, 100]) - self.assertEqual(N.schedule.schedule_servers, [1, 2, 1, 3]) + self.assertEqual(N.schedule.shift_end_dates, [30, 60, 90, 100]) + self.assertEqual(N.schedule.numbers_of_servers, [1, 2, 1, 3]) self.assertEqual(N.next_event_date, 30) self.assertEqual(N.interrupted_individuals, []) self.assertFalse(N.reneging) Q = ciw.Simulation(N_priorities) N = Q.transitive_nodes[0] + N.have_event() + N.update_next_event_date() self.assertEqual(N.c, 4) self.assertEqual(Q.network.priority_class_mapping, {'Class 0': 0, 'Class 1': 1}) self.assertEqual(Q.number_of_priority_classes, 2) @@ -753,8 +753,9 @@ def test_reset_individual_attributes(self): self.assertFalse(ind.exit_date) def test_date_from_schedule_generator(self): - sg = ciw.Schedule(schedule=[[1, 30], [0, 60], [2, 90], [3, 100]]) + sg = ciw.Schedule(numbers_of_servers=[1, 0, 2, 3], shift_end_dates=[30, 60, 90, 100]) sg.initialise() + sg.get_next_shift() self.assertEqual(sg.c, 1) self.assertEqual(sg.next_c, 0) self.assertEqual(sg.next_shift_change_date, 30) @@ -765,6 +766,7 @@ def test_date_from_schedule_generator(self): self.assertEqual(next(sg.schedule_generator), (160, 2)) sg.initialise() + sg.get_next_shift() self.assertEqual(sg.c, 1) self.assertEqual(sg.next_c, 0) self.assertEqual(sg.next_shift_change_date, 30) @@ -885,7 +887,7 @@ def test_server_utilisation_with_schedules(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([2.0, 4.0, 4.0, 0.0, 7.0, 1000.0])], service_distributions=[ciw.dists.Sequential([4.0, 2.0, 6.0, 6.0, 3.0])], - number_of_servers=[ciw.Schedule(schedule=[[1, 9], [2, 23]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[9, 23])], ) Q = ciw.Simulation(N) Q.simulate_until_max_time(23) @@ -1012,7 +1014,7 @@ def prioritise_highest_id(srv, ind): arrival_distributions=[ciw.dists.Exponential(1), ciw.dists.Exponential(1)], service_distributions=[ciw.dists.Exponential(2), ciw.dists.Exponential(2)], number_of_servers=[2, 2], - routing=[[0, 0], [0, 0]], + routing=[[0.0, 0.0], [0.0, 0.0]], server_priority_functions=[prioritise_less_busy, prioritise_highest_id], ) Q = ciw.Simulation(N) @@ -1156,7 +1158,7 @@ def test_reneging_sends_to_destination(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Deterministic(7), None], service_distributions=[ciw.dists.Deterministic(11), ciw.dists.Deterministic(2)], - routing=[[0, 0], [0, 0]], + routing=[[0.0, 0.0], [0.0, 0.0]], number_of_servers=[1, 1], reneging_time_distributions=[ciw.dists.Deterministic(3), None], reneging_destinations=[2, -1], @@ -1222,7 +1224,7 @@ def test_reneging_with_schedules(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Deterministic(7)], service_distributions=[ciw.dists.Deterministic(11)], - number_of_servers=[ciw.Schedule(schedule=[[1, 16], [0, 10000]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0], shift_end_dates=[16, 10000])], reneging_time_distributions=[ciw.dists.Deterministic(3)], ) Q = ciw.Simulation(N) @@ -1856,7 +1858,7 @@ def test_data_records_for_interrupted_individuals(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Deterministic(7)], service_distributions=[ciw.dists.Deterministic(4)], - number_of_servers=[ciw.Schedule(schedule=[[1, 24], [0, 29], [1, 37]], preemption="resume")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[24, 29, 37], preemption="resume")], ) ciw.seed(0) Q = ciw.Simulation(N) @@ -1906,7 +1908,7 @@ def test_preemptive_priorities_resume_options_due_to_schedule(self): "Class 0": [ciw.dists.Deterministic(10)], "Class 1": [ciw.dists.Sequential([6, 3])], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 5], [2, 100]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[5, 100])], priority_classes=({"Class 0": 0, "Class 1": 1}, ["restart"]), ) Q = ciw.Simulation(N) @@ -1935,7 +1937,7 @@ def test_preemptive_priorities_resume_options_due_to_schedule(self): "Class 0": [ciw.dists.Deterministic(10)], "Class 1": [ciw.dists.Sequential([6, 3])], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 5], [2, 100]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[5, 100])], priority_classes=({"Class 0": 0, "Class 1": 1}, ["resume"]), ) Q = ciw.Simulation(N) @@ -1964,7 +1966,7 @@ def test_preemptive_priorities_resume_options_due_to_schedule(self): "Class 0": [ciw.dists.Deterministic(10)], "Class 1": [ciw.dists.Sequential([6, 3])], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 5], [2, 100]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[5, 100])], priority_classes=({"Class 0": 0, "Class 1": 1}, ["resample"]), ) Q = ciw.Simulation(N) @@ -2014,7 +2016,7 @@ def test_shift_change_before_arrival(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential(sequence=[2.0, float('inf')])], service_distributions=[ciw.dists.Deterministic(value=1.0)], - number_of_servers=[ciw.Schedule(schedule=[[1, 1.0], [2, 10.0]], preemption=False)] + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[1.0, 10.0], preemption=False)] ) Q = ciw.Simulation(N) Q.simulate_until_max_time(10.5) diff --git a/ciw/tests/test_process_based.py b/ciw/tests/test_process_based.py index 86cc6864..63f37f19 100644 --- a/ciw/tests/test_process_based.py +++ b/ciw/tests/test_process_based.py @@ -3,54 +3,33 @@ import random from collections import Counter +def generator_function_1(ind, simulation): + return [1, 1] -def generator_function_1(ind): - return [1, 1, 1] +def generator_function_2(ind, simulation): + return [1, 2, 1, 3] -def generator_function_2(ind): - return [1, 1, 2, 1, 3] - -def generator_function_3(ind): +def generator_function_3(ind, simulation): rnd = random.random() if rnd < 0.4: - return [1, 2, 2, 3, 2] + return [2, 2, 3, 2] if rnd < 0.5: - return [1, 1] - return [1, 3, 2, 3] - - -def generator_function_4(ind): - return [2, 1, 3, 1, 1] - - -def generator_function_5(ind): - return [3, 2, 3, 2, 3] - - -def generator_function_6(ind): - return [3] - - -def generator_function_7(ind): - rnd = random.random() - if rnd < 0.4: - return [2, 1, 2] - return [2, 1, 1, 2] - - -def generator_function_8(ind): - if ind.customer_class == 'Class 0': return [1] - return [1, 1, 1] + return [3, 2, 3] + +def generator_function_4(ind, simulation): + return [] class ClassForProcessBasedMethod: def __init__(self, n): self.n = n - def generator_method(self, ind): - return [1, 1, 1] + def generator_method(self, ind, simulation): + return [1, 1] +def flexible_generator_1(ind, simulation): + return [[2], [3, 4, 5], [6]] class TestProcessBased(unittest.TestCase): def test_network_takes_routing_function(self): @@ -58,9 +37,8 @@ def test_network_takes_routing_function(self): arrival_distributions=[ciw.dists.Exponential(1)], service_distributions=[ciw.dists.Exponential(2)], number_of_servers=[1], - routing=[generator_function_1], + routing=ciw.routing.ProcessBased(generator_function_1), ) - self.assertEqual(N.process_based, True) Q = ciw.Simulation(N) self.assertEqual(str(Q), "Simulation") @@ -69,13 +47,13 @@ def test_individuals_recieve_route(self): arrival_distributions=[ciw.dists.Deterministic(1)], service_distributions=[ciw.dists.Deterministic(1000)], number_of_servers=[1], - routing=[generator_function_1], + routing=ciw.routing.ProcessBased(generator_function_1), ) Q = ciw.Simulation(N) Q.simulate_until_max_time(4.5) inds = Q.nodes[1].all_individuals for ind in inds: - self.assertEqual(ind.route, [1, 1, 1]) + self.assertEqual(ind.route, [1, 1]) def test_routing_correct(self): N = ciw.create_network( @@ -86,27 +64,27 @@ def test_routing_correct(self): ciw.dists.Deterministic(2), ], number_of_servers=[1, 1, 1], - routing=[generator_function_2, ciw.no_routing, ciw.no_routing], + routing=ciw.routing.ProcessBased(generator_function_2), ) Q = ciw.Simulation(N) Q.simulate_until_max_time(4) ind = Q.get_all_individuals()[0] - self.assertEqual(ind.route, [1, 2, 1, 3]) + self.assertEqual(ind.route, [2, 1, 3]) self.assertEqual([dr.node for dr in ind.data_records], [1]) Q = ciw.Simulation(N) Q.simulate_until_max_time(6) ind = Q.get_all_individuals()[0] - self.assertEqual(ind.route, [2, 1, 3]) + self.assertEqual(ind.route, [1, 3]) self.assertEqual([dr.node for dr in ind.data_records], [1, 1]) Q = ciw.Simulation(N) Q.simulate_until_max_time(8) ind = Q.get_all_individuals()[0] - self.assertEqual(ind.route, [1, 3]) + self.assertEqual(ind.route, [3]) self.assertEqual([dr.node for dr in ind.data_records], [1, 1, 2]) Q = ciw.Simulation(N) Q.simulate_until_max_time(10) ind = Q.get_all_individuals()[0] - self.assertEqual(ind.route, [3]) + self.assertEqual(ind.route, []) self.assertEqual([dr.node for dr in ind.data_records], [1, 1, 2, 1]) Q = ciw.Simulation(N) Q.simulate_until_max_time(12) @@ -123,7 +101,7 @@ def test_probablistic_process_routes(self): ciw.dists.Exponential(2), ], number_of_servers=[1, 1, 1], - routing=[generator_function_3, ciw.no_routing, ciw.no_routing], + routing=ciw.routing.ProcessBased(generator_function_3), ) ciw.seed(0) Q = ciw.Simulation(N) @@ -137,182 +115,141 @@ def test_probablistic_process_routes(self): Counter({(1, 3, 2, 3): 244, (1, 2, 2, 3, 2): 204, (1, 1): 52}), ) - def test_error_when_ind_sent_wrong_place(self): + def test_customer_class_based_routing(self): N = ciw.create_network( - arrival_distributions=[ciw.dists.Exponential(1)], - service_distributions=[ciw.dists.Exponential(2)], + arrival_distributions={ + "Class 0": [ciw.dists.Exponential(1)], + "Class 1": [ciw.dists.Exponential(1)], + }, + service_distributions={ + "Class 0": [ciw.dists.Exponential(2)], + "Class 1": [ciw.dists.Exponential(2)], + }, number_of_servers=[1], - routing=[ciw.no_routing], - ) - Q = ciw.Simulation(N) - self.assertRaises(ValueError, Q.simulate_until_max_time, 20) - - N = ciw.create_network( - arrival_distributions=[None, ciw.dists.Exponential(1), None], - service_distributions=[ - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ], - number_of_servers=[1, 1, 1], - routing=[ciw.no_routing, generator_function_2, ciw.no_routing], - ) - Q = ciw.Simulation(N) - self.assertRaises(ValueError, Q.simulate_until_max_time, 20) - - def test_routing_from_different_starting_points(self): - N = ciw.create_network( - arrival_distributions=[ - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - None, - ], - service_distributions=[ - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ], - number_of_servers=[1, 1, 1], - routing=[generator_function_2, generator_function_4, ciw.no_routing], + routing={ + "Class 0": ciw.routing.ProcessBased(generator_function_4), + "Class 1": ciw.routing.ProcessBased(generator_function_1) + } ) ciw.seed(0) Q = ciw.Simulation(N) Q.simulate_until_max_customers(500, method="Finish") inds = Q.nodes[-1].all_individuals - routes_counter = Counter([tuple(dr.node for dr in ind.data_records) for ind in inds]) - self.assertEqual( - routes_counter, - Counter({(1, 1, 2, 1, 3): 245, (2, 1, 3, 1, 1): 255}) - ) + routes_counter = set([tuple([ind.customer_class, tuple(dr.node for dr in ind.data_records)]) for ind in inds]) + self.assertEqual(routes_counter, {('Class 1', (1, 1, 1)), ('Class 0', (1,))}) + def test_process_based_takes_methods(self): + import types + G = ClassForProcessBasedMethod(5) + self.assertTrue(isinstance(G.generator_method, types.MethodType)) N = ciw.create_network( - arrival_distributions=[ - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - ], - service_distributions=[ - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ], - number_of_servers=[1, 1, 1], - routing=[generator_function_1, generator_function_4, generator_function_5], + arrival_distributions=[ciw.dists.Deterministic(1)], + service_distributions=[ciw.dists.Deterministic(1000)], + number_of_servers=[1], + routing=ciw.routing.ProcessBased(G.generator_method), ) - ciw.seed(0) Q = ciw.Simulation(N) - Q.simulate_until_max_customers(500, method="Finish") - inds = Q.nodes[-1].all_individuals - routes_counter = Counter([tuple(dr.node for dr in ind.data_records) for ind in inds]) - self.assertEqual( - routes_counter, - Counter({(3, 2, 3, 2, 3): 256, (1, 1, 1): 155, (2, 1, 3, 1, 1): 89}), - ) + Q.simulate_until_max_time(4.5) + inds = Q.nodes[1].all_individuals + for ind in inds: + self.assertEqual(ind.route, [1, 1]) + def test_flexible_process_based(self): + ## First test 'any-random': N = ciw.create_network( - arrival_distributions=[ - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - ], - service_distributions=[ - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ], - number_of_servers=[1, 1, 1], - routing=[generator_function_2, generator_function_4, generator_function_5], + arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None], + service_distributions=[ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2)], + number_of_servers=[3, 3, 3, 3, 3, 3], + routing=ciw.routing.FlexibleProcessBased( + route_function=flexible_generator_1, + rule='any', + choice='random' + ) ) ciw.seed(0) Q = ciw.Simulation(N) - Q.simulate_until_max_customers(500, method="Finish") + Q.simulate_until_max_customers(1000) inds = Q.nodes[-1].all_individuals - routes_counter = Counter([tuple(dr.node for dr in ind.data_records) for ind in inds]) + routes_counter = Counter( + [tuple(dr.node for dr in ind.data_records) for ind in inds] + ) self.assertEqual( routes_counter, - Counter({(3, 2, 3, 2, 3): 233, (1, 1, 2, 1, 3): 148, (2, 1, 3, 1, 1): 119}), + Counter({(1, 2, 4, 6): 350, (1, 2, 5, 6): 333, (1, 2, 3, 6): 317}), # all evenly spread ) + ## Now test 'any-jsq' (flooding Node 4 so no customers ever go here): N = ciw.create_network( - arrival_distributions=[ - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - ], - service_distributions=[ - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ], - number_of_servers=[1, 1, 1], - routing=[generator_function_2, generator_function_4, generator_function_6], + arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None], + service_distributions=[ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Deterministic(value=200000), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2)], + number_of_servers=[3, 3, 3, 3, 3, 3], + routing=ciw.routing.FlexibleProcessBased( + route_function=flexible_generator_1, + rule='any', + choice='jsq' + ) ) ciw.seed(0) Q = ciw.Simulation(N) - Q.simulate_until_max_customers(500, method="Finish") + Q.simulate_until_max_customers(1000) inds = Q.nodes[-1].all_individuals - routes_counter = Counter([tuple(dr.node for dr in ind.data_records) for ind in inds]) + routes_counter = Counter( + [tuple(dr.node for dr in ind.data_records) for ind in inds] + ) self.assertEqual( routes_counter, - Counter({(3,): 385, (1, 1, 2, 1, 3): 57, (2, 1, 3, 1, 1): 58}), + Counter({(1, 2, 5, 6): 503, (1, 2, 3, 6): 497}), # evenly spread between the two unflooded nodes ) + ## Now test 'any-lb' (flooding Node 4 so no customers ever go here): N = ciw.create_network( - arrival_distributions=[ - ciw.dists.Exponential(1), - ciw.dists.Exponential(1), - None, - ], - service_distributions=[ - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ciw.dists.Exponential(2), - ], - number_of_servers=[1, 1, 1], - routing=[generator_function_1, generator_function_7, ciw.no_routing], + arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None], + service_distributions=[ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Deterministic(value=200000), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2)], + number_of_servers=[3, 3, 3, 3, 3, 3], + routing=ciw.routing.FlexibleProcessBased( + route_function=flexible_generator_1, + rule='any', + choice='lb' + ) ) ciw.seed(0) Q = ciw.Simulation(N) - Q.simulate_until_max_customers(500, method="Finish") + Q.simulate_until_max_customers(1000) inds = Q.nodes[-1].all_individuals - routes_counter = Counter([tuple(dr.node for dr in ind.data_records) for ind in inds]) + routes_counter = Counter( + [tuple(dr.node for dr in ind.data_records) for ind in inds] + ) self.assertEqual( routes_counter, - Counter({(2, 1, 1, 2): 172, (2, 1, 2): 169, (1, 1, 1): 159}) + Counter({(1, 2, 5, 6): 508, (1, 2, 3, 6): 492}), # evenly spread between the two unflooded nodes ) - def test_customer_class_based_routing(self): + + ## Now test 'all-random': N = ciw.create_network( - arrival_distributions={ - "Class 0": [ciw.dists.Exponential(1)], - "Class 1": [ciw.dists.Exponential(1)], - }, - service_distributions={ - "Class 0": [ciw.dists.Exponential(2)], - "Class 1": [ciw.dists.Exponential(2)], - }, - number_of_servers=[1], - routing=[generator_function_8], + arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None], + service_distributions=[ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2)], + number_of_servers=[3, 3, 3, 3, 3, 3], + routing=ciw.routing.FlexibleProcessBased( + route_function=flexible_generator_1, + rule='all', + choice='random' + ) ) ciw.seed(0) Q = ciw.Simulation(N) - Q.simulate_until_max_customers(500, method="Finish") + Q.simulate_until_max_customers(1000) inds = Q.nodes[-1].all_individuals - routes_counter = set([tuple([ind.customer_class, tuple(dr.node for dr in ind.data_records)]) for ind in inds]) - self.assertEqual(routes_counter, {('Class 1', (1, 1, 1)), ('Class 0', (1,))}) - - def test_process_based_takes_methods(self): - import types - G = ClassForProcessBasedMethod(5) - self.assertTrue(isinstance(G.generator_method, types.MethodType)) - N = ciw.create_network( - arrival_distributions=[ciw.dists.Deterministic(1)], - service_distributions=[ciw.dists.Deterministic(1000)], - number_of_servers=[1], - routing=[G.generator_method], + routes_counter = Counter( + [tuple(dr.node for dr in ind.data_records) for ind in inds] ) - Q = ciw.Simulation(N) - Q.simulate_until_max_time(4.5) - inds = Q.nodes[1].all_individuals - for ind in inds: - self.assertEqual(ind.route, [1, 1, 1]) + self.assertEqual(routes_counter[(1, 2, 3, 4, 5, 6)], 169) + self.assertEqual(routes_counter[(1, 2, 3, 5, 4, 6)], 139) + self.assertEqual(routes_counter[(1, 2, 4, 3, 5, 6)], 185) + self.assertEqual(routes_counter[(1, 2, 4, 5, 3, 6)], 166) + self.assertEqual(routes_counter[(1, 2, 5, 3, 4, 6)], 186) + self.assertEqual(routes_counter[(1, 2, 5, 4, 3, 6)], 155) + + def test_flexible_process_based_error_raising(self): + self.assertRaises(ValueError, ciw.routing.FlexibleProcessBased, flexible_generator_1, 'something', 'random') + self.assertRaises(ValueError, ciw.routing.FlexibleProcessBased, flexible_generator_1, 'all', 'something') diff --git a/ciw/tests/test_processor_sharing.py b/ciw/tests/test_processor_sharing.py index 66681513..c671a68d 100644 --- a/ciw/tests/test_processor_sharing.py +++ b/ciw/tests/test_processor_sharing.py @@ -104,7 +104,7 @@ "Class 0": [ciw.dists.Exponential(0.05), ciw.dists.Exponential(0.04)], "Class 1": [ciw.dists.Exponential(0.04), ciw.dists.Exponential(0.06)], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 30], [2, 60], [1, 90], [3, 100]]), 3], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2, 1, 3], shift_end_dates=[30, 60, 90, 100]), 3], queue_capacities=[float("Inf"), 10], service_distributions={ "Class 0": [ciw.dists.Deterministic(5.0), ciw.dists.Exponential(0.2)], @@ -120,10 +120,34 @@ def test_init_method(self): N = ciw.PSNode(1, Q) self.assertEqual(N.ps_capacity, 9) self.assertEqual(N.c, float("inf")) - self.assertEqual( - [[round(p, 10) for p in row] for row in N.transition_row.values()], - [[0.1, 0.2, 0.1, 0.4, 0.2], [0.6, 0.0, 0.0, 0.2, 0.2], [0.0, 0.0, 0.4, 0.3, 0.3]], - ) + router0 = Q.network.customer_classes["Class 0"].routing + router1 = Q.network.customer_classes["Class 1"].routing + router2 = Q.network.customer_classes["Class 2"].routing + self.assertEqual(router0.routers[0].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router0.routers[1].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router0.routers[2].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router0.routers[3].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router1.routers[0].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router1.routers[1].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router1.routers[2].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router1.routers[3].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router2.routers[0].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router2.routers[1].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router2.routers[2].destinations, [1, 2, 3, 4, -1]) + self.assertEqual(router2.routers[3].destinations, [1, 2, 3, 4, -1]) + self.assertEqual([round(p, 2) for p in router0.routers[0].probs], [0.1, 0.2, 0.1, 0.4, 0.2]) + self.assertEqual([round(p, 2) for p in router0.routers[1].probs], [0.2, 0.2, 0.0, 0.1, 0.5]) + self.assertEqual([round(p, 2) for p in router0.routers[2].probs], [0.0, 0.8, 0.1, 0.1, 0.0]) + self.assertEqual([round(p, 2) for p in router0.routers[3].probs], [0.4, 0.1, 0.1, 0.0, 0.4]) + self.assertEqual([round(p, 2) for p in router1.routers[0].probs], [0.6, 0.0, 0.0, 0.2, 0.2]) + self.assertEqual([round(p, 2) for p in router1.routers[1].probs], [0.1, 0.1, 0.2, 0.2, 0.4]) + self.assertEqual([round(p, 2) for p in router1.routers[2].probs], [0.9, 0.0, 0.0, 0.0, 0.1]) + self.assertEqual([round(p, 2) for p in router1.routers[3].probs], [0.2, 0.1, 0.1, 0.1, 0.5]) + self.assertEqual([round(p, 2) for p in router2.routers[0].probs], [0.0, 0.0, 0.4, 0.3, 0.3]) + self.assertEqual([round(p, 2) for p in router2.routers[1].probs], [0.1, 0.1, 0.1, 0.1, 0.6]) + self.assertEqual([round(p, 2) for p in router2.routers[2].probs], [0.1, 0.3, 0.2, 0.2, 0.2]) + self.assertEqual([round(p, 2) for p in router2.routers[3].probs], [0.0, 0.0, 0.0, 0.3, 0.7]) + self.assertEqual(N.next_event_date, float("inf")) self.assertEqual(N.all_individuals, []) self.assertEqual(N.id_number, 1) @@ -137,19 +161,10 @@ def test_init_method(self): self.assertEqual(N2.class_change, {'Class 0': {'Class 0': 1.0, 'Class 1': 0.0}, 'Class 1': {'Class 0': 0.0, 'Class 1': 1.0}}) self.assertEqual(N.interrupted_individuals, []) - Q = ciw.Simulation(N_schedule, node_class=ciw.PSNode) - N = Q.transitive_nodes[0] - self.assertEqual(N.schedule.cyclelength, 100) - self.assertEqual(N.ps_capacity, 1) - self.assertEqual(N.c, float("inf")) - self.assertEqual(N.schedule.schedule_dates, [30, 60, 90, 100]) - self.assertEqual(N.schedule.schedule_servers, [1, 2, 1, 3]) - self.assertEqual(N.next_event_date, 30) - self.assertEqual(N.interrupted_individuals, []) - self.assertEqual(N.last_occupancy, 0) - Q = ciw.Simulation(N_priorities, node_class=ciw.PSNode) N = Q.transitive_nodes[0] + N.have_event() + N.update_next_event_date() self.assertEqual(N.ps_capacity, 4) self.assertEqual(N.c, float("inf")) self.assertEqual(Q.network.priority_class_mapping, {'Class 0': 0, 'Class 1': 1}) diff --git a/ciw/tests/test_routing.py b/ciw/tests/test_routing.py new file mode 100644 index 00000000..4abacad9 --- /dev/null +++ b/ciw/tests/test_routing.py @@ -0,0 +1,335 @@ +import unittest +import ciw +from collections import Counter + +N = ciw.create_network( + arrival_distributions=[ + ciw.dists.Exponential(rate=1.0), + ciw.dists.Exponential(rate=1.0), + ciw.dists.Exponential(rate=1.0), + ], + service_distributions=[ + ciw.dists.Exponential(rate=2.0), + ciw.dists.Exponential(rate=2.0), + ciw.dists.Exponential(rate=2.0), + ], + number_of_servers=[1, 2, 2], + routing=[ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0] + ] +) + +class TestRouting(unittest.TestCase): + def test_generic_network_router(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R = ciw.routing.NodeRouting() + R.initialise(Q, 1) + self.assertEqual(R.simulation, Q) + self.assertEqual(R.node, 1) + + + def test_probabilistic_routing(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.Probabilistic(destinations=[1, 2, 3], probs=[0.6, 0.3, 0.1]) + R2 = ciw.routing.Probabilistic(destinations=[1, 2, 3], probs=[0.0, 0.0, 0.3]) + R3 = ciw.routing.Probabilistic(destinations=[1, 2, 3], probs=[0.33, 0.34, 0.33]) + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + samples_1 = Counter([r.id_number for r in [R1.next_node(ind) for _ in range(10000)]]) + samples_2 = Counter([r.id_number for r in [R2.next_node(ind) for _ in range(10000)]]) + samples_3 = Counter([r.id_number for r in [R3.next_node(ind) for _ in range(10000)]]) + self.assertEqual([samples_1[i] for i in [1, 2, 3, -1]], [5976, 3067, 957, 0]) + self.assertEqual([samples_2[i] for i in [1, 2, 3, -1]], [0, 0, 3019, 6981]) + self.assertEqual([samples_3[i] for i in [1, 2, 3, -1]], [3278, 3444, 3278, 0]) + + + def test_network_routing(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R = ciw.routing.NetworkRouting(routers=[ + ciw.routing.Probabilistic(destinations=[1, 2, 3], probs=[0.6, 0.3, 0.1]), + ciw.routing.Probabilistic(destinations=[1, 2, 3], probs=[0.0, 0.0, 0.3]), + ciw.routing.Probabilistic(destinations=[1, 2, 3], probs=[0.33, 0.34, 0.33]) + ]) + R.initialise(Q) + ind = ciw.Individual(1) + samples_1 = Counter([r.id_number for r in [R.next_node(ind, 1) for _ in range(10000)]]) + samples_2 = Counter([r.id_number for r in [R.next_node(ind, 2) for _ in range(10000)]]) + samples_3 = Counter([r.id_number for r in [R.next_node(ind, 3) for _ in range(10000)]]) + self.assertEqual([samples_1[i] for i in [1, 2, 3, -1]], [5976, 3067, 957, 0]) + self.assertEqual([samples_2[i] for i in [1, 2, 3, -1]], [0, 0, 3019, 6981]) + self.assertEqual([samples_3[i] for i in [1, 2, 3, -1]], [3278, 3444, 3278, 0]) + + + def test_transition_matrix_router(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R = ciw.routing.TransitionMatrix(transition_matrix=[ + [0.6, 0.3, 0.1], + [0.0, 0.0, 0.3], + [0.33, 0.34, 0.33] + ]) + R.initialise(Q) + ind = ciw.Individual(1) + samples_1 = Counter([r.id_number for r in [R.next_node(ind, 1) for _ in range(10000)]]) + samples_2 = Counter([r.id_number for r in [R.next_node(ind, 2) for _ in range(10000)]]) + samples_3 = Counter([r.id_number for r in [R.next_node(ind, 3) for _ in range(10000)]]) + self.assertEqual([samples_1[i] for i in [1, 2, 3, -1]], [5976, 3067, 957, 0]) + self.assertEqual([samples_2[i] for i in [1, 2, 3, -1]], [0, 0, 3019, 6981]) + self.assertEqual([samples_3[i] for i in [1, 2, 3, -1]], [3278, 3444, 3278, 0]) + + + def test_direct_routing(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.Direct(to=3) + R2 = ciw.routing.Direct(to=1) + R3 = ciw.routing.Direct(to=2) + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + samples_1 = [r.id_number for r in [R1.next_node(ind) for _ in range(1000)]] + samples_2 = [r.id_number for r in [R2.next_node(ind) for _ in range(1000)]] + samples_3 = [r.id_number for r in [R3.next_node(ind) for _ in range(1000)]] + self.assertTrue(all(r == 3 for r in samples_1)) + self.assertTrue(all(r == 1 for r in samples_2)) + self.assertTrue(all(r == 2 for r in samples_3)) + + + def test_leave_routing(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R = ciw.routing.Leave() + R.initialise(Q, 1) + ind = ciw.Individual(1) + samples = [r.id_number for r in [R.next_node(ind) for _ in range(1000)]] + self.assertTrue(all(r == -1 for r in samples)) + + + def test_join_shortest_queue(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.JoinShortestQueue(destinations=[1, 2, 3]) + R2 = ciw.routing.JoinShortestQueue(destinations=[1, 3]) + R3 = ciw.routing.JoinShortestQueue(destinations=[2, 3]) + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + Q.nodes[1].number_of_individuals = 10 + Q.nodes[1].number_in_service = 1 + Q.nodes[2].number_of_individuals = 20 + Q.nodes[2].number_in_service = 2 + Q.nodes[3].number_of_individuals = 40 + Q.nodes[3].number_in_service = 2 + samples_1 = [r.id_number for r in [R1.next_node(ind) for _ in range(1000)]] + samples_2 = [r.id_number for r in [R2.next_node(ind) for _ in range(1000)]] + samples_3 = [r.id_number for r in [R3.next_node(ind) for _ in range(1000)]] + self.assertTrue(all(r == 1 for r in samples_1)) + self.assertTrue(all(r == 1 for r in samples_2)) + self.assertTrue(all(r == 2 for r in samples_3)) + + def test_join_shortest_queue_tiebreaks(self): + """ + There is a tie between nodes 2 and 3 + """ + # Default tie-break (random) + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.JoinShortestQueue(destinations=[1, 2, 3]) + R2 = ciw.routing.JoinShortestQueue(destinations=[1, 3]) + R3 = ciw.routing.JoinShortestQueue(destinations=[3, 2]) + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + Q.nodes[1].number_of_individuals = 60 + Q.nodes[1].number_in_service = 1 + Q.nodes[2].number_of_individuals = 20 + Q.nodes[2].number_in_service = 2 + Q.nodes[3].number_of_individuals = 20 + Q.nodes[3].number_in_service = 2 + samples_1 = Counter([r.id_number for r in [R1.next_node(ind) for _ in range(1000)]]) + samples_2 = Counter([r.id_number for r in [R2.next_node(ind) for _ in range(1000)]]) + samples_3 = Counter([r.id_number for r in [R3.next_node(ind) for _ in range(1000)]]) + self.assertEqual([samples_1[i] for i in [1, 2, 3, -1]], [0, 507, 493, 0]) + self.assertEqual([samples_2[i] for i in [1, 2, 3, -1]], [0, 0, 1000, 0]) + self.assertEqual([samples_3[i] for i in [1, 2, 3, -1]], [0, 516, 484, 0]) + + # Explicitly set random tie-break + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.JoinShortestQueue(destinations=[1, 2, 3], tie_break="random") + R2 = ciw.routing.JoinShortestQueue(destinations=[1, 3], tie_break="random") + R3 = ciw.routing.JoinShortestQueue(destinations=[3, 2], tie_break="random") + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + Q.nodes[1].number_of_individuals = 60 + Q.nodes[1].number_in_service = 1 + Q.nodes[2].number_of_individuals = 20 + Q.nodes[2].number_in_service = 2 + Q.nodes[3].number_of_individuals = 20 + Q.nodes[3].number_in_service = 2 + samples_1 = Counter([r.id_number for r in [R1.next_node(ind) for _ in range(1000)]]) + samples_2 = Counter([r.id_number for r in [R2.next_node(ind) for _ in range(1000)]]) + samples_3 = Counter([r.id_number for r in [R3.next_node(ind) for _ in range(1000)]]) + self.assertEqual([samples_1[i] for i in [1, 2, 3, -1]], [0, 507, 493, 0]) + self.assertEqual([samples_2[i] for i in [1, 2, 3, -1]], [0, 0, 1000, 0]) + self.assertEqual([samples_3[i] for i in [1, 2, 3, -1]], [0, 516, 484, 0]) + + # Explicitly set ordered tie-break + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.JoinShortestQueue(destinations=[1, 2, 3], tie_break="order") + R2 = ciw.routing.JoinShortestQueue(destinations=[1, 3], tie_break="order") + R3 = ciw.routing.JoinShortestQueue(destinations=[3, 2], tie_break="order") + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + Q.nodes[1].number_of_individuals = 60 + Q.nodes[1].number_in_service = 1 + Q.nodes[2].number_of_individuals = 20 + Q.nodes[2].number_in_service = 2 + Q.nodes[3].number_of_individuals = 20 + Q.nodes[3].number_in_service = 2 + samples_1 = Counter([r.id_number for r in [R1.next_node(ind) for _ in range(1000)]]) + samples_2 = Counter([r.id_number for r in [R2.next_node(ind) for _ in range(1000)]]) + samples_3 = Counter([r.id_number for r in [R3.next_node(ind) for _ in range(1000)]]) + self.assertTrue(all(r == 2 for r in samples_1)) + self.assertTrue(all(r == 3 for r in samples_2)) + self.assertTrue(all(r == 3 for r in samples_3)) + + + def test_jsq_raises_errors(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.JoinShortestQueue(destinations=[1, 2, 7]) + self.assertRaises(ValueError, R1.initialise, Q, 1) + + + def test_jsq_in_simulation(self): + """ + First consider a 2 node system, and one dummy node. The + dummy node is where all customers arrive, and routes customers + to the other nodes in a 50%-50% schema. + """ + N = ciw.create_network( + arrival_distributions=[ + ciw.dists.Exponential(rate=3.0), + None, + None, + ], + service_distributions=[ + ciw.dists.Deterministic(value=0.0), + ciw.dists.Exponential(rate=2.0), + ciw.dists.Exponential(rate=2.0), + ], + number_of_servers=[float('inf'), 1, 1], + routing=ciw.routing.NetworkRouting(routers=[ + ciw.routing.Probabilistic(destinations=[2, 3], probs=[0.5, 0.5]), + ciw.routing.Leave(), + ciw.routing.Leave() + ]) + ) + ciw.seed(0) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(100) + recs = Q.get_all_records() + waits_2 = [r.waiting_time for r in recs if r.node == 2] + waits_3 = [r.waiting_time for r in recs if r.node == 3] + self.assertEqual(round(sum(waits_2) / len(waits_2), 6), 0.941912) + self.assertEqual(round(sum(waits_3) / len(waits_3), 6), 2.209535) + self.assertEqual(round(max(waits_2), 6), 4.588178) + self.assertEqual(round(max(waits_3), 6), 8.652946) + """ + Now consider a 2 node system, and one dummy node. The + dummy node is where all customers arrive, and routes customers + to the other nodes with JSQ. + The average, and maximum waitint times are both lowered. + """ + N = ciw.create_network( + arrival_distributions=[ + ciw.dists.Exponential(rate=3.0), + None, + None, + ], + service_distributions=[ + ciw.dists.Deterministic(value=0.0), + ciw.dists.Exponential(rate=2.0), + ciw.dists.Exponential(rate=2.0), + ], + number_of_servers=[float('inf'), 1, 1], + routing=ciw.routing.NetworkRouting(routers=[ + ciw.routing.JoinShortestQueue(destinations=[2, 3]), + ciw.routing.Leave(), + ciw.routing.Leave() + ]) + ) + ciw.seed(0) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(100) + recs = Q.get_all_records() + waits_2 = [r.waiting_time for r in recs if r.node == 2] + waits_3 = [r.waiting_time for r in recs if r.node == 3] + self.assertEqual(round(sum(waits_2) / len(waits_2), 6), 0.536455) + self.assertEqual(round(sum(waits_3) / len(waits_3), 6), 0.531789) + self.assertEqual(round(max(waits_2), 6), 2.777136) + self.assertEqual(round(max(waits_3), 6), 2.507875) + + + def test_load_balancing(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.LoadBalancing(destinations=[1, 2, 3]) + R2 = ciw.routing.LoadBalancing(destinations=[1, 3]) + R3 = ciw.routing.LoadBalancing(destinations=[2, 3]) + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + Q.nodes[1].number_of_individuals = 3 + Q.nodes[1].number_in_service = 1 + Q.nodes[2].number_of_individuals = 3 + Q.nodes[2].number_in_service = 2 + Q.nodes[3].number_of_individuals = 10 + Q.nodes[3].number_in_service = 2 + samples_1 = Counter([r.id_number for r in [R1.next_node(ind) for _ in range(1000)]]) + samples_2 = [r.id_number for r in [R2.next_node(ind) for _ in range(1000)]] + samples_3 = [r.id_number for r in [R3.next_node(ind) for _ in range(1000)]] + self.assertEqual([samples_1[i] for i in [1, 2, 3, -1]], [507, 493, 0, 0]) + self.assertTrue(all(r == 1 for r in samples_2)) + self.assertTrue(all(r == 2 for r in samples_3)) + + + def test_cycle_routing(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.Cycle(cycle=[2, 3, 3]) + R2 = ciw.routing.Cycle(cycle=[1, 2]) + R3 = ciw.routing.Cycle(cycle=[1, -1, 2, -1]) + R1.initialise(Q, 1) + R2.initialise(Q, 2) + R3.initialise(Q, 3) + ind = ciw.Individual(1) + samples_1 = [r.id_number for r in [R1.next_node(ind) for _ in range(20)]] + samples_2 = [r.id_number for r in [R2.next_node(ind) for _ in range(20)]] + samples_3 = [r.id_number for r in [R3.next_node(ind) for _ in range(20)]] + self.assertEqual([2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 3], samples_1) + self.assertEqual([1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2], samples_2) + self.assertEqual([1, -1, 2, -1, 1, -1, 2, -1, 1, -1, 2, -1, 1, -1, 2, -1, 1, -1, 2, -1], samples_3) + + def test_cycle_routing_raises_errors(self): + ciw.seed(0) + Q = ciw.Simulation(N) + R1 = ciw.routing.Cycle(cycle=[1, 2, 7]) + self.assertRaises(ValueError, R1.initialise, Q, 1) diff --git a/ciw/tests/test_scheduling.py b/ciw/tests/test_scheduling.py index b74ea389..5ea9ae83 100644 --- a/ciw/tests/test_scheduling.py +++ b/ciw/tests/test_scheduling.py @@ -9,7 +9,7 @@ "Class 0": [ciw.dists.Exponential(0.05), ciw.dists.Exponential(0.04)], "Class 1": [ciw.dists.Exponential(0.04), ciw.dists.Exponential(0.06)], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 30], [2, 60], [1, 90], [3, 100]]), 3], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 2, 1, 3], shift_end_dates=[30, 60, 90, 100]), 3], queue_capacities=[float("Inf"), 10], service_distributions={ "Class 0": [ciw.dists.Deterministic(5.0), ciw.dists.Exponential(0.2)], @@ -23,6 +23,7 @@ class TestScheduling(unittest.TestCase): def test_change_shift_method(self): Q = ciw.Simulation(N_schedule) N = Q.transitive_nodes[0] + N.have_event() N.next_event_date = 30 self.assertEqual([str(obs) for obs in N.servers], ["Server 1 at Node 1"]) self.assertEqual([obs.busy for obs in N.servers], [False]) @@ -63,6 +64,7 @@ def test_change_shift_method(self): def test_take_servers_off_duty_method(self): Q = ciw.Simulation(N_schedule) N = Q.transitive_nodes[0] + N.have_event() N.add_new_servers(3) self.assertEqual( [str(obs) for obs in N.servers], @@ -106,6 +108,7 @@ def test_take_servers_off_duty_method(self): def test_kill_server_method(self): Q = ciw.Simulation(N_schedule) N = Q.transitive_nodes[0] + N.have_event() s = N.servers[0] self.assertEqual([str(obs) for obs in N.servers], ["Server 1 at Node 1"]) N.kill_server(s) @@ -128,6 +131,7 @@ def test_kill_server_method(self): def test_add_new_servers_method(self): Q = ciw.Simulation(N_schedule) N = Q.transitive_nodes[0] + N.have_event() self.assertEqual([str(obs) for obs in N.servers], ["Server 1 at Node 1"]) N.add_new_servers(3) self.assertEqual( @@ -179,6 +183,7 @@ def test_take_servers_off_duty_preempt_method(self): N.service_centres[0].number_of_servers.preemption = 'resample' Q = ciw.Simulation(N) N = Q.transitive_nodes[0] + N.have_event() N.add_new_servers(3) self.assertEqual( [str(obs) for obs in N.servers], @@ -233,7 +238,7 @@ def test_full_preemptive_simulation(self): arrival_distributions=[ciw.dists.Deterministic(7.0)], service_distributions=[ciw.dists.Deterministic(5.0)], routing=[[0.0]], - number_of_servers=[ciw.Schedule(schedule=[[1, 15], [0, 17], [2, 100]], preemption="resample")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 2], shift_end_dates=[15, 17, 100], preemption="resample")], ) Q = ciw.Simulation(N) @@ -257,7 +262,7 @@ def test_full_preemptive_simulation(self): arrival_distributions=[ciw.dists.Deterministic(7.0)], service_distributions=[ciw.dists.Deterministic(5.0)], routing=[[0.0]], - number_of_servers=[ciw.Schedule(schedule=[[1, 15], [0, 17], [2, 100]], preemption="resume")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 2], shift_end_dates=[15, 17, 100], preemption="resume")], ) Q = ciw.Simulation(N) @@ -277,7 +282,7 @@ def test_full_preemptive_simulation(self): arrival_distributions=[ciw.dists.Deterministic(3.0)], service_distributions=[ciw.dists.Deterministic(10.0)], routing=[[0.0]], - number_of_servers=[ciw.Schedule(schedule=[[4, 12.5], [0, 17], [1, 100]], preemption="resume")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[4, 0, 1], shift_end_dates=[12.5, 17, 100], preemption="resume")], ) Q = ciw.Simulation(N) self.assertEqual(Q.nodes[1].schedule.preemption, 'resume') @@ -299,7 +304,7 @@ def test_overtime(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([1.0, 0.0, 0.0, 8.0, 0.0, 3.0, 10000.0])], service_distributions=[ciw.dists.Sequential([5.0, 7.0, 9.0, 4.0, 5.0, 5.0])], - number_of_servers=[ciw.Schedule(schedule=[[3, 7.0], [2, 11.0], [1, 20.0]], preemption=False)], + number_of_servers=[ciw.Schedule(numbers_of_servers=[3, 2, 1], shift_end_dates=[7.0, 11.0, 20.0], preemption=False)], ) Q = ciw.Simulation(N) Q.simulate_until_max_time(19.0) @@ -312,7 +317,7 @@ def test_overtime_exact(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([1.0, 0.0, 0.0, 8.0, 0.0, 3.0, 10000.0])], service_distributions=[ciw.dists.Sequential([5.0, 7.0, 9.0, 4.0, 5.0, 5.0])], - number_of_servers=[ciw.Schedule(schedule=[[3, 7.0], [2, 11.0], [1, 20.0]], preemption=False)], + number_of_servers=[ciw.Schedule(numbers_of_servers=[3, 2, 1], shift_end_dates=[7.0, 11.0, 20.0], preemption=False)], ) Q = ciw.Simulation(N, exact=26) Q.simulate_until_max_time(19.0) @@ -351,7 +356,7 @@ def test_preemptive_schedules_resume_options(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([1, float("inf")])], service_distributions=[ciw.dists.Sequential([10, 20])], - number_of_servers=[ciw.Schedule(schedule=[[1, 5], [0, 9], [1, 100]], preemption="restart")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[5, 9, 100], preemption="restart")], ) Q = ciw.Simulation(N) Q.simulate_until_max_time(40) @@ -367,7 +372,7 @@ def test_preemptive_schedules_resume_options(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([1, float("inf")])], service_distributions=[ciw.dists.Sequential([10, 20])], - number_of_servers=[ciw.Schedule(schedule=[[1, 5], [0, 9], [1, 100]], preemption="resume")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[5, 9, 100], preemption="resume")], ) Q = ciw.Simulation(N) Q.simulate_until_max_time(40) @@ -383,7 +388,7 @@ def test_preemptive_schedules_resume_options(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([1, float("inf")])], service_distributions=[ciw.dists.Sequential([10, 20])], - number_of_servers=[ciw.Schedule(schedule=[[1, 5], [0, 9], [1, 100]], preemption="resample")], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[5, 9, 100], preemption="resample")], ) Q = ciw.Simulation(N) Q.simulate_until_max_time(40) @@ -413,7 +418,7 @@ def test_priority_preemption_when_zero_servers(self): "Class 0": [ciw.dists.Deterministic(5.5)], "Class 1": [ciw.dists.Deterministic(1.5)], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 20.3], [0, 20.6], [1, 100]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[20.3, 20.6, 100])], priority_classes=({"Class 0": 1, "Class 1": 0}, ["resume"]), ) @@ -480,7 +485,7 @@ def test_priority_preemption_when_zero_servers(self): "Class 0": [ciw.dists.Deterministic(5.5)], "Class 1": [ciw.dists.Deterministic(1.5)], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 20.3], [0, 22], [1, 100]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[20.3, 22, 100])], priority_classes=({"Class 0": 1, "Class 1": 0}, ["resume"]), ) @@ -541,7 +546,7 @@ def test_resuming_interruption_after_blockage(self): N = ciw.create_network( arrival_distributions=[ciw.dists.Sequential([5, float('inf')]), ciw.dists.Sequential([1, float('inf')])], service_distributions=[ciw.dists.Deterministic(1), ciw.dists.Deterministic(9)], - number_of_servers=[ciw.Schedule(schedule=[[1, 8], [0, 200]], preemption='resume'), 1], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0], shift_end_dates=[8, 200], preemption='resume'), 1], queue_capacities=[float('inf'), 0], routing=[[0.0, 1.0], [0.0, 0.0]] ) @@ -628,6 +633,30 @@ def test_slotted_services(self): self.assertEqual(observed_service_dates, expected_service_dates) self.assertFalse(any([ind.server for ind in Q.nodes[-1].all_individuals])) + def test_slotted_services_with_offset(self): + """ + Arrivals occur at times [0.3, 0.5, 0.7, 1.2, 1.3, 1.8, 2.6, 2.7, 3.1] + All services last 0.5 time units + Services are slotted at times [1.05, 2.05, 3.05, 4.05] + Slotted services have capacities [3, 2, 5, 3] + """ + N = ciw.create_network( + arrival_distributions=[ciw.dists.Sequential([0.3, 0.2, 0.2, 0.5, 0.1, 0.5, 0.8, 0.1, 0.4, float('inf')])], + service_distributions=[ciw.dists.Deterministic(0.5)], + number_of_servers=[ciw.Slotted(slots=[1, 2, 3, 4], slot_sizes=[3, 2, 5, 3], offset=0.05)] + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(5.6) + recs = Q.get_all_records() + recs = sorted(recs, key=lambda rec: rec.id_number) + + self.assertEqual(len(Q.nodes[-1].all_individuals), 9) + + expected_service_dates = [1.05, 1.05, 1.05, 2.05, 2.05, 3.05, 3.05, 3.05, 4.05] + observed_service_dates = [r.service_start_date for r in recs] + self.assertEqual(observed_service_dates, expected_service_dates) + self.assertFalse(any([ind.server for ind in Q.nodes[-1].all_individuals])) + def test_slotted_services_repeat(self): """ Arrivals occur at times [0.7, 1.4, 2.6, 2.9, 4.3, 5.5, 6.1, 15.0] @@ -717,7 +746,7 @@ def test_slotted_services_capacitated(self): self.assertEqual(observed_service_dates, expected_service_dates) def test_invalid_preemption_options(self): - self.assertRaises(ValueError, lambda: ciw.Schedule(schedule=[[2, 10], [1, 12]], preemption='something')) + self.assertRaises(ValueError, lambda: ciw.Schedule(numbers_of_servers=[2, 1], shift_end_dates=[10, 12], preemption='something')) self.assertRaises(ValueError, lambda: ciw.Slotted(slots=[2, 3], slot_sizes=[4, 1], capacitated=False, preemption='resume')) self.assertRaises(ValueError, lambda: ciw.Slotted(slots=[2, 3], slot_sizes=[4, 1], capacitated=True, preemption='something')) @@ -863,3 +892,41 @@ def test_slotted_services_capacitated_preemption(self): self.assertEqual(recs[2].exit_date, 25) self.assertEqual(recs[2].record_type, 'service') + def test_offset_error_raising(self): + self.assertRaises(ValueError, lambda: ciw.Slotted(slots=[4, 11], slot_sizes=[3, 1], offset='something')) + self.assertRaises(ValueError, lambda: ciw.Slotted(slots=[4, 11], slot_sizes=[3, 1], offset=-6.7)) + self.assertRaises(ValueError, lambda: ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[10, 25], offset='something')) + self.assertRaises(ValueError, lambda: ciw.Schedule(numbers_of_servers=[1, 2], shift_end_dates=[10, 25], offset=-6.7)) + + def test_schedules_with_offset(self): + """ + First with no offset + """ + N = ciw.create_network( + arrival_distributions=[ciw.dists.Sequential([0.1, float('inf')])], + service_distributions=[ciw.dists.Deterministic(10)], + batching_distributions=[ciw.dists.Deterministic(10)], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 6, 0], shift_end_dates=[5, 20, 23, 30])] + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(3000) + recs = Q.get_all_records() + + expected_service_dates = [0.1, 20, 20, 20, 20, 20, 20, 30, 50, 50] + self.assertEqual(expected_service_dates, [r.service_start_date for r in recs]) + """ + Now with an offset + """ + N = ciw.create_network( + arrival_distributions=[ciw.dists.Sequential([0.1, float('inf')])], + service_distributions=[ciw.dists.Deterministic(10)], + batching_distributions=[ciw.dists.Deterministic(10)], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 6, 0], shift_end_dates=[5, 20, 23, 30], offset=0.5)] + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(3000) + recs = Q.get_all_records() + + expected_service_dates = [0.5, 20.5, 20.5, 20.5, 20.5, 20.5, 20.5, 30.5, 50.5, 50.5] + self.assertEqual(expected_service_dates, [r.service_start_date for r in recs]) + diff --git a/ciw/tests/test_simulation.py b/ciw/tests/test_simulation.py index ae6122d5..29c8b72a 100644 --- a/ciw/tests/test_simulation.py +++ b/ciw/tests/test_simulation.py @@ -8,6 +8,7 @@ import csv from itertools import cycle import types +import math N_params = ciw.create_network( arrival_distributions={ @@ -416,7 +417,7 @@ def test_exactness(self): arrival_distributions=[ciw.dists.Exponential(20)], service_distributions=[ciw.dists.Deterministic(0.01)], routing=[[0.0]], - number_of_servers=[ciw.Schedule(schedule=[[0, 0.5], [1, 0.55], [0, 3.0]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[0, 1, 0], shift_end_dates=[0.5, 0.55, 3.0])], ) ciw.seed(777) Q = ciw.Simulation(N) @@ -437,7 +438,7 @@ def test_exactness(self): arrival_distributions=[ciw.dists.Exponential(20)], service_distributions=[ciw.dists.Deterministic(0.01)], routing=[[0.0]], - number_of_servers=[ciw.Schedule(schedule=[[0, 0.5], [1, 0.55], [0, 3.0]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[0, 1, 0], shift_end_dates=[0.5, 0.55, 3.0])], ) ciw.seed(777) @@ -578,7 +579,7 @@ class DummyArrivalNode(ciw.ArrivalNode): arrival_distributions=[ciw.dists.Exponential(20)], service_distributions=[ciw.dists.Deterministic(0.01)], routing=[[0.0]], - number_of_servers=[ciw.Schedule(schedule=[[0, 0.5], [1, 0.55], [0, 3.0]])], + number_of_servers=[ciw.Schedule(numbers_of_servers=[0, 1, 0], shift_end_dates=[0.5, 0.55, 3.0])], ) Q = ciw.Simulation(N, node_class=None, arrival_node_class=None) self.assertEqual(Q.NodeTypes, [ciw.Node]) @@ -629,7 +630,7 @@ class DummyNode3(ciw.Node): ciw.dists.Exponential(10), ciw.dists.Exponential(10), ], - routing=[[0, 0, 0], [0, 0, 0], [0, 0, 0]], + routing=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], number_of_servers=[1, 1, 1], ) @@ -715,7 +716,7 @@ def create_simulation_with_node_classes(node_classes): ciw.dists.Exponential(10), ciw.dists.Exponential(10), ], - routing=[[0, 0, 0], [0, 0, 0], [0, 0, 0]], + routing=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], number_of_servers=[1, 1, 1], ) return ciw.Simulation(N, node_class=node_classes) @@ -1094,7 +1095,7 @@ def test_schedules_and_blockages_work_together(self): "Class 0": [ciw.dists.Exponential(0.8), ciw.dists.Exponential(1.2)], "Class 1": [ciw.dists.Exponential(0.5), ciw.dists.Exponential(1.0)], }, - number_of_servers=[ciw.Schedule(schedule=[[1, 10], [0, 20], [2, 30]], preemption="resample"), 2], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 2], shift_end_dates=[10, 20, 30], preemption="resample"), 2], routing={ "Class 0": [[0.1, 0.3], [0.2, 0.2]], "Class 1": [[0.0, 0.6], [0.2, 0.1]], @@ -1105,7 +1106,7 @@ def test_schedules_and_blockages_work_together(self): ], queue_capacities=[2, 2], ) - ciw.seed(11) + ciw.seed(10) Q = ciw.Simulation( N, deadlock_detector=ciw.deadlock.StateDigraph(), @@ -1113,7 +1114,7 @@ def test_schedules_and_blockages_work_together(self): ) Q.simulate_until_deadlock() ttd = Q.times_to_deadlock[((0, 0), (0, 0))] - self.assertEqual(round(ttd, 5), 119.65819) + self.assertEqual(round(ttd, 5), 51.75691) N = ciw.create_network( arrival_distributions=[ciw.dists.Deterministic(1.0), None], @@ -1121,7 +1122,7 @@ def test_schedules_and_blockages_work_together(self): ciw.dists.Deterministic(0.1), ciw.dists.Deterministic(3.0), ], - number_of_servers=[ciw.Schedule(schedule=[[1, 2.5], [0, 2.8]], preemption="resample"), 1], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0], shift_end_dates=[2.5, 2.8], preemption="resample"), 1], queue_capacities=[float("inf"), 0], routing=[[0.0, 1.0], [0.0, 0.0]], ) @@ -1140,6 +1141,373 @@ def test_generic_deadlock_detector(self): DD = ciw.deadlock.NoDetection() self.assertEqual(DD.detect_deadlock(), False) + def test_system_capacity(self): + """ + Expected results would be: + + node id arrival_date current_capacity start_date end_date + ---------------------------------------------------------------- + 1 1 0.8 1 0.8 5.9 + 2 2 1.0 2 1.0 6.1 + 1 3 1.6 3 5.9 11.0 + 2 4 2.0 4 6.1 11.2 + 1 5 2.4 5 11.0 16.1 + 2 6 3.0 reject + 1 7 3.2 reject + 1,2 8,9 4.0 reject + 1 10 4.8 reject + 2 11 5.0 reject + 1 12 5.6 reject + 2 13 6.0 5 11.2 16.3 + 1 14 6.4 5 16.1 21.2 + 2 15 7.0 reject + 1 16 7.2 reject + 1,2 17,18 8.0 reject + 1 19 8.8 reject + 2 20 9.0 reject + 1 21 9.6 reject + 2 22 10.0 reject + 1 23 10.4 reject + 2 24 11.0 5 16.3 21.4 + 1 25 11.2 5 21.2 26.3 (incomplete) + 2 26 12.0 reject + 1 27 12.0 reject + """ + N = ciw.create_network( + arrival_distributions=[ciw.dists.Deterministic(0.8), ciw.dists.Deterministic(1)], + service_distributions=[ciw.dists.Deterministic(5.1), ciw.dists.Deterministic(5.1)], + routing=[[0.0, 0.0], [0.0, 0.0]], + number_of_servers=[1, 1], + system_capacity=5 + ) + ciw.seed(0) ## Set seed to exactly replicate simultaneous events + Q = ciw.Simulation(N, tracker=ciw.trackers.SystemPopulation()) + Q.simulate_until_max_time(22) + recs_service = sorted(Q.get_all_records(only=['service']), key=lambda r: r.arrival_date) + recs_reject = sorted(Q.get_all_records(only=['rejection']), key=lambda r: r.arrival_date) + + expected_arrivals = [0.8, 1.0, 1.6, 2.0, 2.4, 6.0, 6.4, 11.0] + expected_ssds = [0.8, 1.0, 5.9, 6.1, 11.0, 11.2, 16.1, 16.3] + expected_seds = [5.9, 6.1, 11.0, 11.2, 16.1, 16.3, 21.2, 21.4] + expected_nodes = [1, 2, 1, 2, 1, 2, 1, 2] + expected_first_10_rejection_times = [3.0, 3.2, 4.0, 4.0, 4.8, 5.0, 5.6, 7.0, 7.2, 8.0] + + self.assertTrue(all([s[1] <= 5 for s in Q.statetracker.history])) + self.assertEqual([round(r.arrival_date, 2) for r in recs_service], expected_arrivals) + self.assertEqual([r.node for r in recs_service], expected_nodes) + self.assertEqual([round(r.service_start_date, 2) for r in recs_service], expected_ssds) + self.assertEqual([round(r.service_end_date, 2) for r in recs_service], expected_seds) + self.assertEqual([round(r.arrival_date, 2) for r in recs_reject][:10], expected_first_10_rejection_times) + + def test_reroute_preemption_classpriorities(self): + """ + Class 0 arrive to Node 1 every 0.7, service lasts 0.2 + Class 1 arrive to Node 1 every 1.0, service lasts 0.2 + Class 0 have priority over class 1 + Class 1 rerouted to Node 2 upon preemption + + Ind arr clss end + 1 0.7 0 0.9 + 2 1.0 1 1.2 + 3 1.4 0 1.6 + 4 2.0 1 interrupted (goes to Node 2 for 0.5) + 5 2.1 0 2.3 + """ + class Reroute(ciw.routing.NodeRouting): + def next_node(self, ind): + """ + Chooses the exit node with probability 1. + """ + return self.simulation.nodes[-1] + + def next_node_for_rerouting(self, ind): + """ + Chooses Node 2 + """ + return self.simulation.nodes[2] + + N = ciw.create_network( + arrival_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.7), None], + 'Class 1': [ciw.dists.Deterministic(value=1.0), None] + }, + service_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.5)], + 'Class 1': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.5)] + }, + number_of_servers=[1, 1], + priority_classes=({'Class 0': 0, 'Class 1': 1}, ['reroute', False]), + routing={ + 'Class 0': ciw.routing.NetworkRouting(routers=[ciw.routing.Leave(), ciw.routing.Leave()]), + 'Class 1': ciw.routing.NetworkRouting(routers=[Reroute(), ciw.routing.Leave()]) + } + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(2.7) + recs = Q.get_all_records() + + nd_1 = sorted([r for r in recs if r.node == 1], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_1], [0.7, 1.0, 1.4, 2.0, 2.1]) + self.assertEqual([round(r.service_time, 5) for r in nd_1], [0.2, 0.2, 0.2, 0.2, 0.2]) + self.assertEqual([round(r.exit_date, 5) for r in nd_1], [0.9, 1.2, 1.6, 2.1, 2.3]) + self.assertEqual([r.destination for r in nd_1], [-1, -1, -1, 2, -1]) + self.assertEqual([r.record_type for r in nd_1], ['service', 'service', 'service', 'interrupted service', 'service']) + + nd_2 = sorted([r for r in recs if r.node == 2], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_2], [2.1]) + self.assertEqual([round(r.service_time, 5) for r in nd_2], [0.5]) + self.assertEqual([round(r.exit_date, 5) for r in nd_2], [2.6]) + self.assertEqual([r.destination for r in nd_2], [-1]) + self.assertEqual([r.record_type for r in nd_2], ['service']) + + """ + Class 0 arrive to Node 1 every 0.7, service lasts 0.2 + Class 1 arrive to Node 1 every 1.0, service lasts 0.2 + Class 0 have priority over class 1 + All classes go Node 1 then Node 2 + + Node 1 + Ind arr clss end + 1 0.7 0 0.9 + 2 1.0 1 1.2 + 3 1.4 0 1.6 + 4 2.0 1 interrupted + 5 2.1 0 2.3 + + Node 2 + Ind arr clss end + 1 0.9 0 1.0 + 2 1.2 1 1.3 + 3 1.6 0 1.7 + 4 2.1 1 2.2 + 5 2.3 0 2.4 + """ + N = ciw.create_network( + arrival_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.7), None], + 'Class 1': [ciw.dists.Deterministic(value=1.0), None] + }, + service_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.1)], + 'Class 1': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.1)] + }, + number_of_servers=[1, 1], + priority_classes=({'Class 0': 0, 'Class 1': 1}, ['reroute', False]), + routing={ + 'Class 0': ciw.routing.TransitionMatrix(transition_matrix=[[0.0, 1.0], [0.0, 0.0]]), + 'Class 1': ciw.routing.TransitionMatrix(transition_matrix=[[0.0, 1.0], [0.0, 0.0]]) + } + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(2.7) + recs = Q.get_all_records() + + nd_1 = sorted([r for r in recs if r.node == 1], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_1], [0.7, 1.0, 1.4, 2.0, 2.1]) + self.assertEqual([round(r.service_time, 5) for r in nd_1], [0.2, 0.2, 0.2, 0.2, 0.2]) + self.assertEqual([round(r.exit_date, 5) for r in nd_1], [0.9, 1.2, 1.6, 2.1, 2.3]) + self.assertEqual([r.destination for r in nd_1], [2, 2, 2, 2, 2]) + self.assertEqual([r.record_type for r in nd_1], ['service', 'service', 'service', 'interrupted service', 'service']) + + nd_2 = sorted([r for r in recs if r.node == 2], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_2], [0.9, 1.2, 1.6, 2.1, 2.3]) + self.assertEqual([round(r.service_time, 5) for r in nd_2], [0.1, 0.1, 0.1, 0.1, 0.1]) + self.assertEqual([round(r.exit_date, 5) for r in nd_2], [1.0, 1.3, 1.7, 2.2, 2.4]) + self.assertEqual([r.destination for r in nd_2], [-1, -1, -1, -1, -1]) + self.assertEqual([r.record_type for r in nd_2], ['service', 'service', 'service', 'service', 'service']) + + def test_reroute_preemption_classpriorities_process_based(self): + """ + Class 0 arrive to Node 1 every 0.7, service lasts 0.2 + Class 1 arrive to Node 1 every 1.0, service lasts 0.2 + Class 0 have priority over class 1 + All classes go Node 1 then Node 2 + + Node 1 + Ind arr clss end + 1 0.7 0 0.9 + 2 1.0 1 1.2 + 3 1.4 0 1.6 + 4 2.0 1 interrupted + 5 2.1 0 2.3 + + Node 2 + Ind arr clss end + 1 0.9 0 1.0 + 2 1.2 1 1.3 + 3 1.6 0 1.7 + 4 2.1 1 2.2 + 5 2.3 0 2.4 + """ + def from_1_to_2(ind, simulation): + return [2] + + N = ciw.create_network( + arrival_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.7), None], + 'Class 1': [ciw.dists.Deterministic(value=1.0), None] + }, + service_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.1)], + 'Class 1': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.1)] + }, + number_of_servers=[1, 1], + priority_classes=({'Class 0': 0, 'Class 1': 1}, ['reroute', False]), + routing={ + 'Class 0': ciw.routing.ProcessBased(from_1_to_2), + 'Class 1': ciw.routing.ProcessBased(from_1_to_2) + } + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(2.7) + recs = Q.get_all_records() + + nd_1 = sorted([r for r in recs if r.node == 1], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_1], [0.7, 1.0, 1.4, 2.0, 2.1]) + self.assertEqual([round(r.service_time, 5) for r in nd_1], [0.2, 0.2, 0.2, 0.2, 0.2]) + self.assertEqual([round(r.exit_date, 5) for r in nd_1], [0.9, 1.2, 1.6, 2.1, 2.3]) + self.assertEqual([r.destination for r in nd_1], [2, 2, 2, 2, 2]) + self.assertEqual([r.record_type for r in nd_1], ['service', 'service', 'service', 'interrupted service', 'service']) + + nd_2 = sorted([r for r in recs if r.node == 2], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_2], [0.9, 1.2, 1.6, 2.1, 2.3]) + self.assertEqual([round(r.service_time, 5) for r in nd_2], [0.1, 0.1, 0.1, 0.1, 0.1]) + self.assertEqual([round(r.exit_date, 5) for r in nd_2], [1.0, 1.3, 1.7, 2.2, 2.4]) + self.assertEqual([r.destination for r in nd_2], [-1, -1, -1, -1, -1]) + self.assertEqual([r.record_type for r in nd_2], ['service', 'service', 'service', 'service', 'service']) + + def test_rerouting_ignores_queue_capacities(self): + """ + Class 0 arrive to Node 1 every 0.7, service lasts 0.2 + Class 1 arrive to Node 1 every 1.0, service lasts 0.2 + Class 0 have priority over class 1 + Class 1 rerouted to Node 2 upon preemption + Class 2 arrive at Node 2 every 0.6 + + Node 1 + Ind arr clss end + 2 0.7 0 0.9 + 3 1.0 1 1.2 + 5 1.4 0 1.6 + 6 2.0 1 interrupted (goes to Node 2 for 0.5) + 7 2.1 0 2.3 + + Node 2 + Ind arr clss end + 1 0.6 2 1.3 + 4 1.2 2 rejected + 3 1.8 2 2.5 + 6 2.1 1 (2.5 + 0.7 =) 3.2 (individual is accepted even though a queue capacity of 0) + 8 2.4 2 rejected + """ + class Reroute(ciw.routing.NodeRouting): + def next_node(self, ind): + """ + Chooses the exit node with probability 1. + """ + return self.simulation.nodes[-1] + + def next_node_for_rerouting(self, ind): + """ + Chooses Node 2 + """ + return self.simulation.nodes[2] + + N = ciw.create_network( + arrival_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.7), None], + 'Class 1': [ciw.dists.Deterministic(value=1.0), None], + 'Class 2': [None, ciw.dists.Deterministic(value=0.6)] + }, + service_distributions={ + 'Class 0': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.7)], + 'Class 1': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.7)], + 'Class 2': [ciw.dists.Deterministic(value=0.2), ciw.dists.Deterministic(value=0.7)] + }, + number_of_servers=[1, 1], + queue_capacities=[float('inf'), 0], + priority_classes=({'Class 0': 0, 'Class 1': 1, 'Class 2': 1}, ['reroute', False]), + routing={ + 'Class 0': ciw.routing.NetworkRouting(routers=[ciw.routing.Leave(), ciw.routing.Leave()]), + 'Class 1': ciw.routing.NetworkRouting(routers=[Reroute(), ciw.routing.Leave()]), + 'Class 2': ciw.routing.NetworkRouting(routers=[ciw.routing.Leave(), ciw.routing.Leave()]) + } + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(3.3) + recs = Q.get_all_records() + + nd_1 = sorted([r for r in recs if r.node == 1], key=lambda r: r.arrival_date)[:5] + self.assertEqual([round(r.arrival_date, 5) for r in nd_1], [0.7, 1.0, 1.4, 2.0, 2.1]) + self.assertEqual([round(r.service_time, 5) for r in nd_1], [0.2, 0.2, 0.2, 0.2, 0.2]) + self.assertEqual([round(r.exit_date, 5) for r in nd_1], [0.9, 1.2, 1.6, 2.1, 2.3]) + self.assertEqual([r.destination for r in nd_1], [-1, -1, -1, 2, -1]) + self.assertEqual([r.record_type for r in nd_1], ['service', 'service', 'service', 'interrupted service', 'service']) + + nd_2 = sorted([r for r in recs if r.node == 2], key=lambda r: r.arrival_date)[:5] + self.assertEqual([round(r.arrival_date, 5) for r in nd_2], [0.6, 1.2, 1.8, 2.1, 2.4]) + self.assertEqual([str(round(r.service_time, 5)) for r in nd_2], ['0.7', 'nan', '0.7', '0.7', 'nan']) + self.assertEqual([round(r.exit_date, 5) for r in nd_2], [1.3, 1.2, 2.5, 3.2, 2.4]) + self.assertEqual([str(r.destination) for r in nd_2], ['-1', 'nan', '-1', '-1', 'nan']) + self.assertEqual([r.record_type for r in nd_2], ['service', 'rejection', 'service', 'service', 'rejection']) + + def test_reroute_at_shift_change(self): + """ + Two nodes: arrivals to Node 1 every 1 time unit; service lasts 0.4 time units. + One server on duty from - to 4.2; 0 from 4.2 to 6.1; 1 from 6.1 to 100. + Interrupted individuals go to node 2, have service time 1. + + Node 1 + Ind arr exit + 1 1.0 1.4 + 2 2.0 2.4 + 3 3.0 4.4 + 4 4.0 4.2 (interrupted) + 5 5.0 6.5 (service started at 6.1 when server back on duty) + 6 6.0 6.9 (service starts after ind 5 finishes service) + 7 7.0 7.4 + + Node 2 + Ind arr exit + 4 4.2 5.2 + """ + class Reroute(ciw.routing.NodeRouting): + def next_node(self, ind): + """ + Chooses the exit node with probability 1. + """ + return self.simulation.nodes[-1] + + def next_node_for_rerouting(self, ind): + """ + Chooses Node 2 + """ + return self.simulation.nodes[2] + + N = ciw.create_network( + arrival_distributions=[ciw.dists.Deterministic(value=1.0), None], + service_distributions=[ciw.dists.Deterministic(value=0.4), ciw.dists.Deterministic(value=1.0)], + number_of_servers=[ciw.Schedule(numbers_of_servers=[1, 0, 1], shift_end_dates=[4.2, 6.1, 100], preemption="reroute"), 1], + routing=ciw.routing.NetworkRouting(routers=[Reroute(), ciw.routing.Leave()]) + ) + Q = ciw.Simulation(N) + Q.simulate_until_max_time(7.5) + + recs = Q.get_all_records() + nd_1 = sorted([r for r in recs if r.node == 1], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_1], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) + self.assertEqual([round(r.service_time, 5) for r in nd_1], [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]) + self.assertEqual([round(r.exit_date, 5) for r in nd_1], [1.4, 2.4, 3.4, 4.2, 6.5, 6.9, 7.4]) + self.assertEqual([r.destination for r in nd_1], [-1, -1, -1, 2, -1, -1, -1]) + self.assertEqual([r.record_type for r in nd_1], ['service', 'service', 'service', 'interrupted service', 'service', 'service', 'service']) + + nd_2 = sorted([r for r in recs if r.node == 2], key=lambda r: r.arrival_date) + self.assertEqual([round(r.arrival_date, 5) for r in nd_2], [4.2]) + self.assertEqual([round(r.service_time, 5) for r in nd_2], [1.0]) + self.assertEqual([round(r.exit_date, 5) for r in nd_2], [5.2]) + self.assertEqual([r.destination for r in nd_2], [-1]) + self.assertEqual([r.record_type for r in nd_2], ['service']) + class TestServiceDisciplines(unittest.TestCase): def test_first_in_first_out(self): @@ -1326,3 +1694,4 @@ def test_names_for_customer_classes(self): mean_child_wait = sum(child_waits) / len(child_waits) self.assertEqual(round(mean_adult_wait, 8), 0.00301455) self.assertEqual(round(mean_child_wait, 8), 0.00208601) + diff --git a/ciw/tests/test_state_tracker.py b/ciw/tests/test_state_tracker.py index d0482714..e904c018 100644 --- a/ciw/tests/test_state_tracker.py +++ b/ciw/tests/test_state_tracker.py @@ -1922,3 +1922,93 @@ def test_nodeclassmatrix_accept_method_within_simulation_custnames(self): [0, 0, 1], [0, 0, 0]] ) + +class TestGroupedNodePopulation(unittest.TestCase): + def test_groupednodepop_init_method(self): + Q = ciw.Simulation(N_params) + B = ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]]) + B.initialise(Q) + self.assertEqual(B.simulation, Q) + self.assertEqual(B.state, [0, 0]) + + def test_groupednodepop_change_state_accept_method(self): + Q = ciw.Simulation(N_params) + B = ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]]) + B.initialise(Q) + N = Q.nodes[1] + ind = ciw.Individual(1) + self.assertEqual(B.state, [0, 0]) + B.change_state_accept(N, ind) + self.assertEqual(B.state, [1, 0]) + + def test_groupednodepop_change_state_block_method(self): + Q = ciw.Simulation(N_params) + B = ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]]) + B.initialise(Q) + N1 = Q.nodes[1] + N2 = Q.nodes[2] + ind = ciw.Individual(1) + B.state = [1, 0] + B.change_state_block(N1, N2, ind) + self.assertEqual(B.state, [1, 0]) + + def test_groupednodepop_change_state_release_method(self): + Q = ciw.Simulation(N_params) + B = ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]]) + B.initialise(Q) + N = Q.nodes[1] + N2 = Q.nodes[2] + Nex = Q.nodes[-1] + ind1 = ciw.Individual(1) + ind2 = ciw.Individual(2) + ind3 = ciw.Individual(3) + B.state = [12, 3] + B.change_state_release(N, Nex, ind1, False) + self.assertEqual(B.state, [11, 3]) + B.change_state_release(N, Nex, ind2, True) + self.assertEqual(B.state, [10, 3]) + B.change_state_release(N2, Nex, ind3, True) + self.assertEqual(B.state, [10, 2]) + + def test_groupednodepop_hash_state_method(self): + Q = ciw.Simulation(N_params) + B = ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]]) + B.initialise(Q) + B.state = [7, 3] + self.assertEqual(B.hash_state(), (7, 3)) + + def test_groupednodepop_release_method_within_simulation(self): + Q = ciw.Simulation(N_params, tracker=ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]])) + N = Q.transitive_nodes[2] + inds = [ciw.Individual(i, 'Class 0') for i in range(5)] + N.individuals = [inds] + for ind in N.individuals[0]: + srvr = N.find_free_server(ind) + N.attach_server(srvr, ind) + Q.statetracker.state = [11, 3] + self.assertEqual(Q.statetracker.state, [11, 3]) + Q.current_time = 43.11 + N.release(N.all_individuals[0], Q.nodes[1]) + self.assertEqual(Q.statetracker.state, [11, 3]) + N.all_individuals[1].is_blocked = True + Q.current_time = 46.72 + N.release(N.all_individuals[1], Q.nodes[1]) + self.assertEqual(Q.statetracker.state, [11, 3]) + N.release(N.all_individuals[1], Q.nodes[-1]) + self.assertEqual(Q.statetracker.state, [10, 3]) + + def test_groupednodepop_block_method_within_simulation(self): + Q = ciw.Simulation(N_params, tracker=ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]])) + N = Q.transitive_nodes[2] + Q.statetracker.state = [11, 3] + self.assertEqual(Q.statetracker.state, [11, 3]) + N.block_individual(ciw.Individual(1), Q.nodes[1]) + self.assertEqual(Q.statetracker.state, [11, 3]) + + def test_groupednodepop_accept_method_within_simulation(self): + Q = ciw.Simulation(N_params, tracker=ciw.trackers.GroupedNodePopulation(groups=[[0, 2, 3], [1]])) + N = Q.transitive_nodes[2] + self.assertEqual(Q.statetracker.state, [0, 0]) + Q.current_time = 45.6 + N.accept(ciw.Individual(3, 'Class 2')) + self.assertEqual(Q.statetracker.state, [1, 0]) diff --git a/ciw/trackers/state_tracker.py b/ciw/trackers/state_tracker.py index c9c9b5fd..5fdc06b9 100644 --- a/ciw/trackers/state_tracker.py +++ b/ciw/trackers/state_tracker.py @@ -242,6 +242,63 @@ def hash_state(self): return tuple(self.state) +class GroupedNodePopulation(StateTracker): + """ + The node population tracker records the number of customers at each group + of nodes, where node groups are defined by the user. + + Example: + (3, 1) + This denotes 3 customers at the first node group, and 1 customer at the + second node group. + """ + + def __init__(self, groups): + """ + Pre-initialises the object with keyword `observed_nodes` + """ + self.groups = groups + self.observed_nodes = [nd for group in groups for nd in group] + + def initialise(self, simulation): + """ + Initialises the state tracker class. + """ + self.simulation = simulation + self.state = [0 for i in self.groups] + self.history = [[self.simulation.current_time, self.hash_state()]] + + def change_state_accept(self, node, ind): + """ + Changes the state of the system when a customer is accepted. + """ + if node.id_number - 1 in self.observed_nodes: + node_in_group = [(node.id_number - 1) in group for group in self.groups] + group_index = node_in_group.index(True) + self.state[group_index] += 1 + + def change_state_block(self, node, destination, ind): + """ + Changes the state of the system when a customer gets blocked. + """ + pass + + def change_state_release(self, node, destination, ind, blocked): + """ + Changes the state of the system when a customer is released. + """ + if node.id_number - 1 in self.observed_nodes: + node_in_group = [(node.id_number - 1) in group for group in self.groups] + group_index = node_in_group.index(True) + self.state[group_index] -= 1 + + def hash_state(self): + """ + Returns a hashable state. + """ + return tuple(self.state) + + class NodeClassMatrix(StateTracker): """ The node-class matrix tracker records the number of customers of each diff --git a/docs/Background/codestructure.rst b/docs/Background/codestructure.rst deleted file mode 100644 index 560b75c2..00000000 --- a/docs/Background/codestructure.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _code-structure: - -============== -Code Structure -============== - -Ciw is structured in an object orientated way: - - -.. image:: ../_static/codestructure.svg - :scale: 100 % - :alt: Code structure for Ciw. - :align: center - -Ciw consists of 3 types of objects, Core, Input, and Optional: - -Core: - -- Simulation -- Arrival Node -- Exit Node -- Node -- Server -- Individual - -Input: - -- Network -- Service Centre -- Customer Classe -- Distributions - -Optional: - -- State Tracker -- Deadlock Detector \ No newline at end of file diff --git a/docs/Background/index.rst b/docs/Background/index.rst index 57ff8b54..1c698875 100644 --- a/docs/Background/index.rst +++ b/docs/Background/index.rst @@ -9,6 +9,5 @@ Contents: simulationpractice.rst mechanisms.rst kendall.rst - codestructure.rst other.rst references.rst \ No newline at end of file diff --git a/docs/Background/mechanisms.rst b/docs/Background/mechanisms.rst index b4b53c41..3669dfc6 100644 --- a/docs/Background/mechanisms.rst +++ b/docs/Background/mechanisms.rst @@ -7,7 +7,7 @@ Notes on Ciw's Mechanisms General ~~~~~~~ -Ciw uses the *event scheduling* approach [SW14]_ , similar to the three phase approach. +Ciw uses the *event scheduling* approach [SR14]_ , similar to the three phase approach. In the event scheduling approach, three types of event take place: **A Events** move the clock forward, **B Events** are prescheduled events, and **C Events** are events that arise because a **B Event** has happened. Here **A-events** correspond to moving the clock forward to the next **B-event**. @@ -24,23 +24,6 @@ In event scheduling the following process occurs: 6. Repeat (2.) - (5.) until a terminating criteria has been satisfied -Blocking Mechanism -~~~~~~~~~~~~~~~~~~ - -In Ciw, Type I blocking (blocking after service) is implemented for restricted networks. - -After service, a customer's next destination is sampled from the transition matrix. -If there is space at the destination node, that customer will join the queue there. -Else if the destination node's queueing capacity is full, then that customer will be blocked. -That customer remains at that node, with its server, until space becomes available at the destination. -This means the server that was serving that customer remains attached to that customer, being unable to serve anyone else until that customer is unblocked. - -At the time of blockage, information about this customer is added to the destination node's :code:`blocked_queue`, a virtual queue containing information about all the customers blocked to that node, and *the order in which they became blocked*. -Thus, the sequence of unblockages happen in the order which customers were blocked. - -Circular blockages can lead to :ref:`deadlock `. - - .. _simultaneous_events: diff --git a/docs/Background/references.rst b/docs/Background/references.rst index 101b47dc..4caf2218 100644 --- a/docs/Background/references.rst +++ b/docs/Background/references.rst @@ -4,8 +4,9 @@ Bibliography ============ -.. [SW14] S. Robinson. *Simulation: the practice of model development and use.* Palgrave Macmillan, 2014. +.. [SR14] S. Robinson. *Simulation: the practice of model development and use.* Palgrave Macmillan, 2014. .. [WS09] W. Stewart. *Probability, markov chains, queues, and simulation.* Princeton university press, 2009. +.. [JJ57] J. Jackson. *Networks of Waiting Lines.* Operations Research, 1957. .. [JZ09] J. Zhang. G. Dai, and B. Zwart. *Law of large number limits of limited processor-sharing queues.* Mathematics of Operations Research 34.4, pp937-970, 2009. .. [XL09] X. Li. *Radio Access Network Dimensioning for 3G UMTS.* PhD Thesis, 2009. .. [GP21] G. Palmer and Y. Tian. *Implementing hybrid simulations that integrate DES+SD in Python.* Journal of Simulation, 2021. \ No newline at end of file diff --git a/docs/Background/simulationpractice.rst b/docs/Background/simulationpractice.rst index 4f60bb00..afeb4d12 100644 --- a/docs/Background/simulationpractice.rst +++ b/docs/Background/simulationpractice.rst @@ -5,8 +5,8 @@ Simulation Practice =================== Ensuring good practice when simulation modelling is important to get meaningful analyses from the models. -This is shown in :ref:`Tutorial IV `. -A recommended resource on the subject is [SW14]_. +This is shown in :ref:`Part 4 of Tutorial I `. +A recommended resource on the subject is [SR14]_. This page will briefly summarise some important aspects of carrying out simulation model analysis. ------------------------------- diff --git a/docs/Guides/batching.rst b/docs/Guides/Arrivals/batching.rst similarity index 100% rename from docs/Guides/batching.rst rename to docs/Guides/Arrivals/batching.rst diff --git a/docs/Guides/Arrivals/index.rst b/docs/Guides/Arrivals/index.rst new file mode 100644 index 00000000..6a535b54 --- /dev/null +++ b/docs/Guides/Arrivals/index.rst @@ -0,0 +1,9 @@ +Arrivals +======== + +Contents: + +.. toctree:: + :maxdepth: 2 + + batching.rst diff --git a/docs/Guides/baulking.rst b/docs/Guides/CustomerBehaviour/baulking.rst similarity index 98% rename from docs/Guides/baulking.rst rename to docs/Guides/CustomerBehaviour/baulking.rst index 5c0c13df..91cd8a19 100644 --- a/docs/Guides/baulking.rst +++ b/docs/Guides/CustomerBehaviour/baulking.rst @@ -46,7 +46,7 @@ When the system is simulated, the baulked customers are recorded as data records >>> baulked_recs = [r for r in recs if r.record_type=="baulk"] >>> r = baulked_recs[0] >>> (r.id_number, r.customer_class, r.node, r.arrival_date) - (44, 'Class 0', 1, 9.45892050639243) + (44, 'Class 0', 1, 9.45892050639...) Note that baulking works and behaves differently to simply setting a queue capacity. Filling a queue's capacity results in arriving customers being *rejected* (and recorded as data records of type :code:`"rejection"`), and transitioning customers to be blocked. diff --git a/docs/Guides/CustomerBehaviour/index.rst b/docs/Guides/CustomerBehaviour/index.rst new file mode 100644 index 00000000..30789517 --- /dev/null +++ b/docs/Guides/CustomerBehaviour/index.rst @@ -0,0 +1,10 @@ +Customer Behaviour +================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + baulking.rst + reneging.rst diff --git a/docs/Guides/reneging.rst b/docs/Guides/CustomerBehaviour/reneging.rst similarity index 98% rename from docs/Guides/reneging.rst rename to docs/Guides/CustomerBehaviour/reneging.rst index 019bfd10..3d5730ce 100644 --- a/docs/Guides/reneging.rst +++ b/docs/Guides/CustomerBehaviour/reneging.rst @@ -51,8 +51,8 @@ Consider a two node network, the first node is an M/M/1, :math:`\lambda = 5` and ... service_distributions=[ciw.dists.Exponential(2), ... ciw.dists.Exponential(4)], ... number_of_servers=[1, 1], - ... routing=[[0, 0], - ... [0, 0]], + ... routing=[[0.0, 0.0], + ... [0.0, 0.0]], ... reneging_time_distributions=[ciw.dists.Deterministic(6), ... None], ... reneging_destinations=[2, -1] diff --git a/docs/Guides/change-class-after-service.rst b/docs/Guides/CustomerClasses/change-class-after-service.rst similarity index 100% rename from docs/Guides/change-class-after-service.rst rename to docs/Guides/CustomerClasses/change-class-after-service.rst diff --git a/docs/Guides/change-class-while-queueing.rst b/docs/Guides/CustomerClasses/change-class-while-queueing.rst similarity index 100% rename from docs/Guides/change-class-while-queueing.rst rename to docs/Guides/CustomerClasses/change-class-while-queueing.rst diff --git a/docs/Guides/CustomerClasses/customer-classes.rst b/docs/Guides/CustomerClasses/customer-classes.rst new file mode 100644 index 00000000..9e980b64 --- /dev/null +++ b/docs/Guides/CustomerClasses/customer-classes.rst @@ -0,0 +1,71 @@ +.. _customer-classes: + +======================================= +How to Set Multiple Classes of Customer +======================================= + +Ciw allows us to define different system parameters for different sets of customers sharing the same system infrastructure. These sets of customers are called different customers classes. This is defined by inserting dictionaries of parameters for the keywords in :code:`ciw.create_network`; these dictionaries will have keys of strings representing customer class names, and values corresponding to the parameters themselves. + +For example, consider an M/M/3 queue:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=5)], + ... service_distributions=[ciw.dists.Exponential(rate=7)], + ... number_of_servers=[2] + ... ) + +Now imagine we had two different types of customer: 40% of customers are Adults, while 60% of customers are children. Children twice as long to process than adults, and so will have a different service time distribution. In this case, Children and Adults will also have different arrival distributions: remember that arrival distributions define the distribution of inter-arrival times, and the inter-arrival times are defined as the inter-arrival times between two customers of the same class. + +Hence we would get:: + + >>> N = ciw.create_network( + ... arrival_distributions={ + ... "Adult": [ciw.dists.Exponential(rate=5 * 0.4)], + ... "Child": [ciw.dists.Exponential(rate=5 * 0.6)] + ... }, + ... service_distributions={ + ... "Adult": [ciw.dists.Exponential(rate=7)], + ... "Child": [ciw.dists.Exponential(rate=3.5)] + ... }, + ... number_of_servers=[2] + ... ) + +The decomposition of the arrival rates is due to `thinning of Poisson processes <(https://galton.uchicago.edu/~lalley/Courses/312/PoissonProcesses.pdf>`_. However, in general, think of arrival distributions as the distribution of inter-arrival times between customers of the same class. + +It is important that the keys of these parameter dictionaries are the same throughout. If splitting one keyword by customer class, then *all* keywords that can be split by customer class need to be split by customer class. + +When collecting results, the class of the customer associated with each service is also recorded:: + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_time(50) + >>> recs = Q.get_all_records() + >>> recs[0].customer_class + 'Child' + + >>> from collections import Counter + >>> Counter([r.customer_class for r in recs]) + Counter({'Child': 138, 'Adult': 89}) + +Nearly all parameters of :code:`ciw.create_network` can be split by customer class, unless they describe the architecture of the network itself. Those that can and cannot be split by customer class are listed below: + +Can be split by customer class: + + :code:`arrival_distributions`, + + :code:`baulking_functions`, + + :code:`class_change_matrices`, + + :code:`class_change_time_distributions`, + + :code:`service_distributions`, + + :code:`routing`, + + :code:`batching_distributions`, + + :code:`reneging_time_distributions`, + + :code:`reneging_destinations`, + +Cannot be split by customer class: + + :code:`number_of_servers`, + + :code:`priority_classes`, + + :code:`queue_capacities`, + + :code:`ps_thresholds`, + + :code:`server_priority_functions`, + + :code:`service_disciplines`, + + :code:`system_capacity` diff --git a/docs/Guides/CustomerClasses/index.rst b/docs/Guides/CustomerClasses/index.rst new file mode 100644 index 00000000..01ec0d42 --- /dev/null +++ b/docs/Guides/CustomerClasses/index.rst @@ -0,0 +1,12 @@ +Customer Classes +================ + +Contents: + +.. toctree:: + :maxdepth: 2 + + customer-classes.rst + priority.rst + change-class-after-service.rst + change-class-while-queueing.rst diff --git a/docs/Guides/priority.rst b/docs/Guides/CustomerClasses/priority.rst similarity index 97% rename from docs/Guides/priority.rst rename to docs/Guides/CustomerClasses/priority.rst index dd04f1cd..b03921a3 100644 --- a/docs/Guides/priority.rst +++ b/docs/Guides/CustomerClasses/priority.rst @@ -58,10 +58,3 @@ There are a number of options that can be used to pre-emptively replace lower pr + :ref:`Pre-emption options `. -.. toctree:: - :maxdepth: 1 - :hidden: - - preemption.rst - - diff --git a/docs/Guides/Distributions/combining_distributions.rst b/docs/Guides/Distributions/combining_distributions.rst new file mode 100644 index 00000000..57127d5a --- /dev/null +++ b/docs/Guides/Distributions/combining_distributions.rst @@ -0,0 +1,65 @@ +.. _combine-dists: + +How to Combine Distributions +============================ + +There are two ways to combine distributions in Ciw: arithmetically, and probabilistically. + + +Arithmetically Combining Distributions +-------------------------------------- + +As distribution objects inherit from the generic distirbution function, they can be *combined* using the operations :code:`+`, :code:`-`, :code:`*`, and :code:`/`. + +For example, let's combine an Exponential distribution with a Deterministic distribution in all four ways:: + + >>> import ciw + >>> Exp_add_Det = ciw.dists.Exponential(rate=0.05) + ciw.dists.Deterministic(value=3.0) + >>> Exp_sub_Det = ciw.dists.Exponential(rate=0.05) - ciw.dists.Deterministic(value=3.0) + >>> Exp_mul_Det = ciw.dists.Exponential(rate=0.05) * ciw.dists.Deterministic(value=3.0) + >>> Exp_div_Det = ciw.dists.Exponential(rate=0.05) / ciw.dists.Deterministic(value=3.0) + +These combined distributions return the combined sampled values: + + >>> ciw.seed(10) + >>> Ex = ciw.dists.Exponential(rate=0.05) + >>> Dt = ciw.dists.Deterministic(value=3.0) + >>> [round(Ex.sample(), 2) for _ in range(5)] + [16.94, 11.2, 17.26, 4.62, 33.57] + >>> [round(Dt.sample(), 2) for _ in range(5)] + [3.0, 3.0, 3.0, 3.0, 3.0] + + >>> # Addition + >>> ciw.seed(10) + >>> [round(Exp_add_Det.sample(), 2) for _ in range(5)] + [19.94, 14.2, 20.26, 7.62, 36.57] + + >>> # Subtraction + >>> ciw.seed(10) + >>> [round(Exp_sub_Det.sample(), 2) for _ in range(5)] + [13.94, 8.2, 14.26, 1.62, 30.57] + + >>> # Multiplication + >>> ciw.seed(10) + >>> [round(Exp_mul_Det.sample(), 2) for _ in range(5)] + [50.83, 33.61, 51.78, 13.85, 100.7] + + >>> # Division + >>> ciw.seed(10) + >>> [round(Exp_div_Det.sample(), 2) for _ in range(5)] + [5.65, 3.73, 5.75, 1.54, 11.19] + + +Probabilistically Combining Distributions +----------------------------------------- + +Distributions can also be combined probabilistically. A countable and finite mixture distriubtion probabilistically chooses to sample from one of a number of given distributions. This is given by the :code:`MixtureDistribution` object. Given a number of distributions each with PDF :math:`D_i(x)`, each with a probability :math:`p_i`, such that :math:`\sum_i p_i = 1`, then the Mixture distribution has PMF :math:`f(x) = \sum_i p_i D_i(x)` + +For example, say let's make a mixture distribution that samples from an Exponential distribution rate 1 with probability 0.5, another Exponential distribution rate 2 with probability 0.2, a Uniform distribution between 0.2 and 0.8 with probability 0.2, and returns a Deterministic value of 0.5 with probability 0.1. We can do this with: + + >>> Exp1 = ciw.dists.Exponential(rate=1) + >>> Exp2 = ciw.dists.Exponential(rate=2) + >>> Unif = ciw.dists.Uniform(lower=0.2, upper=0.8) + >>> Det5 = ciw.dists.Deterministic(0.5) + + >>> M = ciw.dists.MixtureDistribution(dists=[Exp1, Exp2, Unif, Det5], probs=[0.5, 0.2, 0.2, 0.1]) diff --git a/docs/Guides/Distributions/index.rst b/docs/Guides/Distributions/index.rst new file mode 100644 index 00000000..6375c0b2 --- /dev/null +++ b/docs/Guides/Distributions/index.rst @@ -0,0 +1,12 @@ +Distributions +============= + +Contents: + +.. toctree:: + :maxdepth: 2 + + set_distributions.rst + phasetype.rst + combining_distributions.rst + time_dependent.rst diff --git a/docs/Guides/phasetype.rst b/docs/Guides/Distributions/phasetype.rst similarity index 97% rename from docs/Guides/phasetype.rst rename to docs/Guides/Distributions/phasetype.rst index 50887cfd..05934ee9 100644 --- a/docs/Guides/phasetype.rst +++ b/docs/Guides/Distributions/phasetype.rst @@ -11,7 +11,7 @@ It is therefore a random sum of exponential variables. The diagram below gives a general representation of an absorbing Markov chain with four states, 1, 2, 3, and the absorbing state :math:`\star`: -.. image:: ../_static/phasetype.svg +.. image:: ../../_static/phasetype.svg :width: 100% :alt: Absorbing Markov chain of a general Phase-Type distribution. :align: center @@ -52,7 +52,7 @@ Erlang Distributions An Erlang distribution with parameters :math:`\lambda` and :math:`n` is the sum of :math:`n` Exponential distributions with parameter :math:`\lambda`. This can be equivalently defined as the Phase-Type distribution with the following structure: -.. image:: ../_static/erlang.svg +.. image:: ../../_static/erlang.svg :width: 100% :alt: Absorbing Markov chain of an Erlang distribution. :align: center @@ -84,7 +84,7 @@ HyperExponential Distributions An HyperExponential distribution is defined by a probability vector :math:`\mathbf{p}` and rate vector :math:`\mathbf{\lambda}`, and samples an Exponential distribution with parameter :math:`\lambda_i` with probability :math:`p_i`. This can be equivalently defined as the Phase-Type distribution with the following structure: -.. image:: ../_static/hyperexponential.svg +.. image:: ../../_static/hyperexponential.svg :width: 100% :alt: Absorbing Markov chain of a HyperExponential distribution. :align: center @@ -117,7 +117,7 @@ HyperErlang Distributions A HyperErlang distribution is defined by parameters :math:`\mathbf{\lambda}`, :math:`\mathbf{p}`, and :math:`\mathbf{n}`, and samples an Erlang distribution of size :math:`n_i` with parameter :math:`\lambda_i` with probability :math:`p_i`. This can be equivalently defined as the Phase-Type distribution with the following structure: -.. image:: ../_static/hypererlang.svg +.. image:: ../../_static/hypererlang.svg :width: 100% :alt: Absorbing Markov chain of a HyperErlang distribution. :align: center @@ -150,7 +150,7 @@ Coxian Distributions A Coxian distribution is defined by parameters :math:`\mathbf{\lambda}`, the rates of each phase, and :math:`\mathbf{p}`, the probability of going to the absorbing state after each phase. This can be equivalently defined as the Phase-Type distribution with the following structure: -.. image:: ../_static/coxian.svg +.. image:: ../../_static/coxian.svg :width: 100% :alt: Absorbing Markov chain of a general Coxian distribution. :align: center diff --git a/docs/Guides/set_distributions.rst b/docs/Guides/Distributions/set_distributions.rst similarity index 70% rename from docs/Guides/set_distributions.rst rename to docs/Guides/Distributions/set_distributions.rst index b33bc209..1c9db91c 100644 --- a/docs/Guides/set_distributions.rst +++ b/docs/Guides/Distributions/set_distributions.rst @@ -113,60 +113,3 @@ Consider a distribution that samples the value `3.0` 50% of the time, and sample ... return random.random() This can then be implemented into a :code:`Network` object in the usual way. - - -Combined Distributions ----------------------- - -As distribution objects inherit from the generic distirbution function, they can be *combined* using the operations :code:`+`, :code:`-`, :code:`*`, and :code:`/`. - -For example, let's combine an Exponential distribution with a Deterministic distribution in all four ways:: - - >>> Exp_add_Det = ciw.dists.Exponential(rate=0.05) + ciw.dists.Deterministic(value=3.0) - >>> Exp_sub_Det = ciw.dists.Exponential(rate=0.05) - ciw.dists.Deterministic(value=3.0) - >>> Exp_mul_Det = ciw.dists.Exponential(rate=0.05) * ciw.dists.Deterministic(value=3.0) - >>> Exp_div_Det = ciw.dists.Exponential(rate=0.05) / ciw.dists.Deterministic(value=3.0) - -These combined distributions return the combined sampled values: - - >>> ciw.seed(10) - >>> Ex = ciw.dists.Exponential(rate=0.05) - >>> Dt = ciw.dists.Deterministic(value=3.0) - >>> [round(Ex.sample(), 2) for _ in range(5)] - [16.94, 11.2, 17.26, 4.62, 33.57] - >>> [round(Dt.sample(), 2) for _ in range(5)] - [3.0, 3.0, 3.0, 3.0, 3.0] - - >>> # Addition - >>> ciw.seed(10) - >>> [round(Exp_add_Det.sample(), 2) for _ in range(5)] - [19.94, 14.2, 20.26, 7.62, 36.57] - - >>> # Subtraction - >>> ciw.seed(10) - >>> [round(Exp_sub_Det.sample(), 2) for _ in range(5)] - [13.94, 8.2, 14.26, 1.62, 30.57] - - >>> # Multiplication - >>> ciw.seed(10) - >>> [round(Exp_mul_Det.sample(), 2) for _ in range(5)] - [50.83, 33.61, 51.78, 13.85, 100.7] - - >>> # Division - >>> ciw.seed(10) - >>> [round(Exp_div_Det.sample(), 2) for _ in range(5)] - [5.65, 3.73, 5.75, 1.54, 11.19] - - -Mixture Distributions ---------------------- -Distributions can also be combined probabilistically. A countable and finite mixture distriubtion probabilistically chooses to sample from one of a number of given distributions. Given a number of distributions each with PDF :math:`D_i(x)`, each with a probability :math:`p_i`, such that :math:`\sum_i p_i = 1`, then the Mixture distribution has PMF :math:`f(x) = \sum_i p_i D_i(x)` - -For example, say let's make a mixture distribution that samples from an Exponential distribution rate 1 with probability 0.5, another Exponential distribution rate 2 with probability 0.2, a Uniform distribution between 0.2 and 0.8 with probability 0.2, and returns a Deterministic value of 0.5 with probability 0.1. We can do this with: - - >>> Exp1 = ciw.dists.Exponential(rate=1) - >>> Exp2 = ciw.dists.Exponential(rate=2) - >>> Unif = ciw.dists.Uniform(lower=0.2, upper=0.8) - >>> Det5 = ciw.dists.Deterministic(0.5) - - >>> M = ciw.dists.MixtureDistribution(dists=[Exp1, Exp2, Unif, Det5], probs=[0.5, 0.2, 0.2, 0.1]) diff --git a/docs/Guides/time_dependent.rst b/docs/Guides/Distributions/time_dependent.rst similarity index 100% rename from docs/Guides/time_dependent.rst rename to docs/Guides/Distributions/time_dependent.rst diff --git a/docs/Guides/Queues/index.rst b/docs/Guides/Queues/index.rst new file mode 100644 index 00000000..b5f23c60 --- /dev/null +++ b/docs/Guides/Queues/index.rst @@ -0,0 +1,10 @@ +Queues +====== + +Contents: + +.. toctree:: + :maxdepth: 2 + + queue_capacities.rst + system_capacity.rst diff --git a/docs/Guides/Queues/queue_capacities.rst b/docs/Guides/Queues/queue_capacities.rst new file mode 100644 index 00000000..6fb7e531 --- /dev/null +++ b/docs/Guides/Queues/queue_capacities.rst @@ -0,0 +1,74 @@ +.. _queue-capacities: + +==================================== +How to Set Maximium Queue Capacities +==================================== + +A maximum queueing capacity can be set for each node. This means that once the number of people waiting at that node reaches the capacity, then that node cannot receive any more customers until some individuals leave the node. This affects newly arriving customers and customers transitioning from another node differently: + ++ Newly arriving customers who wish to enter the node once capacity is reached are *rejected*. They instead leave the system immediately, and have a data record written that records this rejection (:ref:`see below`). ++ Customers wishing to transition to the node after finishing service at another node are blocked (:ref:`see below`). This means thet remain at their original node, with a server whi is unable to begin another customer's service, until space becomes available. + +In order to implement this, we use the :code:`queue_capacities` keyworks when creating the network:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=1)], + ... service_distributions=[ciw.dists.Exponential(rate=1), + ... ciw.dists.Exponential(rate=1)], + ... routing=[[0.0, 0.5], + ... [0.0, 0.0]], + ... number_of_servers=[3, 2], + ... queue_capacities=[float('inf'), 10] + ... ) + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_time(100) + +In the example above, the second node has 2 servers and a queueing capacity of 10, and so once there are 12 customers present, it will not accept any more customers. In the first node, there is infinite queueing capacity, and so no blocking or rejection will occur here. + +If the :code:`queue_capacities` keyword is omitted, then infinite capacities are assumed, that is an uncapacitated system. + +.. _rejection-records: + +Rejection Records +~~~~~~~~~~~~~~~~~ + +Any customer that is rejected and leaves the system will be recorded by producing a data record of the rejection. This will look like this:: + + >>> recs = Q.get_all_records(only=['rejection']) + >>> dr = recs[0] + >>> dr + Record(id_number=283, customer_class='Customer', original_customer_class='Customer', node=2, arrival_date=86.79600309018552, waiting_time=nan, service_start_date=nan, service_time=nan, service_end_date=nan, time_blocked=nan, exit_date=86.79600309018552, destination=nan, queue_size_at_arrival=12, queue_size_at_departure=nan, server_id=nan, record_type='rejection') + +These records will have field :code:`record_type` as the string :code:`'rejection'`; will have information about the customer such as their ID number, and customer class; will have the :code:`node` they were rejected from; they will have equal :code:`arrival_date` and :code:`exit_date` representing the date they arrived and got rejected; and they will have a `queue_size_at_arrival` showing the number of people at the queue when they got rejected. All other fields will have :code:`nan` values. + + +.. _blocking-mechanism: + +Blocking Mechanism +~~~~~~~~~~~~~~~~~~ + +In Ciw, Type I blocking (blocking after service) is implemented for restricted networks. + +After service, a customer's next destination is sampled from the transition matrix. +If there is space at the destination node, that customer will join the queue there. +Else if the destination node's queueing capacity is full, then that customer will be blocked. +That customer remains at that node, with its server, until space becomes available at the destination. +This means the server that was serving that customer remains attached to that customer, being unable to serve anyone else until that customer is unblocked. + +At the time of blockage, information about this customer is added to the destination node's :code:`blocked_queue`, a virtual queue containing information about all the customers blocked to that node, and *the order in which they became blocked*. Therefore, when space becomes available, the customer to be unblocked will be the customer who was blocked first. That is, the sequence of unblockages happen in the order which customers were blocked. + +Circular blockages can lead to :ref:`deadlock `. + +Information about blockages are visible in the service data records:: + + >>> recs = Q.get_all_records(only=['service']) + >>> dr = recs[381] + >>> dr + Record(id_number=281, customer_class='Customer', original_customer_class='Customer', node=1, arrival_date=86.47159..., waiting_time=0.23440..., service_start_date=86.70600..., service_time=0.60807..., service_end_date=87.31407..., time_blocked=0.75070..., exit_date=88.06478..., destination=2, queue_size_at_arrival=4, queue_size_at_departure=2, server_id=3, record_type='service') + +In the case above, the customer ended service at date :code:`87.31407...`, but didn't exit until date :code:`88.06478...`, giving a :code:`time_blocked` of :code:`0.75070...`. + diff --git a/docs/Guides/Queues/system_capacity.rst b/docs/Guides/Queues/system_capacity.rst new file mode 100644 index 00000000..44fba041 --- /dev/null +++ b/docs/Guides/Queues/system_capacity.rst @@ -0,0 +1,31 @@ +.. _system-capacity: + +=================================================== +How to Set a Maximium Capacity for the Whole System +=================================================== + +We have seen that :ref:`node capacities` can define restricted queueing networks. Ciw also allows for a whole system capacity to be set. When a system capacity is set, when the total number of customers present in *all* the nodes of the system is equal to the system capacity, then newly arriving customers will be rejected. Once the total number of customers drops back below the system capacity, then customers will be accepted into the system again. + +In order to implement this, we use the :code:`system_capacity` keyworks when creating the network:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=1)], + ... service_distributions=[ciw.dists.Exponential(rate=1), + ... ciw.dists.Exponential(rate=1)], + ... routing=[[0.0, 0.5], + ... [0.0, 0.0]], + ... number_of_servers=[3, 2], + ... system_capacity=4 + ... ) + +In this case, the total capacity of nodes 1 and 2 is 4, and the system will never have more than 4 individuals. To see this, let's run this with a :ref:`state tracker` and see that the system never reaches more than 4 people:: + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N, tracker=ciw.trackers.SystemPopulation()) + >>> Q.simulate_until_max_time(100) + >>> state_probs = Q.statetracker.state_probabilities() + >>> state_probs + {0: 0.03369..., 1: 0.15927..., 2: 0.18950..., 3: 0.29834..., 4: 0.31917...} + diff --git a/docs/Guides/Routing/custom_routing.rst b/docs/Guides/Routing/custom_routing.rst new file mode 100644 index 00000000..adcfa3f7 --- /dev/null +++ b/docs/Guides/Routing/custom_routing.rst @@ -0,0 +1,118 @@ +.. _custom-routing: + +==================================== +How to Define Custom Logical Routing +==================================== + +In the guide on :ref:`routing objects` we saw that node routing objects determine the where customers are routed to the next node. Custom routing objects can be created and used in Ciw, in order to define any logical routing that we wish. + +In order to define a routing object, they must inherit from Ciw'r :code:`ciw/routing.NodeRouting` object. We then need to define the :code:`next_node` method, that takes in the individual to route, and returns a node of the network that is that individual's next destination. If required, we can also re-write the object's :code:`__init__` method. Note that we will have access to some useful attributes: + + + :code:`self.simulation`: the simulation object itself, with access to its nodes, and the current time. + + :code:`self.node`: the node associated with this routing object. + +As an example, the built-in :code:`ciw.routing.Direct` router would look like this:: + + >>> import ciw + >>> class Direct(ciw.routing.NodeRouting): + ... """ + ... A router that sends the individual directly to another node. + ... """ + ... def __init__(self, to): + ... """ + ... Initialises the routing object. + ... + ... Takes: + ... - to: a the node index to send to. + ... """ + ... self.to = to + ... + ... def next_node(self, ind): + ... """ + ... Chooses the node 'to' with probability 1. + ... """ + ... return self.simulation.nodes[self.to] + + +Example +------- + +Imagine we have a four node network. Nodes 2, 3, and 4 all route customers out of the system. Node 1 however has some logical routing - before time 50 it will route to node 2 if it is empty, and node 3 otherwise; after time 50 it always routes to node 4. This is defined by:: + + >>> class CustomRouting(ciw.routing.NodeRouting): + ... def next_node(self, ind): + ... """ + ... Chooses node 2 if it is empty and it is before date 50, + ... Chooses node 3 if node 2 is not empty and it is before date 50 + ... Chooses node 4 is the date is after 50. + ... """ + ... if self.node.now >= 50: + ... return self.simulation.nodes[4] + ... if self.simulation.nodes[2].number_of_individuals == 0: + ... return self.simulation.nodes[2] + ... return self.simulation.nodes[3] + +Now if the only arrivals are to node 1, and we run this for 100 time units, we should observe that: services at node 2 had no waiting time; services at node 4 occurred after date 50; services at nodes 2 and 3 occurred before date 50:: + + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... ciw.dists.Exponential(rate=1), + ... None, + ... None, + ... None + ... ], + ... service_distributions=[ + ... ciw.dists.Deterministic(value=0.2), + ... ciw.dists.Deterministic(value=0.2), + ... ciw.dists.Deterministic(value=0.2), + ... ciw.dists.Deterministic(value=0.2) + ... ], + ... number_of_servers=[3, 1, 1, 1], + ... routing=ciw.routing.NetworkRouting( + ... routers=[ + ... CustomRouting(), + ... ciw.routing.Leave(), + ... ciw.routing.Leave(), + ... ciw.routing.Leave() + ... ] + ... ) + ... ) + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_time(100) + >>> recs = Q.get_all_records() + + >>> all(r.service_start_date >= 50 for r in recs if r.node == 4) + True + >>> all(r.service_start_date <= 50 for r in recs if r.node in [2, 3]) + True + >>> all(r.queue_size_at_arrival == 0 for r in recs if r.node == 2) + True + + + +.. _custom-rerouting: + +Custom Pre-emptive Re-routing +----------------------------- + +Custom routing objects can be used to use different routing logic for when a customer finishes service, to when a customer has a service interrupted and must be re-routed. In order to do this, we need to create a custom routing object, and re-write the :code:`next_node_for_rerouting` method, which is called when deciding which node the customer will be re-routed to after pre-emption. By default, this calls the object's :code:`next_node` method, and so identical logic occurs. But we can rewrite this to use different logic when rerouting customers. + +Consider, for example, a two node system where customers always arrive to Node 1, and immediately leave the system. However, if they have service interrupted at Node 1, they are re-routed to Node 2:: + + >>> class CustomRerouting(ciw.routing.NodeRouting): + ... def next_node(self, ind): + ... """ + ... Always leaves the system. + ... """ + ... return self.simulation.nodes[-1] + ... + ... def next_node_for_rerouting(self, ind): + ... """ + ... Always sends to Node 2. + ... """ + ... return self.simulation.nodes[2] + + +**Note that re-routing customers ignores queue capacities.** That means that interrupted customers can be re-routed to nodes that already have full queues, nodes that would otherwise reject or block other arriving individuals; and so that node would be temporarily over-capacity. diff --git a/docs/Guides/Routing/index.rst b/docs/Guides/Routing/index.rst new file mode 100644 index 00000000..f2622a8e --- /dev/null +++ b/docs/Guides/Routing/index.rst @@ -0,0 +1,14 @@ +Routing +======== + +Contents: + +.. toctree:: + :maxdepth: 2 + + transition_matrix.rst + routing_objects.rst + join_shortest_queue.rst + process_based.rst + custom_routing.rst + diff --git a/docs/Guides/Routing/join_shortest_queue.rst b/docs/Guides/Routing/join_shortest_queue.rst new file mode 100644 index 00000000..2a07d84d --- /dev/null +++ b/docs/Guides/Routing/join_shortest_queue.rst @@ -0,0 +1,173 @@ +.. _join-shortest-queue: + +====================================== +How to Set Join-Shortest-Queue Routing +====================================== + +Ciw has two built-in routing objects that allow two different forms of join-shortest-queue routing logic. Both objects take in a :code:`destinations` argument, which outlines which nodes are possibilities to send the individual to. The forms are: + ++ **Join-Shortest-Queue** + + Usage: :code:`ciw.routing.JoinShortestQueue(destinations=[1, 3])` + + Individuals are sent to the node, out of it's destinations, with the shortest *queue*. That is, which node has the least number of people waiting for service. + ++ **Load Balancing** + + Usage: :code:`ciw.routing.LoadBalancing(destinations=[1, 3])` + + Individuals are sent to the node, out of it's destinations, that has the lowest load. That is, which node has the least number of customers present, regardless if the customers are waiting for service or already in service. This can be useful for infinite server systems. + + +Extensive examples are given below: + +- :ref:`example_jsq` +- :ref:`example_lb` + + +.. _example_jsq: + +----------------------------- +Example - Join Shortest Queue +----------------------------- + +In this example we will consider a network where customers are routed differently depending on the system state. We will look at a system without this behaviour first, and then the system with the desired behaviour, for comparison. + + +**Without desired behaviour** + +Consider the following three node network, where arrivals only occur at the first node, then customers are randomly routed to either the 2nd or 3rd node before leaving:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... ciw.dists.Exponential(rate=10), + ... None, + ... None], + ... service_distributions=[ + ... ciw.dists.Exponential(rate=25), + ... ciw.dists.Exponential(rate=6), + ... ciw.dists.Exponential(rate=8)], + ... routing=[[0.0, 0.5, 0.5], + ... [0.0, 0.0, 0.0], + ... [0.0, 0.0, 0.0]], + ... number_of_servers=[1, 1, 1] + ... ) + +Now we run the system for 80 time units using a state tracker to track the number of customers at Node 1 and Node 2:: + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N, tracker=ciw.trackers.NodePopulation()) + >>> Q.simulate_until_max_time(80) + >>> ts = [ts[0] for ts in Q.statetracker.history] + >>> n2 = [ts[1][1] for ts in Q.statetracker.history] + >>> n3 = [ts[1][2] for ts in Q.statetracker.history] + +Plotting `n2` and `n3` we see that the numbers of customers at each node can diverge greatly:: + + >>> import matplotlib.pyplot as plt # doctest:+SKIP + >>> plt.plot(ts, n2); # doctest:+SKIP + >>> plt.plot(ts, n3); # doctest:+SKIP + +.. image:: ../../_static/custom_routing_without.svg + :alt: Plot of node populations diverging. + :align: center + + +**With desired behaviour** + +We will now replace the transition matrix with a network routing object, where the first node routes to the shortest queue out of Nodes 2 and 3, while the other nodes route the customer out of the system:: + + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... ciw.dists.Exponential(rate=10), + ... None, + ... None], + ... service_distributions=[ + ... ciw.dists.Exponential(rate=25), + ... ciw.dists.Exponential(rate=6), + ... ciw.dists.Exponential(rate=8)], + ... routing=ciw.routing.NetworkRouting( + ... routers=[ + ... ciw.routing.JoinShortestQueue(destinations=[2, 3]), + ... ciw.routing.Leave(), + ... ciw.routing.Leave() + ... ] + ... ), + ... number_of_servers=[1, 1, 1] + ... ) + +Now rerun the same system :: + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N, tracker=ciw.trackers.NodePopulation()) + >>> Q.simulate_until_max_time(80) + >>> ts = [ts[0] for ts in Q.statetracker.history] + >>> n2 = [ts[1][1] for ts in Q.statetracker.history] + >>> n3 = [ts[1][2] for ts in Q.statetracker.history] + +and now plotting `n2` and `n3`, we see that the numbers of customers at each node can follow one another closely, as we are always 'evening out' the nodes' busyness by always filling up the least busy node:: + + >>> plt.plot(ts, n2); # doctest:+SKIP + >>> plt.plot(ts, n3); # doctest:+SKIP + +.. image:: ../../_static/custom_routing_with.svg + :alt: Plot of node populations closely following each other. + :align: center + + + +.. _example_lb: + +------------------------ +Example - Load Balancing +------------------------ + +In this example we will consider multiple parallel :ref:`processor sharing` queues, where customers are routed to the least busy node. Here, because in processor sharing customers do not generally wait, just add to the servers' loads, we will use the Load Balancing routing, rather than the Join Shortest Queue routing. + +Consider three independent parallel processor sharing nodes. Customers arrive and are sent to the least busy node. +This can be modelled as a 4 node system: the first node is a dummy node where customers arrive, and routes the customer to one of the thee remaining processor sharing nodes. +If the arrival distribution is Poisson with rate 8, and required service times are exponentially distributed with parameter 10, then our network is:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=8), + ... None, + ... None, + ... None], + ... service_distributions=[ciw.dists.Deterministic(value=0), + ... ciw.dists.Exponential(rate=10), + ... ciw.dists.Exponential(rate=10), + ... ciw.dists.Exponential(rate=10)], + ... number_of_servers=[float('inf'), + ... float('inf'), + ... float('inf'), + ... float('inf')], + ... routing=ciw.routing.NetworkRouting( + ... routers=[ + ... ciw.routing.LoadBalancing(destinations=[2, 3, 4]), + ... ciw.routing.Leave(), + ... ciw.routing.Leave(), + ... ciw.routing.Leave() + ... ] + ... ) + ... ) + +For each of the three parallel processor sharing nodes, we can use the :code:`ciw.PSNode` class. The routing decisions are derived from the routing objects, where the first node is given the :code:`LoadBalancing` object, that balances the load in nodes 2, 3 and 4; that is, it sends the individual one of these nodes, whichever currently has the least individuals. + +Now let's build a simulation object, where the first node uses the usual :code:`ciw.Node` class, and the others use the built-in :code:`ciw.PSNode` class. We'll also add a state tracker for analysis:: + + >>> ciw.seed(0) + >>> Q = ciw.Simulation( + ... N, tracker=ciw.trackers.SystemPopulation(), + ... node_class=[ciw.Node, ciw.PSNode, ciw.PSNode, ciw.PSNode]) + +We'll run this for 100 time units:: + + >>> Q.simulate_until_max_time(100) + +We can look at the state probabilities, that is, the proportion of time the system spent in each state, where a state represents the number of customers present in the system:: + + >>> state_probs = Q.statetracker.state_probabilities(observation_period=(10, 90)) + >>> state_probs + {1: 0.37895..., 2: 0.13628..., 3: 0.03237..., 0: 0.43600..., 4: 0.01254..., 5: 0.00224..., 6: 0.00108..., 7: 0.00050...} diff --git a/docs/Guides/Routing/process_based.rst b/docs/Guides/Routing/process_based.rst new file mode 100644 index 00000000..4abb10bc --- /dev/null +++ b/docs/Guides/Routing/process_based.rst @@ -0,0 +1,186 @@ +.. _process-based: + +=================================== +How to Define Process-Based Routing +=================================== + +Ciw has the capability to run simulations with process-based routing. This means a customer's entire route is determined at the start and not determined probabilistically as they progress through the network. +This allows routes to account for an individuals history, for example, repeating nodes a certain number of times. + +A customer's entire route is determined at the start, generated from a routing function, that takes in an individual and returns a route, which is a list containing the order of the nodes they are to visit. The function should also take in the simulation itself, allowing time and state-dependent routing. For example:: + + >>> def routing_function(ind, simulation): + ... return [2, 2, 1] + +This takes in an individual and assigns it the route [2, 2, 1]. Whichever node this individual arrives to (determined by the arrival distributions), after service there they are sent to Node 2, then back Node 2, then back to Node 1, before exiting the system. Ensuring the exact repetition of these nodes would not be possible in a purely probabilistic system. + +In order to utilise this, we use a :code:`ProcessBased` routing object, that takes in this routing function. For example:: + + >>> import ciw + + >>> def repeating_route(ind, simulation): + ... return [1, 1] + + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=1)], + ... service_distributions=[ciw.dists.Exponential(rate=2)], + ... number_of_servers=[1], + ... routing=ciw.routing.ProcessBased(repeating_route) + ... ) + +Here, customers arrive at Node 1, and have service there and then repeat this two more times before exiting the system. + +Let's run this and look at the routes of those that have left the system. + + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_time(100.0) + + >>> inds = Q.nodes[-1].all_individuals # Gets all individuals from exit node + >>> set([tuple(dr.node for dr in ind.data_records) for ind in inds]) # Gets all unique routes of completed individuals + {(1, 1, 1)} + +Now we can see that all individuals who have left the system, that is they have completed their route, repeated service at Node 1 three times. + + +Further example +--------------- + +The routing functions can be as complicated as necessary. They take in an individual, and therefore can take in any attribute of an individual when determining the route (including their :code:`customer_class`). + +Lets make a network with three nodes with the following routes: + ++ For customers arriving at Node 1: + + if individual has an even :code:`id_number`, repeat Node 1 twice, then exit. + + otherwise route from Node 1 to Node 2, to Node 3, and then exit. ++ Arrivals at Node 2: + + have 50% chance of routing to Node 3, and then exit. + + have 50% chance of routing to Node 1, and then exit. ++ There are no arrivals at Node 3. + +For this we will require a routing function that returns different things depending on the individual's starting node:: + + >>> import random + >>> def routing_function(ind, simulation): + ... if ind.starting_node == 1: + ... if ind.id_number % 2 == 0: + ... return [1, 1, 1] + ... return [1, 2, 3] + ... if ind.starting_node == 2: + ... if random.random() <= 0.5: + ... return [2, 3] + ... return [2, 1] + + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=1), + ... ciw.dists.Deterministic(value=1), + ... None], + ... service_distributions=[ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2)], + ... number_of_servers=[1, 1, 1], + ... routing=ciw.routing.ProcessBased(routing_function) + ... ) + + + +Flexible Process Based Routing +------------------------------ + +In the examples above, once a route was sampled, the customer's entire journey was set out before them. However, with a :code:`FlexibleProcessBased` object, we can define sequences of sets of nodes that must be visited in order. Within a set of nodes, either the individual must visit at least one node here, or must visit all nodes here, but the order is irrelevant. This is defined with the :code:`rule` keyword. + +Consider for example the following sequence of sets of destinations:: + + [[1, 2, 3], [4], [5, 6]] + +There are three sets of nodes in the sequence, the set :code:`[1, 2, 3]`, followed by the set :code:`[4]`, followed by the set :code:`[5, 6]`. Routes are then determined by the :code:`rule` keyword: + ++ :code:`rule='any'`: this means that at just one node from each set should be visited, in the order of the sets. The choice of which node is chosen from each set is set with the :code:`choice` keyword. Valid routes include (1, 4, 5), (2, 4, 5), and (3, 4, 6), amongst others. ++ :code:`rule='all'`: this means that every node in a set must be visited before moving on to the next set. The order at which a node is visited in a set is set with the :code:`choice` keyword. Valid routes include (1, 2, 3, 4, 5, 6), (3, 2, 1, 4, 6, 5), and (3, 1, 2, 4, 5, 6), amongst others. + +The current options for choices are: + - :code:`'random'`: randomly chooses a node from the set. + - :code:`'jsq'`: chooses the node with the smallest queue from the set (like the :ref:`join-shortest-queue` router). + - :code:`'lb'`: chooses the node with the least number of customers present from the set (like the :ref:`load-balancing` router). + +When all nodes in a set must be visited, these rules apply to choosing the next node from the set minus the nodes already visited, applied at the current time when the choice is made. + +Example:: + + >>> def routing_function(ind, simulation): + ... return [[1, 2], [3], [1, 2]] + +A route where the first and third sets include nodes 1 and 2, and the second set only includes node 3. All customers arrive to node 4. Let's compare the :code:`'any'` and :code:`'all'` rules. First with :code:`'any'`:: + + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... None, + ... None, + ... None, + ... ciw.dists.Exponential(rate=1) + ... ], + ... service_distributions=[ + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ], + ... number_of_servers=[1, 1, 1, 1], + ... routing=ciw.routing.FlexibleProcessBased( + ... route_function=routing_function, + ... rule='any', + ... choice='random' + ... ) + ... ) + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_customers(6) + >>> routes = [[dr.node for dr in ind.data_records] for ind in Q.nodes[-1].all_individuals] + >>> for route in routes: + ... print(route) + [4, 2, 3, 2] + [4, 1, 3, 1] + [4, 1, 3, 1] + [4, 1, 3, 1] + [4, 2, 3, 1] + [4, 2, 3, 1] + +We see that all customers that completed their journey arrived at node 4, took either node 1 or 2 first, then node 3, then either node 1 or 2. + +Now compare with :code:`'all'`:: + + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... None, + ... None, + ... None, + ... ciw.dists.Exponential(rate=1) + ... ], + ... service_distributions=[ + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ciw.dists.Exponential(rate=2), + ... ], + ... number_of_servers=[1, 1, 1, 1], + ... routing=ciw.routing.FlexibleProcessBased( + ... route_function=routing_function, + ... rule='all', + ... choice='random' + ... ) + ... ) + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_customers(6) + >>> routes = [[dr.node for dr in ind.data_records] for ind in Q.nodes[-1].all_individuals] + >>> for route in routes: + ... print(route) + [4, 2, 1, 3, 2, 1] + [4, 1, 2, 3, 2, 1] + [4, 2, 1, 3, 2, 1] + [4, 1, 2, 3, 2, 1] + [4, 1, 2, 3, 1, 2] + [4, 1, 2, 3, 2, 1] + +We see that all customers that completed their journey arrived at node 4, took both node 1 or 2 in either order, then node 3, then both node 1 or 2 in either order. + diff --git a/docs/Guides/Routing/routing_objects.rst b/docs/Guides/Routing/routing_objects.rst new file mode 100644 index 00000000..4fbc630c --- /dev/null +++ b/docs/Guides/Routing/routing_objects.rst @@ -0,0 +1,83 @@ +.. _routing-objects: + +========================== +How to Use Routing Objects +========================== + +Routing objects in Ciw are objects that determine what an individual's next node will be. There are two types: + ++ **Network routing objects**: these determine routing for the entire network; ++ **Node routing objects**: these determine routing from a particular node. + +Ciw has a number of these built in, however their primary use is to be defined by the user. Examples of Network routing objects are: + ++ :code:`ciw.routing.TransitionMatrix`, allowing users to define :ref:`transition matrices`, that is a matrix of probabilities of being transferred to each node in the network after service at every other node. ++ :code:`ciw.routing.ProcessBased`, allowing pre-defined routes to be given to individuals when they arrive, that is :ref:`process-based routing`. + +However, the most flexible Network routing object is the generic :code:`ciw.routing.NetworkRouting`. This takes in a list of Node routing objects. Node routing objects are objects that determine routing out of a particular node. A :ref:`full list is given` in the References section. The following are some of the most basic built-in routers available in Ciw, but importantly, they can also be user defined: + ++ :code:`ciw.routing.Direct(to=2)`: Sends the individual directly to another node. For example here, a customer is always send to node 2. ++ :code:`ciw.routing.Leave()`: The individual leaves the system. ++ :code:`ciw.routing.Probabilistic(destinations=[1, 3], probs=[0.1, 0.4])`: Probabilistically sends the individual to either of the destination, according to their corresponding probabilities. In this case, they are send to node 1 with probability 0.1, node 3 with probability 0.4, and leave the system with the rest of the probability, 0.5. + + +Example +~~~~~~~ + +Consider a four node system: + + At the node 1 customers are send directly to node 2; + + At node 2 customers can be send to either nodes 1, 3 or 4 with probabilities 0.1, 0.5, and 0.4; + + At node 3 customers can either repeat service there with probability 0.25, or leave the system; + + At node 4, all customers leave the system. + +We can construct a network routing object for this by choosing a set of node routing obejects from the list above. We place them in a list and use the :code:`routers` keyword to create the network object. In this case, the network object would be:: + + >>> import ciw + >>> R = ciw.routing.NetworkRouting( + ... routers=[ + ... ciw.routing.Direct(to=2), + ... ciw.routing.Probabilistic(destinations=[1, 3, 4], probs=[0.1, 0.5, 0.4]), + ... ciw.routing.Probabilistic(destinations=[3], probs=[0.25]), + ... ciw.routing.Leave() + ... ] + ... ) + +This network object can then be used to create a network:: + + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=1), ciw.dists.Exponential(rate=1), ciw.dists.Exponential(rate=1), ciw.dists.Exponential(rate=1)], + ... service_distributions=[ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2), ciw.dists.Exponential(rate=2)], + ... number_of_servers=[1, 2, 3, 1], + ... routing=R + ... ) + + +Notes +~~~~~ + ++ Note that a the :code:`routing` keywork when creating a network object requires a network routing object, *not* a node routing object. This is true even when there is only one node in the network. ++ Note also that, similar to the use of most other keywords when creating a network object, that routing can be customer class dependent. + +For example, a one node network with two customer classes, both classes having different routing:: + + >>> N = ciw.create_network( + ... arrival_distributions={ + ... "Class 0": [ciw.dists.Exponential(rate=1)], + ... "Class 1": [ciw.dists.Exponential(rate=3)] + ... }, + ... service_distributions={ + ... "Class 0": [ciw.dists.Exponential(rate=2)], + ... "Class 1": [ciw.dists.Exponential(rate=2)] + ... }, + ... number_of_servers=[2], + ... routing={ + ... "Class 0": ciw.routing.NetworkRouting(routers=[ciw.routing.Leave()]), + ... "Class 1": ciw.routing.TransitionMatrix(transition_matrix=[[0.3]]) + ... } + ... ) + + +Custom Routing Objects +~~~~~~~~~~~~~~~~~~~~~~ + +Ciw allows for custom built routing objects. These allow for both time and state dependent routing, allowing flexible and routing logic. A guide is given :ref:`here`. diff --git a/docs/Guides/Routing/transition_matrix.rst b/docs/Guides/Routing/transition_matrix.rst new file mode 100644 index 00000000..3755ff04 --- /dev/null +++ b/docs/Guides/Routing/transition_matrix.rst @@ -0,0 +1,83 @@ +.. _transition-matrix: + +============================================== +How to Define Routing with a Transition Matrix +============================================== + +There are a number of methods of defining how customers are routed from one node to another after service. This is done with the :code:`routing` keyword when creating a network. + +One common method is by defining a transition matrix, common when defining Jackson networks [JJ57]_. +A transition matrix is an :math:`n \times n` matrix (where :math:`n` is the number of nodes in the network) such that the :math:`(i,j)\text{th}` element corresponds to the probability of transitioning to node :math:`j` after service at node :math:`i`. + +For example, in a three node network, a transition matrix might look like: + +.. math:: + + \begin{pmatrix} + 0.0 & 0.3 & 0.7 \\ + 0.0 & 0.0 & 1.0 \\ + 0.0 & 0.0 & 0.2 \\ + \end{pmatrix} + +This represents that: + + customers finishing service at node 1 enter node 2 with probability 0.3, and envet node 3 with probability 0.7; + + customers finishing service at node 2 are certain to enter node 3; + + customers finishing service at node 3 re-enter node 3 with probability 0.2, and with probability 0.8 leave the system. + +In Ciw there are two ways to define this, with a list of lists, or a :ref:`routing object`. + + +List of Lists +~~~~~~~~~~~~~ + +To define this with a list of lists, we can use:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... ciw.dists.Exponential(1), + ... ciw.dists.Exponential(1), + ... ciw.dists.Exponential(1) + ... ], + ... service_distributions=[ + ... ciw.dists.Exponential(2), + ... ciw.dists.Exponential(2), + ... ciw.dists.Exponential(2) + ... ], + ... number_of_servers=[3, 3, 3], + ... routing=[ + ... [0.0, 0.3, 0.7], + ... [0.0, 0.0, 0.1], + ... [0.0, 0.0, 0.2] + ... ] + ... ) + + +Routing Object +~~~~~~~~~~~~~~ + +To define with with a routing object we can make use of the built-in :code:`TransitionMatrix` routing object, and give it the list of lists above:: + + >>> N = ciw.create_network( + ... arrival_distributions=[ + ... ciw.dists.Exponential(1), + ... ciw.dists.Exponential(1), + ... ciw.dists.Exponential(1) + ... ], + ... service_distributions=[ + ... ciw.dists.Exponential(2), + ... ciw.dists.Exponential(2), + ... ciw.dists.Exponential(2) + ... ], + ... number_of_servers=[3, 3, 3], + ... routing=ciw.routing.TransitionMatrix( + ... transition_matrix=[ + ... [0.0, 0.3, 0.7], + ... [0.0, 0.0, 0.1], + ... [0.0, 0.0, 0.2] + ... ] + ... ) + ... ) + +Routing objects can be much more flexible that this, allowing logic based routing in addition to probabilistic based routing. More information can be found :ref:`here`. + diff --git a/docs/Guides/Services/index.rst b/docs/Guides/Services/index.rst new file mode 100644 index 00000000..3258d5bb --- /dev/null +++ b/docs/Guides/Services/index.rst @@ -0,0 +1,14 @@ +Services +======== + +Contents: + +.. toctree:: + :maxdepth: 2 + + service_disciplines.rst + server_priority.rst + server_schedule.rst + slotted.rst + preemption.rst + processor-sharing.rst diff --git a/docs/Guides/preemption.rst b/docs/Guides/Services/preemption.rst similarity index 71% rename from docs/Guides/preemption.rst rename to docs/Guides/Services/preemption.rst index 58c143b1..670dc07e 100644 --- a/docs/Guides/preemption.rst +++ b/docs/Guides/Services/preemption.rst @@ -13,7 +13,8 @@ In Ciw they can either: + Have their service resampled (:code:`"resample"`); + Restart the exact same service (:code:`"restart"`); -+ Continue the original service from where they left off (:code:`"resume"`). ++ Continue the original service from where they left off (:code:`"resume"`); ++ Be re-routed to another node (:code:`"reroute"`). @@ -24,7 +25,7 @@ During non-pre-emptive priorities, customers cannot be interrupted. Therefore th In order to implement pre-emptive or non-pre-emptive priorities, put the priority class mapping in a tuple with a list of the chosen pre-emption options for each node in the network. For example:: - priority_classes=({'Class 0': 0, 'Class 1': 1}, [False, "resample", "restart", "resume"]) + priority_classes=({'Class 0': 0, 'Class 1': 1}, [False, "resample", "restart", "resume", "reroute"]) This indicates that non-pre-emptive priorities will be used at the first node, and pre-emptive priorities will be used at the second, third and fourth nodes. Interrupted individuals will have their services resampled at the second node, they will restart their original service time at the third node, and they will continue where they left off at the fourth node. @@ -46,15 +47,24 @@ During a pre-emptive schedule, that server will immediately stop service and lea In order to implement pre-emptive or non-pre-emptive schedules, the :code:`ciw.Schedule` object takes in a keywords argument :code:`preemption` the chosen pre-emption option. For example:: number_of_servers=[ - ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]], preemption=False) # non-preemptive - ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]], preemption="resample") # preemptive and resamples service time - ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]], preemption="restart") # preemptive and restarts origional service time - ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]], preemption="resume") # preemptive continutes services where left off + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100], preemption=False), # non-preemptive + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100], preemption="resample"), # preemptive and resamples service time + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100], preemption="restart"), # preemptive and restarts origional service time + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100], preemption="resume"), # preemptive and continues services where left off + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100], preemption="reroute") # preemptive and sends the individual to another node ] Ciw defaults to non-pre-emptive schedules, and so the following code implies a non-pre-emptive schedule:: - number_of_servers=[ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]])] # non-preemptive + number_of_servers=[ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100])] # non-preemptive + + +Re-routing Customers +-------------------- + +If the :code:`"reroute"` pre-emptive option is chosen, then interrupted customers have their service cut short at the current node, and are then sent to another node. Ordinarily the next node is chosen in same way as if the customer had completed service, using the transition matrices, process based routes, or routing objects. However, it may be useful to have separate routing logic for preemptive reroutes, and a description of how to do that is given :ref:`here`. + +**Note that re-routing customers ignores queue capacities.** That means that interrupted customers can be re-routed to nodes that already have full queues, nodes that would otherwise reject or block other arriving individuals; and so that node would be temporarily over-capacity. Records of Interrupted Services diff --git a/docs/Guides/processor-sharing.rst b/docs/Guides/Services/processor-sharing.rst similarity index 98% rename from docs/Guides/processor-sharing.rst rename to docs/Guides/Services/processor-sharing.rst index 8a7ff228..6eff1a1d 100644 --- a/docs/Guides/processor-sharing.rst +++ b/docs/Guides/Services/processor-sharing.rst @@ -23,7 +23,7 @@ Now we create a simulation object using :code:`ciw.PSNode` rather than :code:`ci >>> Q = ciw.Simulation(N, node_class=ciw.PSNode) Note that this applies the process sharing node to every node of the network. -Alternatively we could provide a list of different node classes, for use on each different node of the network (see :ref:`this example ` for an in depth example of this):: +Alternatively we could provide a list of different node classes, for use on each different node of the network (see :ref:`this example ` for an in depth example of this):: >>> ciw.seed(0) >>> Q = ciw.Simulation(N, node_class=[ciw.PSNode]) @@ -137,7 +137,7 @@ where :math:`R` is the process sharing capacity, :math:`\rho = \frac{\lambda}{R\ Plotting these theoretical results against a single run of our simulation shows a good alignment: -.. image:: ../_static/ps_capacitated_verification.svg +.. image:: ../../_static/ps_capacitated_verification.svg :alt: Alignment of theoretical and simulation results for a capacitated PS queue. :align: center diff --git a/docs/Guides/server_priority.rst b/docs/Guides/Services/server_priority.rst similarity index 95% rename from docs/Guides/server_priority.rst rename to docs/Guides/Services/server_priority.rst index 1cc28fd8..c1a6ec0c 100644 --- a/docs/Guides/server_priority.rst +++ b/docs/Guides/Services/server_priority.rst @@ -27,7 +27,7 @@ Observing the utilisation of each server we can see that server 1 is far more bu >>> Q.simulate_until_max_time(1000) >>> [srv.utilisation for srv in Q.nodes[1].servers] - [0.3184942440259139, 0.1437617661984246, 0.04196395909329539] + [0.31849424..., 0.14376176..., 0.04196395...] Ciw allows servers to be prioritised according to a custom server priority function. @@ -52,7 +52,7 @@ The :code:`server_priority_functions` keyword takes a list of server priority fu >>> Q.simulate_until_max_time(1000) >>> [srv.utilisation for srv in Q.nodes[1].servers] - [0.16784616228588892, 0.16882711899030475, 0.1675466880414403] + [0.16784616..., 0.16882711..., 0.16754668...] @@ -95,7 +95,7 @@ Now let's see this in action when we have equal numbers of individuals of class >>> Q = ciw.Simulation(N) >>> Q.simulate_until_max_time(1000) >>> [srv.utilisation for srv in Q.nodes[1].servers] - [0.36132860028585134, 0.3667939476202799, 0.2580202674603771] + [0.36132860..., 0.36679394..., 0.25802026...] Utilisation is fairly even between the first two servers, with the third server picking up any slack. Now let's see what happens when there is three times as many individuals of class 0 entering the system as there are of class 1:: @@ -116,7 +116,7 @@ Utilisation is fairly even between the first two servers, with the third server >>> Q = ciw.Simulation(N) >>> Q.simulate_until_max_time(1000) >>> [srv.utilisation for srv in Q.nodes[1].servers] - [0.447650059165907, 0.2678754897968868, 0.29112382084389343] + [0.44765005..., 0.26787548..., 0.29112382...] Now the first server is much busier than the others. diff --git a/docs/Guides/server_schedule.rst b/docs/Guides/Services/server_schedule.rst similarity index 57% rename from docs/Guides/server_schedule.rst rename to docs/Guides/Services/server_schedule.rst index f385c6d3..a989f626 100644 --- a/docs/Guides/server_schedule.rst +++ b/docs/Guides/Services/server_schedule.rst @@ -15,16 +15,17 @@ An example cyclic work schedule is shown in the table below: This schedule is cyclic, therefore after the last shift (30-100), schedule begins again with the shift (0-10). The cycle length for this schedule is 100. -The schedule itself is defined by a list of lists indicating the number of servers that should be on duty during that shift, and the end date of that shift:: +The schedule itself is defined by a list of lists indicating the numbers of servers that should be on duty during the shifts, and the end dates of the shifts:: - [[2, 10], [0, 30], [1, 100]] + numbers_of_servers = [2, 0, 1] + shift_end_dates = [10, 30, 100] Here we are saying that there will be 2 servers scheduled between times 0 and 10, 0 between 10 and 30, etc. This fully defines the cyclic work schedule. From this we create a schedule object in Ciw:: - ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]]) + ciw.Schedule(number_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100]) To tell Ciw to use this schedule for a given node, in the :code:`number_of_servers` keyword we replace an integer with the schedule:: @@ -32,7 +33,12 @@ To tell Ciw to use this schedule for a given node, in the :code:`number_of_serve >>> N = ciw.create_network( ... arrival_distributions=[ciw.dists.Exponential(rate=5)], ... service_distributions=[ciw.dists.Exponential(rate=10)], - ... number_of_servers=[ciw.Schedule(schedule=[[2, 10], [0, 30], [1, 100]])] + ... number_of_servers=[ + ... ciw.Schedule( + ... numbers_of_servers=[2, 0, 1], + ... shift_end_dates=[10, 30, 100] + ... ) + ... ] ... ) Simulating this system, we'll see that no services begin between dates 10 and 30, nor between dates 110 and 130:: @@ -51,6 +57,37 @@ Simulating this system, we'll see that no services begin between dates 10 and 30 +Schedule Offsets +---------------- + +A schedule can be offset by a given amount of time. This means that the cyclic schedule will have a delayed start, where no servers are present. This is defined using the :code:`offset` keyword. It's effect can be compared below:: + + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100]) + +gives: + + +-------------------+---------+--------+--------+---------+---------+--------+ + | Shift Times | 0-10 | 10-30 | 30-100 | 100-110 | 110-130 | ... | + +-------------------+---------+--------+--------+---------+---------+--------+ + | Number of Servers | 2 | 0 | 1 | 2 | 0 | ... | + +-------------------+---------+--------+--------+---------+---------+--------+ + +whereas:: + + ciw.Schedule(numbers_of_servers=[2, 0, 1], shift_end_dates=[10, 30, 100], offset=7.0) + +gives: + + +-------------------+---------+---------+--------+--------+---------+---------+--------+ + | Shift Times | 0-7 | 7-17 | 17-37 | 37-107 | 107-117 | 117-137 | ... | + +-------------------+---------+---------+--------+--------+---------+---------+--------+ + | Number of Servers | 0 | 2 | 0 | 1 | 2 | 0 | ... | + +-------------------+---------+---------+--------+--------+---------+---------+--------+ + + +An offset of 7 here delays the beginning of the first shift by 7 time units, and that offset does not appear again in the cyclic schedule. The offset should only be defined as a positive float. + + Pre-emption ----------- @@ -76,7 +113,12 @@ Consider the following example:: >>> N = ciw.create_network( ... arrival_distributions=[ciw.dists.Deterministic(value=3.0)], ... service_distributions=[ciw.dists.Deterministic(value=5.0)], - ... number_of_servers=[ciw.Schedule(schedule=[[1, 4.0], [2, 10.0], [0, 100.0]])] + ... number_of_servers=[ + ... ciw.Schedule( + ... numbers_of_servers=[1, 2, 0], + ... shift_end_dates=[4.0, 10.0, 100.0] + ... ) + ... ] ... ) >>> Q = ciw.Simulation(N) >>> Q.simulate_until_max_time(20.0) diff --git a/docs/Guides/service_disciplines.rst b/docs/Guides/Services/service_disciplines.rst similarity index 98% rename from docs/Guides/service_disciplines.rst rename to docs/Guides/Services/service_disciplines.rst index 8aaec703..40a97666 100644 --- a/docs/Guides/service_disciplines.rst +++ b/docs/Guides/Services/service_disciplines.rst @@ -40,7 +40,7 @@ Custom Disciplines Other service disciplines can also be implemented by writing a custom service discipline function. These functions take in a list of individuals, and the current time, and returns an individual from that list that represents the next individual to be served. As this is a list of individuals, we can access the individuals' attributes when making the service discipline decision. -For example, say we wish to implement a service discipline that chooses the customers randomly, but with probability proportional to their arrival order, we could write: +For example, say we wish to implement a service discipline that chooses the customers randomly, but with probability proportional to their arrival order, we could write:: >>> def SIRO_proportional(individuals, t): ... n = len(inds) diff --git a/docs/Guides/slotted.rst b/docs/Guides/Services/slotted.rst similarity index 70% rename from docs/Guides/slotted.rst rename to docs/Guides/Services/slotted.rst index 3cb73e9e..872b4ba4 100644 --- a/docs/Guides/slotted.rst +++ b/docs/Guides/Services/slotted.rst @@ -47,6 +47,38 @@ Simulating this system, we'll see that services only begin between during the sl {1.5, 2.3, 2.8, 4.3, 5.1, 5.6} +Schedule Offsets +---------------- + +A slotted schedule can be offset by a given amount of time. This means that the cyclic schedule will have a delayed start. This is defined using the :code:`offset` keyword. It's effect can be compared below:: + + ciw.Slotted(slots=[1.5, 2.3, 2.8], slot_sizes=[2, 5, 3]) + +gives: + + +----------------------------+------+------+------+------+------+------+------+------+ + | Slot Times :math:`t` | 1.5 | 2.3 | 2.8 | 4.3 | 5.1 | 5.6 | 7.1 | ... | + +----------------------------+------+------+------+------+------+------+------+------+ + | Slot Sizes :math:`s_t` | 2 | 5 | 3 | 2 | 5 | 3 | 2 | ... | + +----------------------------+------+------+------+------+------+------+------+------+ + +whereas:: + + ciw.Slotted(slots=[1.5, 2.3, 2.8], slot_sizes=[2, 5, 3], offset=10.0) + +gives: + + +----------------------------+-------+-------+-------+-------+-------+-------+-------+------+ + | Slot Times :math:`t` | 11.5 | 12.3 | 12.8 | 14.3 | 15.1 | 15.6 | 17.1 | ... | + +----------------------------+-------+-------+-------+-------+-------+-------+-------+------+ + | Slot Sizes :math:`s_t` | 2 | 5 | 3 | 2 | 5 | 3 | 2 | ... | + +----------------------------+-------+-------+-------+-------+-------+-------+-------+------+ + + +An offset of 10 here delays the beginning of the first slot by 10 time units, and that offset does not appear again in the cyclic schedule. The offset should only be defined as a positive float. + + + Capacitated & Non-capacitated Slots ----------------------------------- @@ -58,7 +90,7 @@ Slots can be capacitated or non-capacitated. This effects their behaviour when s This is shown visually below: -.. image:: ../_static/slotted.svg +.. image:: ../../_static/slotted.svg :scale: 20 % :alt: Comparing capacitated and non-capacitated slotted schedules. :align: center diff --git a/docs/Guides/exact.rst b/docs/Guides/Simulation/exact.rst similarity index 100% rename from docs/Guides/exact.rst rename to docs/Guides/Simulation/exact.rst diff --git a/docs/Guides/Simulation/index.rst b/docs/Guides/Simulation/index.rst new file mode 100644 index 00000000..3088eb9a --- /dev/null +++ b/docs/Guides/Simulation/index.rst @@ -0,0 +1,16 @@ +Simulation +========== + +Contents: + +.. toctree:: + :maxdepth: 2 + + seed.rst + sim_maxtime.rst + sim_numcusts.rst + pause_restart.rst + results.rst + progressbar.rst + parallel_process.rst + exact.rst diff --git a/docs/Guides/parallel_process.rst b/docs/Guides/Simulation/parallel_process.rst similarity index 93% rename from docs/Guides/parallel_process.rst rename to docs/Guides/Simulation/parallel_process.rst index 54837362..18d415af 100644 --- a/docs/Guides/parallel_process.rst +++ b/docs/Guides/Simulation/parallel_process.rst @@ -44,10 +44,10 @@ repeated and the average taken:: To obtain the above by running 2 simulations at the same time (assuming that 2 cores are available), the :code:`multiprocessing` library can be used. In which case the following :download:`main.py -<../_static/script_for_parallel_processing/main.py>` script gives a working +<../../_static/script_for_parallel_processing/main.py>` script gives a working example: -.. literalinclude:: ../_static/script_for_parallel_processing/main.py +.. literalinclude:: ../../_static/script_for_parallel_processing/main.py It is possible to use :code:`multiprocessing.cpu_count()` to obtain the number of available cores. diff --git a/docs/Guides/pause_restart.rst b/docs/Guides/Simulation/pause_restart.rst similarity index 100% rename from docs/Guides/pause_restart.rst rename to docs/Guides/Simulation/pause_restart.rst diff --git a/docs/Guides/progressbar.rst b/docs/Guides/Simulation/progressbar.rst similarity index 91% rename from docs/Guides/progressbar.rst rename to docs/Guides/Simulation/progressbar.rst index 69b06d73..e4361dda 100644 --- a/docs/Guides/progressbar.rst +++ b/docs/Guides/Simulation/progressbar.rst @@ -15,7 +15,7 @@ An example when using the :code:`simulate_until_max_time` method:: The image below shows an example of the output: -.. image:: ../_static/progress_bar_time.png +.. image:: ../../_static/progress_bar_time.png :scale: 100 % :alt: Output of progress bar (simulate_until_max_time). :align: center @@ -26,7 +26,7 @@ An example when using the :code:`simulate_until_max_customers` method:: And the image below shows the output: -.. image:: ../_static/progress_bar_customers.png +.. image:: ../../_static/progress_bar_customers.png :scale: 100 % :alt: Output of progress bar (simulate_until_max_customers). :align: center \ No newline at end of file diff --git a/docs/Guides/Simulation/results.rst b/docs/Guides/Simulation/results.rst new file mode 100644 index 00000000..3c6be79a --- /dev/null +++ b/docs/Guides/Simulation/results.rst @@ -0,0 +1,80 @@ +.. _collect-results: + +====================== +How to Collect Results +====================== + +Once a simulation has been run, results can be collected. Results take the form of a data record. Certain events in a simulation's run cause data records to be created that describe those events. Those results are: + ++ Services ++ :ref:`Pre-empted services` ++ :ref:`Baulking customers` ++ :ref:`Reneging customers` ++ :ref:`Rejected customers` + +In order to collect all data records, we use the :code:`get_all_records()` method of the Simulation object. + +For example, in an M/M/3 queue:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Exponential(rate=1)], + ... service_distributions=[ciw.dists.Exponential(rate=2)], + ... number_of_servers=[1] + ... ) + >>> ciw.seed(0) + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_time(100) + +To collect a list of all data records:: + + >>> recs = Q.get_all_records() + +This gives a list of :code:`DataRecord` objects, which are named tuples with a number of fields with useful information about the event in question:: + + >>> r = recs[14] + >>> r + Record(id_number=15, customer_class='Customer', original_customer_class='Customer', node=1, arrival_date=16.58266..., waiting_time=0.0, service_start_date=16.58266..., service_time=1.69969..., service_end_date=18.28236..., time_blocked=0.0, exit_date=18.28236..., destination=-1, queue_size_at_arrival=0, queue_size_at_departure=1, server_id=1, record_type='service') + +These data records have a number of useful fields, set out in detail :ref:`here`. Importantly, fields can be accessed as attributes:: + + >>> r.service_start_date + 16.58266884119802 + +And so relevant data can be gathered using list comprehension:: + + >>> waiting_times = [r.waiting_time for r in recs] + >>> sum(waiting_times) / len(waiting_times) + 0.3989747... + +For easier manipulation, use in conjuction with `Pandas `_ is recommended, allowing for easier filtering, grouping, and summary statistics calculations. Lists of data records convert to Pandas data frames smoothly: + + >>> import pandas as pd + >>> recs_pd = pd.DataFrame(recs) + >>> recs_pd # doctest: +SKIP + +.. image:: ../../_static/recs_pandas.png + :scale: 30 % + :alt: A Pandas data frame of Ciw data records. + :align: center + + + +Types of Records +~~~~~~~~~~~~~~~~ + +One particular field of note is the :code:`record_type` field, which indicates which of the five events caused that data record to be created. + ++ Services: gives :code:`record_type="service"` ++ Pre-empted services gives :code:`record_type="interrupted service"` ++ Baulking customers gives :code:`record_type="baulk"` ++ Reneging customers gives :code:`record_type="renege"` ++ Rejected customers gives :code:`record_type="rejection"` + +It is only by understanding the record types that we can understand the other data record fields. More information is given on the relevant Guide pages for each feature. + +When a simulation can produce multiple types of data record, it is sometimes useful to be able to only collect data records of a give type or types. we can do this with the optional keyword argument :code:`only`, which takes a list of the record types we are interested in:: + + >>> lost_customer_recs = Q.get_all_records(only=["rejection", "baulk", "renege"]) + >>> service_recs = Q.get_all_records(only=["service"]) + diff --git a/docs/Guides/seed.rst b/docs/Guides/Simulation/seed.rst similarity index 100% rename from docs/Guides/seed.rst rename to docs/Guides/Simulation/seed.rst diff --git a/docs/Guides/Simulation/sim_maxtime.rst b/docs/Guides/Simulation/sim_maxtime.rst new file mode 100644 index 00000000..0d7ea674 --- /dev/null +++ b/docs/Guides/Simulation/sim_maxtime.rst @@ -0,0 +1,33 @@ +.. _until-maxtime: + +============================================ +How to Simulate For a Certain Amount of Time +============================================ + +Once the simulation object has been created, it can be simulated unil a certain amount of simulation-time has passed. This is done with the :code:`simulate_until_max_time` method, which takes in the :code:`max_simulation_time` positional keyword:: + + >>> Q.simulate_until_max_time(max_simulation_time=100.0) # doctest:+SKIP + +Notes +~~~~~ + +Note that the simulation does not finish at exactly this time. It will finish as soon as the time of the next scheduled event is after the :code:`max_simulation_time`. The clock will reach this nect event date, but not carry out the event. E.g. consider the following system with sparce events:: + + >>> import ciw + >>> N = ciw.create_network( + ... arrival_distributions=[ciw.dists.Deterministic(value=7.0)], + ... service_distributions=[ciw.dists.Deterministic(value=2.0)], + ... number_of_servers=[1] + ... ) + + >>> Q = ciw.Simulation(N) + >>> Q.simulate_until_max_time(22.0) + +We will see there there have been three arrivals at dates 7, 14 and 21, but only two completed services at dates 9 and 16, with the next scheduled event at date 23:: + + >>> recs = Q.get_all_records() # Completed records + >>> [r.arrival_date for r in recs] + [7.0, 14.0] + >>> Q.current_time + 23.0 + diff --git a/docs/Guides/sim_numcusts.rst b/docs/Guides/Simulation/sim_numcusts.rst similarity index 100% rename from docs/Guides/sim_numcusts.rst rename to docs/Guides/Simulation/sim_numcusts.rst diff --git a/docs/Guides/behaviour/custom_arrivals.rst b/docs/Guides/System/behaviour/custom_arrivals.rst similarity index 100% rename from docs/Guides/behaviour/custom_arrivals.rst rename to docs/Guides/System/behaviour/custom_arrivals.rst diff --git a/docs/Guides/behaviour/custom_number_servers.rst b/docs/Guides/System/behaviour/custom_number_servers.rst similarity index 96% rename from docs/Guides/behaviour/custom_number_servers.rst rename to docs/Guides/System/behaviour/custom_number_servers.rst index 7db5b74b..0a0793f8 100644 --- a/docs/Guides/behaviour/custom_number_servers.rst +++ b/docs/Guides/System/behaviour/custom_number_servers.rst @@ -29,7 +29,7 @@ Now let's plot the system population over time:: ... [row[1] for row in Q.statetracker.history] ... ); # doctest:+SKIP -.. image:: ../../_static/custom_number_servers_without.svg +.. image:: ../../../_static/custom_number_servers_without.svg :alt: Plot of system population increasing over time. :align: center @@ -70,7 +70,7 @@ Now let's plot the system population over time:: ... [row[1] for row in Q.statetracker.history] ... ); # doctest:+SKIP -.. image:: ../../_static/custom_number_servers_with.svg +.. image:: ../../../_static/custom_number_servers_with.svg :alt: Plot of system population over time. :align: center diff --git a/docs/Guides/behaviour/hybrid.rst b/docs/Guides/System/behaviour/hybrid.rst similarity index 98% rename from docs/Guides/behaviour/hybrid.rst rename to docs/Guides/System/behaviour/hybrid.rst index 498b1e07..256d15f3 100644 --- a/docs/Guides/behaviour/hybrid.rst +++ b/docs/Guides/System/behaviour/hybrid.rst @@ -5,7 +5,7 @@ In the paper [GP21]_ a DES+SD hybrid simulation is defined using Ciw and Scipy. Consider a supermarket modelled as an :math:`M/M/k` queue, whose customers are living in an SIMD disease model. This is show below: -.. image:: ../../_static/hybrid.png +.. image:: ../../../_static/hybrid.png :alt: Stock and Flow / Queueing diagram of the hybrid system. :align: center @@ -158,6 +158,6 @@ Now a function can be written that will run one trial of this hybrid simulation. Running these for one trial only (this is bad practice, :ref:`see here `), we can plot the stock levels over time. We see that changing the number of servers from :math:`k = 1` to :math:`k = 3`, a parameter associated with the discrete component, changes the stock levels, results associated with the continuous component. -.. image:: ../../_static/des+sd_hybrid.svg +.. image:: ../../../_static/des+sd_hybrid.svg :alt: Stock level plots of the hybrid simulation results. :align: center diff --git a/docs/Guides/behaviour/index.rst b/docs/Guides/System/behaviour/index.rst similarity index 97% rename from docs/Guides/behaviour/index.rst rename to docs/Guides/System/behaviour/index.rst index 5d0fc8ea..318ed8dd 100644 --- a/docs/Guides/behaviour/index.rst +++ b/docs/Guides/System/behaviour/index.rst @@ -25,9 +25,7 @@ Here's a library of examples of this functionality: .. toctree:: :maxdepth: 1 - custom_routing.rst custom_arrivals.rst custom_number_servers.rst - ps_routing.rst server_dependent_dist.rst hybrid.rst diff --git a/docs/Guides/behaviour/server_dependent_dist.rst b/docs/Guides/System/behaviour/server_dependent_dist.rst similarity index 97% rename from docs/Guides/behaviour/server_dependent_dist.rst rename to docs/Guides/System/behaviour/server_dependent_dist.rst index 7cc66113..6a644663 100644 --- a/docs/Guides/behaviour/server_dependent_dist.rst +++ b/docs/Guides/System/behaviour/server_dependent_dist.rst @@ -40,7 +40,7 @@ For each server we can plot their cumulative count of individuals served over ti ... label=f"Server {s.id_number}") # doctest:+SKIP >>> plt.legend() # doctest:+SKIP -.. image:: ../../_static/server_dependent_dist_without.svg +.. image:: ../../../_static/server_dependent_dist_without.svg :alt: Plot server commission over time, all with the same behaviour. :align: center @@ -94,7 +94,7 @@ Plotting the cumulative counts for each server population over time:: ... label=f"Server {s.id_number}") # doctest:+SKIP >>> plt.legend() # doctest:+SKIP -.. image:: ../../_static/server_dependent_dist_with.svg +.. image:: ../../../_static/server_dependent_dist_with.svg :alt: Plot server commission over time, all with the different behaviour. :align: center diff --git a/docs/Guides/deadlock.rst b/docs/Guides/System/deadlock.rst similarity index 98% rename from docs/Guides/deadlock.rst rename to docs/Guides/System/deadlock.rst index 372cc323..a367fa6b 100644 --- a/docs/Guides/deadlock.rst +++ b/docs/Guides/System/deadlock.rst @@ -8,7 +8,7 @@ Deadlock is the phenomenon whereby all movement and customer flow in a restricte The diagram below shows an example, where the customer at the top node is blocked to the bottom node, and the customer at the bottom node is blocked to the top node. This circular blockage results is no more natural movement happening. -.. image:: ../_static/2nodesindeadlock.svg +.. image:: ../../_static/2nodesindeadlock.svg :alt: A 2 node queueing network in deadlock. :align: center diff --git a/docs/Guides/System/index.rst b/docs/Guides/System/index.rst new file mode 100644 index 00000000..a118ad59 --- /dev/null +++ b/docs/Guides/System/index.rst @@ -0,0 +1,11 @@ +System +====== + +Contents: + +.. toctree:: + :maxdepth: 2 + + state_trackers.rst + deadlock.rst + behaviour/index.rst diff --git a/docs/Guides/state_trackers.rst b/docs/Guides/System/state_trackers.rst similarity index 100% rename from docs/Guides/state_trackers.rst rename to docs/Guides/System/state_trackers.rst diff --git a/docs/Guides/behaviour/custom_routing.rst b/docs/Guides/behaviour/custom_routing.rst deleted file mode 100644 index 2fbb559c..00000000 --- a/docs/Guides/behaviour/custom_routing.rst +++ /dev/null @@ -1,92 +0,0 @@ -State-dependent Routing -======================= - -In this example we will consider a network where customers are routed differently depending on the system state. We will look at a system without this behaviour first, and then the system with the desired behaviour, for comparison. - - -Without desired behaviour -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Consider the following three node network, where arrivals only occur at the first node, then customers are randomly routed to either the 2nd or 3rd node before leaving:: - - >>> import ciw - - >>> N = ciw.create_network( - ... arrival_distributions=[ - ... ciw.dists.Exponential(rate=10), - ... None, - ... None], - ... service_distributions=[ - ... ciw.dists.Exponential(rate=25), - ... ciw.dists.Exponential(rate=6), - ... ciw.dists.Exponential(rate=8)], - ... routing=[[0.0, 0.5, 0.5], - ... [0.0, 0.0, 0.0], - ... [0.0, 0.0, 0.0]], - ... number_of_servers=[1, 1, 1] - ... ) - -Now we run the system for 80 time units using a state tracker to track the number of customers at Node 1 and Node 2:: - - >>> ciw.seed(0) - >>> Q = ciw.Simulation(N, tracker=ciw.trackers.NodePopulation()) - >>> Q.simulate_until_max_time(80) - - >>> ts = [ts[0] for ts in Q.statetracker.history] - >>> n2 = [ts[1][1] for ts in Q.statetracker.history] - >>> n3 = [ts[1][2] for ts in Q.statetracker.history] - -Plotting `n2` and `n3` we see that the numbers of customers at each node can diverge greatly:: - - >>> import matplotlib.pyplot as plt # doctest:+SKIP - >>> plt.plot(ts, n2); # doctest:+SKIP - >>> plt.plot(ts, n3); # doctest:+SKIP - -.. image:: ../../_static/custom_routing_without.svg - :alt: Plot of node populations diverging. - :align: center - - -With desired behaviour -~~~~~~~~~~~~~~~~~~~~~~ - -We will now create a new :code:`CustomRouting` for Node 1, that will send its customers to the least busy of Nodes 2 and 3. -First create the :code:`CustomRouting` that inherits from :code:`ciw.Node`, and overwrites the :code:`next_node` method:: - - >>> class CustomRouting(ciw.Node): - ... """ - ... Chooses either Node 2 or Node 3 as the destination node, - ... whichever has the least customers. Chooses randomly in - ... the event of a tie. - ... """ - ... def next_node(self, ind): - ... n2 = self.simulation.nodes[2].number_of_individuals - ... n3 = self.simulation.nodes[3].number_of_individuals - ... if n2 < n3: - ... return self.simulation.nodes[2] - ... elif n3 < n2: - ... return self.simulation.nodes[3] - ... return ciw.random_choice([self.simulation.nodes[2], self.simulation.nodes[3]]) - -Now rerun the same system, using the same network object :code:`N` (notice the transition matrix will be unused now). -We tell Ciw which node class to use for each node of the network, by giving the :code:`node_class` argumument a list of classes. -We'll use the new :code:`CustomRouting` class for Node 1, and the regular :code:`ciw.Node` class for Nodes 2 and 3:: - - >>> ciw.seed(0) - >>> Q = ciw.Simulation( - ... N, tracker=ciw.trackers.NodePopulation(), - ... node_class=[CustomRouting, ciw.Node, ciw.Node]) - >>> Q.simulate_until_max_time(80) - - >>> ts = [ts[0] for ts in Q.statetracker.history] - >>> n2 = [ts[1][1] for ts in Q.statetracker.history] - >>> n3 = [ts[1][2] for ts in Q.statetracker.history] - -Plotting `n2` and `n3` now, we see that the numbers of customers at each node can follow one another closely, as we are always 'evening out' the nodes' busyness by always filling up the least busy node:: - - >>> plt.plot(ts, n2); # doctest:+SKIP - >>> plt.plot(ts, n3); # doctest:+SKIP - -.. image:: ../../_static/custom_routing_with.svg - :alt: Plot of node populations closely following each other. - :align: center diff --git a/docs/Guides/behaviour/ps_routing.rst b/docs/Guides/behaviour/ps_routing.rst deleted file mode 100644 index 16f24472..00000000 --- a/docs/Guides/behaviour/ps_routing.rst +++ /dev/null @@ -1,67 +0,0 @@ -.. _ps-routing: - -================================================ -Join Shortest Queue in Processor Sharing Systems -================================================ - -In this example we will consider multiple parallel processor sharing queues, where customers are routed to the least busy node. This is calles a Join Shortest Queue, or JSQ system. - -Consider three independent parallel processor sharing nodes. Customers arrive and are sent to the least busy node. -This can be modelled as a 4 node system: the first node is a dummy node where customers arrive, and routes the customer to one of the thee remaining processor sharing nodes. -If the arrival distribution is Poisson with rate 8, and required service times are exponentially distributed with parameter 10, then our network is:: - - >>> import ciw - >>> N = ciw.create_network( - ... arrival_distributions=[ciw.dists.Exponential(rate=8), - ... None, - ... None, - ... None], - ... service_distributions=[ciw.dists.Deterministic(value=0), - ... ciw.dists.Exponential(rate=10), - ... ciw.dists.Exponential(rate=10), - ... ciw.dists.Exponential(rate=10)], - ... number_of_servers=[float('inf'), - ... float('inf'), - ... float('inf'), - ... float('inf')], - ... routing=[[0, 0, 0, 0], - ... [0, 0, 0, 0], - ... [0, 0, 0, 0], - ... [0, 0, 0, 0]] - ... ) - -For each of the three parallel processor sharing nodes, we can use the :code:`ciw.PSNode` class. -However, we now need a custom node class for the initial dummy node, to take care of the routing decisions. -We'll call this class :code:`RoutingDecision`:: - - >>> class RoutingDecision(ciw.Node): - ... def next_node(self, ind): - ... """ - ... Finds the next node by looking at nodes 2, 3, and 4, - ... seeing how busy they are, and routing to the least busy. - ... """ - ... busyness = {n: self.simulation.nodes[n].number_of_individuals for n in [2, 3, 4]} - ... chosen_n = sorted(busyness.keys(), key=lambda x: busyness[x])[0] - ... return self.simulation.nodes[chosen_n] - -Now let's build a simulation object, where the first node uses our custom :code:`RoutingDecision` class, and the others use the built-in :code:`ciw.PSNode` class. We'll also add a state tracker for analysis:: - - >>> ciw.seed(0) - >>> Q = ciw.Simulation( - ... N, tracker=ciw.trackers.SystemPopulation(), - ... node_class=[RoutingDecision, ciw.PSNode, ciw.PSNode, ciw.PSNode]) - -We'll run this for 100 time units:: - - >>> Q.simulate_until_max_time(100) - -We can look at the state probabilities, that is, the proportion of time the system spent in each state, where a state represents the number of customers present in the system:: - - >>> Q.statetracker.state_probabilities(observation_period=(10, 90)) # doctest:+SKIP - {0: 0.425095024227593, - 1: 0.35989517302304014, - 2: 0.14629711075255158, - 3: 0.054182634504608064, - 4: 0.01124224242623659, - 5: 0.002061285633093934, - 6: 0.0012265294328765105} diff --git a/docs/Guides/dynamic_customerclasses.rst b/docs/Guides/dynamic_customerclasses.rst deleted file mode 100644 index 10d4c3e0..00000000 --- a/docs/Guides/dynamic_customerclasses.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _dynamic-classes: - -=================================== -How to Set Dynamic Customer Classes -=================================== - -Ciw has two ways in which customers can change their customer class: - -+ :ref:`Probabilistically after service at a node `. -+ :ref:`At a randomly distributed time while queueing `. - -These can be used in conjunction to one another. - -.. toctree:: - :maxdepth: 1 - :hidden: - - change-class-after-service.rst - change-class-while-queueing.rst \ No newline at end of file diff --git a/docs/Guides/index.rst b/docs/Guides/index.rst index d83814f0..633f6a2d 100644 --- a/docs/Guides/index.rst +++ b/docs/Guides/index.rst @@ -1,33 +1,19 @@ Guides ====== -This selection of How-to guides will give a tour of some of the features that Ciw has to offer. +This selection of How-to guides will give a tour of some of the features that Ciw has to offer. It is organised by what part of the simulation is effected by the behaviour described. Contents: .. toctree:: :maxdepth: 2 - - seed.rst - progressbar.rst - exact.rst - sim_numcusts.rst - pause_restart.rst - set_distributions.rst - phasetype.rst - time_dependent.rst - priority.rst - service_disciplines.rst - processor-sharing.rst - batching.rst - baulking.rst - reneging.rst - server_schedule.rst - slotted.rst - server_priority.rst - dynamic_customerclasses.rst - state_trackers.rst - deadlock.rst - process_based.rst - behaviour/index.rst - parallel_process.rst + + Simulation/index.rst + Distributions/index.rst + Arrivals/index.rst + Queues/index.rst + Services/index.rst + Routing/index.rst + CustomerBehaviour/index.rst + CustomerClasses/index.rst + System/index.rst diff --git a/docs/Guides/process_based.rst b/docs/Guides/process_based.rst deleted file mode 100644 index 57163959..00000000 --- a/docs/Guides/process_based.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. _process-based: - -=================================== -How to Define Process-Based Routing -=================================== - -Ciw has the capability to run simulations with process-based routing. This means a customer's entire route is determined at the start and not determined probablistically as they progress through the network. -This allows routes to account for an individuals history, for example, repeating nodes a certain number of times. - -A customer's entire route is determined at the start, generated from a routing function, that takes in an individual and returns a route, which is a list of the order of the nodes. For example:: - - >>> def routing_function(ind): - ... return [1, 2, 2, 1] - -This takes in an individual at Node 1 and assigns it the route [1, 2, 2, 1]. Then after service at Node 1 the individual is sent to Node 2, then back Node 2, then back to Node 1, before exiting the system. Ensuring the exact repetition of these nodes would not be possible in a purely probabilistic system. - -In order to utilise this, we replace the routing matrix with a list of these routing functions to be used at each starting point. For example:: - - >>> import ciw - - >>> def repeating_route(ind): - ... return [1, 1, 1] - - >>> N = ciw.create_network( - ... arrival_distributions=[ciw.dists.Exponential(rate=1)], - ... service_distributions=[ciw.dists.Exponential(rate=2)], - ... number_of_servers=[1], - ... routing=[repeating_route] - ... ) - -Here, customers arrive at Node 1, and have service there and then repeat this two more times before exiting the system. - -Let's run this and look at the routes of those that have left the system. - - >>> ciw.seed(0) - >>> Q = ciw.Simulation(N) - >>> Q.simulate_until_max_time(100.0) - - >>> inds = Q.nodes[-1].all_individuals # Get's all individuals from exit node - >>> set([tuple(dr.node for dr in ind.data_records) for ind in inds]) # Get's all unique routes of completed individuals - {(1, 1, 1)} - -Now we can see that all individuals who have left the system, that is they have completed their route, repeated service at Node 1 three times. - -Important Notice ----------------- - -**How it works:** You can think of this as, when an individual arrives at their first node, based on their :code:`arrival_distributions`, it is assigned a route that should start at this Node. This will ensure that the first Node which an individual arrives at is the same as the first Node in their assigned route. - -If this is not the case then the error :code:`'Individual process route sent to wrong node'` will occur. - -*Make sure that the routing function for Node* :math:`i` *yields routes that begin with Node* :math:`i`. - -Further example ---------------- - -The routing functions can be as complicated as necessary. They take in an individual, and therefore can take in any attribute of an individual when determining the route (including their :code:`customer_class`). - -Lets make a network with three nodes with the following routes: - -* For customers arriving at Node 1: - - * if individual has an even :code:`id_number`, repeat Node 1 twice, then exit. - - * otherwise route from Node 1 to Node 2, to Node 3, and then exit. - -* Arrivals at Node 2: - - * have 50% chance of routing to Node 3, and then exit. - - * have 50% chance of routing to Node 1, and then exit. - -* There are no arrivals at Node 3. - -For this we will require two routing functions: :code:`routing_function_Node_1`, :code:`routing_function_Node_2`:: - - >>> def routing_function_Node_1(ind): - ... if ind.id_number % 2 == 0: - ... return [1, 1, 1] - ... return [1, 2, 3] - - >>> import random - >>> def routing_function_Node_2(ind): - ... if random.random() <= 0.5: - ... return [2, 3] - ... return [2, 1] - -As there are no arrivals at Node 3, no customer will need routing assigned here. However, we need to use the placeholder function :code:`ciw.no_routing` to account for this:: - - >>> N = ciw.create_network( - ... arrival_distributions=[ciw.dists.Exponential(rate=1), - ... ciw.dists.Deterministic(value=1), - ... None], - ... service_distributions=[ciw.dists.Exponential(rate=2), - ... ciw.dists.Exponential(rate=2), - ... ciw.dists.Exponential(rate=2)], - ... number_of_servers=[1,1,1], - ... routing=[routing_function_Node_1, - ... routing_function_Node_2, - ... ciw.no_routing] - ... ) diff --git a/docs/Reference/index.rst b/docs/Reference/index.rst index 8bf137e6..2d29a91c 100644 --- a/docs/Reference/index.rst +++ b/docs/Reference/index.rst @@ -9,6 +9,7 @@ Contents: parameters.rst results.rst distributions.rst + routers.rst state_trackers.rst citation.rst contributing.rst diff --git a/docs/Reference/parameters.rst b/docs/Reference/parameters.rst index 7e526e49..128c676b 100644 --- a/docs/Reference/parameters.rst +++ b/docs/Reference/parameters.rst @@ -76,7 +76,7 @@ class_change_matrices *Optional* A dictionary of class change matrices for each node. -For more details see :ref:`dynamic-classes`. +For more details see :ref:`changeclass-afterservice`. An example for a two node network with two classes of customer:: @@ -91,6 +91,22 @@ An example for a two node network with two classes of customer:: +class_change_time_distributions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Optional* + +A dictionary of distributions representing the time it takes to change from one class into another while waiting. For more details see :ref:`changeclass-whilequeueing`. + +An example of a two class network where customers of class 0 change to customers of class 1 according to an exponential distribution:: + + class_change_dist_dict = { + 'Class 0': {'Class 1': ciw.dists.Exponential(rate=5)} + } + + + + number_of_servers ~~~~~~~~~~~~~~~~~ @@ -123,19 +139,63 @@ Example:: +ps_thresholds +~~~~~~~~~~~~~ + +A list of thresholds for capacitated processor sharing queues. +For more information see :ref:`processor-sharing`. + +Example:: + + ps_thresholds=[3] + + + + queue_capacities ~~~~~~~~~~~~~~~~ *Optional* A list of maximum queue capacities at each node. -If ommitted, default values of :code:`float('inf')` for every node are given. +If omitted, default values of :code:`float('inf')` for every node are given. +For more details see :ref:`queue-capacities`. Example:: queue_capacities=[5, float('inf'), float('inf'), 10] +reneging_destinations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Optional* + +A dictionary of lists representing the destination a customer goes to when they renege, or abandon the queue, while waiting. For more details see :ref:`reneging-customers`. + +An example of a one node, two class network where customers of class 0 renege to node 2, and customers of class 1 renege and leave the system:: + + reneging_destinations = { + 'Class 0': [2], + 'Class 1': [-1] + } + + + +reneging_time_distributions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Optional* + +A dictionary of distributions representing the time it takes for a customer to renege, or abandon the queue, while waiting. For more details see :ref:`reneging-customers`. + +An example of a one node, two class network where customers of class 0 renege after a 5 time units, and customers of class 1 do not renege:: + + reneging_time_distributions = { + 'Class 0': [ciw.dists.Deterministic(value=5)], + 'Class 1': [None] + } + routing ~~~~~~~ @@ -144,31 +204,59 @@ routing *Optional for 1 node* -Describes how each customer class routes around the system. -This may be a routing matrix for each customer class, or a list routing function for process-based simulations, see :ref:`process-based`. +Describes how each customer class routes around the system. +This may be a routing matrix for each customer class, or a routing object, see :ref:`routing-objects`. -This is a dictionary, with keys as customer classes, and values are lists of lists (matrices) containing the routing probabilities. -If only one class of customer is required it is sufficient to simply enter single routing matrix (a list of lists). +This is a dictionary, with keys as customer classes, and values are routing objects (or lists of of lists, matrices, containing the routing probabilities). +If only one class of customer is required it is sufficient to simply enter single routing object or matrix. -An example is shown:: +An example of using a routing object:: + + routing = ciw.routing.NetworkRouting( + routers=[ + ciw.routing.Direct(to=2), + ciw.routing.Leave() + ] + ) + +And an example of using transition matrices is shown:: routing={'Class 0': [[0.1, 0.3], [0.0, 0.8]], 'Class 1': [[0.0, 1.0], [0.0, 0.0]]} -An example where only one class of customer is required:: - routing=[[0.5, 0.3], - [0.2, 0.6]] +server_priority_functions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Optional* -If using only one node, the default value is:: +A function for each node that decides how to choose between multiple servers in the same node. +For more details see :ref:`server-priority`. - routing={'Class 0': [[0.0]]} +Example:: + + server_priority_functions=[custom_server_priority] + + + +service_disciplines +~~~~~~~~~~~~~~~~~~~ + +*Optional* -Otherwise a process-based routing function:: +A list of service discipline functions, that describe the order in which customers are taken from the queue and served. +For more details see :ref:`service-disciplines`. + +If omitted, FIFO service disciplines are assumed. + +Example of a 3 node network, one using FIFO, one using LIFO, and one using SIRO:: + + service_disciplines=[ciw.disciplines.FIFO, + ciw.disciplines.LIFO, + ciw.disciplines.SIRO] - routing=[routing_function] @@ -194,3 +282,20 @@ An example where only one class of customer is required:: service_distributions=[ciw.dists.Exponential(rate=4.8), ciw.dists.Exponential(rate=5.2)] + + + +system_capacity +~~~~~~~~~~~~~~~ + +*Optional* + +The maximum queue capacity for the system. +If omitted, a default value of :code:`float('inf')` is given. +For more details see :ref:`system-capacity`. + +Example:: + + system_capacity=12 + + diff --git a/docs/Reference/results.rst b/docs/Reference/results.rst index 6d4f491a..db193406 100644 --- a/docs/Reference/results.rst +++ b/docs/Reference/results.rst @@ -16,7 +16,7 @@ Data records are Named Tuples with the following attributes: * - :code:`id_number` - The unique identification number for that customer. * - :code:`customer_class` - - The number of that customer's customer class. If dynamic customer classes are used, this is the customer's class that they were when they recieved service at that node. + - The number of that customer's customer class. If dynamic customer classes are used, this is the customer's class that they were when they received service at that node. * - :code:`original_customer_class` - The number of the customer's class when they arrived at the node. * - :code:`node` @@ -34,7 +34,7 @@ Data records are Named Tuples with the following attributes: * - :code:`time_blocked` - The amount of time spent blocked at that node. That is the time between finishing service at exiting the node. * - :code:`exit_date` - - The date which the customer exited the node. This may be immediatly after service if no blocking occured, or after some period of being blocked. + - The date which the customer exited the node. This may be immediately after service if no blocking occured, or after some period of being blocked. * - :code:`destination` - The number of the customer's destination, that is the next node the customer will join after leaving the current node. If the customer leaves the system, this will be -1. * - :code:`queue_size_at_arrival` @@ -49,7 +49,7 @@ Data records are Named Tuples with the following attributes: The attribute :code:`record_type` can be one of: - :code:`"service"`: a completed service - - :code:`"interrupted service"`: an interupted service + - :code:`"interrupted service"`: an interrupted service - :code:`"renege"`: the waiting time while a customer waited before reneging - :code:`"baulk"`: the record of a customer baulking - :code:`"rejection"`: the record of a customer being rejected due to the queue being full @@ -60,7 +60,7 @@ You may access these records as a list of named tuples, using the Simulation's : >>> recs = Q.get_all_records() # doctest:+SKIP -By default, all record types are included. However, we may only want some of the record types, and these can be filtered by passing a list of desired record types to the :code:`only` keyword argument. For example, to only recieve service and reneging customers, we can use:: +By default, all record types are included. However, we may only want some of the record types, and these can be filtered by passing a list of desired record types to the :code:`only` keyword argument. For example, to only receive service and reneging customers, we can use:: >>> recs = Q.get_all_records(only=["service", "renege"]) # doctest:+SKIP diff --git a/docs/Reference/routers.rst b/docs/Reference/routers.rst new file mode 100644 index 00000000..157de79d --- /dev/null +++ b/docs/Reference/routers.rst @@ -0,0 +1,174 @@ +.. _refs-routing: + +======================= +List of Routing Objects +======================= + +Ciw allows a number network router objects and node router objects. +The following network routers are currently supported: + +- :ref:`network_router` +- :ref:`trans_mat` +- :ref:`pb_route` +- :ref:`pb_flex` + +The following node routers are currently supported: + +- :ref:`leave` +- :ref:`direct` +- :ref:`probabilistic` +- :ref:`jsq` +- :ref:`load_balancing` +- :ref:`cycle` + + + +Network Routing Objects +======================= + +.. _network_router: + +-------------------------- +The General Network Router +-------------------------- + +The general network router allows us to define separate node routing objects for each node. For a two node network:: + + ciw.routing.NetworkRouting( + routers=[ + ciw.routing.NodeRouting(), # should be replaced with one of the node routers below + ciw.routing.NodeRouting(), # should be replaced with one of the node routers below + ciw.routing.NodeRouting() # should be replaced with one of the node routers below + ] + ) + + +.. _trans_mat: + +---------------------------- +The Transition Matrix Router +---------------------------- + +The :ref:`transition matrix` router takes in a transition matrix, a list of lists, with elements :math:`R_{ij}` representing the probability of entering node :math:`j` after service at node :math:`i`. For a two node network:: + + ciw.routing.TransitionMatrix( + transition_matrix=[ + [0.2, 0.8], + [0.0, 0.3] + ] + ) + + +.. _pb_route: + +------------------------ +The Process Based Router +------------------------ + +The :ref:`process based` router takes in a function that returns a pre-defined list of destinations that a customer will follow in order. E.g. all routing where all customers go to Node 2, then Node 1, then Node 1 again:: + + ciw.routing.ProcessBased( + routing_function=lambda ind, simulation: [2, 1, 1] + ) + + +.. _pb_flex: + +--------------------------------- +The Flexible Process Based Routed +--------------------------------- + +The flexible process based router is an extension of the above process based router, where the routing function gives a list of sets (defined by lists). For example:: + + ciw.routing.FlexibleProcessBased( + routing_function=lambda ind, simulation: [[2, 1], [1], [1, 2, 3]], + rule='any', + choice='jsq' + ) + +Arguments: + - The :code:`rule` argument can take one of two strings: :code:`'any'` or :code:`'all'`. For each set of nodes in the pre-defined route, using :code:`'any'` means that the customer must visit only one of the nodes in the set; while using :code:`'all'` means that the customer should visit all the nodes in that set, but not necessarily in that order. + - The :code:`choice` argument can take one of a number of strings. When using the :code:`'any'` rule, it determines how a node is chosen from the set. When using the :code:`'all'` rule, it determines at each instance, the choice of next node out of the set. The current options are: + - :code:`'random'`: randomly chooses a node from the set. + - :code:`'jsq'`: chooses the node with the smallest queue from the set (like the :ref:`join-shortest-queue` router). + - :code:`'lb'`: chooses the node with the least number of customers present from the set (like the :ref:`load-balancing` router). + + + + +Node Routing Objects +==================== + +.. _leave: + +----- +Leave +----- + +Leaves the system after service at the node:: + + ciw.routing.Leave() + + +.. _direct: + +------ +Direct +------ + +The customer goes direct to another node after service at the node. E.g. going to Node 2 direct form the node:: + + ciw.routing.Direct(to=2) + + + +.. _probabilistic: + +------------- +Probabilistic +------------- + +The customer is routed to another node probabilistically after service at the node. E.g. going to Node 1 with probability 0.4, and Node 3 with probability 0.1:: + + ciw.routing.Probabilistic( + destinations=[1, 3], + probs=[0.4, 0.1] + ) + + +.. _jsq: + +------------------- +Join Shortest Queue +------------------- + +The customer goes :ref:`joins the shortest queue` out of a subset of destinations:: + + ciw.routing.JoinShortestQueue(destinations=[1, 3], tie_break='random') + +The :code:`tie_break` argument is optional, and can take one of two strings: :code:`'random'` or :code:`'order'`. When there is a tie between the nodes with the shortest queue, tie breaks are either dealt with by choosing randomly between the ties (:code:`'random'`), or take precedence by the order listed in the :code:`destinations` list (:code:`'order'`). If omitted, random tie-breaking is used. + + +.. _load_balancing: + +-------------- +Load Balancing +-------------- + +The customer goes :ref:`joins the node with the least amount of customers present` out of a subset of destinations:: + + ciw.routing.LoadBalancing(destinations=[1, 3], tie_break='random') + +The :code:`tie_break` argument is optional, and can take one of two strings: :code:`'random'` or :code:`'order'`. When there is a tie between the nodes with the least amount of customers present, tie breaks are either dealt with by choosing randomly between the ties (:code:`'random'`), or take precedence by the order listed in the :code:`destinations` list (:code:`'order'`). If omitted, random tie-breaking is used. + + +.. _cycle: + +----- +Cycle +----- + +The customers' destinations out of the node cycles through a given list. For example, the first customer is routed to Node 1, the second to Node 1, the third to Node 3, the fourth to Node 2, then henceforth the order cycles, so the fifth is routed to Node 1, the sixth to Node 1, and so on:: + + ciw.routing.Cycle(cycle=[1, 1, 3, 2]) + diff --git a/docs/Reference/state_trackers.rst b/docs/Reference/state_trackers.rst index a72ba43f..ecd968b6 100644 --- a/docs/Reference/state_trackers.rst +++ b/docs/Reference/state_trackers.rst @@ -9,6 +9,7 @@ Currently Ciw has the following state trackers: - :ref:`population` - :ref:`nodepop` - :ref:`nodepopsubset` +- :ref:`groupednodepop` - :ref:`nodeclssmatrix` - :ref:`naiveblock` - :ref:`matrixblock` @@ -68,6 +69,24 @@ The Simulation object takes in the optional argument :code:`tracker`, which take >>> Q = ciw.Simulation(N, tracker=ciw.trackers.NodePopulationSubset([0, 1, 4])) # doctest:+SKIP +.. _groupednodepop: + +--------------------------------- +The GroupedNodePopulation Tracker +--------------------------------- + +The GroupedNodePopulation Tracker, similar to the NodePopulation Tracker, records the number of customers at each node. However this allows users to group nodes together into one state. +States take the form of list of numbers. An example of tracking a queueing network with three grouped nodes is shown below:: + + (2, 0, 5) + +This denotes that there are two customers at the first group of nodes, no customers at the second group of nodes, and five customers at the third group of nodes. + +The Simulation object takes in the optional argument :code:`tracker`, which takes an argument :code:`groups` a list of groups of nodes (a list of lists, nodes in the same list are in the same group), used as follows (the first group observes the total population in the 1st and 7th nodes, the second group observed the population of the 4th node, and the 3th group observes the population in the 2nd, 3rd, 5th and 6th nodes:: + + >>> Q = ciw.Simulation(N, tracker=ciw.trackers.GroupedNodePopulation(groups=[[0, 6], [3], [1, 2, 4, 5]])) # doctest:+SKIP + + .. _nodeclssmatrix: --------------------------- diff --git a/docs/Tutorial-I/index.rst b/docs/Tutorial-I/index.rst deleted file mode 100644 index a0f189b0..00000000 --- a/docs/Tutorial-I/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Tutorials - Part 1 -================== - -Part 1 of the Ciw tutorial will cover what is simulation and why we simulate. - -Contents: - -.. toctree:: - :maxdepth: 2 - - tutorial_i.rst - tutorial_ii.rst - tutorial_iii.rst - tutorial_iv.rst \ No newline at end of file diff --git a/docs/Tutorial-II/index.rst b/docs/Tutorial-II/index.rst deleted file mode 100644 index 6021b501..00000000 --- a/docs/Tutorial-II/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Tutorials - Part 2 -================== - -Part 2 of the Ciw tutorial will cover some of the other core features of the library. -It covers networks of queues, the routing matrix, blocking, and multiple classes of customer. - -Contents: - -.. toctree:: - :maxdepth: 2 - - tutorial_v.rst - tutorial_vi.rst - tutorial_vii.rst \ No newline at end of file diff --git a/docs/Tutorial/GettingStarted/index.rst b/docs/Tutorial/GettingStarted/index.rst new file mode 100644 index 00000000..b0c48fca --- /dev/null +++ b/docs/Tutorial/GettingStarted/index.rst @@ -0,0 +1,12 @@ +Tutorial I - Getting Started +============================ + +Contents: + +.. toctree:: + :maxdepth: 2 + + part_1.rst + part_2.rst + part_3.rst + part_4.rst \ No newline at end of file diff --git a/docs/Tutorial-I/tutorial_i.rst b/docs/Tutorial/GettingStarted/part_1.rst similarity index 91% rename from docs/Tutorial-I/tutorial_i.rst rename to docs/Tutorial/GettingStarted/part_1.rst index eb356535..8a4541fd 100644 --- a/docs/Tutorial-I/tutorial_i.rst +++ b/docs/Tutorial/GettingStarted/part_1.rst @@ -1,8 +1,8 @@ -.. _tutorial-i: +.. _tutorial-i-p1: -=========================================== -Tutorial I: Defining & Running a Simulation -=========================================== +==================================================== +Tutorial I: Part 1 - Defining & Running a Simulation +==================================================== Assume you are a bank manager and would like to know how long customers wait in your bank. Customers arrive randomly, roughly 12 per hour, regardless of the time of day. @@ -64,4 +64,4 @@ As we parametrised the system with time units of minutes, a day will be 1440 tim >>> Q.simulate_until_max_time(1440) Well done! We've now defined and simulated the bank for a day. -In the next tutorial, we will explore the :code:`Simulation` object some more. \ No newline at end of file +In the next part, we will explore the :code:`Simulation` object some more. \ No newline at end of file diff --git a/docs/Tutorial-I/tutorial_ii.rst b/docs/Tutorial/GettingStarted/part_2.rst similarity index 86% rename from docs/Tutorial-I/tutorial_ii.rst rename to docs/Tutorial/GettingStarted/part_2.rst index 62a2232c..230c189f 100644 --- a/docs/Tutorial-I/tutorial_ii.rst +++ b/docs/Tutorial/GettingStarted/part_2.rst @@ -1,10 +1,10 @@ -.. _tutorial-ii: +.. _tutorial-i-p2: -============================================ -Tutorial II: Exploring the Simulation Object -============================================ +==================================================== +Tutorial I: Part 2 - Exploring the Simulation Object +==================================================== -In the previous tutorial, we defined and simulated our bank for a week:: +In the previous part, we defined and simulated our bank for a week:: >>> import ciw >>> N = ciw.create_network( @@ -23,7 +23,7 @@ Although our queueing system consisted of one node (the bank), the object :code: [Arrival Node, Node 1, Exit Node] + **The Arrival Node:** - This is where customers are created. They are spawned here, and can :ref:`baulk `, :ref:`be rejected ` or sent to a service node. Accessed using:: + This is where customers are created. They are spawned here, and can :ref:`baulk `, :ref:`be rejected ` or sent to a service node. Accessed using:: >>> Q.nodes[0] Arrival Node @@ -76,4 +76,4 @@ Individuals carry data records, that contain information such as arrival date, w 10.88093... This isn't a very efficient way to look at results. -In the next tutorial we will look at generating lists of records to gain some summary statistics. \ No newline at end of file +In the next part we will look at generating lists of records to gain some summary statistics. \ No newline at end of file diff --git a/docs/Tutorial-I/tutorial_iii.rst b/docs/Tutorial/GettingStarted/part_3.rst similarity index 83% rename from docs/Tutorial-I/tutorial_iii.rst rename to docs/Tutorial/GettingStarted/part_3.rst index 88a50319..433d2ec2 100644 --- a/docs/Tutorial-I/tutorial_iii.rst +++ b/docs/Tutorial/GettingStarted/part_3.rst @@ -1,10 +1,10 @@ -.. _tutorial-iii: +.. _tutorial-i-p3: -================================ -Tutorial III: Collecting Results -================================ +======================================= +Tutorial I: Part 3 - Collecting Results +======================================= -In the previous tutorials, we defined and simulated our bank for a week, and saw how to access parts of the simulation engine:: +In the previous parts, we defined and simulated our bank for a week, and saw how to access parts of the simulation engine:: >>> import ciw >>> N = ciw.create_network( @@ -63,7 +63,7 @@ Now we can get summary statistics simply by manipulating these lists:: 4.230543... We now know the mean waiting time of the customers! -In next tutorial we will show how to get more representative results (as we have only simulated one given day here). +In next part we will show how to get more representative results (as we have only simulated one given day here). Further summary statistics can be obtained using external libraries. We recommend `numpy `_, `pandas `_ and `matplotlib `_. @@ -73,7 +73,7 @@ The histogram of waits below was created using matplotlib, using the following c >>> import matplotlib.pyplot as plt # doctest:+SKIP >>> plt.hist(waits); # doctest:+SKIP -.. image:: ../_static/tutorial_iii_waitshist.svg +.. image:: ../../_static/tutorial_iii_waitshist.svg :alt: Histogram of waits for Tutorial III. :align: center @@ -85,4 +85,4 @@ This is the average utilisation of each server, which is the amount of time a se Thus in our bank, on average the servers were busy 75.3% of the time. -The next tutorial will show how to use Ciw to get trustworthy results, and finally find out the average waiting time at the bank. +The next part will show how to use Ciw to get trustworthy results, and finally find out the average waiting time at the bank. diff --git a/docs/Tutorial-I/tutorial_iv.rst b/docs/Tutorial/GettingStarted/part_4.rst similarity index 93% rename from docs/Tutorial-I/tutorial_iv.rst rename to docs/Tutorial/GettingStarted/part_4.rst index 13a75d87..f35523bf 100644 --- a/docs/Tutorial-I/tutorial_iv.rst +++ b/docs/Tutorial/GettingStarted/part_4.rst @@ -1,10 +1,10 @@ -.. _tutorial-iv: +.. _tutorial-i-p4: -======================================== -Tutorial IV: Trials, Warm-up & Cool-down -======================================== +================================================ +Tutorial I: Part 4 - Trials, Warm-up & Cool-down +================================================ -In Tutorials I-III we investigated one run of a simulation of a bank. +In parts 1-3 we investigated one run of a simulation of a bank. Before we draw any conclusions about the behaviour of the bank, there are three things we should consider: 1. Our original description of the bank described it as open 24/7. This means the bank would never start from an empty state, as our simulation does. @@ -45,7 +45,7 @@ The list :code:`average_waits` will now contain ten numbers denoting the mean wa Notice that we set a different seed every time, so each trial will yield different results:: >>> average_waits - [3.91950..., 4.34163..., 4.61779..., 5.33537..., 5.06224..., 2.90274..., 4.93209..., 17.95093128538666, 4.06136..., 3.14126...] + [3.91950..., 4.34163..., 4.61779..., 5.33537..., 5.06224..., 2.90274..., 4.93209..., 17.95093..., 4.06136..., 3.14126...] We can see that the range of waits are quite high, between 1.6 and 7.5. This shows that running a single trial wouldn't have given us a very confident answer. diff --git a/docs/Tutorial/index.rst b/docs/Tutorial/index.rst new file mode 100644 index 00000000..c16e66ac --- /dev/null +++ b/docs/Tutorial/index.rst @@ -0,0 +1,16 @@ +Tutorials +========= + +Tutorial I is all about Getting Started, and will cover what is simulation and why we simulate. +The other tutorials are shorter and will cover some of the other core features of the library. +It covers networks of queues, the routing matrix, blocking, and multiple classes of customer. + +Contents: + +.. toctree:: + :maxdepth: 2 + + GettingStarted/index.rst + tutorial_ii.rst + tutorial_iii.rst + tutorial_iv.rst \ No newline at end of file diff --git a/docs/Tutorial-II/tutorial_v.rst b/docs/Tutorial/tutorial_ii.rst similarity index 97% rename from docs/Tutorial-II/tutorial_v.rst rename to docs/Tutorial/tutorial_ii.rst index aedfc6be..fdaa40b6 100644 --- a/docs/Tutorial-II/tutorial_v.rst +++ b/docs/Tutorial/tutorial_ii.rst @@ -1,8 +1,8 @@ -.. _tutorial-v: +.. _tutorial-ii: -=============================== -Tutorial V: A Network of Queues -=============================== +================================ +Tutorial II: A Network of Queues +================================ Ciw's real power comes when modelling networks of queues. That is many service nodes, such that when customers finish service, there is a probability of joining another node, rejoining the current node, or leaving the system. diff --git a/docs/Tutorial-II/tutorial_vi.rst b/docs/Tutorial/tutorial_iii.rst similarity index 97% rename from docs/Tutorial-II/tutorial_vi.rst rename to docs/Tutorial/tutorial_iii.rst index 0a429ad6..45a6a9d7 100644 --- a/docs/Tutorial-II/tutorial_vi.rst +++ b/docs/Tutorial/tutorial_iii.rst @@ -1,8 +1,8 @@ -.. _tutorial-vi: +.. _tutorial-iii: -================================ -Tutorial VI: Restricted Networks -================================ +================================= +Tutorial III: Restricted Networks +================================= Imagine a manufacturing plant that produces stools: diff --git a/docs/Tutorial-II/tutorial_vii.rst b/docs/Tutorial/tutorial_iv.rst similarity index 97% rename from docs/Tutorial-II/tutorial_vii.rst rename to docs/Tutorial/tutorial_iv.rst index 06e96115..71e08dc1 100644 --- a/docs/Tutorial-II/tutorial_vii.rst +++ b/docs/Tutorial/tutorial_iv.rst @@ -1,8 +1,8 @@ -.. _tutorial-vii: +.. _tutorial-iv: -========================================== -Tutorial VII: Multiple Classes of Customer -========================================== +========================================= +Tutorial IV: Multiple Classes of Customer +========================================= Imagine a 24 hour paediatricians clinic: diff --git a/docs/_static/custom_routing_with.svg b/docs/_static/custom_routing_with.svg index 2befc9ca..2daf64ac 100644 --- a/docs/_static/custom_routing_with.svg +++ b/docs/_static/custom_routing_with.svg @@ -1,2918 +1,2862 @@ - - + + + + + + 2024-04-26T13:29:41.985021 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + - + - +" style="fill: #ffffff"/> - +" style="fill: #ffffff"/> + + + - +" style="stroke: #333333; stroke-width: 0.8"/> - + - - + + - - - +" transform="scale(0.015625)"/> + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - +" style="stroke: #333333; stroke-width: 0.8"/> - + - - + + - + + + + - + - - - + + + - + + + + - + - - - + + + - + + + + - + - - - + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - + - - - - - - - + - - + + diff --git a/docs/_static/custom_routing_without.svg b/docs/_static/custom_routing_without.svg index e6c22078..5b3c80ac 100644 --- a/docs/_static/custom_routing_without.svg +++ b/docs/_static/custom_routing_without.svg @@ -1,2863 +1,2853 @@ - - + + + + + + 2024-04-26T13:29:06.791084 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + - + - +" style="fill: #ffffff"/> - +" style="fill: #ffffff"/> + + + - +" style="stroke: #333333; stroke-width: 0.8"/> - + - - + + - - - +" transform="scale(0.015625)"/> + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - + - - + + - - - - +" transform="scale(0.015625)"/> + + + - + + + + - +" style="stroke: #333333; stroke-width: 0.8"/> - + - - + + - + + + + - + - - + + - + + + + - + - - - + + + - + + + + - + - - - + + + - + + + + - + - - - + + + - + + + + - + - - - + + + - - + + - - + + - + - - - - - - - + - - + + diff --git a/docs/_static/recs_pandas.png b/docs/_static/recs_pandas.png new file mode 100644 index 00000000..2eac768d Binary files /dev/null and b/docs/_static/recs_pandas.png differ diff --git a/docs/_static/style.css b/docs/_static/style.css index b965f883..9ae64575 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -14,4 +14,5 @@ th { td { font-size: 100%; + border: 1px solid #ddd !important; } \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 0e687a23..29c2f2b6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,18 +11,17 @@ disciplines, dynamic customer classes, and deadlock detection. The name **Ciw** is the Welsh word for a queue. -Ciw is currently supported for and regularly tested on Python versions 3.7, -3.8, 3.9, 3.10 and 3.11. +Ciw is currently supported for and regularly tested on Python versions 3.8, +3.9, 3.10, 3.11 and 3.12. Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :titlesonly: installation.rst - Tutorial-I/index.rst - Tutorial-II/index.rst + Tutorial/index.rst Guides/index.rst Reference/index.rst Background/index.rst diff --git a/requirements.txt b/requirements.txt index 490908e7..e83c891d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +setuptools>=69 networkx>=2.3 tqdm>=4.64.0 numpy>=1.21.6 \ No newline at end of file