I have a model where:
resources are available between opening hours (8am-8pm).
Any entities using resources are kicked out at 1 minute to 8pm, then a closing function is run to claim all the resources with a -1 priority for 12 hours until 8am
The resources stop accepting new requests at 6pm:
entities who arrive between 6-8pm have a timeout until 8:01pm before requesting a resource, by which time the shop should be closed and they can't claim one until the shop is open again at 8am.
If an entity doesn't get a resource during the day before 6pm, they timeout for ~2 hours until close where the shop should be closed and they wait for a resource to become available again when the shop opens at 8am.
My MRE is below (its quite long):
import simpy
import random
import math
import numpy as np
class default_params():
random.seed(10)
run_time = 10080
iterations = 1
no_resources = 4
inter_arr = 120
resource_time = 240
#shop Opening Hours
shop_open_time = 8
shop_stop_accept = 18
shop_close_time = 20
class spawn_entity:
def __init__(self, p_id):
self.id = p_id
self.arrival_time = np.nan
self.resource_gained_time = np.nan
self.leave_time = np.nan
class shop_model:
def __init__(self, run_number, input_params):
#start environment, set entity counter to 0 and set run number
self.env = simpy.Environment()
self.input_params = input_params
self.entity_counter = 0
self.run_number = run_number
#establish resources
self.resources = simpy.PriorityResource(self.env,
capacity=input_params.no_resources)
##############################MODEL TIME###############################
def model_time(self, time):
#Function to work out day and hour in the model
day = math.floor(time / (24*60))
day_of_week = day % 7
#If day 0, hour is time / 60, otherwise it is the remainder time once
#divided by number of days
hour = math.floor((time % (day*(24*60)) if day != 0 else time) / 60)
return day, day_of_week, hour
###########################ARRIVALS##################################
def arrivals(self):
yield self.env.timeout(1)
while True:
#up entity counter and spawn a new entity
self.entity_counter += 1
p = spawn_entity(self.entity_counter)
#begin entity to shop process
self.env.process(self.entity_journey(p))
#randomly sample the time until the next arrival
sampled_interarrival = round(random.expovariate(1.0
/ self.input_params.inter_arr))
yield self.env.timeout(sampled_interarrival)
######################### CLOSE SHOP ################################
def close_shop(self):
while True:
#if first day, close shop until open time, then wait until close
if self.env.now == 0:
time_closed = self.input_params.shop_open_time
time_out = self.input_params.shop_close_time
else:
#close for 12 hour overnight and time out process until next day
time_closed = 12
time_out = 24
#Take away all the resources for the close time to simulate the shop
# being closed.
print(f'--!!!!!CLOSING SHOP AT {self.env.now} FOR {time_closed * 60} MINS!!!!!--')
i=0
for _ in range(self.input_params.no_resources):
i += 1
print(f'--claiming resource {i}')
self.env.process(self.fill_shop(time_closed * 60))
print(f'--!!!!!SHOP CLOSED!!!!!--')
#Timout for timeout period until next closure.
yield self.env.timeout(time_out * 60)
def fill_shop(self, time_closed):
#If close time, take away all the resources
with self.resources.request(priority=-1) as req:
yield req
print(f'--resource claimed for close at {self.env.now} for {time_closed} mins')
yield self.env.timeout(time_closed)
######################## ENTITY JOURNEY #############################
def entity_journey(self, entity):
#Arrival
entity.arrival_time = self.env.now
day, day_of_week, hour = self.model_time(entity.arrival_time)
print(f'entity {entity.id} starting process at {entity.arrival_time}')
#If arrival hour between stop accept and close, add an extra wait
#to ensure they don't sneak in between these times
if ((hour >= self.input_params.shop_stop_accept)
and (hour < self.input_params.shop_close_time)):
#Time out until shop close, where resources will all be claimed
#then entity can wait until next open.
next_close = ((day * 60 * 24)
+ (self.input_params.shop_close_time * 60))
time_out = (next_close - entity.arrival_time) + 1
print(f'entity {entity.id} arrived in queue after stop accepting. Time out {time_out} mins until close')
yield self.env.timeout(time_out)
print(f'entity {entity.id} has waited until close at {self.env.now}')
#request resource
i=1
with self.resources.request(priority=1) as req:
#Find out current model time
time = self.env.now
print(f'entity {entity.id} requesting resource at time {time} - attempt {i}')
day, day_of_week, hour = self.model_time(time)
#Work out the minutes until the next shop stop accept time (next day
#if hour after stop accept hour)
next_stop_accept_day = (day + 1 if hour
>= self.input_params.shop_stop_accept
else day)
next_stop_accept = ((next_stop_accept_day * 60 * 24)
+ (self.input_params.shop_stop_accept * 60))
time_to_stop_accept = (next_stop_accept - time) + 1
print(f'entity {entity.id} has {time_to_stop_accept} mins until shop stops accepting')
#entity either gets resource or timesout if past stop accept time
yield req | self.env.timeout(time_to_stop_accept)
#If entity doesn't get a bed the first attempt, keep trying
#until they do
while not req.triggered:
i += 1
print(f'entity {entity.id} did not get resource as shop stopped accepting at {self.env.now}, entity waiting 2 hours then rejoining queue for attempt {i}')
#Time out for the time between stop accepting and close, to
#ensure no entities get a resource during this time.
yield self.env.timeout(((self.input_params.shop_close_time
- self.input_params.shop_stop_accept)
* 60) + 1)
print(f'entity {entity.id} requesting resource at time {self.env.now} - attempt {i}')
#entity tries again to get a resource until the next stop accept
#time.
time = self.env.now
day, day_of_week, hour = self.model_time(time)
next_stop_accept_day = (day + 1 if hour
>= self.input_params.shop_stop_accept
else day)
next_stop_accept = ((next_stop_accept_day * 60 * 24)
+ (self.input_params.shop_stop_accept
* 60))
time_to_stop_accept = (next_stop_accept - time) + 1
print(f'entity {entity.id} has {time_to_stop_accept} until shop stops accepting')
#entity either gets resource or timesout if past stop accept time
yield req | self.env.timeout(time_to_stop_accept)
#Entity got resource, record the time and randomly sample
#process time
print(f'entity {entity.id} got resource at {self.env.now} on attempt {i}')
entity.resource_gained_time = self.env.now
#Time entity spends is randomly sampled, with them being kicked out
#1 minute before 8pm.
day, day_of_week, hour = self.model_time(self.env.now)
next_close_day = (day + 1 if hour
>= self.input_params.shop_close_time
else day)
next_close = ((next_close_day * 60 * 24)
+ (self.input_params.shop_close_time * 60)) - 1
time_to_next_close = next_close - self.env.now
sampled_shop_time = round(random.expovariate(1.0
/ self.input_params.resource_time))
yield self.env.timeout(min(sampled_shop_time, time_to_next_close))
#record entity leave time
entity.leave_time = self.env.now
########################RUN#######################
def run(self):
self.env.process(self.arrivals())
self.env.process(self.close_shop())
self.env.run(until = self.input_params.run_time)
def run_the_model(input_params):
#run the model for the number of iterations specified
for run in range(input_params.iterations):
print(f"Run {run+1} of {input_params.iterations}")
model = shop_model(run, input_params)
model.run()
run_the_model(default_params)
Sometimes the close code runs perfectly, and all the resources are claimed at 8pm, which looks like:
--!!!!!CLOSING SHOP AT 4080 FOR 720 MINS!!!!!--
--claiming resource 1
--claiming resource 2
--claiming resource 3
--claiming resource 4
--!!!!!SHOP CLOSED!!!!!--
--resource claimed for close at 4080 for 720 mins
--resource claimed for close at 4080 for 720 mins
--resource claimed for close at 4080 for 720 mins
--resource claimed for close at 4080 for 720 mins
The issue I have is that sometimes when I'm closing the shop, not all resources get claimed at exactly 8pm and some entity activity will cut in before all resources have been claimed, sometimes even claiming a resource for a short time! An example from the above code output is:
--!!!!!CLOSING SHOP AT 2640 FOR 720 MINS!!!!!--
--claiming resource 1
--claiming resource 2
--claiming resource 3
--claiming resource 4
--!!!!!SHOP CLOSED!!!!!--
--resource claimed for close at 2640 for 720 mins
--resource claimed for close at 2640 for 720 mins
entity 21 has waited until close at 2641
entity 21 requesting resource at time 2641 - attempt 1
entity 21 has 1320 mins until shop stops accepting
entity 22 has waited until close at 2641
entity 22 requesting resource at time 2641 - attempt 1
entity 22 has 1320 mins until shop stops accepting
entity 19 requesting resource at time 2642 - attempt 2
entity 19 has 1319 until shop stops accepting
entity 20 requesting resource at time 2642 - attempt 2
entity 20 has 1319 until shop stops accepting
entity 19 got resource at 2642 on attempt 2
entity 20 got resource at 2642 on attempt 2
--resource claimed for close at 2683 for 720 mins
--resource claimed for close at 2703 for 720 mins
Here, 2 of the 4 resources gets closed exactly at 2640mins as expected, but the other 2 don't get claimed until 2683 and 2703. Entities 19 and 20 both manage to claim a resource briefly before they get closed, and leave the model. I don't understanding how they are managing to jump ahead of the close in the queue, as the close is priority -1 compared to their priority of 1. Nor do I understand why they sometimes don't get claimed to so many minutes after the close. Any help greatly appreciated.
Ok, I've banged by head against the keyboard over the last few days trying many different ways to do this, but I finally have something that has the desired behaviour. It uses similar logic to @Michael's answer in that it cancels requests when the shop is not open, and waits until the next opening to ask again, but I am using a list to manually keep track of my waiting list overnight, even if the requests have been cancelled. The key additions/changes are:
Using a list to store entities when they arrive at the shop, then removing them once they have gained the resource, and keeping track of this list to monitor the queue.
created a shop accepting event in a function of that name to keep track of if the shop is open (shop_accepting_event.triggered
) or not (shop_accepting_event = self.env.event
), which I can then use in a yield statement such that entities cannot progress when the event is not triggered.
Removed closure logic of occupying all the beds at a time, instead checking every hour (60 mins) if the shop should be open or close, and changing the shop_accepting_event
status accordingly (using another function shop_open
to return the shops state as a bool)
changed the model logic to avoid multiple calculations of yield times, instead using a while loop, waiting until the shop_accepting_event is triggered before requesting a resource, then timing out and repeating the loop again if a resource was not gained before the shop stops accepting.
Full code:
import simpy
import random
import math
import numpy as np
class default_params():
random.seed(10)
run_time = 10080
iterations = 1
no_resources = 4
inter_arr = 120
resource_time = 240
#shop Opening Hours
shop_open_time = 8
shop_stop_accept = 18
shop_close_time = 20
shop_day_of_week = [0, 1, 2, 3, 4]
class spawn_entity:
def __init__(self, p_id):
self.id = p_id
self.arrival_time = np.nan
self.resource_gained_time = np.nan
self.leave_time = np.nan
class shop_model:
def __init__(self, run_number, input_params):
#start environment, set entity counter to 0 and set run number
self.env = simpy.Environment()
self.input_params = input_params
self.entity_counter = 0
self.run_number = run_number
#establish resources
self.resources = simpy.PriorityResource(self.env,
capacity=input_params.no_resources)
#Create list to manually track the queue
self.entity_queue = []
#Start shop accepting condition event
self.shop_is_open = False
self.shop_accepting = False
self.shop_accepting_event = self.env.event()
##############################MODEL TIME###############################
def model_time(self, time):
#Function to work out day and hour in the model
day = math.floor(time / (24*60))
day_of_week = day % 7
#If day 0, hour is time / 60, otherwise it is the remainder time once
#divided by number of days
hour = math.floor((time % (day*(24*60)) if day != 0 else time) / 60)
return day, day_of_week, hour
def shop_open(self, day, day_of_week, hour):
#Function to work out if shop is open and/or accepting at any time
shop_accepting = ((day_of_week in self.input_params.shop_day_of_week)
and ((hour >= self.input_params.shop_open_time)
and (hour < self.input_params.shop_stop_accept)))
shop_open = ((day_of_week in self.input_params.shop_day_of_week)
and ((hour >= self.input_params.shop_open_time)
and (hour < self.input_params.shop_close_time)))
return shop_accepting, shop_open
###########################ARRIVALS##################################
def arrivals(self):
yield self.env.timeout(1)
while True:
#up entity counter and spawn a new entity
self.entity_counter += 1
p = spawn_entity(self.entity_counter)
#begin entity to shop process
self.env.process(self.entity_journey(p))
#randomly sample the time until the next arrival
sampled_interarrival = round(random.expovariate(1.0
/ self.input_params.inter_arr))
yield self.env.timeout(sampled_interarrival)
######################### CLOSE SHOP ################################
def create_shop_accepting_event(self):
#Event to monitor if shop is open or closed
if self.shop_accepting:
if not self.shop_accepting_event.triggered:
self.shop_accepting_event.succeed()
else:
# If it's already triggered, and we are now closing shop, replace with a new untriggered event
if self.shop_accepting_event.triggered:
self.shop_accepting_event = self.env.event()
def close_shop(self):
#Manage shop opening and closing schedule
while True:
current_time = self.env.now
day, day_of_week, hour = self.model_time(current_time)
# Check current status
should_be_accepting, should_be_open = self.shop_open(day, day_of_week, hour)
if should_be_accepting and not self.shop_accepting:
# Time to open shop and start accepting customers
print(f'!!!!!SHOP START ACCEPTING AT {current_time}!!!!!')
self.shop_accepting = True
self.shop_is_open = True
self.create_shop_accepting_event()
elif not should_be_accepting and self.shop_accepting:
# Time to stop accepting new customers (6pm)
print(f'!!!!!SHOP STOP ACCEPTING AT {current_time}!!!!!')
self.shop_accepting = False
self.create_shop_accepting_event() # Create new event but don't trigger
elif not should_be_open and self.shop_is_open:
# Time to close completely (8pm) - claim all beds
print(f'!!!!!SHOP CLOSING AT {current_time}!!!!!')
self.shop_is_open = False
self.shop_accepting = False
# Calculate how long to stay closed
if day_of_week < 4: # Monday-Thursday, reopen next morning
close_duration = (24 - self.input_params.shop_close_time +
self.input_params.shop_open_time) * 60
else: # Friday, closed for weekend
close_duration = (72 - self.input_params.shop_close_time +
self.input_params.shop_open_time) * 60
#time out until next open
print(f'!!!!!SHOP CLOSED!!!!!')
yield self.env.timeout(close_duration)
#shop Re-opening
print(f'!!!!!SHOP REOPENING AT {self.env.now}!!!!!')
self.shop_is_open = True
self.shop_accepting = True
self.create_shop_accepting_event()
# Check again in 60 minutes
yield self.env.timeout(60)
######################## ENTITY JOURNEY #############################
def entity_journey(self, entity):
#Arrival
entity.arrival_time = self.env.now
day, day_of_week, hour = self.model_time(entity.arrival_time)
print(f'entity {entity.id} starting process at {entity.arrival_time}')
#Add entity to manually monitored queue
self.entity_queue.append(entity)
# Keep trying to get a bed until successful
req_secured = False
req = None
while not req_secured:
if not self.shop_accepting:
yield self.shop_accepting_event
print(f'entity {entity.id} requesting resource at {self.env.now}')
# shop is now accepting, make a bed request
req = self.resources.request()
#Work out time until the shop stops accepting
time = self.env.now
day, day_of_week, hour = self.model_time(time)
next_stop_accept_day = (day + 1 if hour
>= self.input_params.shop_stop_accept
else day)
next_stop_accept = ((next_stop_accept_day * 60 * 24)
+ (self.input_params.shop_stop_accept
* 60))
time_to_stop_accept = max((next_stop_accept - time), 1)
#entity either gets bed or timesout if past stop accept time
success_or_timeout = yield req | self.env.timeout(time_to_stop_accept)
if req in success_or_timeout:
req_secured = True
print(f'++ entity {entity.id} got resource at {self.env.now}')
else:
# Timed out, cancel request and wait for next opening
if not req.triggered:
req.cancel()
print(f'entity {entity.id} did not get resource at {self.env.now} - retrying...')
#Entity got resource, record the time and randomly sample
#process time
entity.resource_gained_time = self.env.now
#Time entity spends is randomly sampled, with them being kicked out
#1 minute before 8pm.
day, day_of_week, hour = self.model_time(self.env.now)
next_close_day = (day + 1 if hour
>= self.input_params.shop_close_time
else day)
next_close = ((next_close_day * 60 * 24)
+ (self.input_params.shop_close_time * 60)) - 1
time_to_next_close = next_close - self.env.now
sampled_shop_time = round(random.expovariate(1.0
/ self.input_params.resource_time))
yield self.env.timeout(min(sampled_shop_time, time_to_next_close))
# Release the resource
self.resources.release(req)
print(f'-- entity {entity.id} released resource at {self.env.now}')
#record entity leave time
entity.leave_time = self.env.now
########################RUN#######################
def run(self):
self.env.process(self.arrivals())
self.env.process(self.close_shop())
self.env.run(until = self.input_params.run_time)
def run_the_model(input_params):
#run the model for the number of iterations specified
for run in range(input_params.iterations):
print(f"Run {run+1} of {input_params.iterations}")
model = shop_model(run, input_params)
model.run()
run_the_model(default_params)
The output (with additional print statements) now looks like:
!!!!!SHOP CLOSING AT 2640!!!!!
!!!!!SHOP CLOSED!!!!!
entity 24 starting process at 2641
entity 25 starting process at 2646
entity 26 starting process at 2728
entity 27 starting process at 2932
entity 28 starting process at 3061
entity 29 starting process at 3152
!!!!!SHOP REOPENING AT 3360!!!!!
entity 21 requesting resource at 3360
entity 22 requesting resource at 3360
entity 23 requesting resource at 3360
entity 19 requesting resource at 3360
entity 20 requesting resource at 3360
entity 24 requesting resource at 3360
entity 25 requesting resource at 3360
entity 26 requesting resource at 3360
entity 27 requesting resource at 3360
entity 28 requesting resource at 3360
entity 29 requesting resource at 3360
++ entity 21 got resource at 3360
++ entity 22 got resource at 3360
++ entity 23 got resource at 3360
++ entity 19 got resource at 3360
entity 30 starting process at 3384
entity 30 requesting resource at 3384
entity 31 starting process at 3398
entity 31 requesting resource at 3398
-- entity 21 released resource at 3399
++ entity 20 got resource at 3399
-- entity 20 released resource at 3423
++ entity 24 got resource at 3423
-- entity 24 released resource at 3467
++ entity 25 got resource at 3467
-- entity 23 released resource at 3472
++ entity 26 got resource at 3472
-- entity 22 released resource at 3561
++ entity 27 got resource at 3561
entity 32 starting process at 3577
entity 32 requesting resource at 3577
-- entity 19 released resource at 3581
++ entity 28 got resource at 3581
entity 33 starting process at 3641
entity 33 requesting resource at 3641
-- entity 28 released resource at 3649
++ entity 29 got resource at 3649
entity 34 starting process at 3680
entity 34 requesting resource at 3680
-- entity 27 released resource at 3697
++ entity 30 got resource at 3697
entity 35 starting process at 3704
entity 35 requesting resource at 3704
-- entity 30 released resource at 3728
++ entity 31 got resource at 3728
-- entity 31 released resource at 3771
++ entity 32 got resource at 3771
entity 36 starting process at 3777
entity 36 requesting resource at 3777
-- entity 25 released resource at 3862
++ entity 33 got resource at 3862
-- entity 29 released resource at 3879
++ entity 34 got resource at 3879
!!!!!SHOP STOP ACCEPTING AT 3960!!!!!
entity 35 did not get resource at 3960 - retrying...
entity 36 did not get resource at 3960 - retrying...
entity 37 starting process at 3983
entity 38 starting process at 3984
-- entity 32 released resource at 4022
-- entity 34 released resource at 4036
-- entity 35 released resource at 4062
-- entity 26 released resource at 4079
-- entity 33 released resource at 4079
-- entity 36 released resource at 4079
!!!!!SHOP CLOSING AT 4080!!!!!