Advanced Trajectory Usage

Iñaki Ucar, Bart Smeets

2023-11-28

library(simmer)

Available set of activities

When a generator creates an arrival, it couples the arrival to a given trajectory. A trajectory is defined as an interlinkage of activities which together form the arrivals’ lifetime in the system. Once an arrival is coupled to the trajectory, it will (in general) start processing activities in the specified order and, eventually, leave the system. Consider the following:

traj <- trajectory() %>%
  seize(resource = "doctor", amount = 1) %>%
  timeout(task = 3) %>%
  release(resource = "doctor", amount = 1)

Here we create a trajectory where a patient seizes a doctor for 3 minutes and then releases him again. This is a very straightforward example, however, most of the trajectory-related functions allow for more advanced usage.

Usage examples are provided in the help page for each activity. The complete set of activities can be found and navigated in the reference page, or you can list them as follows:

methods(class="trajectory")
#>  [1] activate                batch                   branch                 
#>  [4] clone                   deactivate              get_n_activities       
#>  [7] handle_unfinished       join                    leave                  
#> [10] length                  log_                    plot                   
#> [13] print                   release_all             release_selected_all   
#> [16] release_selected        release                 renege_abort           
#> [19] renege_if               renege_in               rep                    
#> [22] rollback                seize_selected          seize                  
#> [25] select                  send                    separate               
#> [28] set_attribute           set_capacity_selected   set_capacity           
#> [31] set_global              set_prioritization      set_queue_size_selected
#> [34] set_queue_size          set_source              set_trajectory         
#> [37] stop_if                 synchronize             timeout_from_attribute 
#> [40] timeout_from_global     timeout                 [                      
#> [43] [<-                     [[                      [[<-                   
#> [46] trap                    untrap                  wait                   
#> see '?methods' for accessing help and source code

Additionally, you may want to try the simmer.bricks package, a plugin for simmer which provides helper methods for trajectories. Each brick wraps a common activity pattern that can be used to build trajectories more conveniently (see the Introduction to simmer.bricks).

Dynamic arguments

Many activities accept functions as arguments to be evaluated dynamically during the simulation. For example, see help(timeout):

task: the timeout duration supplied by either passing a numeric or a callable object (a function) which must return a numeric.

Be aware that if you want the timeout()’s task parameter to be evaluated dynamically, you should supply a callable function. For example in timeout(function() rexp(1, 10)), rexp(1, 10) will be evaluated every time the timeout activity is executed. However, if you supply it in the form of timeout(rexp(1, 10)), it will only be evaluated once when the trajectory is defined, and will remain static after that.

trajectory() %>%
  timeout(rexp(1, 10)) %>%        # fixed
  timeout(function() rexp(1, 10)) # dynamic
#> trajectory: anonymous, 2 activities
#> { Activity: Timeout      | delay: 0.0246759 }
#> { Activity: Timeout      | delay: function() }

Of course, this task, supplied as a function, may be as complex as you need and, for instance, it may check the status of a particular resource, interact with other entities in your simulation model… The same applies to all the activities when they accept a function as a parameter.

Interaction with the environment

Dynamic arguments may interact with the environment to extract parameters of interest such as the current simulation time (see ?now), status of resources (see ?get_capacity), status of generators (see ?get_n_generated), or directly to gather the history of monitored values (see ?get_mon). The only requirement is that the simulation environment must be in the scope of the trajectory.

Therefore, this will not work:

traj <- trajectory() %>%
  log_(function() as.character(now(env)))

env <- simmer() %>%
  add_generator("dummy", traj, function() 1) %>%
  run(4)
#> 1: dummy0: 2.50175860496223
#> 2: dummy1: 2.50175860496223
#> 3: dummy2: 2.50175860496223

because the global env is not available at runtime: the simulation runs and then the resulting object is assigned to env. For env to be in the scope of t during this simulation, it is enough to detach the run() method from the definition pipe:

traj <- trajectory() %>%
  log_(function() as.character(now(env)))

env <- simmer() %>%
  add_generator("dummy", traj, function() 1)

env %>% run(4) %>% invisible
#> 1: dummy0: 1
#> 2: dummy1: 2
#> 3: dummy2: 3

And we get the expected output. However, as a general rule of good practice, it is recommended to instantiate the environment always in the first place to avoid possible mistakes, and because the code becomes more readable:

# first, instantiate the environment
env <- simmer()

# here I'm using it
traj <- trajectory() %>%
  log_(function() as.character(now(env)))

# and finally, run it
env %>%
  add_generator("dummy", traj, function() 1) %>%
  run(4) %>% invisible
#> 1: dummy0: 1
#> 2: dummy1: 2
#> 3: dummy2: 3

Trajectory toolbox: joining and subsetting

The join(...) method is very useful to concatenate together any number of trajectories. It may be used as a standalone function as follows:

t1 <- trajectory() %>% seize("dummy", 1)
t2 <- trajectory() %>% timeout(1)
t3 <- trajectory() %>% release("dummy", 1)

t0 <- join(t1, t2, t3)
t0
#> trajectory: anonymous, 3 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Timeout      | delay: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

Or it may operate inline, like another activity:

t0 <- trajectory() %>%
  join(t1) %>%
  timeout(1) %>%
  join(t3)
t0
#> trajectory: anonymous, 3 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Timeout      | delay: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

You can think about a trajectory object as a list of activities that has a length

length(t0)
#> [1] 3

and can be subset using the standard operator [. For instance, you can select the activities you want with a logical vector:

t0[c(TRUE, FALSE, TRUE)]
#> trajectory: anonymous, 2 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

Or a set of indices that respect the order given:

t0[c(1, 3)]
#> trajectory: anonymous, 2 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }
t0[c(3, 1)]
#> trajectory: anonymous, 2 activities
#> { Activity: Release      | resource: dummy, amount: 1 }
#> { Activity: Seize        | resource: dummy, amount: 1 }

Or a set of indices to remove from the selection:

t0[-2]
#> trajectory: anonymous, 2 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

Or by name, but note that this does not respect the order given though, because it performs a match:

t0[c("seize", "release")]
#> trajectory: anonymous, 2 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }
t0[c("release", "seize")]
#> trajectory: anonymous, 2 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

If you provide no indices, the whole trajectory is returned:

t0[]
#> trajectory: anonymous, 3 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Timeout      | delay: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

In fact, you are cloning the trajectory with the latter command. It is equivalent to t0[1:length(t0)] or join(t0).

The generics head() and tail() use the [ operator under the hood, thus you can use them as well:

head(t0, 2)
#> trajectory: anonymous, 2 activities
#> { Activity: Seize        | resource: dummy, amount: 1 }
#> { Activity: Timeout      | delay: 1 }
tail(t0, -1)
#> trajectory: anonymous, 2 activities
#> { Activity: Timeout      | delay: 1 }
#> { Activity: Release      | resource: dummy, amount: 1 }

The [[ operator can also be used to extract only one element:

t0[[2]]
#> trajectory: anonymous, 1 activities
#> { Activity: Timeout      | delay: 1 }

which is equivalent to t0[2]. If a string is provided, it ensures that only the first match is returned:

join(t0, t0)["timeout"]
#> trajectory: anonymous, 2 activities
#> { Activity: Timeout      | delay: 1 }
#> { Activity: Timeout      | delay: 1 }
join(t0, t0)[["timeout"]]
#> trajectory: anonymous, 1 activities
#> { Activity: Timeout      | delay: 1 }