/
simmer-03-trajectories.Rmd
1184 lines (915 loc) · 40 KB
/
simmer-03-trajectories.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "Advanced Trajectory Usage"
author: "Iñaki Ucar, Bart Smeets"
date: "`r Sys.Date()`"
output:
rmarkdown::html_vignette:
toc: yes
vignette: >
%\VignetteIndexEntry{03. Advanced Trajectory Usage}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, cache = FALSE, include=FALSE}
knitr::opts_chunk$set(collapse = T, comment = "#>",
fig.width = 6, fig.height = 4, fig.align = "center")
required <- c("simmer.plot")
if (!all(sapply(required, requireNamespace, quietly = TRUE)))
knitr::opts_chunk$set(eval = FALSE)
```
```{r, message=FALSE}
library(simmer)
library(simmer.plot)
```
## Interaction with the environment
Many activities accept functions as arguments to be evaluated dynamically during the simulation. Those functions 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:
```{r, error = TRUE}
t <- trajectory() %>%
log_(function() as.character(now(env)))
env <- simmer() %>%
add_generator("dummy", t, function() 1) %>%
run(4)
```
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:
```{r}
t <- trajectory() %>%
log_(function() as.character(now(env)))
env <- simmer() %>%
add_generator("dummy", t, function() 1)
env %>% run(4) %>% invisible
```
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:
```{r}
# First, instantiate the environment
env <- simmer()
# Here I'm using it
t <- trajectory() %>%
log_(function() as.character(now(env)))
# And finally, run it
env %>%
add_generator("dummy", t, function() 1) %>%
run(4) %>% invisible
```
## 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 the activities in the trajectory in the specified order and, eventually, leave the system. Consider the following:
```{r}
patient_traj <- trajectory(name = "patient_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. The different functions are introduced below.
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`_](http://r-simmer.org/extensions/bricks/articles/introduction.html)).
### `log_()`
The `log_(., message, level`) method just prints a given message preceded by the simulation time and the name of the arrival, for debugging purposes. The logging level is defined in the simulation environment:
```{r}
t <- trajectory() %>%
log_("this is always printed") %>% # level = 0 by default
log_("this is printed if `log_level>=1`", level = 1) %>%
log_("this is printed if `log_level>=2`", level = 2)
simmer() %>%
add_generator("dummy", t, at(0)) %>%
run() %>% invisible
simmer(log_level = 1) %>%
add_generator("dummy", t, at(0)) %>%
run() %>% invisible
simmer(log_level = Inf) %>%
add_generator("dummy", t, at(0)) %>%
run() %>% invisible
```
### `set_attribute()`
The `set_attribute(., keys, values, global)` method sets the `values` of an arrival's attributes `keys`. Both `keys` and `values` can be vectors (and their lengths must match) or functions returning vectors. But either way, the `values` stored can only be numeric.
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
set_attribute(keys = "my_key", values = 123) %>%
timeout(5) %>%
set_attribute(keys = "my_key", values = 456)
env <- simmer() %>%
add_generator("patient", patient_traj, at(0), mon = 2) %>%
run()
get_mon_attributes(env)
```
Above, a trajectory which _only_ sets attribute `my_key` to value `123` is launched once by an arrival generated at time 0 (check `?at`). The `mon=2` of `add_generator()` makes the simulation environment monitor the attributes' evolution (disabled by default). Using `get_mon_attributes()`, we can look at the evolution of the value of `my_key`.
If you want to set an attribute that depends on another attribute, or on the current value of the attribute to be set, this is also possible. Attributes can be retrieved inside user-supplied functions using `get_attribute(., keys)`. Below, you can see an example of this in practice.
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
set_attribute("my_key", 123) %>%
timeout(5) %>%
set_attribute("my_key", 1, mod="+") %>%
timeout(5) %>%
set_attribute("dependent_key", function() ifelse(get_attribute(env, "my_key")<=123, 1, 0)) %>%
timeout(5) %>%
set_attribute("independent_key", function() runif(1))
env<- simmer() %>%
add_generator("patient", patient_traj, at(0), mon = 2)
env %>% run()
get_mon_attributes(env)
```
Attributes are _per arrival_ by default (`global=FALSE`), meaning that each arrival has its own set of attributes, not visible by anyone else:
```{r}
writer <- trajectory() %>%
set_attribute(keys = "my_key", values = 123)
reader <- trajectory() %>%
log_(function() paste0(get_attribute(env, "my_key")))
env <- simmer() %>%
add_generator("writer", writer, at(0), mon = 2) %>%
add_generator("reader", reader, at(1), mon = 2)
env %>% run()
get_mon_attributes(env)
```
Thus, the reader in the example above returns `NA`, missing. However, attributes can also be global with `global=TRUE` or `set_global(., keys, values)`, a shortcut for `set_attributes(..., global=TRUE)`:
```{r}
writer <- trajectory() %>%
set_global(keys = "my_key", values = 123)
reader <- trajectory() %>%
log_(function() paste0(get_attribute(env, "my_key"), ", ",
get_attribute(env, "my_key", global = TRUE)))
env <- simmer() %>%
add_generator("writer", writer, at(0), mon = 2) %>%
add_generator("reader", reader, at(1), mon = 2)
env %>% run()
get_mon_attributes(env)
```
As shown above, global attributes are reported in `get_mon_attributes()` as unnamed key/value pairs.
### `timeout()`, `timeout_from_attribute()`
At its simplest, the `timeout(., task)` method delays the arrival's advance through the trajectory for a specified amount of time. Consider the following minimal example where we simply supply a static value to the timeout's `task` parameter.
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
timeout(task = 3)
env <- simmer() %>%
add_generator("patient", patient_traj, at(0)) %>%
run()
get_mon_arrivals(env)
```
Often, however, you want a timeout to be dependent on a distribution or, for example, an earlier set attribute. This is achieved by passing a function to the `task` parameter, as we explained before, and using `get_attribute(.)`. In the following example, this functionality is demonstrated:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
set_attribute("health", function() sample(20:80, 1)) %>%
# distribution-based timeout
timeout(function() rexp(1, 10)) %>%
# attribute-dependent timeout
timeout(function() (100 - get_attribute(env, "health")) * 2)
env <- simmer() %>%
add_generator("patient", patient_traj, at(0), mon = 2)
env %>% run()
get_mon_arrivals(env)
get_mon_attributes(env)
```
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 at the initalization and will remain static after that.
Of course, this `task`, supplied as a function, may be as complex as you need and, for instance, it may check a resource's status, interact with other entities in your simulation model... The same applies to all the activities when they accept a function as a parameter.
If you simply need a delay previously set as an attribute, you may use `timeout_from_attribute(., key, global=FALSE)`. Thus, the following timeouts are equivalent, but the second one is much simpler and faster:
```{r}
traj <- trajectory() %>%
set_attribute("delay", 2) %>%
timeout(function() get_attribute(env, "delay")) %>%
log_("first timeout") %>%
timeout_from_attribute("delay") %>%
log_("second timeout")
env <- simmer() %>%
add_generator("dummy", traj, at(0))
env %>% run() %>% invisible
```
### `seize()`, `release()`
The `seize(., resource, amount)` method seizes a specified `amount` of the resource named `resource`. Conversely, the `release(., resource, amount)` method releases a specified `amount` of the resource named `resource`. Be aware that, in order to use these functions in relation to a specific resource, you have to create it in your definition of the simulation environment (check `?add_resource`).
Consider the following example:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
seize(resource = "doctor", amount = 1) %>%
timeout(3) %>%
release(resource = "doctor", amount = 1)
env <- simmer() %>%
add_resource("doctor", capacity=1, mon = 1) %>%
add_generator("patient", patient_traj, at(0)) %>%
run()
get_mon_resources(env)
```
Here the `mon=1` argument (=default) of `add_resource()` makes the simulation environment monitor the resource usage. Using the `get_mon_resources(env)` method you can get access to the log of the usage evolution of resources.
There are situations where you want to let the amount of resources seized/released be dependent on a specific function or on a previously set attribute. To achieve this, you can pass a function to the `amount` parameter instead of a numeric value and use `get_attribute(.)`:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
set_attribute("health", function() sample(20:80, 1)) %>%
set_attribute("docs_to_seize", function() ifelse(get_attribute(env, "health")<50, 1, 2)) %>%
seize("doctor", function() get_attribute(env, "docs_to_seize")) %>%
timeout(3) %>%
release("doctor", function() get_attribute(env, "docs_to_seize"))
env <- simmer() %>%
add_resource("doctor", capacity = 2, mon = 1) %>%
add_generator("patient", patient_traj, at(0), mon = 2)
env %>% run()
get_mon_resources(env)
get_mon_attributes(env)
```
By default, an unsuccessful `seize()` results in the rejection of the arrival. In the following example, the second patient tries to seize the only doctor while the first patient is being attended. There is no waiting room available, therefore it is rejected:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
log_("arriving...") %>%
seize("doctor", 1) %>%
# the second patient won't reach this point
log_("doctor seized") %>%
timeout(5) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
get_mon_resources(env)
```
Sometimes, you don't want to reject an unsuccessful `seize()`, but to follow another path. Let's modify the example above to enable the second patient to visit a nurse instead:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
log_("arriving...") %>%
seize("doctor", 1, continue = FALSE,
reject = trajectory("rejected patient") %>%
log_("rejected!") %>%
seize("nurse", 1) %>%
log_("nurse seized") %>%
timeout(2) %>%
release("nurse", 1)) %>%
# the second patient won't reach this point
log_("doctor seized") %>%
timeout(5) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_resource("nurse", capacity = 10, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
get_mon_resources(env)
```
The flag `continue` indicates whether the `reject` sub-trajectory should be connected to the main trajectory or not. In this case, with `continue=FALSE`, the rejected arrival seizes the nurse and its lifetime ends after releasing him/her. Otherwise, it would keep executing activities in the main trajectory.
Note that the second patient may also keep trying if he/she must see the doctor:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
log_("arriving...") %>%
seize("doctor", 1, continue = FALSE,
reject = trajectory("rejected patient") %>%
log_("rejected!") %>%
# go for a walk and try again
timeout(2) %>%
log_("retrying...") %>%
rollback(amount = 4, times = Inf)) %>%
# the second patient will reach this point after a couple of walks
log_("doctor seized") %>%
timeout(5) %>%
release("doctor", 1) %>%
log_("leaving")
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
get_mon_resources(env)
```
There is another optional sub-trajectory called `post.seize` and, as its name suggests, it is executed after a successful `seize()`. Thus, you can do the following:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
log_("arriving...") %>%
seize("doctor", 1, continue = c(TRUE, TRUE),
post.seize = trajectory("admitted patient") %>%
log_("admitted") %>%
timeout(5) %>%
release("doctor", 1),
reject = trajectory("rejected patient") %>%
log_("rejected!") %>%
seize("nurse", 1) %>%
timeout(2) %>%
release("nurse", 1)) %>%
# both patients will reach this point, as continue = c(TRUE, TRUE)
timeout(10) %>%
log_("leaving...")
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_resource("nurse", capacity = 10, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
get_mon_resources(env)
```
### `set_capacity()`, `set_queue_size()`
The `set_capacity(., resource, value)` method modifies the capacity of the specified resource by name, and the `set_queue_size(., resource, value)` method modifies its queue size. Be aware that, in order to use these functions in relation to a specific resource, you have to create it in your definition of the simulation environment (check `?add_resource`). You can supply the `value` parameter either as a numeric value or as a function which must return a numeric value.
These activities are interesting to introduce dynamical changes in the resources. For instance, consider the following example in which two trajectories fight for the capacity of their resource:
```{r}
set.seed(12345)
t1 <- trajectory() %>%
seize("res1", 1) %>%
set_capacity(resource = "res1", value = 1, mod="+") %>%
set_capacity(resource = "res2", value = -1, mod="+") %>%
timeout(function() rexp(1, 1)) %>%
release("res1", 1)
t2 <- trajectory() %>%
seize("res2", 1) %>%
set_capacity(resource = "res2", value = 1, mod="+") %>%
set_capacity(resource = "res1", value = -1, mod="+") %>%
timeout(function() rexp(1, 1)) %>%
release("res2", 1)
env <- simmer() %>%
add_resource("res1", capacity = 20, queue_size = Inf) %>%
add_resource("res2", capacity = 20, queue_size = Inf) %>%
add_generator("t1_", t1, function() rexp(1, 1)) %>%
add_generator("t2_", t2, function() rexp(1, 1)) %>%
run(100)
plot(get_mon_resources(env), "usage", c("res1", "res2"), steps = TRUE)
```
### `select()`
`seize()`, `release()`, `set_capacity()` and `set_queue_size()` work well when you know the resources implied beforehand. But sometimes the resource to choose may depend on a certain policy. For these situations, the `select(., resources, policy, id)` method offers the possibility of selecting a resource at any point, and this choice will be observed by `seize_selected()`, `release_selected()`, `set_capacity_selected()` and `set_queue_size_selected()`:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
select(resources = c("doctor1", "doctor2", "doctor3"), policy = "round-robin") %>%
set_capacity_selected(1) %>%
seize_selected(amount = 1) %>%
timeout(5) %>%
release_selected(amount = 1)
env <- simmer() %>%
add_resource("doctor1", capacity = 0) %>%
add_resource("doctor2", capacity = 0) %>%
add_resource("doctor3", capacity = 0) %>%
add_generator("patient", patient_traj, at(0, 1, 2)) %>%
run()
get_mon_arrivals(env)
get_mon_resources(env)
```
If you provide `select()` with `resources` as a vector of names, you can use one of the predefined policies (see `?select`). If you need some custom policy, you can define it and supply it as a function. For instance, let's pick a resource based on a previously set attribute:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
set_attribute("resource", function() sample(1:3, 1)) %>%
select(resources = function() paste0("doctor", get_attribute(env, "resource"))) %>%
seize_selected(amount = 1) %>%
timeout(5) %>%
release_selected(amount = 1)
env <- simmer() %>%
add_resource("doctor1", capacity = 1) %>%
add_resource("doctor2", capacity = 1) %>%
add_resource("doctor3", capacity = 1) %>%
add_generator("patient", patient_traj, at(0, 1, 2), mon = 2)
env %>% run()
get_mon_attributes(env)
get_mon_arrivals(env)
get_mon_resources(env)
```
And, of course, everything learned for `seize()`, `release()`, `set_capacity()` and `set_queue_size()` applies to all their `_selected` versions.
### `activate()`, `deactivate()`
The `activate(., source)` and `deactivate(., source)` methods are able to start or stop, respectively, a source by name. This name can be supplied as either a string or a function returning a string. In the following simple example, the use of these methods spreads the interarrival times which otherwise would be of 1 unit of time, as defined in the source:
```{r}
t <- trajectory() %>%
deactivate(source = "dummy") %>%
timeout(1) %>%
activate(source = "dummy")
simmer() %>%
add_generator("dummy", t, function() 1) %>%
run(10) %>%
get_mon_arrivals()
```
### `set_trajectory()`, `set_source()`
The `set_trajectory(., source, trajectory)` and `set_source(., source, object)` methods are able to install a new trajectory or source object (function or data frame, depending on the source type), respectively, in a source by name. This name can be supplied as either a string or a function returning a string.
In the following distribution, `t2` changes the distribution and then switches to `t1`, so that only the first arrival takes `t2`:
```{r}
t1 <- trajectory() %>%
timeout(1)
t2 <- trajectory() %>%
set_source("dummy", function() 1) %>%
set_trajectory("dummy", t1) %>%
timeout(2)
simmer() %>%
add_generator("dummy", trajectory = t2, distribution = function() 2) %>%
run(10) %>%
get_mon_arrivals()
```
### `set_prioritization()`
The `add_generator()` method assigns a set of prioritization values to each generated arrival: by default, `priority=0`, `preemptible=priority`, `restart=FALSE` (see `?add_generator` for more details). The `set_prioritization(., values)` method can change those values with more granularity at any point in the trajectory, and they can also be retrieved inside the trajectory using `get_prioritization(.)`:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
set_attribute("priority", 3) %>%
# static values
set_prioritization(values = c(3, 7, TRUE)) %>%
# dynamically with a function
set_prioritization(values = function() {
prio <- get_prioritization(env)
attr <- get_attribute(env, "priority")
c(attr, prio[[2]]+1, FALSE)
})
```
More details on prioritization in the _Advanced Resource Usage_ vignette (pending).
### `branch()`
The `branch(., option, continue, ...)` method offers the possibility of adding alternative paths in the trajectory. The following example shows how a trajectory can be built with a 50-50 chance for an arrival to pass through each path of a two-path branch.
```{r}
t1 <- trajectory("trajectory with a branch") %>%
seize("server", 1) %>%
branch(option = function() sample(1:2, 1), continue = c(T, F),
trajectory("branch1") %>%
timeout(function() 1),
trajectory("branch2") %>%
timeout(function() rexp(1, 3)) %>%
release("server", 1)
) %>%
release("server", 1)
```
When an arrival gets to the branch, the first argument is evaluated to select a specific path to follow, so it must be callable and must return a numeric value between 1 and `n`, where `n` is the number of paths defined. The second argument, `continue`, indicates whether the arrival must continue executing the activities after the selected path or not. In the example above, only the first path continues to the last `release()`.
Sometimes you may need to count how many times a certain trajectory in a certain branch is entered, or how much time arrivals spend inside that trajectory. For these situations, it is handy to use resources with infinite capacity just for _accounting_ purposes, like in the example below.
```{r, message = FALSE}
t0 <- trajectory() %>%
branch(function() sample(c(1, 2), 1), continue = c(T, T),
trajectory() %>%
seize("branch1", 1) %>%
# do stuff here
timeout(function() rexp(1, 1)) %>%
release("branch1", 1),
trajectory() %>%
seize("branch2", 1) %>%
# do stuff here
timeout(function() rexp(1, 1/2)) %>%
release("branch2", 1))
env <- simmer() %>%
add_generator("dummy", t0, at(rep(0, 1000))) %>%
# Resources with infinite capacity, just for accounting purposes
add_resource("branch1", Inf) %>%
add_resource("branch2", Inf) %>%
run()
arrivals <- get_mon_arrivals(env, per_resource = T)
# Times that each branch was entered
table(arrivals$resource)
# The `activity_time` is the total time inside each branch for each arrival
# Let's see the distributions
ggplot(arrivals) + geom_histogram(aes(x=activity_time)) + facet_wrap(~resource)
```
### `rollback()`
The `rollback(., amount, times, check)` method allows an arrival to rollback the trajectory an `amount` number of steps.
Consider the following where a string is printed in the timeout function. After the first run, the trajectory is rolled back 3 times (so that it prints "Hello!" _four_ times).
```{r}
t0 <- trajectory() %>%
log_("Hello!") %>%
timeout(1) %>%
rollback(amount = 2, times = 3)
simmer() %>%
add_generator("hello_sayer", t0, at(0)) %>%
run() %>% invisible
```
The `rollback()` method also accepts an optional `check` parameter which overrides the default amount-based behaviour. This parameter must be a function that returns a logical value. Each time an arrival reaches the activity, this `check` is evaluated to determine whether the `rollback()` with `amount` steps must be performed or not. Consider the following example:
```{r}
t0 <- trajectory() %>%
set_attribute("happiness", 0) %>%
log_(function() {
level <- get_attribute(env, "happiness")
paste0(">> Happiness level is at: ", level, " -- ",
ifelse(level<25,"PETE: I'm feeling crappy...",
ifelse(level<50,"PETE: Feelin' a bit moody",
ifelse(level<75,"PETE: Just had a good espresso",
"PETE: Let's do this! (and stop this loop...)"))))
}) %>%
set_attribute("happiness", 25, mod="+") %>%
rollback(amount = 2, check = function() get_attribute(env, "happiness") < 100)
env <- simmer() %>%
add_generator("mood_swinger", t0, at(0))
env %>% run() %>% invisible()
```
### `leave()`
The `leave(., prob)` method allows an arrival to leave the trajectory with some probability:
```{r}
patient_traj <- trajectory(name = "patient_trajectory") %>%
seize("nurse", 1) %>%
timeout(3) %>%
release("nurse", 1) %>%
log_("before leave") %>%
leave(prob = 1) %>%
log_("after leave") %>%
# patients will never seize the doctor
seize("doctor", 1) %>%
timeout(3) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("nurse", capacity=1) %>%
add_resource("doctor", capacity=1) %>%
add_generator("patient", patient_traj, at(0)) %>%
run()
get_mon_resources(env)
```
And of course, this probability may be evaluated dynamically also:
```{r}
set.seed(1234)
patient_traj <- trajectory(name = "patient_trajectory") %>%
seize("nurse", 1) %>%
timeout(3) %>%
release("nurse", 1) %>%
log_("before leave") %>%
leave(prob = function() runif(1) < 0.5) %>%
log_("after leave") %>%
# some patients will seize the doctor
seize("doctor", 1) %>%
timeout(3) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("nurse", capacity=1) %>%
add_resource("doctor", capacity=1) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
get_mon_resources(env)
```
### `clone()`, `synchronize()`
The `clone(., n, ...)` method offers the possibility of replicating an arrival `n-1` times to be processed through up to `n` sub-trajectories in parallel. Then, the `synchronize(., wait, mon_all)` method synchronizes and removes replicas. By default, `synchronize()` waits for all of the replicas to arrive and allows the last one to continue:
```{r}
t <- trajectory() %>%
clone(n = 3,
trajectory("original") %>%
timeout(1),
trajectory("clone 1") %>%
timeout(2),
trajectory("clone 2") %>%
timeout(3)) %>%
synchronize(wait = TRUE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
get_mon_arrivals(env)
```
Note that the parameter `n` may also be a function. If there are more sub-trajectories than clones, the extra ones are ignored. If there are less sub-trajectories than clones, some clones will continue to the next activity directly:
```{r}
t <- trajectory() %>%
clone(n = 3,
trajectory("original") %>%
timeout(1),
trajectory("clone 1") %>%
timeout(2)) %>%
synchronize(wait = TRUE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
get_mon_arrivals(env)
```
The behaviour of `synchronize()` can be modified in order to let the first clone pass and remove the others by setting `wait=FALSE`:
```{r}
t <- trajectory() %>%
clone(n = 3,
trajectory("original") %>%
timeout(1),
trajectory("clone 1") %>%
timeout(2),
trajectory("clone 2") %>%
timeout(3)) %>%
synchronize(wait = FALSE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
get_mon_arrivals(env)
```
By default, `synchronize()` does not record information about the clones removed (`mon_all=FALSE`). However, if it is required, you can get it by setting `mon_all=TRUE`:
```{r}
t <- trajectory() %>%
clone(n = 3,
trajectory("original") %>%
timeout(1),
trajectory("clone 1") %>%
timeout(2),
trajectory("clone 2") %>%
timeout(3)) %>%
synchronize(wait = FALSE, mon_all = TRUE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
get_mon_arrivals(env)
```
### `batch()`, `separate()`
The `batch(., n, timeout, permanent, name, rule)` method offers the possibility of collecting a number of arrivals before they can continue processing as a block. Then, the `separate(.)` method splits a previously established non-permanent batch. This allows us to implement a rollercoaster process, for instance.
Let us consider a rollercoaster, with up to 10 places and a queue of 20 people, that lasts 5 minutes. We can model this problem as follows:
```{r}
set.seed(1234)
t <- trajectory() %>%
batch(10, timeout = 5, permanent = FALSE) %>%
seize("rollercoaster", 1) %>%
timeout(5) %>%
release("rollercoaster", 1) %>%
separate()
env <- simmer() %>%
# capacity and queue_size are defined in batches of 10
add_resource("rollercoaster", capacity = 1, queue_size = 2) %>%
add_generator("person", t, function() rexp(1, 2)) %>%
run(15)
get_mon_arrivals(env, per_resource = TRUE)
```
We can see above that 3 batches have been created. The first 10 people arrive within 3.8 minutes and goes into the rollercoaster. When the ride ends, at 8.8, there are only 6 people waiting, but the `batch()` timer (`timeout=5`) has run out, and another ride starts with them. These batches are non-permanent (`permanent=FALSE`), so that `separate()` can split them and people can go their separate ways.
The optional argument `rule` accepts a function to perform a fine-grained selection of which arrivals should be batched. For each particular arrival, it is batched if the function returns `TRUE`, or it simply continues otherwise. For instance, in the example above, we can prevent batching by returning always `FALSE`:
```{r}
t_batch <- trajectory() %>%
batch(10, timeout = 5, permanent = FALSE, rule = function() FALSE) %>%
seize("rollercoaster", 1) %>%
timeout(5) %>%
release("rollercoaster", 1) %>%
separate()
t_nobatch <- trajectory() %>%
seize("rollercoaster", 1) %>%
timeout(5) %>%
release("rollercoaster", 1)
set.seed(1234)
env_batch <- simmer() %>%
# capacity and queue_size are defined in batches of 10
add_resource("rollercoaster", capacity = 1, queue_size = 2) %>%
add_generator("person", t_batch, function() rexp(1, 2)) %>%
run(15)
set.seed(1234)
env_nobatch <- simmer() %>%
# capacity and queue_size are defined in batches of 10
add_resource("rollercoaster", capacity = 1, queue_size = 2) %>%
add_generator("person", t_nobatch, function() rexp(1, 2)) %>%
run(15)
get_mon_arrivals(env_batch, per_resource = TRUE)
get_mon_arrivals(env_nobatch, per_resource = TRUE)
```
By default, batches are unnamed (`name=""`), which makes them independent of one another. However, it may be interesting to feed a common batch from different trajectories. For instance, we can try this:
```{r}
t0 <- trajectory() %>%
batch(2) %>%
timeout(2) %>%
separate()
t1 <- trajectory() %>%
timeout(1) %>%
join(t0)
env <- simmer(verbose = TRUE) %>%
add_generator("t0_", t0, at(0)) %>%
add_generator("t1_", t1, at(0)) %>%
run()
get_mon_arrivals(env)
```
But we don't get the expected output because the arrivals are feeding two different batches. The arrival following `t1` join `t0` after the timeout, but effectively this is __a clone__ of `t0`, which means that the above definition is equivalent to the following:
```{r}
t0 <- trajectory() %>%
batch(2) %>%
timeout(2) %>%
separate()
t1 <- trajectory() %>%
timeout(1) %>%
batch(2) %>%
timeout(2) %>%
separate()
```
Thus, arrivals following a different trajectory will end up in a different batch in general. Nonetheless, there is one way to share a common batch across `batch()` activities. This can be done by using a common name:
```{r}
t0 <- trajectory() %>%
batch(2, name = "mybatch") %>%
timeout(2) %>%
separate()
t1 <- trajectory() %>%
timeout(1) %>%
batch(2, name = "mybatch") %>%
timeout(2) %>%
separate()
env <- simmer(verbose = TRUE) %>%
add_generator("t0_", t0, at(0)) %>%
add_generator("t1_", t1, at(0)) %>%
run()
get_mon_arrivals(env)
```
Or, equivalently,
```{r}
t0 <- trajectory() %>%
batch(2, name = "mybatch") %>%
timeout(2) %>%
separate()
t1 <- trajectory() %>%
timeout(1) %>%
join(t0)
env <- simmer(verbose = TRUE) %>%
add_generator("t0_", t0, at(0)) %>%
add_generator("t1_", t1, at(0)) %>%
run()
get_mon_arrivals(env)
```
### `send()`, `trap()`, `untrap()`, `wait()`
These activities enable asynchronous programming. The `send(., signals, delay)` method broadcasts a signal or a list of signals to all the arrivals subscribed to them. Signals can be triggered immediately:
```{r}
t <- trajectory() %>%
send(signals = c("signal1", "signal2"))
simmer(verbose = TRUE) %>%
add_generator("signaler", t, at(0)) %>%
run() %>% invisible
```
or after some delay:
```{r}
t <- trajectory() %>%
send(signals = c("signal1", "signal2"), delay = 3)
simmer(verbose = TRUE) %>%
add_generator("signaler", t, at(0)) %>%
run() %>% invisible
```
Note that both arguments, `signals` and `delay`, can be functions, and therefore they can retrieve the arrival's attributes.
This is not very useful if nobody is listening. Arrivals can subscribe to signals and (optionally) assign a handler with the `trap(., signals, handler, interruptible)`. In the following example, an arrival subscribes to a signal and blocks until its reception using the `wait(.)` method.
```{r}
t_blocked <- trajectory() %>%
trap("you shall pass") %>%
log_("waiting...") %>%
wait() %>%
log_("continuing!")
t_signaler <- trajectory() %>%
log_("you shall pass") %>%
send("you shall pass")
simmer() %>%
add_generator("blocked", t_blocked, at(0)) %>%
add_generator("signaler", t_signaler, at(5)) %>%
run() %>% invisible
```
Note that signals are ignored when an arrival is waiting in a resource's queue. The same applies inside a batch: all the signals subscribed before entering the batch are ignored. Thus, the following batch will block indifinitely:
```{r}
t_blocked <- trajectory() %>%
trap("you shall pass") %>%
log_("waiting inside a batch...") %>%
batch(1) %>%
wait() %>%
log_("continuing!")
t_signaler <- trajectory() %>%
log_("you shall pass") %>%
send("you shall pass")
simmer() %>%
add_generator("blocked", t_blocked, at(0)) %>%
add_generator("signaler", t_signaler, at(5)) %>%
run() %>% invisible
```
Upon a signal reception, the arrival stops the current activity and executes the handler if provided. Then, the execution returns to the activity following the point of the interruption:
```{r}
t_worker <- trajectory() %>%
trap("you are free to go",
handler = trajectory() %>%
log_("ok, I'm packing...") %>%
timeout(1)
) %>%
log_("performing a looong task...") %>%
timeout(100) %>%
log_("and I'm leaving!")
t_signaler <- trajectory() %>%
log_("you are free to go") %>%
send("you are free to go")
simmer() %>%
add_generator("worker", t_worker, at(0)) %>%
add_generator("signaler", t_signaler, at(5)) %>%
run() %>% invisible
```
Finally, the `untrap(., signals)` method can be used to unsubscribe from signals:
```{r}
t_worker <- trajectory() %>%
trap("you are free to go",
handler = trajectory() %>%
log_("ok, I'm packing...") %>%
timeout(1)
) %>%
log_("performing a looong task...") %>%
untrap("you are free to go") %>%
timeout(100) %>%
log_("and I'm leaving!")
t_signaler <- trajectory() %>%
log_("you are free to go") %>%
send("you are free to go")
simmer() %>%
add_generator("worker", t_worker, at(0)) %>%
add_generator("signaler", t_signaler, at(5)) %>%
run() %>% invisible
```
Signal handlers can be interrupted as well by default, meaning that a handler may keep restarting if there are frequent enough signals:
```{r}
t_worker <- trajectory() %>%