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 
>>> plt.plot(ts, n2); 
>>> plt.plot(ts, n3); 
Plot of node populations diverging.

With desired behaviour

We will now create a new CustomRouting for Node 1, that will send its customers to the least busy of Nodes 2 and 3. First create the CustomRouting that inherits from ciw.Node, and overwrites the 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 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 node_class argumument a list of classes. We’ll use the new CustomRouting class for Node 1, and the regular 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); 
>>> plt.plot(ts, n3); 
Plot of node populations closely following each other.