Skip to content

Commit 33bee5a

Browse files
committed
add flexible process based routing
1 parent 9259c22 commit 33bee5a

3 files changed

Lines changed: 187 additions & 1 deletion

File tree

ciw/routing/routing.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,67 @@ def next_node_for_rerouting(self, ind, node_id):
101101
"""
102102
return self.next_node(ind, node_id)
103103

104+
class FlexibleProcessBased(ProcessBased):
105+
"""
106+
A class to route an individual based on a pre-defined process.
107+
"""
108+
def __init__(self, route_function, rule, choice):
109+
"""
110+
Initialises the routing object.
111+
112+
Takes:
113+
- route_function: a function that returns a pre-defined route
114+
"""
115+
self.route_function = route_function
116+
if rule not in ['any', 'all']:
117+
raise ValueError("Flexible routing rules must be one of 'any' or 'all'.")
118+
if choice not in ['random', 'jsq', 'lb']:
119+
raise ValueError("Flexible routing choices must be one of 'random', 'jsq', or 'lb'.")
120+
self.rule = rule
121+
self.choice = choice
122+
123+
def find_next_node_from_subset(self, subset, ind):
124+
"""
125+
Finds the next node within a subset of nodes
126+
according to the 'choice' parameter
127+
"""
128+
if self.choice == 'random':
129+
return ciw.random_choice(subset)
130+
if self.choice == 'jsq':
131+
temp_router = JoinShortestQueue(destinations=subset)
132+
temp_router.initialise(self.simulation, None)
133+
nd = temp_router.next_node(ind)
134+
return nd.id_number
135+
if self.choice == 'lb':
136+
temp_router = LoadBalancing(destinations=subset)
137+
temp_router.initialise(self.simulation, None)
138+
nd = temp_router.next_node(ind)
139+
return nd.id_number
140+
141+
def update_individual_route(self, ind, next_node_id):
142+
"""
143+
Updates the individual route by removing chosen nodes
144+
along the route, according to the 'rule' parameter
145+
"""
146+
if self.rule == 'any':
147+
ind.route = ind.route[1:]
148+
if self.rule == 'all':
149+
ind.route[0].remove(next_node_id)
150+
if len(ind.route[0]) == 0:
151+
ind.route = ind.route[1:]
152+
153+
def next_node(self, ind, node_id):
154+
"""
155+
Chooses the next node from the process-based pre-defined route.
156+
"""
157+
if len(ind.route) == 0:
158+
node_index = -1
159+
else:
160+
node_index = self.find_next_node_from_subset(ind.route[0], ind)
161+
self.update_individual_route(ind, node_index)
162+
return self.simulation.nodes[node_index]
163+
164+
104165

105166
class NodeRouting:
106167
"""

ciw/tests/test_process_based.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import random
44
from collections import Counter
55

6-
76
def generator_function_1(ind, simulation):
87
return [1, 1]
98

@@ -29,6 +28,8 @@ def __init__(self, n):
2928
def generator_method(self, ind, simulation):
3029
return [1, 1]
3130

31+
def flexible_generator_1(ind, simulation):
32+
return [[2], [3, 4, 5], [6]]
3233

3334
class TestProcessBased(unittest.TestCase):
3435
def test_network_takes_routing_function(self):
@@ -152,3 +153,103 @@ def test_process_based_takes_methods(self):
152153
inds = Q.nodes[1].all_individuals
153154
for ind in inds:
154155
self.assertEqual(ind.route, [1, 1])
156+
157+
def test_flexible_process_based(self):
158+
## First test 'any-random':
159+
N = ciw.create_network(
160+
arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None],
161+
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)],
162+
number_of_servers=[3, 3, 3, 3, 3, 3],
163+
routing=ciw.routing.FlexibleProcessBased(
164+
route_function=flexible_generator_1,
165+
rule='any',
166+
choice='random'
167+
)
168+
)
169+
ciw.seed(0)
170+
Q = ciw.Simulation(N)
171+
Q.simulate_until_max_customers(1000)
172+
inds = Q.nodes[-1].all_individuals
173+
routes_counter = Counter(
174+
[tuple(dr.node for dr in ind.data_records) for ind in inds]
175+
)
176+
self.assertEqual(
177+
routes_counter,
178+
Counter({(1, 2, 4, 6): 350, (1, 2, 5, 6): 333, (1, 2, 3, 6): 317}), # all evenly spread
179+
)
180+
181+
## Now test 'any-jsq' (flooding Node 4 so no customers ever go here):
182+
N = ciw.create_network(
183+
arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None],
184+
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)],
185+
number_of_servers=[3, 3, 3, 3, 3, 3],
186+
routing=ciw.routing.FlexibleProcessBased(
187+
route_function=flexible_generator_1,
188+
rule='any',
189+
choice='jsq'
190+
)
191+
)
192+
ciw.seed(0)
193+
Q = ciw.Simulation(N)
194+
Q.simulate_until_max_customers(1000)
195+
inds = Q.nodes[-1].all_individuals
196+
routes_counter = Counter(
197+
[tuple(dr.node for dr in ind.data_records) for ind in inds]
198+
)
199+
self.assertEqual(
200+
routes_counter,
201+
Counter({(1, 2, 5, 6): 503, (1, 2, 3, 6): 497}), # evenly spread between the two unflooded nodes
202+
)
203+
204+
## Now test 'any-lb' (flooding Node 4 so no customers ever go here):
205+
N = ciw.create_network(
206+
arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None],
207+
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)],
208+
number_of_servers=[3, 3, 3, 3, 3, 3],
209+
routing=ciw.routing.FlexibleProcessBased(
210+
route_function=flexible_generator_1,
211+
rule='any',
212+
choice='lb'
213+
)
214+
)
215+
ciw.seed(0)
216+
Q = ciw.Simulation(N)
217+
Q.simulate_until_max_customers(1000)
218+
inds = Q.nodes[-1].all_individuals
219+
routes_counter = Counter(
220+
[tuple(dr.node for dr in ind.data_records) for ind in inds]
221+
)
222+
self.assertEqual(
223+
routes_counter,
224+
Counter({(1, 2, 5, 6): 508, (1, 2, 3, 6): 492}), # evenly spread between the two unflooded nodes
225+
)
226+
227+
228+
## Now test 'all-random':
229+
N = ciw.create_network(
230+
arrival_distributions=[ciw.dists.Exponential(rate=1), None, None, None, None, None],
231+
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)],
232+
number_of_servers=[3, 3, 3, 3, 3, 3],
233+
routing=ciw.routing.FlexibleProcessBased(
234+
route_function=flexible_generator_1,
235+
rule='all',
236+
choice='random'
237+
)
238+
)
239+
ciw.seed(0)
240+
Q = ciw.Simulation(N)
241+
Q.simulate_until_max_customers(1000)
242+
inds = Q.nodes[-1].all_individuals
243+
routes_counter = Counter(
244+
[tuple(dr.node for dr in ind.data_records) for ind in inds]
245+
)
246+
self.assertEqual(routes_counter[(1, 2, 3, 4, 5, 6)], 169)
247+
self.assertEqual(routes_counter[(1, 2, 3, 5, 4, 6)], 139)
248+
self.assertEqual(routes_counter[(1, 2, 4, 3, 5, 6)], 185)
249+
self.assertEqual(routes_counter[(1, 2, 4, 5, 3, 6)], 166)
250+
self.assertEqual(routes_counter[(1, 2, 5, 3, 4, 6)], 186)
251+
self.assertEqual(routes_counter[(1, 2, 5, 4, 3, 6)], 155)
252+
253+
def test_flexible_process_based_error_raising(self):
254+
self.assertRaises(ValueError, ciw.routing.FlexibleProcessBased, flexible_generator_1, 'something', 'random')
255+
self.assertRaises(ValueError, ciw.routing.FlexibleProcessBased, flexible_generator_1, 'all', 'something')

docs/Reference/routers.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The following network routers are currently supported:
1010
- :ref:`network_router`
1111
- :ref:`trans_mat`
1212
- :ref:`pb_route`
13+
- :ref:`pb_flex`
1314

1415
The following node routers are currently supported:
1516

@@ -71,6 +72,28 @@ The :ref:`process based<process-based>` router takes in a function that returns
7172
)
7273

7374

75+
.. _pb_flex:
76+
77+
---------------------------------
78+
The Flexible Process Based Routed
79+
---------------------------------
80+
81+
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::
82+
83+
ciw.routing.FlexibleProcessBased(
84+
routing_function=lambda ind, simulation: [[2, 1], [1], [1, 2, 3]],
85+
rule='any',
86+
choice='jsq'
87+
)
88+
89+
Arguments:
90+
- 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.
91+
- 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:
92+
- :code:`'random'`: randomly chooses a node from the set.
93+
- :code:`'jsq'`: chooses the node with the smallest queue from the set (like the :ref:`join-shortest-queue<jsq>` router).
94+
- :code:`'lb'`: chooses the node with the least number of customers present from the set (like the :ref:`load-balancing<load_balancing>` router).
95+
96+
7497

7598

7699
Node Routing Objects
@@ -125,6 +148,7 @@ The customer goes :ref:`joins the shortest queue<join-shortest-queue>` out of a
125148

126149
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.
127150

151+
128152
.. _load_balancing:
129153

130154
--------------

0 commit comments

Comments
 (0)