To view this discussion on the web visit https://groups.google.com/d/msgid/simmer-devel/CALEXWq24mBBmFDFA%3DxXzAQZeSw2a7echLQTFNNVPk1xPk72t0Q%40mail.gmail.com.
Hi,I know it might be considered offtopic regarding the main theme of this group, but when studying the chat-support model from above, I could not resist modelling this problem using kalasim (another DES framework of which I happen to be the author). See https://github.com/holgerbrandl/kalasim/blob/master/docs/userguide/docs/articles/callcenter.ipynbThe article is still work in progress and not yet published, but it's good enough already to be shared with experts in here I think.
Not sure, how easy it would be to model it in simmer, but it took me some time to ensure that the model will correctly transition started tasks to a shift with a lower capacity. From a queue-length perspective this is is easy, but ensuring that exactly the same messages are processed first in the next shift while respecting the reduced capacity was a nice brain teaser this weekend.As I was getting into the topic, I also picked up the idea from above and modelled the problem independently using 2 resources for A and B. This worked out pretty elegantly as well, see https://github.com/holgerbrandl/kalasim/blob/master/src/test/kotlin/org/kalasim/scratch/callcenter/multiresource/MultiResourceCallCenter.kt with interesting dynamics when analyzing queue lengths and other resource metrics...
library(simmer)
<- paste0("shift", c("A", "B"))
shifts
# schedules for shifts A and B, overlapping from 4 to 5
<- list(
shift_sched schedule(c(0, 5), c(1, 0)),
schedule(c(0, 4), c(0, 1))
)
# at any time, the common resource must match the sum of all shifts
<- schedule(c(0, 4, 5), c(1, 2, 1))
shift_common
<- trajectory() %>%
callcenter log_("arrived") %>%
# message processing time, for later use
set_attribute("delay", 10) %>%
# this resource implements the queue
seize("common") %>%
# in case messages are dropped from a shift down below, reenter the common
# queue with a higher priority and an updated delay
handle_unfinished(
trajectory() %>%
log_("shift closed, transferring with a higher priority...") %>%
release("common") %>%
set_prioritization(c(1, NA, NA)) %>%
set_attribute("delay", function()
sum(get_attribute(env, c("start", "delay"))) - now(env)) %>%
rollback(6) # back to seize("common")
%>%
) # select and seize a proper shift
select(shifts, "first-available") %>%
seize_selected() %>%
# save the start time to be able to calculate the remaining delay after
# rejection; this will be easier after https://github.com/r-simmer/simmer/issues/186
set_attribute("start", function() now(env)) %>%
log_(function() paste0("processing in ", get_selected(env), "...")) %>%
timeout_from_attribute("delay") %>%
release_all() %>%
log_("finished")
<- simmer() %>%
env add_resource("common", shift_common) %>%
# no queue; preemptive=TRUE and queue_size_strict=TRUE ensure that messages
# are rejected when the capacity of a shift decreases
add_resource(shifts[1], shift_sched[[1]], 0, preemptive=TRUE, queue_size_strict=TRUE) %>%
add_resource(shifts[2], shift_sched[[2]], 0, preemptive=TRUE, queue_size_strict=TRUE) %>%
add_generator("msg", callcenter, at(0, 0, 0))
run(env)
#> 0: msg0: arrived
#> 0: msg0: processing in shiftA...
#> 0: msg1: arrived
#> 0: msg2: arrived
#> 4: msg1: processing in shiftB...
#> 5: msg0: shift closed, transferring with a higher priority...
#> 14: msg1: finished
#> 14: msg0: processing in shiftB...
#> 19: msg0: finished
#> 19: msg2: processing in shiftB...
#> 29: msg2: finished
#> simmer environment: anonymous | now: 29 | next:
#> { Monitor: in memory }
#> { Resource: common | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
#> { Resource: shiftA | monitored: TRUE | server status: 0(0) | queue status: 0(0) }
#> { Resource: shiftB | monitored: TRUE | server status: 0(1) | queue status: 0(0) }
#> { Source: msg | monitored: 1 | n_generated: 3 }
I wonder if message identities could be modelled with simmer as well using `add_generator`?
To view this discussion on the web visit https://groups.google.com/d/msgid/simmer-devel/CAFMhyOHHtYQO6Nd2Pvw0GO2K-nu5K9qGvOFo94cUZPhoi7Scvg%40mail.gmail.com.
Thanks for sharing the code Iñaki. I was also about to send an email earlier to verify my solution so seeing this gives hope that I’m on the right track. However, I’m still encountering an issue.
I was able to set up the shifts as advised, although instead of 24/7 availability, I had a window where none of the shifts were available. I also encountered a minor issue during the selection of shifts, there was an instance where two messages simultaneously seized the pool/common resource, despite only having 1 capacity. For this I used the reject parameter in seize_selected, to return the excess message back to the pool.
And then for the trajectory I was able to use handle_unfinished() if the message was dropped due to a closing shift, or preempted by a higher priority message. In the handler, I increased the message's priority by 1, so that it's responded to first, before doing rollback() to join the queue for pool. Also in the handler, the remaining timeout for the message is re-computed.
It was able to run properly when I generated two messages at time 9 (time unit is in hrs). However, when I generated four messages at time 9, 9, 10, and 11, the fourth message was not able to leave the simulation. Instead, the fourth message only rolled back to queue for the pool resource, but did not seize it nor proceed to the next steps. (Highlighted in green).
What could I have missed in the code?
Below is the reproducible code and output. This is actually my first project using simmer, I hope the usage of set_attribute() and log_() isn't too excessive.
Thanks so much.
-----
# shifts, with 1
resource each
# 9am - 7pm
# 3pm - 1am
# 10pm - 6am
set.seed(22)
library(simmer)
shift_names <- c("Shift 1", "Shift
2", "Shift 3")
shift_time2 <- rbind(
c(9,19,33,43,57,67,81,91,105,115,129),
c(1,15,25,39,49,63,73,87,97,111,121),
c(6,22,30,46,54,70,78,94,102,118,126)
)
shift_fte2 <- rbind(
c(1,0,1,0,1,0,1,0,1,0,0),
c(0,1,0,1,0,1,0,1,0,1,0),
c(0,1,0,1,0,1,0,1,0,1,0)
)
workmonths <- 1
workdays <- 30
shift_update_times
<- sort(sapply(c(shift_time), `+`, c(sapply(24, `*`, 0:(workmonths*workdays-1)))))
env <- simmer()
for (i in 1:length(shift_names)){
env %>%
add_resource(name=shift_names[i], schedule(shift_time2[i,],
shift_fte2[i,], period=168), preemptive=TRUE, queue_size=0, queue_size_strict=TRUE)
}
env %>% add_resource(name="pool", capacity=0, preemptive=TRUE)
traj_shift_update <- trajectory() %>%
set_capacity("pool", function()
{sum(get_capacity(env, shift_names))} ) %>%
log_(function() {paste0("new pool capacity:
", get_capacity(env, "pool"))})
traj <- trajectory() %>%
# set attributes
set_attribute(c("start", "prio", "last_responded_time"), function(){c(now(env), 0, now(env))}) %>%
set_attribute(c("total_timeout", "consumed_timeout", "remaining_timeout"), function(){c(5,0,5)}) %>%
#set initial prioritization
set_prioritization(function(){c(get_attribute(env, "prio"), NA, NA)}) %>%
# announce arrival
log_(function() {paste0("message arrived:
", get_attribute(env, "start"))}) %>%
# join big queue in pool. also announce
log_("joining queue") %>%
timeout(function(){runif(1,0,1/60)}) %>%
seize("pool") %>%
log_("pool seized!") %>%
# select shift - choose one randomly among active shifts
simmer::select(function(){
free <- shift_names[get_capacity(env,
shift_names) > 0]
sample(free, 1)}) %>%
# select shift - seize selected. add handler for rejected message
seize_selected(continue=FALSE, reject= trajectory() %>%
log_("should
not have cut in line") %>%
release_all() %>%
rollback(8))%>%
log_(function(){paste0("selected resource:
", paste(c("pool",
shift_names)[get_seized(env, c("pool",
shift_names))==1], collapse=" "))}) %>%
# set attribute - when was the message responded/picked up. also
announce
set_attribute("last_responded_time", function(){now(env)}) %>%
log_(function() {paste0("last_responded_time:
", get_attribute(env, "last_responded_time"), ".
remaining timeout:
", get_attribute(env, "remaining_timeout"))}) %>%
# handle discarded message due to end of shift or preemption
handle_unfinished(
trajectory() %>%
log_("end of shift. / preempted. i
need new resource") %>%
release_all() %>%
# recalculate remaining timeout
set_attribute("consumed_timeout", function(){now(env) - get_attribute(env, "last_responded_time")}, mod="+") %>%
set_attribute("remaining_timeout", function(){get_attribute(env, "total_timeout") - get_attribute(env, "consumed_timeout")}) %>%
log_(function()
{paste0("consumed_timeout:
", get_attribute(env, "consumed_timeout"), ".
",
"remaining_timeout:
", get_attribute(env, "remaining_timeout"))}) %>%
#release message if no more remaining
timeout
branch(
function()
(get_attribute(env, "remaining_timeout")<=0)*1, continue=FALSE,
trajectory() %>% log_("exit
the simulation")
) %>%
set_prioritization(c(1, NA, NA), mod="+") %>%
timeout(function(){runif(1,0,1/60)}) %>%
rollback(18)
) %>%
# resolve issue in message
timeout(function(){get_attribute(env, "remaining_timeout")}) %>%
# resolution complete. release all
release_all() %>%
set_attribute(c("end", "duration"), function(){c(now(env),
now(env) - get_attribute(env, "start"))}) %>%
log_(function() {paste0("finished:
", get_attribute(env, "end"), ". ",
"duration:
", get_attribute(env, "duration"))})
env %>%
add_generator("shift_updater_timed",
traj_shift_update, at(shift_update_times), mon=2) %>%
add_generator("tix_",
traj, at(9, 9, 10, 11), mon=2) %>%
run(until= 24*10)
arrivals
<- env %>% get_mon_arrivals(per_resource=T, ongoing=T)
arrivals[order(arrivals$name, arrivals$start_time),]
While this one was all the log outputs for tix_3 showing how it's last activity was joining the common/pool queue
To view this discussion on the web visit https://groups.google.com/d/msgid/simmer-devel/1d1ccd1c-4f50-476f-a021-192ed53ef1f3n%40googlegroups.com.
For #1, will try to define a schedule for the pool, and will let you know how it goes. I'll be simulating different combinations of shifts, hence the pool schedule should be easily changed depending on the different schedules of shifts.For #2 and #3, I'm wondering if there's a way to handle the error in case all resources are occupied? I haven't found yet a workaround to do some sort of rollback to pool when using "random-available" but no free resource.
For the issue encountered on tix_3, what could be the reason that it did not re-seize the pool resource, despite it being the only arrival left in the simulation? The log outputs on later timepoints showed that the pool capacity was still updating, but I'm not sure why tix_3 no longer moved forward.
To view this discussion on the web visit https://groups.google.com/d/msgid/simmer-devel/1a2dfb09-662e-4b8e-84c3-2308f773d3e3n%40googlegroups.com.
==== Output for Scenario B (t=c(9,10,11))
env %>% add_resource(name="pool", pool_sched, preemptive=TRUE)