# Other SimPy Examples

#### Iñaki Ucar

#### 2016-11-26

## Introduction

These examples are adapted from the Python package ‘SimPy’, here. Users familiar with SimPy may find these examples helpful for transitioning to `simmer`. Some very basic material is not covered. Beginners should first read the vignettes ‘Introduction’, ‘Terminology’, ‘Advanced Trajectory Usage’ and ‘The Bank Tutorial’.

## Carwash

Covers:

- Standard resources.
- Waiting for other processes.
- Logging messages into the console.

From here:

> A carwash has a limited number of washing machines and defines a washing process that takes some time. Cars arrive at the carwash at a random time. If one washing machine is available, they start the washing process and wait for it to finish. If not, they wait until they an use one.

Parameters and setup:

    library(simmer)

    NUM_MACHINES <- 2  # Number of machines in the carwash
    WASHTIME <- 5      # Minutes it takes to clean a car
    T_INTER <- 7       # Create a car every ~7 minutes
    SIM_TIME <- 20     # Simulation time in minutes

    # setup
    set.seed(42)
    env <- simmer()

The implementation with `simmer` is as simple as defining the trajectory each arriving `car` will share and follow. This trajectory comprises seizing a wash machine, spending some time and releasing it. The process takes `WASHTIME` and removes some random percentage of dirt between 50 and 90%. The `log_()` messages are merely illustrative.

    car <- create_trajectory() %>%
      log_("arrives at the carwash") %>%
      seize("wash", 1) %>%
      log_("enters the carwash") %>%
      timeout(WASHTIME) %>%
      set_attribute("dirt_removed", function() sample(50:99, 1)) %>%
      log_(function(attr)
        paste0(attr["dirt_removed"], "% of dirt was removed")) %>%
      release("wash", 1) %>%
      log_("leaves the carwash")

Finally, we setup the resources and feed the trajectory with a couple of generators. The result is shown below:

    env %>%
      add_resource("wash", NUM_MACHINES) %>%
      # feed the trajectory with 4 initial cars
      add_generator("car_initial", car, at(rep(0, 4))) %>%
      # new cars approx. every T_INTER minutes
      add_generator("car", car, function() sample((T_INTER-2):(T_INTER+2), 1)) %>%
      # start the simulation
      run(SIM_TIME)
    #> 0: car_initial0: arrives at the carwash
    #> 0: car_initial0: enters the carwash
    #> 0: car_initial1: arrives at the carwash
    #> 0: car_initial1: enters the carwash
    #> 0: car_initial2: arrives at the carwash
    #> 0: car_initial3: arrives at the carwash
    #> 5: car_initial0: 96% of dirt was removed
    #> 5: car_initial1: 64% of dirt was removed
    #> 5: car_initial0: leaves the carwash
    #> 5: car_initial1: leaves the carwash
    #> 5: car_initial2: enters the carwash
    #> 5: car_initial3: enters the carwash
    #> 9: car0: arrives at the carwash
    #> 10: car_initial2: 82% of dirt was removed
    #> 10: car_initial3: 75% of dirt was removed
    #> 10: car_initial2: leaves the carwash
    #> 10: car_initial3: leaves the carwash
    #> 10: car0: enters the carwash
    #> 15: car0: 86% of dirt was removed
    #> 15: car0: leaves the carwash
    #> 18: car1: arrives at the carwash
    #> 18: car1: enters the carwash
    #> simmer environment: anonymous | now: 20 | next: 23
    #> { Resource: wash | monitored: 1 | server status: 1(2) | queue status: 0(Inf) }
    #> { Generator: car_initial | monitored: 1 | n_generated: 4 }
    #> { Generator: car | monitored: 1 | n_generated: 3 }

## Machine Shop

Covers:

- Preemptive resources.
- Interruptions.
- Loops.
- Attributes.

From here:

> This example comprises a workshop with `n` identical machines. A stream of jobs (enough to keep the machines busy) arrives. Each machine breaks down periodically. Repairs are carried out by one repairman. The repairman has other, less important tasks to perform, too. Broken machines preempt theses tasks. The repairman continues them when he is done with the machine repair. The workshop works continuously.

First of all, we load the libraries and prepare the environment. We will use `dplyr` later for the data analysis. There is a reason for loading it before `simmer`, and it is to mask `dplyr::select` with `simmer::select` and not the other way round.

    detach("package:simmer")
    library(dplyr)
    library(simmer)

    PT_MEAN <- 10.0         # Avg. processing time in minutes
    PT_SIGMA <- 2.0         # Sigma of processing time
    MTTF <- 300.0           # Mean time to failure in minutes
    BREAK_MEAN <- 1 / MTTF  # Param. for exponential distribution
    REPAIR_TIME <- 30.0     # Time it takes to repair a machine in minutes
    JOB_DURATION <- 30.0    # Duration of other jobs in minutes
    NUM_MACHINES <- 10      # Number of machines in the machine shop
    WEEKS <- 4              # Simulation time in weeks
    SIM_TIME <- WEEKS * 7 * 24 * 60  # Simulation time in minutes

    # setup
    set.seed(42)
    env <- simmer()

The `make_parts` trajectory defines a machine’s operating loop. A worker seizes the machine and starts manufacturing and counting parts in an infinite loop. Provided we want more than one machine, we parametrise the trajectory as a function of the machine:

    make_parts <- function(machine)
      create_trajectory() %>%
        set_attribute("parts", 0) %>%
        seize(machine, 1) %>%
        timeout(function() rnorm(1, PT_MEAN, PT_SIGMA)) %>%
        set_attribute("parts", function(attr) attr["parts"] + 1) %>%
        rollback(2, Inf) # go to 'timeout' over and over

Repairman’s unimportant jobs may be modelled in the same way (without the accounting part):

    other_jobs <- create_trajectory() %>%
      seize("repairman", 1) %>%
      timeout(JOB_DURATION) %>%
      rollback(1, Inf)

Failures are high-priority arrivals, both for the machines and for the repairman. Each random generated failure will randomly select and seize (break) a machine, and will seize (call) the repairman. After the machine is repaired, both resources are released and the corresponding workers begin where they left.

    machines <- paste0("machine", 1:NUM_MACHINES-1)

    failure <- create_trajectory() %>%
      select(machines, policy = "random") %>%
      seize_selected(1) %>%
      seize("repairman", 1) %>%
      timeout(REPAIR_TIME) %>%
      release("repairman", 1) %>%
      release_selected(1)

The machines and their workers are appended to the simulation environment. Note that each machine, which is defined as preemptive, has space for one worker (or a failure) and no space in queue.

    lapply(machines, function(i) {
      env %>%
        add_resource(i, 1, 0, preemptive = TRUE) %>%
        add_generator(paste0(i, "_worker"), make_parts(i), at(0), mon = 2)
    }) %>% invisible

The same for the repairman, but this time the queue is infinite, since there could be any number of machines at any time waiting for repairments.

    env %>%
      add_resource("repairman", 1, Inf, preemptive = TRUE) %>%
      add_generator("repairman_worker", other_jobs, at(0)) %>%
      invisible

Finally, the failure generator is defined with `priority=1` (default: 0), and the simulation begins:

    env %>%
      add_generator("failure", failure,
                    function() rexp(1, BREAK_MEAN * NUM_MACHINES),
                    priority = 1) %>%
      run(SIM_TIME) %>% invisible

The last value per worker from the attributes table reports the number of parts made:

    get_mon_attributes(env) %>%
      group_by(name) %>%
      slice(n()) %>%
      arrange(name)
    #> Source: local data frame [10 x 5]
    #> Groups: name [10]
    #>
    #>        time             name   key value replication
    #>       <dbl>            <chr> <chr> <dbl>       <int>
    #> 1  40275.55 machine0_worker0 parts  3329           1
    #> 2  40312.71 machine1_worker0 parts  3266           1
    #> 3  40244.22 machine2_worker0 parts  3231           1
    #> 4  40319.42 machine3_worker0 parts  3334           1
    #> 5  40312.07 machine4_worker0 parts  3238           1
    #> 6  40316.85 machine5_worker0 parts  3306           1
    #> 7  40223.46 machine6_worker0 parts  3247           1
    #> 8  40313.86 machine7_worker0 parts  3269           1
    #> 9  40310.88 machine8_worker0 parts  3327           1
    #> 10 40309.67 machine9_worker0 parts  3291           1

## Movie Renege

Covers:

- Standard resources.
- Condition events.
- Shared events.
- Attributes.

From here:

> This example models a movie theater with one ticket counter selling tickets for three movies (next show only). People arrive at random times and try to buy a random number (1-6) of tickets for a random movie. When a movie is sold out, all people waiting to buy a ticket for that movie renege (leave the queue).

First of all, we load the libraries and prepare the environment. We will use `dplyr` later for the data analysis. There is a reason for loading it before `simmer`, and it is to mask `dplyr::select` with `simmer::select` and not the other way round.

    detach("package:simmer")
    library(dplyr)
    library(simmer)

    TICKETS <- 50     # Number of tickets per movie
    SIM_TIME <- 120   # Simulate until
    movies <- c("R Unchained", "Kill Process", "Pulp Implementation")

    # setup
    set.seed(42)
    env <- simmer()

The main actor of this simulation is the moviegoer, a process that will try to buy a number of tickets for a certain movie. The logic is as follows:

1. Select a movie and go to the theater.
2. At any moment, the moviegoer reneges if the movie becomes sold out (the “sold out” event is received).
3. In particular, leave immediately if the movie already became sold out.
4. Reach the ticket counter and stay in the queue. When the turn comes (counter is seized),
   1. Try to buy (seize the resource) tickets (randomly chosen between 1 and 6).
      - If there are not enough tickets, the moviegoer is rejected after some discussion.
   2. Provided the moviegoer has tickets, the reneging condition is aborted.
   3. Check the tickets available and, if at most one ticket is left…
      - Set instant rejection for future customers (at point 3.).
      - Send the “sold out” event (subscribed at point 2.) for current customers (waiting at point 4.).
5. Release the ticket counter after the duration of the purchase.
6. Enjoy the movie!

This recipe is directly translated into a `simmer` trajectory as follows:

    moviegoer <- create_trajectory() %>%
      # select a movie
      set_attribute("movie", function() sample(3, 1)) %>%
      select(function(attr) movies[attr["movie"]]) %>%
      # set reneging condition
      renege_if(function(attr) paste0(movies[attr["movie"]], " sold out")) %>%
      # leave immediately if the movie was already sold out
      leave(function(attr) get_capacity(env, movies[attr["movie"]]) == 0) %>%
      # wait for my turn
      seize("counter", 1) %>%
      # buy tickets
      seize_selected(function() sample(6, 1), continue = FALSE,
                     reject = create_trajectory() %>%
                       # not enough tickets, leave after some discussion
                       timeout(0.5) %>%
                       release("counter", 1)
      ) %>%
      # abort reneging condition
      renege_abort() %>%
      # check the tickets available
      branch(function(attr) get_server_count(env, movies[attr["movie"]]) > (TICKETS - 2),
             continue = TRUE,
             # trigger the "sold out" event for the movie
             create_trajectory() %>%
               set_capacity_selected(0) %>%
               send(function(attr) paste0(movies[attr["movie"]], " sold out"))
      ) %>%
      timeout(1) %>%
      release("counter", 1) %>%
      # watch the movie
      wait()

which actually constitutes a quite interesting subset of `simmer`’s capabilities. The next step is to add the required resources to the environment `env`: the three movies and the ticket counter.

    # add movies as resources with capacity TICKETS and no queue
    lapply(movies, function(i) add_resource(env, i, TICKETS, 0)) %>% invisible

    # add ticket counter with capacity 1 and infinite queue
    env %>% add_resource("counter", 1, Inf)
    #> simmer environment: anonymous | now: 0 | next:
    #> { Resource: R Unchained | monitored: 1 | server status: 0(50) | queue status: 0(0) }
    #> { Resource: Kill Process | monitored: 1 | server status: 0(50) | queue status: 0(0) }
    #> { Resource: Pulp Implementation | monitored: 1 | server status: 0(50) | queue status: 0(0) }
    #> { Resource: counter | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }

And finally, we attach an exponential moviegoer generator to the `moviegoer` trajectory and the simulation starts:

    # add a moviegoer generator and start simulation
    env %>%
      add_generator("moviegoer", moviegoer, function() rexp(1, 1 / 0.5), mon=2) %>%
      run(SIM_TIME)
    #> simmer environment: anonymous | now: 120 | next: 120.289360000071
    #> { Resource: R Unchained | monitored: 1 | server status: 50(0) | queue status: 0(0) }
    #> { Resource: Kill Process | monitored: 1 | server status: 49(0) | queue status: 0(0) }
    #> { Resource: Pulp Implementation | monitored: 1 | server status: 49(0) | queue status: 0(0) }
    #> { Resource: counter | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }
    #> { Generator: moviegoer | monitored: 2 | n_generated: 227 }

`simmer` automagically monitors the system status, and this information can be retrieved with three methods:

    arrivals <- get_mon_arrivals(env)
    resources <- get_mon_resources(env)
    attributes <- get_mon_attributes(env)

As we promised, the analysis is performed with the help of `dplyr`:

    # get the three rows with the sold out instants
    sold_time <- resources %>%
      filter(resource != "counter" & capacity == 0)

    # get the arrivals that left at the sold out instants
    # count the number of arrivals per movie
    n_reneges <- left_join(
        arrivals %>%
          filter(finished == FALSE & end_time %in% sold_time$time),
        attributes
      ) %>%
      mutate(resource = movies[value]) %>%
      group_by(resource) %>%
      count()

    # merge the info
    result <- left_join(sold_time, n_reneges)
    # and print
    apply(result, 1, function(i) {
      cat("Movie '", i["resource"], "' sold out ", i["time"],
          " minutes after ticket counter opening.\n",
          "  Number of people leaving queue when film sold out: ", i["n"], "\n", sep="")
    }) %>% invisible
    #> Movie 'Pulp Implementation' sold out 31.09917 minutes after ticket counter opening.
    #>   Number of people leaving queue when film sold out: 18
    #> Movie 'R Unchained' sold out 40.59917 minutes after ticket counter opening.
    #>   Number of people leaving queue when film sold out: 14
    #> Movie 'Kill Process' sold out 41.59917 minutes after ticket counter opening.
    #>   Number of people leaving queue when film sold out: 10

## Gas Station Refuelling

Covers:

- Standard resources.
- Waiting for other processes.
- Condition events.
- Shared events.
- Concatenating trajectories.
- Loops.
- Logging messages into the console.

From here:

> This example models a gas station and cars that arrive for refuelling. The gas station has a limited number of fuel pumps and a fuel tank that is shared between the fuel pumps.

> Vehicles arriving at the gas station first request a fuel pump from the station. Once they acquire one, they try to take the desired amount of fuel from the fuel pump. They leave when they are done.

> The fuel level is reqularly monitored by a controller. When the level drops below a certain threshold, a tank truck is called for refilling the tank.

Parameters and setup:

    library(simmer)

    GAS_STATION_SIZE <- 200     # liters
    THRESHOLD <- 10             # Threshold for calling the tank truck (in %)
    FUEL_TANK_SIZE <- 50        # liters
    FUEL_TANK_LEVEL <- c(5, 25) # Min/max levels of fuel tanks (in liters)
    REFUELING_SPEED <- 2        # liters / second
    TANK_TRUCK_TIME <- 300      # Seconds it takes the tank truck to arrive
    T_INTER <- c(30, 100)       # Create a car every [min, max] seconds
    SIM_TIME <- 1000            # Simulation time in seconds

    # setup
    set.seed(42)
    env <- simmer()

SimPy solves this problem using a special kind of resource called container, which is not present in `simmer` so far. However, a container is nothing more than an embellished counter. Therefore, we can implement this in `simmer` with a global counter (`GAS_STATION_LEVEL` here), some signaling and some care checking the counter bounds.

Let us consider just the refuelling process in the first place. A car needs to check whether there is enough fuel available to fill its tank. If not, it needs to block until the gas station is refilled.

    GAS_STATION_LEVEL <- GAS_STATION_SIZE
    signal <- "gas station refilled"

    refuelling <- create_trajectory() %>%
      # check if there is enough fuel available
      branch(function(attr) FUEL_TANK_SIZE - attr["level"] > GAS_STATION_LEVEL,
             continue = TRUE,
             # if not, block until the signal "gas station refilled" is received
             create_trajectory() %>%
               trap(signal) %>%
               wait() %>%
               untrap(signal)
      ) %>%
      # refuel
      timeout(function(attr) {
        liters_required <- FUEL_TANK_SIZE - attr["level"]
        GAS_STATION_LEVEL <<- GAS_STATION_LEVEL - liters_required
        return(liters_required / REFUELING_SPEED)
      })

Now a car’s trajectory is straightforward. First, the starting time is saved as well as the tank level. Then, the car seizes a pump, refuels and leaves.

    car <- create_trajectory() %>%
      set_attribute("start", function() now(env)) %>%
      set_attribute("level", function()
        sample(FUEL_TANK_LEVEL[1]:FUEL_TANK_LEVEL[2], 1)) %>%
      log_("arriving at gas station") %>%
      seize("pump", 1) %>%
      # 'join()' concatenates the refuelling trajectory here
      join(refuelling) %>%
      release("pump", 1) %>%
      log_(function(attr)
        paste0("finished refuelling in ", now(env) - attr["start"], " seconds"))

The tank truck takes some time to arrive. Then, the gas station is completely refilled and the signal “gas station refilled” is sent to the blocked cars.

    tank_truck <- create_trajectory() %>%
      timeout(TANK_TRUCK_TIME) %>%
      log_("tank truck arriving at gas station") %>%
      log_(function() {
        refill <- GAS_STATION_SIZE - GAS_STATION_LEVEL
        GAS_STATION_LEVEL <<- GAS_STATION_SIZE
        paste0("tank truck refilling ", refill, " liters")
      }) %>%
      send(signal)

The controller periodically checks the `GAS_STATION_LEVEL` and calls the tank truck when needed.

    controller <- create_trajectory() %>%
      branch(function() GAS_STATION_LEVEL / GAS_STATION_SIZE * 100 < THRESHOLD,
             continue = TRUE,
             create_trajectory() %>%
               log_("calling the tank truck") %>%
               join(tank_truck)
      ) %>%
      timeout(10) %>%
      rollback(2, Inf)

Finally, we need to add a couple of pumps, a controller worker and a car generator.

    env %>%
      add_resource("pump", 2) %>%
      add_generator("controller", controller, at(0)) %>%
      add_generator("car", car, function() sample(T_INTER[1]:T_INTER[2], 1)) %>%
      run(SIM_TIME)
    #> 94: car0: arriving at gas station
    #> 113.5: car0: finished refuelling in 19.5 seconds
    #> 190: car1: arriving at gas station
    #> 206: car1: finished refuelling in 16 seconds
    #> 278: car2: arriving at gas station
    #> 293: car2: finished refuelling in 15 seconds
    #> 344: car3: arriving at gas station
    #> 360: car3: finished refuelling in 16 seconds
    #> 383: car4: arriving at gas station
    #> 401: car4: finished refuelling in 18 seconds
    #> 463: car5: arriving at gas station
    #> 470: controller0: calling the tank truck
    #> 476: car5: finished refuelling in 13 seconds
    #> 544: car6: arriving at gas station
    #> 592: car7: arriving at gas station
    #> 688: car8: arriving at gas station
    #> 726: car9: arriving at gas station
    #> 770: controller0: tank truck arriving at gas station
    #> 770: controller0: tank truck refilling 195 liters
    #> 782.5: car7: finished refuelling in 190.5 seconds
    #> 788: car6: finished refuelling in 244 seconds
    #> 795: car10: arriving at gas station
    #> 800.5: car8: finished refuelling in 112.5 seconds
    #> 801.5: car9: finished refuelling in 75.5 seconds
    #> 813: car10: finished refuelling in 18 seconds
    #> 834: car11: arriving at gas station
    #> 840: controller0: calling the tank truck
    #> 856: car11: finished refuelling in 22 seconds
    #> 931: car12: arriving at gas station
    #> 997: car13: arriving at gas station
    #> simmer environment: anonymous | now: 1000 | next: 1091
    #> { Resource: pump | monitored: 1 | server status: 2(2) | queue status: 0(Inf) }
    #> { Generator: controller | monitored: 1 | n_generated: 1 }
    #> { Generator: car | monitored: 1 | n_generated: 15 }