Chapter 3 Simulator: After Renovation

After renovation, the key difference in the flow chart is an additional pickup lane with extra car spaces.

Thus, we redefine the model parameters as follows.

################################################################################
########### Model Parameters ###################################################
################################################################################
# time unit 1 minute
simTime = 2000

# cashier service rate
mu_cashier = 0.8

# cook service rate
mu_cook = 1/3.5 

# number of cashiers
num_cashier = 2

# number of cooks
num_cook = 3

# car spaces in ordering lane
space_ordering = 200

# car spaces in each pickup lane
space_pickup = 10

# Poisson arrival rate of customers
arrival_rate = 0.8 

In the customer’s trajectory, the car will move from the ordering lane to its corresponding pickup lane instead of merging into a single pickup lane.

customer = trajectory("Customer's path") %>%
  set_attribute("lane", function() {
    (get_server_count(env, "lane1")>=get_server_count(env, "lane2"))*1+1}
  ) %>% 
  select(function() {
    paste0("lane",get_attribute(env, "lane"))
  }) %>% 
  seize_selected(1) %>% # Choose the shortest ordering lane
  
  seize("cashier",amount=1) %>% # Try to seize a cashier for placing the order
  timeout(function() {rexp(1, mu_cashier)}) %>%
  release("cashier",amount=1) %>%
  
  select(function() {ifelse(get_attribute(env, "lane")==1,"lane1pickup","lane2pickup")}, id=2) %>% 
  seize_selected(amount=1, id=2) %>% # Try to go on to the corresponding pickup lane
  release_selected(1) %>% # Once going to the pickup lane, release the ordering lane
  
  seize("cook",amount=1) %>% # Try to seize a cook for preparing the order
  timeout(function() {rexp(1, mu_cook)}) %>%
  release("cook",amount=1) %>%
  release_selected(amount=1, id=2) # After the order is ready, the car immediately leaves the system

dummy = trajectory() %>% # A dummy trajectory for recording the system carline of lane1 and lane2
  set_attribute("carline_lane1",function() {
    carline1 = get_server_count(env,"lane1")+get_server_count(env,"lane1pickup")}) %>%
  set_attribute("carline_lane2",function() {
    carline2 = get_server_count(env,"lane2")+get_server_count(env,"lane2pickup")})  

3.1 A single run

We first implement a single run of the after-renovation simulator.

env = simmer()
env %>%
  add_resource("cashier", num_cashier) %>% # 2 Cashiers for taking orders
  add_resource("lane1", capacity=space_ordering) %>% # Ample car spaces in ordering lane1
  add_resource("lane2", capacity=space_ordering) %>% # Ample car spaces in ordering lane2
  add_resource("lane1pickup", capacity=space_pickup) %>% # Car spaces in pick up lane1
  add_resource("lane2pickup", capacity=space_pickup) %>% # Car spaces in pick up lane2
  add_resource("cook", num_cook) %>% # 3 Cooks for preparing the orders
  add_generator("Customer", customer, function() rexp(1, arrival_rate), mon=2) %>% # Customer's arrival process
  add_generator("Dummy recorder", dummy, function() 1, mon=2) %>% # Dummy trajectory records every 1 minute
  run(simTime)
## simmer environment: anonymous | now: 2000 | next: 2000
## { Monitor: in memory }
## { Resource: cashier | monitored: TRUE | server status: 1(2) | queue status: 0(Inf) }
## { Resource: lane1 | monitored: TRUE | server status: 0(200) | queue status: 0(Inf) }
## { Resource: lane2 | monitored: TRUE | server status: 1(200) | queue status: 0(Inf) }
## { Resource: lane1pickup | monitored: TRUE | server status: 3(10) | queue status: 0(Inf) }
## { Resource: lane2pickup | monitored: TRUE | server status: 4(10) | queue status: 0(Inf) }
## { Resource: cook | monitored: TRUE | server status: 3(3) | queue status: 4(Inf) }
## { Source: Customer | monitored: 2 | n_generated: 1590 }
## { Source: Dummy recorder | monitored: 2 | n_generated: 2000 }

Similarly to the previous experiment, we retrieve the simulation data to compute the system performance.

############ Retrieve data from the simulator ############
############ Metric 1: System carline ################
df_att = get_mon_attributes(env)
df_att = df_att[substr(df_att$name,1,1)=='D',] # Extract values for the dummy recorder
carline1 = df_att[df_att$key=='carline_lane1',] 
carline2 = df_att[df_att$key=='carline_lane2',] 
system_carline = pmax(carline1$value,carline2$value)

plot(system_carline, type='l',xlab="Time (in minute)",main='System carline')

############ Metric 2: Flow time ######################
df_arr = get_mon_arrivals(env) # All arrivals' data: including customers' and dummy recorder's
df_arr = df_arr[substr(df_arr$name,1,1)=='C',] # Extract customer's arrival data

df_arr$flow_time = df_arr$end_time - df_arr$start_time

plot(df_arr$flow_time, type='l',xlab="Customer ID", ylab="Duration (in minutes)", main='Flow time of customers')

The summary statistics below suggest that after renovation, the system carline reduces from 8 to 6, but the flow time remains 13 minutes.

summary(system_carline)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.000   3.000   5.000   6.149   9.000  16.000
summary(df_arr$flow_time)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.2132  6.1173 10.9864 12.8596 18.6320 41.6773

3.2 Monte Carlo experiment

Now, we repeat the single-run experiment 1000 times to compute a robust measurement of system performances.

#####################################################################
### Monte carlo experiment ##########################################
### Create num_rep repeated simulations #################################
#####################################################################

# Data frame for storing the performance of the Chick-fil-A after renovation
num_rep = 1000
Data_after=data.frame(repetition=1:num_rep,system_carline=1:num_rep,flow_time=1:num_rep)

for(ii in 1:num_rep){
  print(paste0("Repetition ",ii))
  env = simmer()
  env %>%
    add_resource("cashier", num_cashier) %>% # Cashiers for taking orders
    add_resource("lane1", capacity=space_ordering) %>% # Ample car spaces in ordering lane1
    add_resource("lane2", capacity=space_ordering) %>% # Ample car spaces in ordering lane2
    add_resource("lane1pickup", capacity=space_pickup) %>% # Car spaces in pick up lane1
    add_resource("lane2pickup", capacity=space_pickup) %>% # Car spaces in pick up lane2
    add_resource("cook", num_cook) %>% # 3 Cooks for preparing the orders
    add_generator("Customer", customer, function() rexp(1, arrival_rate), mon=2) %>% # Customer's arrival process
    add_generator("Dummy recorder", dummy, function() 1, mon=2) %>% # Dummy trajectory records every 1 minute
    run(simTime)
  
  ## average system carline length
  df_att = get_mon_attributes(env)
  df_att = df_att[substr(df_att$name,1,1)=='D',] # Extract values for the dummy recorder
  carline1 = df_att[df_att$key=='carline_lane1',] 
  carline2 = df_att[df_att$key=='carline_lane2',] 
  system_carline = pmax(carline1$value,carline2$value)
  
  Data_after[ii,'system_carline']=mean(system_carline[500:length(system_carline)])
  
  ## average flow time
  df_arr = get_mon_arrivals(env)
  df_arr = df_arr[substr(df_arr$name,1,1)=='C',] # picking customer's data only
  
  df_arr$flow_time = df_arr$end_time - df_arr$start_time
  
  Data_after[ii,'flow_time']=mean(df_arr$flow_time[200:nrow(df_arr)]) 
}

The following Figure shows the distributions of the average system carline and the average flow time across all 1000 runs.

ggplot(Data_after,aes(x=system_carline)) + geom_density() + theme_bw() +
   geom_vline(aes(xintercept=mean(system_carline)),
            color="blue", linetype="dashed", linewidth=1)+
  labs(title="Distribution of the average system carline",x="System carline", y = "Density")

ggplot(Data_after,aes(x=flow_time)) + geom_density() + theme_bw() +
   geom_vline(aes(xintercept=mean(flow_time)),
            color="blue", linetype="dashed", linewidth=1)+
  labs(title="Distribution of the average flow time",x="Flow time", y = "Density")

3.3 Evaluate the impact of Chick-fil-A renovation

We now compare the metrics of two simulators to understand the impact of Chick-fil-A renovation on its drive-thru congestion. Specifically, we plot the distributions of the flow time and the system carline as follows.

Data_before$facility = 'Before renovation'
Data_after$facility = 'After renovation'

Data = rbind(Data_before,Data_after)

ggplot(Data, aes(x=system_carline,lty=facility)) +
  geom_density(linewidth=0.7)+
  labs(title="Distribution of the average system carline",x="System carline", y = "Density")+
  scale_linetype_discrete(name="Chick-fil-A facility")+
  theme_bw()+
  theme(text=element_text(size=15))

ggplot(Data, aes(x=flow_time,lty=facility)) +
  geom_density(linewidth=0.7)+
  labs(title="Distribution of the average flow time",x="Flow time", y = "Density")+
  scale_linetype_discrete(name="Chick-fil-A facility")+
  theme_bw()+
  theme(text=element_text(size=15))

Compared with the metrics before the renovation, the average system carline of the new facility is reduced by 20%, but the average flow time is almost identical. In other words, Chick-fil-A’s new design is ineffective in reducing customers’ waiting time! A formal t-test also suggests that the reduction in the system carline is significant, but flow time distributions before and after the renovation show no significant difference.

This result highlights that without a rigorous analysis of bottleneck resources, some costly measures, such as a large-scale renovation, may not have any impact in reducing congestion. Therefore, the next module discusses bottleneck analysis, which identifies the scarce resource in the customer’s trajectory that causes long queues in the whole chain. Identifying bottlenecks is crucial in determining which resources the business should prioritize when making investments to improve overall efficiency.