J.C. Kantor (Kantor.1@nd.edu)
This IPython notebook demonstrates use of the SimPy to simulate the order fulfillment operations of a hypothetical warehouse.
from IPython.core.display import HTML
HTML(open("styles/custom.css", "r").read())
The order fulfillment operation is a sequence of eight events
orderGenerator
creates a sequence of orders following a Poisson distribution at an average rate 1/T_ORDER. Each order is tagged with a unique name using itertools.count()
.
Events are logged and stored in a dictionary attached to an instance of the fulfillment class. The keys are an (orderId,event) tuple with the time stamp stored as the value. The raw data trace can be accessed as the ._data
field, or as a pandas Dataframe in the .log
field.
# Global variables
T_ORDER = 10 # average time between orders (Poisson)
T_PICK = 15 # mean time to pick order from warehouse (beta)
T_REVIEW = 2 # mean time to review order prior to shipment (beta)
T_SHIP_MIN = 8; T_SHIP_MAX = 12 # mean time to ship order (uniform)
ALPHA = 2.0; BETA = 5.0 # beta distribution parameters
# Global defaults
T_SIM = 1440 # simulation period
N_PICKER = 2 # number of product pickers
N_REVIEWER = 1 # number of product reviewers
import simpy
import itertools
import random
import pandas as pd
class fulfillment(object):
def __init__(self, n_picker=N_PICKER, n_reviewer=N_REVIEWER):
self.env = simpy.Environment()
self._data = dict()
self.picker = simpy.Resource(self.env,n_picker)
self.reviewer = simpy.Resource(self.env,n_reviewer)
self.env.process(self.orderGenerator())
def writeLog(self,orderId,event):
self._data[orderId,event] = self.env.now
@property
def log(self):
df = pd.DataFrame([[a,b,self._data[a,b]] for (a,b) in self._data.keys()])
df = df.pivot(index=0,columns=1,values=2).reset_index()
return df[list(df.columns)[1:]]
@property
def stats(self):
tdelta = pd.DataFrame()
for c in self.log.columns:
tdelta[c] = self.log[c] - self.log['Ordered']
return pd.DataFrame([tdelta.mean(),tdelta.std()],index=['mean','std'])
def order(self,orderId):
self.writeLog(orderId,'Ordered')
with self.picker.request() as preq:
yield preq
self.writeLog(orderId,'Pick Assigned')
yield self.env.timeout(T_PICK*((ALPHA+BETA)/ALPHA)*random.betavariate(ALPHA,BETA))
self.writeLog(orderId,'Picked')
self.picker.release(preq)
with self.reviewer.request() as rreq:
yield rreq
self.writeLog(orderId,'Review Assigned')
yield self.env.timeout(T_REVIEW*((ALPHA+BETA)/ALPHA)*random.betavariate(ALPHA,BETA))
self.writeLog(orderId,'Reviewed')
self.reviewer.release(rreq)
yield self.env.timeout(random.uniform(T_SHIP_MIN,T_SHIP_MAX))
self.writeLog(orderId,'Shipped')
def orderGenerator(self):
for orderId in itertools.count():
yield self.env.timeout(random.expovariate(1.0/T_ORDER))
self.env.process(self.order(orderId))
def run(self,t_sim = T_SIM):
self.env.run(until=t_sim)
%matplotlib inline
import matplotlib.pyplot as plt
# create an order fulfillment instance
w = fulfillment(n_picker = 4, n_reviewer = 1)
# run a simulation for a specified period
w.run(10000)
print("Elapsed Time from Order Placement")
print(w.stats)
# plot log results
w.log['Shipped'].plot()
plt.xlabel('Order Number')
plt.ylabel('Time [minutes]')
Elapsed Time from Order Placement Ordered Pick Assigned Picked Review Assigned Reviewed Shipped mean 0.0 0.372984 15.342372 15.684901 17.664159 27.649617 std 0.0 1.717008 8.519451 8.533925 8.601076 8.694360
<matplotlib.text.Text at 0x11d5b9748>
torder = w.log['Shipped'] - w.log['Ordered']
plt.subplot(3,1,1)
plt.plot(torder)
plt.xlabel('Order Number')
plt.ylabel('Time [minutes]')
plt.title('Time from Order to Shipment')
plt.subplot(2,1,2)
torder.hist(bins=30)
plt.xlabel('Time [minutes]')
plt.ylabel('Count')
<matplotlib.text.Text at 0x11d7b7470>
twait = w.log['Pick Assigned'] - w.log['Ordered']
subplot(3,1,1)
plot(twait)
xlabel('Order Number')
ylabel('Time [minutes]')
title('Time to Assign Picker')
subplot(2,1,2)
twait.hist(bins=30)
xlabel('Time [minutes]')
ylabel('Count')
<matplotlib.text.Text at 0x109d0aad0>
tpick = w.log['Picked'] - w.log['Pick Assigned']
subplot(3,1,1)
plot(tpick)
xlabel('Order Number')
ylabel('Time [minutes]')
title('Time to Pick Order')
subplot(2,1,2)
tpick.hist(bins=30)
xlabel('Time [minutes]')
ylabel('Count')
<matplotlib.text.Text at 0x109d5ead0>
twait = w.log['Review Assigned'] - w.log['Picked']
subplot(3,1,1)
plot(twait)
xlabel('Order Number')
ylabel('Time [minutes]')
title('Time to Assign Inspector')
subplot(2,1,2)
twait.hist(bins=30)
xlabel('Time [minutes]')
ylabel('Count')
<matplotlib.text.Text at 0x10aa71410>
treview = w.log['Reviewed'] - w.log['Review Assigned']
subplot(3,1,1)
plot(treview)
xlabel('Order Number')
ylabel('Time [minutes]')
title('Time to Review Order')
subplot(2,1,2)
treview.hist(bins=30)
xlabel('Time [minutes]')
ylabel('Count')
<matplotlib.text.Text at 0x10b076350>
tship = w.log['Shipped'] - w.log['Reviewed']
subplot(3,1,1)
plot(tship)
xlabel('Order Number')
ylabel('Time [minutes]')
title('Time to Ship')
subplot(2,1,2)
tship.hist(bins=30)
xlabel('Time [minutes]')
ylabel('Count')
<matplotlib.text.Text at 0x10b0cc0d0>
display(w.log)
1 | Ordered | Pick Assigned | Picked | Review Assigned | Reviewed | Shipped |
---|---|---|---|---|---|---|
0 | 3.366705 | 3.366705 | 37.402323 | 37.402323 | 39.949771 | 48.042636 |
1 | 13.430272 | 13.430272 | 21.922379 | 21.922379 | 22.382470 | 30.712128 |
2 | 19.418406 | 21.922379 | 49.876027 | 49.876027 | 51.697383 | 61.445490 |
3 | 21.600034 | 37.402323 | 55.225490 | 55.225490 | 56.326083 | 67.619233 |
4 | 44.690380 | 49.876027 | 64.700524 | 64.700524 | 65.987656 | 76.276382 |
5 | 54.288239 | 55.225490 | 81.024971 | 81.024971 | 84.335838 | 94.602018 |
6 | 115.896088 | 115.896088 | 121.008058 | 121.008058 | 122.036705 | 130.296162 |
7 | 138.233840 | 138.233840 | 147.719116 | 147.719116 | 150.813193 | 161.399557 |
8 | 141.529573 | 141.529573 | 158.381395 | 159.397793 | 160.363795 | 172.213511 |
9 | 153.438150 | 153.438150 | 157.620364 | 157.620364 | 159.397793 | 169.442468 |
10 | 159.131296 | 159.131296 | 175.103136 | 175.103136 | 176.382204 | 185.881145 |
11 | 163.297610 | 163.297610 | 184.078637 | 184.078637 | 186.888640 | 195.598683 |
12 | 178.897147 | 178.897147 | 195.458748 | 197.502702 | 197.979072 | 209.569898 |
13 | 185.773526 | 185.773526 | 194.232028 | 194.232028 | 197.502702 | 205.966477 |
14 | 190.770863 | 194.232028 | 198.518328 | 198.518328 | 200.241701 | 210.248278 |
15 | 191.436358 | 195.458748 | 207.199782 | 207.199782 | 209.417259 | 218.042570 |
16 | 192.996673 | 198.518328 | 225.358624 | 225.358624 | 228.112898 | 239.672279 |
17 | 235.721763 | 235.721763 | 259.271193 | 261.177311 | 262.964280 | 272.140727 |
18 | 236.982136 | 236.982136 | 248.707889 | 248.707889 | 249.005599 | 257.179921 |
19 | 249.739558 | 249.739558 | 257.191378 | 257.191378 | 261.177311 | 270.570713 |
20 | 251.211860 | 257.191378 | 292.433111 | 292.433111 | 294.317196 | 306.109379 |
21 | 269.896839 | 269.896839 | 290.482557 | 290.482557 | 291.492054 | 301.762160 |
22 | 277.618827 | 290.482557 | 326.783904 | 326.783904 | 330.541525 | 340.451760 |
23 | 316.297180 | 316.297180 | 345.825218 | 345.825218 | 348.454351 | 356.890010 |
24 | 327.438778 | 327.438778 | 348.613301 | 348.613301 | 351.616536 | 362.402863 |
25 | 329.058777 | 345.825218 | 360.018647 | 360.293374 | 361.870379 | 370.748167 |
26 | 331.270089 | 348.613301 | 358.402893 | 358.402893 | 360.293374 | 370.413160 |
27 | 342.801178 | 358.402893 | 360.985239 | 361.870379 | 365.254626 | 375.016625 |
28 | 344.822530 | 360.018647 | 366.005058 | 366.005058 | 366.567726 | 375.151551 |
29 | 356.046606 | 360.985239 | 385.387547 | 385.387547 | 387.873472 | 396.610833 |
30 | 364.586061 | 366.005058 | 390.795758 | 390.795758 | 392.832533 | 404.251737 |
31 | 365.424787 | 385.387547 | 420.092838 | 420.092838 | 424.591573 | 433.471879 |
32 | 391.448971 | 391.448971 | 402.942336 | 402.942336 | 403.217641 | 413.900558 |
33 | 405.971702 | 405.971702 | 414.498846 | 414.498846 | 417.528996 | 427.306887 |
34 | 420.121007 | 420.121007 | 426.304452 | 426.304452 | 427.300927 | 436.224023 |
35 | 436.134513 | 436.134513 | 459.086424 | 459.086424 | 460.308443 | 471.132942 |
36 | 436.209495 | 436.209495 | 457.539077 | 457.539077 | 458.243014 | 467.955627 |
37 | 438.721645 | 457.539077 | 471.973973 | 473.986796 | 475.914645 | 485.816522 |
38 | 441.427207 | 459.086424 | 471.666111 | 471.666111 | 473.986796 | 483.931980 |
39 | 471.557560 | 471.666111 | 495.620128 | 495.620128 | 496.530393 | 505.060393 |
40 | 480.587433 | 480.587433 | 502.549153 | 502.549153 | 502.995927 | 513.842043 |
41 | 500.355643 | 500.355643 | 515.954981 | 515.954981 | 518.426460 | 528.518062 |
42 | 511.943491 | 511.943491 | 535.724551 | 535.724551 | 538.380528 | 548.860482 |
43 | 520.640852 | 520.640852 | 550.562285 | 550.562285 | 554.395111 | 562.597845 |
44 | 522.541796 | 535.724551 | 554.860670 | 554.860670 | 557.044421 | 568.202196 |
45 | 527.026733 | 550.562285 | 568.436347 | 568.436347 | 570.647454 | 580.206084 |
46 | 527.895047 | 554.860670 | 564.437151 | 564.437151 | 565.958044 | 575.275032 |
47 | 538.061558 | 564.437151 | 573.728371 | 573.728371 | 575.261145 | 584.398957 |
48 | 542.604827 | 568.436347 | 586.843015 | 586.843015 | 588.093484 | 597.997861 |
49 | 549.846024 | 573.728371 | 587.726074 | 588.093484 | 588.638568 | 600.166950 |
50 | 593.573719 | 593.573719 | 622.320771 | 622.320771 | 623.469427 | 631.540442 |
51 | 600.933472 | 600.933472 | 614.026085 | 614.026085 | 616.538472 | 625.870210 |
52 | 611.151563 | 614.026085 | 638.060880 | 638.060880 | 639.591900 | 650.086266 |
53 | 614.890535 | 622.320771 | 626.535703 | 626.535703 | 628.924357 | 639.583351 |
54 | 624.824109 | 626.535703 | 634.225679 | 634.225679 | 635.431060 | 645.758394 |
55 | 653.657424 | 653.657424 | 657.297616 | 657.297616 | 658.924891 | 669.187379 |
56 | 665.674698 | 665.674698 | 675.278395 | 675.278395 | 675.537426 | 685.107765 |
57 | 668.401265 | 668.401265 | 683.858594 | 683.858594 | 685.067501 | 696.779362 |
58 | 694.595630 | 694.595630 | 712.863166 | 712.863166 | 712.972219 | 722.550661 |
59 | 698.948035 | 698.948035 | 708.010551 | 708.010551 | 709.042020 | 718.971709 |
... | ... | ... | ... | ... | ... |
984 rows × 6 columns