Set up shift with weekend/weekday distinction

69 views
Skip to first unread message

hjp

unread,
Jan 13, 2022, 12:54:38 AM1/13/22
to simmer-devel
Hi,

Would like to ask help on how to simulate weekday/weekend shift.
I have a model that needs to run for 30 days (22 weekdays, 8 weekends). 
With 2 shifts: Mon-Fri 7am-4pm (6 resources) and Mon-Fri 11pm-8am (3 resources)
The shifts are currently coded as such: 

add_resource(name="Shift1", schedule(c(7, 16), c(6,0), period=24), preemptive=TRUE) %>%
add_resource(name="Shift1", schedule(c(8, 23), c(0,3), period=24), preemptive=TRUE)

However, the current code assumes that the shifts are available throughout the week, instead of just being available only on weekdays.

Is there a way that I can change the capacity to 0 on certain days (weekends), and return it to its usual capacity on weekdays?

Thanks,
Prim

hjp

unread,
Jan 13, 2022, 6:31:52 AM1/13/22
to simmer-devel
Apologies, I found the answer in a different thread:  Question to function schedule (google.com)

But I also have a couple new questions. I read in some threads that we can create a custom policy for select(), but I'm not sure how to go about it.
The custom policy that I have is similar to "first-available" but if there are no available resources, then the customer should wait, instead of giving an error.

Is it possible to use trap/signal and get_capacity to do this? Although I'm not quite sure yet how to execute it...

Thanks,
Prim

Iñaki Ucar

unread,
Jan 13, 2022, 7:06:43 AM1/13/22
to simmer-devel
On Thu, 13 Jan 2022 at 12:31, hjp <pri...@gmail.com> wrote:
>
> Apologies, I found the answer in a different thread: Question to function schedule (google.com)

Excellent!

> But I also have a couple new questions. I read in some threads that we can create a custom policy for select(), but I'm not sure how to go about it.
> The custom policy that I have is similar to "first-available" but if there are no available resources, then the customer should wait, instead of giving an error.
>
> Is it possible to use trap/signal and get_capacity to do this? Although I'm not quite sure yet how to execute it...

You can provide a custom R function to the select() activity that
returns the name of the resource to be selected. In that function, you
can ask the simulator a number of parameters (capacity, queue size,
etc.). See https://r-simmer.org/reference/get_capacity.html

Iñaki

>
> Thanks,
> Prim
>
>
> On Thursday, 13 January 2022 at 13:54:38 UTC+8 hjp wrote:
>>
>> Hi,
>>
>> Would like to ask help on how to simulate weekday/weekend shift.
>> I have a model that needs to run for 30 days (22 weekdays, 8 weekends).
>> With 2 shifts: Mon-Fri 7am-4pm (6 resources) and Mon-Fri 11pm-8am (3 resources)
>> The shifts are currently coded as such:
>>
>> add_resource(name="Shift1", schedule(c(7, 16), c(6,0), period=24), preemptive=TRUE) %>%
>> add_resource(name="Shift1", schedule(c(8, 23), c(0,3), period=24), preemptive=TRUE)
>>
>> However, the current code assumes that the shifts are available throughout the week, instead of just being available only on weekdays.
>>
>> Is there a way that I can change the capacity to 0 on certain days (weekends), and return it to its usual capacity on weekdays?
>>
>> Thanks,
>> Prim
>
> --
> You received this message because you are subscribed to the Google Groups "simmer-devel" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to simmer-devel...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/simmer-devel/b77fd9cf-ac98-48f6-873b-3f0b5e887287n%40googlegroups.com.



--
Iñaki Úcar

hjp

unread,
Jan 14, 2022, 7:55:05 AM1/14/22
to simmer-devel
Thanks, I was able to do a custom function for select() where it only chooses among resources with capacity.

But I can't still quite do this specific scenario: The process is like a customer service chat support, where the arrivals are messages from customers and the resources will have to respond to those messages. The process works as such: 
  • The messages arrive throughout the day, but should join a pooled queue to wait for an available responder. 
  • The responders are available in two shifts, excluding weekends; ideally the two shifts should not have individual queues, since there is already a pooled queue
  • If a responder from Shift A is working on a ticket but is about to end their shift, they will turnover the message that their working on to Shift B.
    • Except for weekends, since there are no available shifts, so it will be worked on by the first available responder the following week
Currently the code can only pool the weekend messages, but when the weekday starts, the all the pooled messages all rush to queue in Shift A, which really isn't ideal since Shift A's queue gets up to double digits thus prolonging the stay of the message in the system, instead of being catered by Shift B. Furthermore, when Shift A's shift ends, the messages wait for Shift A's downtime to finish, instead of being turned over to Shift B.
Is there a different approach that can be done to achieve the scenario in the bullets? Thanks.

Below is a simplified version of the current code:

shift_names <- c("Shift A", "Shift B")

traj <- trajectory() %>%
 # Wait for weekday door to open
    seize("weekday_door") %>% release("weekday_door") %>%
# Select resouce
    select( function() {
        cap <- get_capacity(env, shift_names)
        res <- shift_names[cap>0]
        if (!identical(res, character(0))) {
            # Among shifts with > 0 capacity, choose the shift with least server and queue count
              occ <- get_server_count(env,res) + get_queue_count(env,res)
              return(res[which.min(occ)[1]])
       } else {
            # If no shifts have capacity, choose the next closest shift after the arrival (i.e., if message arrives at 10pm, choose Shift B
              select_shift_start <- shift_start[order(shift_start)]
              select_shift_name <- shift_names[order(shift_start)]
              select_shift_ticketstart <- mod(mod(get_attribute(env, "ticket_start_time"),720),24)
              select_shift_index <- min(which(shift_start > select_shift_ticketstart))
              if (select_shift_index == Inf) {return(select_shift_name[1])} else {return(select_shift_name[select_shift_index])}
       }}) %>%
# Seize selected, timeout, and release
    seize_selected(1) %>% timeout(10) %>% release_selected()

env <- simmer () %>%
# Shift A with 6 capacity available on MF 7am-4pm
add_resource("Shift A", schedule( c(7,16,31,40,55,64,79,88,103,112,127), c(6,0,6,0,6,0,6,0,6,0,0), period=24*7), preemptive=TRUE) %>%
# Shift B with 3 capacity available on MF 11pm-8am
add_resource("Shift B", schedule( c(8,23,32,47,56,71,80,95,104,119,128), c(0,3,0,3,0,3,0,3,0,3,0), period=24*7), preemptive=TRUE) %>%
# Weekday door shift tells the messages to not queue on a shift if it's still the weekend
add_resource("weekday_door", schedule(c(7,128), c(Inf,0), period=168)) %>%
# Generator for message arrival
add_generator("messages_", trajectory=traj, distribution=at(rexp(1,1/5)), mon=2) %>%
run(until= 24*30)

Iñaki Ucar

unread,
Jan 14, 2022, 12:44:29 PM1/14/22
to simmer-devel
On Fri, 14 Jan 2022 at 13:55, hjp <pri...@gmail.com> wrote:
>
> Thanks, I was able to do a custom function for select() where it only chooses among resources with capacity.
>
> But I can't still quite do this specific scenario: The process is like a customer service chat support, where the arrivals are messages from customers and the resources will have to respond to those messages. The process works as such:
>
> The messages arrive throughout the day, but should join a pooled queue to wait for an available responder.
> The responders are available in two shifts, excluding weekends; ideally the two shifts should not have individual queues, since there is already a pooled queue
> If a responder from Shift A is working on a ticket but is about to end their shift, they will turnover the message that their working on to Shift B.
>
> Except for weekends, since there are no available shifts, so it will be worked on by the first available responder the following week
>
> Currently the code can only pool the weekend messages, but when the weekday starts, the all the pooled messages all rush to queue in Shift A, which really isn't ideal since Shift A's queue gets up to double digits thus prolonging the stay of the message in the system, instead of being catered by Shift B. Furthermore, when Shift A's shift ends, the messages wait for Shift A's downtime to finish, instead of being turned over to Shift B.
> Is there a different approach that can be done to achieve the scenario in the bullets? Thanks.

So you don't use a single resource because shifts overlap sometimes,
right? Then I would use a first resource with queue and capacity = the
sum of capacities of all active shifts, and then shifts without a
queue (not needed anymore). Schematically:

- seize intake
- select shift A/B
- seize_selected
- timeout
- release_all

I'm not sure if I understood the last part. But if you need to
transfer messages between shifts, a closing shift with no queue and
queue_size_strict=TRUE will drop messages. But if you previously set a
handler with handle_unfinished(), then you can make them rollback() up
to select() a new shift.

--
Iñaki Úcar
> To view this discussion on the web visit https://groups.google.com/d/msgid/simmer-devel/2f1c4687-ca42-4a33-bf56-bd61922ce1dan%40googlegroups.com.



--
Iñaki Úcar

Holger Brandl

unread,
Jan 16, 2022, 3:47:01 PM1/16/22
to simmer...@googlegroups.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.ipynb 
The 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...

I wonder if message identities could be modelled with simmer as well using `add_generator`?

Kind regards,
Holger




Iñaki Ucar

unread,
Jan 17, 2022, 6:27:24 AM1/17/22
to simmer-devel
Hi,

On Sun, 16 Jan 2022 at 21:47, Holger Brandl <holger...@gmail.com> wrote:
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.ipynb 
The 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.

Interesting, thanks for sharing!
 
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...

In my last message, I already outlined the trajectory structure required to model this with simmer. There were some missing pieces though:
  • How to ensure that transitioned tasks are processed first. In simmer, this is done by increasing the message priority upon reentry.
  • How to ensure that transitioned tasks are processed only for the remaining time. It will be much easier when this is implemented. But for the time being, we need to keep track of the starting time (in an attribute) to be able to subtract it to calculate the remaining time.
Here's a complete example with 3 arrivals and one transfer:

library(simmer)

shifts <- paste0("shift", c("A", "B"))

# schedules for shifts A and B, overlapping from 4 to 5
shift_sched <- list(
  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
shift_common <- schedule(c(0, 4, 5), c(1, 2, 1))

callcenter <- trajectory() %>%
  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")

env <- simmer() %>%
  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 }
As you can see,
  • at t=0, msg0 is served in shiftA. msg1 and msg2 stay in the queue.
  • at t=4, msg1 is served in shiftB when it opens. msg2 stays in the queue.
  • at t=5, shiftA is closed, so msg0 reenters the queue with a higher priority (and only 5 units of time left).
  • at t=14, msg1 finishes, and msg0 is restarted.
  • at t=19, msg0 finishes, and msg2 is started.
 
I wonder if message identities could be modelled with simmer as well using `add_generator`?

What do you mean? Arrivals already have unique names in simmer, as you can see in the example above.

Iñaki
 

hjp

unread,
Jan 17, 2022, 6:43:33 AM1/17/22
to simmer-devel

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),]

hjp

unread,
Jan 17, 2022, 6:59:49 AM1/17/22
to simmer-devel
I forgot to include snippets of the output.

This one was where a message incorrectly seized the common/pool resource.
  • Shift 1 (will end in time 19) was handling tix_1 since time 9
  • Shift 2 starts on time 15, increasing the capacity to 2
  • tix_2 seizes the pool and Shift  2
  • At time 19, tix_1 was preempted and very briefly frees up 1 capacity from the pool
    • This was immediately seized by tix_3
    • But Shift 1 closes at time 19, bringing down the capacity back to 1 (just for Shift 2)
    • Hence the 'should not have cut in line' message from tix_3

  • 15: shift_updater_timed3: new pool capacity: 2
  • 15: tix_2: pool seized!
  • 15: tix_2: selected resource: pool Shift 2
  • 15: tix_2: last_responded_time: 15. remaining timeout: 5
  • 19: tix_1: end of shift. / preempted. i need new resource
  • 19: tix_1: consumed_timeout: 4.9949287194099. remaining_timeout: 0.0050712805901032
  • 19: shift_updater_timed4: new pool capacity: 1
  • 19: tix_3: pool seized!
  • 19: tix_3: should not have cut in line
  • 19: tix_3: joining queue

While this one was all the log outputs for tix_3 showing how it's last activity was joining the common/pool queue
  • Arrived but no available resource, joined queue for pool resource
  • Mistakenly seized the pool resource
  • Seized the pool resource, but was preempted by tix_2 after just a few minutes
  • Re-joined the queue for pool resource
    • From this point it should have re-seized the pool resource but it did not have any more activity after this

  • 11: tix_3: message arrived: 11
  • 11: tix_3: joining queue
  • 19: tix_3: pool seized!
  • 19: tix_3: should not have cut in line
  • 19: tix_3: joining queue
  • 19.0282: tix_3: pool seized!
  • 19.0282: tix_3: selected resource: pool Shift 2
  • 19.0282: tix_3: last_responded_time: 19.0281656479153. remaining timeout: 5
  • 19.0382: tix_3: end of shift. / preempted. i need new resource
  • 19.0382: tix_3: consumed_timeout: 0.0100792219513117. remaining_timeout: 4.98992077804869
  • 19.0453: tix_3: joining queue

Iñaki Ucar

unread,
Jan 17, 2022, 7:12:02 AM1/17/22
to simmer-devel
You can try a few things:
  1. Define a schedule for the pool, as I did in my example, instead of calling set_capacity with a separate generator, because this could lead to race conditions.
  2. Use get_capacity() - get_server_count() instead of just get_capacity() to select resources, because you may have several shifts with some capacity, but some may be full already.
  3. Better yet, use the built-in policy "random-available", which is what you are doing if I understood it correctly.
Iñaki



--
Iñaki Úcar

hjp

unread,
Jan 17, 2022, 7:25:00 AM1/17/22
to simmer-devel
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.

Iñaki Ucar

unread,
Jan 17, 2022, 7:30:45 AM1/17/22
to simmer-devel
On Mon, 17 Jan 2022 at 13:25, hjp <pri...@gmail.com> wrote:
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.

If #1 is implemented correctly, then this cannot happen: whenever a shift decreases its capacity, the pool will too, so messages will be automatically enqueued if there's no server available.
 
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.

Maybe. Let's see how it goes with a schedule for the pool.

Iñaki
 

hjp

unread,
Jan 17, 2022, 9:34:50 AM1/17/22
to simmer-devel
I tried implementing #1 by manually setting the schedule for now. With it I was able to clean my code a bit and also have not encountered the issue with tix_3 earlier
But I encountered a different issue (will share the code at the end).

When I generated messages at t=9 and t=10 (Scenario A), the code works as expected. But when I generated messages at t=9, t=10, and t=11 (Scenario B), the handover between shifts did not occur as expected 
  • Scenario A (t=9 and t=10):
    • at t=9, msg_0 arrived and was picked up by Shift 1
    • at t=10, msg_1 arrived
    • at t=15, msg_0 finishes, and Shift 1 picks up msg_1
    • at t=19, Shift 1 ends their shift, Shift 2 then picks up msg_2
    • at t=21, msg_2 finishes
  • Scenario B (t=9, t=10, t=11)
    • (same as Scenario A) at t=9, msg_0 arrived and was picked up by Shift 1
    • (same as Scenario A) at t=10, msg_1 arrived
    • at t=11, msg_2 arrived
    • (same as Scenario A) at t=15, msg_0 finishes, and Shift 1 picks up msg_1
    • at t=15, Shift 2 picks up msg_2
    • at t=19, nothing happens, Shift 1 should have ended their shift and discarded msg_1
    • at t=21, msg_2 finishes as expected
    • at t=21, msg_1 was preempted by Shift 1, but since it has completed 6 hrs, it exited the simulation

Below is the code and output. 

set.seed(22)

library(simmer)

shift_names <- c("Shift 1", "Shift 2", "Shift 3")

shift_time2 <- rbind(
  c(9,19),
  c(1,15),
  c(6,22)
)
shift_fte2 <- rbind(
  c(1,0),
  c(0,1),
  c(0,1)
)

pool_sched <- schedule(c(1, 6, 9, 15, 19, 22),
                       c(1, 0, 1,  2,  1,  2), period=24)


env <- simmer()
for (i in 1:length(shift_names)){
  env %>%
    add_resource(name=shift_names[i], schedule(shift_time2[i,], shift_fte2[i,], period=24), preemptive=TRUE, queue_size=0, queue_size_strict=TRUE) }

env %>% add_resource(name="pool", pool_sched, preemptive=TRUE)


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(6,0,6)}) %>%

 
  #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
  seize("pool") %>%

  # handle discarded message due to end of shift or preemption
  handle_unfinished(
    trajectory() %>%
      log_("end of shift. / preempted. i need new resource") %>%
      release("pool") %>%

      set_prioritization(c(1, NA, NA), mod="+") %>%
     
      # 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")
      ) %>%
     
      rollback(9)
  ) %>%
 
  # select shift - choose one randomly among shifts with free resources
  simmer::select(function(){
    free <- shift_names[(get_capacity(env, shift_names) - get_server_count(env, shift_names)) > 0]

    sample(free, 1)}) %>%
  log_(function() {paste0("selected:", get_selected(env))}) %>%

   
  # select shift - seize selected. add handler for rejected message
  seize_selected() %>%

 
  # set attribute - when was the message responded/picked up. also announce
  set_attribute("last_responded_time", function(){now(env)}) %>%
  log_(function() {paste0("processing in ", paste(c("pool", shift_names)[get_seized(env, c("pool", shift_names))==1], collapse=" "))}) %>%

 
  # 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("msg_", traj, at(9, 10), mon=2) %>%
  add_generator("msg_", traj, at(9, 10, 11), mon=2) %>%
  run(until= 24*10)

==== Output for Scenario A (t=c(9,10))
9: msg_0: message arrived: 9
9: msg_0: selected:Shift 1
9: msg_0: processing in pool Shift 1
10: msg_1: message arrived: 10
15: msg_0: finished: 15. duration: 6
15: msg_1: selected:Shift 1
15: msg_1: processing in pool Shift 1
19: msg_1: end of shift. / preempted. i need new resource
19: msg_1: consumed_timeout: 4. remaining_timeout: 2
19: msg_1: selected:Shift 2
19: msg_1: processing in pool Shift 2
21: msg_1: finished: 21. duration: 11

==== Output for Scenario B (t=c(9,10,11))

9: msg_0: message arrived: 9
9: msg_0: selected:Shift 1
9: msg_0: processing in pool Shift 1
10: msg_1: message arrived: 10
11: msg_2: message arrived: 11
15: msg_0: finished: 15. duration: 6
15: msg_1: selected:Shift 1
15: msg_1: processing in pool Shift 1
15: msg_2: selected:Shift 2
15: msg_2: processing in pool Shift 2
21: msg_2: finished: 21. duration: 10
21: msg_1: end of shift. / preempted. i need new resource
21: msg_1: consumed_timeout: 6. remaining_timeout: 0
21: msg_1: exit the simulation

Iñaki Ucar

unread,
Jan 18, 2022, 10:05:52 AM1/18/22
to simmer-devel


On Mon, 17 Jan 2022 at 15:35, hjp <pri...@gmail.com> wrote:
<snip>

env %>% add_resource(name="pool", pool_sched, preemptive=TRUE)

 The pool shouldn't be preemptive. Otherwise, it doesn't execute the handler for rejected messages.

--
Iñaki Úcar
Reply all
Reply to author
Forward
0 new messages