CiwPython / Ciw

Ciw is a simulation library for open queueing networks.
http://ciw.readthedocs.io
MIT License
148 stars 42 forks source link

re-entrance customer with dynamic priority #212

Closed Porridge979 closed 1 year ago

Porridge979 commented 1 year ago

Hello,

I want to express my appreciation for the powerful features of Ciw. I am currently working on simulating a queue with specific properties.

The queue will consist of two nodes and two classes of customers. For customer 'Class 0', the routing will only go through Node 1. However, for customer 'Class 1', the routing will go through Node 1, then Node 2, and then back to Node 1.

I also want to prioritize customer 'Class 1' who return to 'Node 1' after finishing the service in 'Node 2'. These customers will be serviced first, but when they initially join the queue at Node 1, they will have the same priority as other classes of customers. This queue is simulating patient re-entrance in outpatient clinics, which occurs after medical tests such as blood work to revisit the physician for discussing test results .

image

I have read the latest Ciw 2.3.6 documentation and tried to simulate this queue network by following the tutorial on setting dynamic customer classes. In the current code, customer 'Class 1' changes to 'Class 2' after finishing the service in Node 2, and customer 'Class 2' has the highest priority. I changed the class in order to prioritize the re-entrance customer. However, I encountered some bugs with error 'Ensure correct nodes used in class_change_matrices'.

N = ciw.create_network(
     arrival_distributions={'Class 0': [ciw.dists.Exponential(rate=5),ciw.dists.NoArrivals()],
                            'Class 1': [ciw.dists.Exponential(rate=5),ciw.dists.NoArrivals()],
                            'Class 2': [ciw.dists.NoArrivals(),ciw.dists.NoArrivals()]},
     service_distributions={'Class 0': [ciw.dists.Exponential(rate=10),ciw.dists.Deterministic(value=0.0)],
                            'Class 1': [ciw.dists.Exponential(rate=10),ciw.dists.Exponential(rate=10)],
                            'Class 2': [ciw.dists.Exponential(rate=10),ciw.dists.Deterministic(value=0.0)]},
     routing={'Class 0': [[0,0],[0,0]],
              'Class 1': [[0,1],[1,0]],
              'Class 2': [[0,0],[0,0]]},
     class_change_matrices={'Node 2': [[0, 0, 0],
                                       [0, 0, 1],
                                       [0, 0, 0]]},
     number_of_servers=[1,1]
 )
ciw.seed(0)
Q = ciw.Simulation(N)
Q.simulate_until_max_time(100.0)

I would greatly appreciate any help with resolving these bugs, and I am also curious if there is a better solution to simulate the re-entrance process with dynamic priority.

Thank you for your assistance.

geraintpalmer commented 1 year ago

Dear @Porridge979 thanks for the query.

This should fix it:

N = ciw.create_network(
     arrival_distributions={'Class 0': [ciw.dists.Exponential(rate=5), ciw.dists.NoArrivals()],
                            'Class 1': [ciw.dists.Exponential(rate=5), ciw.dists.NoArrivals()],
                            'Class 2': [ciw.dists.NoArrivals(), ciw.dists.NoArrivals()]},
     service_distributions={'Class 0': [ciw.dists.Exponential(rate=10), ciw.dists.Deterministic(value=0.0)],
                            'Class 1': [ciw.dists.Exponential(rate=10), ciw.dists.Exponential(rate=10)],
                            'Class 2': [ciw.dists.Exponential(rate=10), ciw.dists.Deterministic(value=0.0)]},
     routing={'Class 0': [[0, 0],[0, 0]],
              'Class 1': [[0, 1],[1, 0]],
              'Class 2': [[0, 0],[0, 0]]},
     class_change_matrices={
         'Node 1': [[1, 0, 0],
                    [0, 1, 0],
                    [0, 0, 1]],
         'Node 2': [[1, 0, 0],
                    [0, 0, 1],
                    [0, 0, 1]]
         },
     number_of_servers=[1, 1]
 )
ciw.seed(0)
Q = ciw.Simulation(N)
Q.simulate_until_max_time(100.0)

Hope that makes sense. Let me know if you have any trouble.

Porridge979 commented 1 year ago

Thank you for providing such a patient and meticulous answer. Although the code is now functioning correctly, I am not getting the expected results. It appears that the customer's class does not change from 'Class 1' to 'Class 2' after completing the service at Node 2. The simulation records show that there are only two classes, 'Class 1' and 'Class 0', throughout the process. Moreover, all customers retain their original classification, i.e., their customer_class remains the same. Could you please check if there is an issue with the code? Thank you once again for your help!

N = ciw.create_network(
     arrival_distributions={'Class 0': [ciw.dists.Exponential(rate=5), ciw.dists.NoArrivals()],
                            'Class 1': [ciw.dists.Exponential(rate=5), ciw.dists.NoArrivals()],
                            'Class 2': [ciw.dists.NoArrivals(), ciw.dists.NoArrivals()]},
     service_distributions={'Class 0': [ciw.dists.Exponential(rate=10), ciw.dists.Deterministic(value=0.0)],
                            'Class 1': [ciw.dists.Exponential(rate=10), ciw.dists.Exponential(rate=10)],
                            'Class 2': [ciw.dists.Exponential(rate=10), ciw.dists.Deterministic(value=0.0)]},
     routing={'Class 0': [[0, 0],[0, 0]],
              'Class 1': [[0, 1],[1, 0]],
              'Class 2': [[0, 0],[0, 0]]},
     class_change_matrices={
         'Node 1': [[1, 0, 0],
                    [0, 1, 0],
                    [0, 0, 1]],
         'Node 2': [[1, 0, 0],
                    [0, 0, 1],
                    [0, 0, 1]]
         },
     number_of_servers=[1, 1]
 )
ciw.seed(0)
Q = ciw.Simulation(N)
Q.simulate_until_max_time(1000.0)

recs = Q.get_all_records()
df = pd.DataFrame(recs)
len(df[df['customer_class']!=df['original_customer_class']])
[Output]:0
df['customer_class'].unique()
[Output]:array([0, 1])
geraintpalmer commented 1 year ago

Ah! So the changing class happens before choosing the next destination. At the moment customers are changing to Class 2, but then leaving straight away, not making a data record object.

I think this should fix it:

     routing={'Class 0': [[0, 0],[0, 0]],
              'Class 1': [[0, 1],[1, 0]],
              'Class 2': [[0, 0],[1, 0]]},

Now customers of Class 2 are routed to Node 1.

Does that work now?

Porridge979 commented 1 year ago

Thank you ๐Ÿ™ ๐Ÿ˜Š! I have successfully implemented the customer routing and the results are accurate. However, I want to clarify my understanding of the difference between 'original_customer_class' and 'customer_class'. I am still seeing that all customers' 'customer_class' is the same as their 'original_customer_class', indicating that any changes in their class are not being recorded. Can you confirm if my understanding is correct?

image
geraintpalmer commented 1 year ago

It is working correctly.

Each row here is a DataRecord, which records all information from when the customer entered the node and started waiting, to when the customer leave the node. In this case the changing class happens between nodes, so is not recorded in the data record. So won't appear here.

So it works as expected.

The original_customer_class column concerns the other type of dynamic customer classes in Ciw, when customers change class while waiting. (https://ciw.readthedocs.io/en/latest/Guides/change-class-while-queueing.html#changeclass-whilequeueing) Here a customer can enter as one class, but start service as another class. So both classes need to be recorded.

Hope that helps.

Porridge979 commented 1 year ago

I really appreciate your patience in helping me understand this๐Ÿ™๐Ÿ™! It's clear to me now.