From 126a14382d2426eb97e271f8ca3724ef24510f5e Mon Sep 17 00:00:00 2001 From: Geraint Palmer Date: Mon, 29 Apr 2024 22:15:14 +0100 Subject: [PATCH] implement rerouting as a preemptive interruption option --- ciw/node.py | 72 ++-- ciw/routing/routing.py | 18 + ciw/schedules.py | 4 +- ciw/tests/test_simulation.py | 310 ++++++++++++++++++ docs/Guides/CustomerBehaviour/index.rst | 2 +- .../CustomerClasses/customer-classes.rst | 2 +- docs/Guides/Routing/custom_routing.rst | 25 ++ docs/Guides/Services/preemption.rst | 22 +- 8 files changed, 419 insertions(+), 36 deletions(-) diff --git a/ciw/node.py b/ciw/node.py index b36b4c0..3bfd6dd 100644 --- a/ciw/node.py +++ b/ciw/node.py @@ -529,25 +529,31 @@ 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 """ 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): """ Removes individual_to_preempt from service and replaces them with 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) @@ -555,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 @@ -573,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 @@ -584,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): """ @@ -687,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): """ @@ -705,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. @@ -848,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. """ @@ -869,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/routing.py b/ciw/routing/routing.py index d7abb02..5423442 100644 --- a/ciw/routing/routing.py +++ b/ciw/routing/routing.py @@ -30,6 +30,12 @@ def next_node(self, ind, node_id): """ 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): """ @@ -88,6 +94,12 @@ def next_node(self, ind, node_id): 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 NodeRouting: """ @@ -104,6 +116,12 @@ def initialise(self, simulation, node): 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): """ diff --git a/ciw/schedules.py b/ciw/schedules.py index 7e4820a..855948c 100644 --- a/ciw/schedules.py +++ b/ciw/schedules.py @@ -52,8 +52,8 @@ def __init__(self, numbers_of_servers: List[int], shift_end_dates: List[float], '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: diff --git a/ciw/tests/test_simulation.py b/ciw/tests/test_simulation.py index 2459be2..29c8b72 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={ @@ -1198,6 +1199,315 @@ def test_system_capacity(self): 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): diff --git a/docs/Guides/CustomerBehaviour/index.rst b/docs/Guides/CustomerBehaviour/index.rst index c1614a2..3078951 100644 --- a/docs/Guides/CustomerBehaviour/index.rst +++ b/docs/Guides/CustomerBehaviour/index.rst @@ -1,5 +1,5 @@ Customer Behaviour -======== +================== Contents: diff --git a/docs/Guides/CustomerClasses/customer-classes.rst b/docs/Guides/CustomerClasses/customer-classes.rst index 5d5f45f..9e980b6 100644 --- a/docs/Guides/CustomerClasses/customer-classes.rst +++ b/docs/Guides/CustomerClasses/customer-classes.rst @@ -48,7 +48,7 @@ When collecting results, the class of the customer associated with each service >>> 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:: +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`, diff --git a/docs/Guides/Routing/custom_routing.rst b/docs/Guides/Routing/custom_routing.rst index f748583..adcfa3f 100644 --- a/docs/Guides/Routing/custom_routing.rst +++ b/docs/Guides/Routing/custom_routing.rst @@ -91,3 +91,28 @@ Now if the only arrivals are to node 1, and we run this for 100 time units, we s 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/Services/preemption.rst b/docs/Guides/Services/preemption.rst index 3e7bdcd..670dc07 100644 --- a/docs/Guides/Services/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,10 +47,11 @@ 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(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 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:: @@ -57,6 +59,14 @@ Ciw defaults to non-pre-emptive schedules, and so the following code implies a n 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 -------------------------------