Monitor

Monitors are a way to collect data from the simulation.

They are automatically collected for

  • components (mode and status)

  • resources (capacity, occupancy, claimed_quantity, available_quantity)

  • queues (length, length_of_stay, capacity, available_quantity)

  • states (value)

On top of that the user can define its own monitors.

Monitors can be used to get statistics, used in animations and as a feed for graphical tools, like matplotlib.

There are two types of monitors:

  • level monitors Level monitors are useful to collect data about a variable that keeps its value over a certain length of time, such as the length of a queue or the colour of a traffic light.

  • non level monitors Non level monitors are useful to collect data about a values that occur just once. Examples, are the length of stay in a queue and the number of processing steps of a part.

For both types, the time is always collected, along with the value.

Non level monitors can be weighted, if required.

Non level monitor

Non level monitors collects values which do not reflect a level, e.g. the processing time of a part.

We define the monitor with processingtime = sim.Monitor('processingtime') and then collect values by processingtime.tally(this_duration)

By default, the collected values are stored in a list. Alternatively, it is possible to store the values in an array of one of the following types:

type

stored as

lowerbound

upperbound

number of bytes

‘any’

list

N/A

N/A

depends on data

‘bool’

integer

False

True

1

‘int8’

integer

-128

127

1

‘uint8’

integer

0

255

1

‘int16’

integer

-32768

32767

2

‘uint16’

integer

0

65535

2

‘int32’

integer

2147483648

2147483647

4

‘uint32’

integer

0

4294967295

4

‘int64’

integer

-9223372036854775808

9223372036854775807

8

‘uint64’

integer

0

18446744073709551615

8

‘float’

float

-inf

inf

8

Monitoring with arrays takes up less space. Particularly when tallying a large number of values, this is strongly advised.

Note that if non numeric values are stored (only possible with the default setting (‘any’)), a tallied value is converted, if required, to a numeric value if possible, or 0 if not.

It is possible to use monitors with weighted data. In that case, just add a second parameter to tally, which defaults to 1. All statistics will take the weights into account.

There is set of statistical data available, which will be all weighed according to the tallied weights (1 by default):

  • number_of_entries

  • number_of_entries_zero

  • weight

  • weight_zero

  • mean

  • std

  • minimum

  • median

  • maximum

  • percentile

  • bin_number_of_entries (number of entries between two given values)

  • bin_weight (total weight of entries between two given values)

  • value_number_of_entries (number of entries equal to a given value or set of values)

  • value_weight (total weight of entries equal to a given value or set of values)

For all these statistics, it is possible to exclude zero entries, e.g. m.mean(ex0=True) returns the mean, excluding zero entries.

Besides, it is possible to get all collected values as an array with x(). In the case of ‘any’ monitors, the values might have to be converted. By specifying force_numeric=False the collected values will be returned as stored.

With the monitor method, the monitor can be enabled or disabled. Note that a tally is just ignored when the monitor is disabled.

Also, the current monitor status (enabled/disabled) can be retrieved.

proctime.monitor(False)  # disable monitoring
proctime.monitor(True)  # enable monitoring
if proctime.monitor():
    print('proctime is enabled')

Calling m.reset() will clear all tallied values.

The statistics of a monitor can be printed with print_statistics(). E.g: waitingline.length_of_stay.print_statistics():

Statistics of Length of stay in waitingline at     50000
                        all    excl.zero         zero
-------------- ------------ ------------ ------------
entries            4995         4933           62
mean                 84.345       85.405
std.deviation        48.309       47.672

minimum               0            0.006
median               94.843       95.411
90% percentile      142.751      142.975
95% percentile      157.467      157.611
maximum             202.153      202.153

And, a histogram can be printed with print_histogram(). E.g. waitingline.length_of_stay.print_histogram(30, 0, 10):

Histogram of Length of stay in waitingline
                        all    excl.zero         zero
-------------- ------------ ------------ ------------
entries            4995         4933           62
mean                 84.345       85.405
std.deviation        48.309       47.672

minimum               0            0.006
median               94.843       95.411
90% percentile      142.751      142.975
95% percentile      157.467      157.611
maximum             202.153      202.153

           <=       entries     %  cum%
        0            62       1.2   1.2 |
       10           169       3.4   4.6 ** |
       20           284       5.7  10.3 ****    |
       30           424       8.5  18.8 ******         |
       40           372       7.4  26.2 *****               |
       50           296       5.9  32.2 ****                     |
       60           231       4.6  36.8 ***                          |
       70           192       3.8  40.6 ***                             |
       80           188       3.8  44.4 ***                                |
       90           136       2.7  47.1 **                                   |
      100           352       7.0  54.2 *****
      110           491       9.8  64.0 *******
      120           414       8.3  72.3 ******
      130           467       9.3  81.6 *******
      140           351       7.0  88.7 *****
      150           224       4.5  93.2 ***
      160           127       2.5  95.7 **
      170            67       1.3  97.0 *
      180            59       1.2  98.2
      190            61       1.2  99.4
      200            24       0.5  99.9
      210             4       0.1 100
      220             0       0   100
      230             0       0   100
      240             0       0   100
      250             0       0   100
      260             0       0   100
      270             0       0   100
      280             0       0   100
      290             0       0   100
      300             0       0   100
          inf         0       0   100

If neither number_of_bins, nor lowerbound nor bin_width are specified, the histogram will be autoscaled.

Histograms can be printed with their values, instead of bins. This is particularly useful for non numeric tallied values, such as names:

import salabim as sim

env = sim.Environment()

monitor_names= sim.Monitor(name='names')
for _ in range(10000):
    name = sim.Pdf(('John', 30, 'Peter', 20, 'Mike', 20, 'Andrew', 20, 'Ruud', 5, 'Jan', 5)).sample()
    monitor_names.tally(name)

monitor_names.print_histogram(values=True)

The ouput of this:

Histogram of names
entries          10000

value               entries
Andrew                 2031( 20.3%) ****************
Jan                     495(  5.0%) ***
John                   2961( 29.6%) ***********************
Mike                   1989( 19.9%) ***************
Peter                  2048( 20.5%) ****************
Ruud                    476(  4.8%) ***

It is also possible to specify the values to be shown

import salabim as sim

env = sim.Environment()

monitor_names= sim.Monitor(name='names')
for _ in range(10000):
    name = sim.Pdf(('John', 30, 'Peter', 20, 'Mike', 20, 'Andrew', 20, 'Ruud', 5, 'Jan', 5)).sample()
    monitor_names.tally(name)

monitor_names.print_histogram(values=('John', 'Andrew', 'Ruud', 'Fred'))

The output of this:

Histogram of names
entries          10000

value               entries     %
John                   2961  29.6 ***********************
Andrew                 2031  20.3 ****************
Ruud                    476   4.8 ***
Fred                      0   0
<rest>                 4532  45.3 ************************************

It is also possible to sort the histogram on the weight (or number of entries) of the value

import salabim as sim

env = sim.Environment()

monitor_names= sim.Monitor(name='names')
for _ in range(10000):
    name = sim.Pdf(('John', 30, 'Peter', 20, 'Mike', 20, 'Andrew', 20, 'Ruud', 5, 'Jan', 5)).sample()
    monitor_names.tally(name)

monitor_names.print_histogram(values=True, sort_on_weight=True)

The output of this:

Histogram of names
entries          10000

value               entries     %
John                   2961  29.6 ***********************
Peter                  2048  20.5 ****************
Andrew                 2031  20.3 ****************
Mike                   1989  19.9 ***************
Jan                     495   5.0 ***
Ruud                    476   4.8 ***

Level monitor

Level monitors tally levels along with the current (simulation) time. e.g. the number of parts a machine is working on.

A level monitor is defined by specifying level=True in the initialization of Monitor, e.g.

working_on_parts = sim.Monitor(name='working_on_parts', level=True, initial_tally=0)

By default, the collected x-values are stored in a list. Alternatively, it is possible to store the x-values in an array of one of the following types:

type

stored as

lowerbound

upperbound

bytes

do not tally (=off)

‘any’

list

N/A

N/A

?

N/A

‘bool’

integer

False

True

1

255

‘int8’

integer

-127

127

1

-128

‘uint8’

integer

0

254

1

255

‘int16’

integer

-32767

32767

2

-32768

‘uint16’

integer

0

65534

2

65535

‘int32’

integer

2147483647

2147483647

4

2147483648

‘uint32’

integer

0

4294967294

4

4294967295

‘int64’

integer

-9223372036854775807

9223372036854775807

8

-9223372036854775808

‘uint64’

integer

0

18446744073709551614

8

18446744073709551615

‘float’

float

-inf

inf

8

-inf

Monitoring with arrays takes up less space. Particularly when tallying a large number of values, this is strongly advised.

Note that if non numeric x-values are stored (only possible with the default setting (‘any’)), the tallied values are converted, if required, to a numeric value if possible, or 0 if not.

During the simulation run, it is possible to retrieve the last tallied value (which represents the ‘current’ value) by calling Monitor.get().
It’s also possible to directly call the level monitor to get the current value, e.g.

mylevel = sim.Monitor('level', level=True, initial_tally=0)
...
mylevel.tally(10)
self.hold(1)
print(mylevel())  # will print 10

For the same reason, the standard length monitor of a queue can be used to get the current length of a queue: q.length() although the more Pythonic len(q) is prefered.

When Monitor.get() is called with a time parameter or a direct call with a time parameter, the value at that time will be returned

print (mylevel.get(4))  # will print the value at time 4
print (mylevel(4))  # will print the value at time 4

There is set of statistical data available, which are all weighted with their duration:

  • duration

  • duration_zero (time that the value was zero)

  • mean

  • std

  • minimum

  • median

  • maximum

  • percentile

  • bin_duration (total duration of entries between two given values)

  • value_duration (total duration of entries equal to a given value or set of values)

For all these statistics, it is possible to exclude zero entries, e.g. m.mean(ex0=True) returns the mean, excluding zero entries.

The individual x-values and their duration can be retrieved xduration(). By default, the x-values will be returned as an array, even if the type is ‘any’. In case the type is ‘any’ (stored as a list), the tallied x-values will be converted to a numeric value or 0 if that’s not possible. By specifying force_numeric=False the collected x-values will be returned as stored.

The individual x-values and the associated timestamps can be retrieved with xt() or tx(). By default, the x-values will be returned as an array, even if the type is ‘any’. In case the type is ‘any’ (stored as a list), the tallied x-values will be converted to a numeric value or 0 if that’s not possible. By specifying force_numeric=False the collected x-values will be returned as stored.

When monitoring is disabled, an off value (see table above) will be tallied. All statistics will ignore the periods from this off to a non-off value. This also holds for the xduration() method, but NOT for xt() and tx(). Thus, the x-arrays of xduration() are not necessarily the same as the x-arrays in xt() and tx(). This is the reason why there’s no x() or t() method.
It is easy to get just the x-array with xduration()[0] or xt()[0].

It is important that a user never tallies an off value! Instead use Monitor.monitor(False)

With the monitor method, a level monitor can be enabled or disabled.

Also, the current monitor status (enabled/disabled) can be retrieved.

mylevel.monitor(False)  # disable monitoring
mylevel.monitor(True)  # enable monitoring
if mylevel.monitor():
    print('level is enabled')

It is strongly advised to keep tallying even when monitoring is off, in order to be able to access the current value at any time. The values tallied when monitoring is off are not stored.

Calling m.reset() will clear all tallied values and timestamps.

The statistics of a level monitor can be printed with print_statistics(). E.g: waitingline.length.print_statistics():

Statistics of Length of waitingline at     50000
                        all    excl.zero         zero
-------------- ------------ ------------ ------------
duration          50000        48499.381     1500.619
mean                  8.427        8.687
std.deviation         4.852        4.691

minimum               0            1
median                9           10
90% percentile       14           14
95% percentile       16           16
maximum              21           21

And, a histogram can be printed with print_histogram(). E.g.

waitingline.length.print_histogram(30, 0, 1)
Histogram of Length of waitingline
                        all    excl.zero         zero
-------------- ------------ ------------ ------------
duration          50000        48499.381     1500.619
mean                  8.427        8.687
std.deviation         4.852        4.691

minimum               0            1
median                9           10
90% percentile       14           14
95% percentile       16           16
maximum              21           21

           <=      duration     %  cum%
        0          1500.619   3.0   3.0 **|
        1          2111.284   4.2   7.2 ***  |
        2          3528.851   7.1  14.3 *****      |
        3          4319.406   8.6  22.9 ******            |
        4          3354.732   6.7  29.6 *****                  |
        5          2445.603   4.9  34.5 ***                        |
        6          2090.759   4.2  38.7 ***                           |
        7          2046.126   4.1  42.8 ***                               |
        8          1486.956   3.0  45.8 **                                  |
        9          2328.863   4.7  50.4 ***
       10          4337.502   8.7  59.1 ******
       11          4546.145   9.1  68.2 *******
       12          4484.405   9.0  77.2 *******
       13          4134.094   8.3  85.4 ******
       14          2813.860   5.6  91.1 ****
       15          1714.894   3.4  94.5 **
       16           992.690   2.0  96.5 *
       17           541.546   1.1  97.6
       18           625.048   1.3  98.8 *
       19           502.291   1.0  99.8
       20            86.168   0.2 100.0
       21             8.162   0.0 100
       22             0       0   100
       23             0       0   100
       24             0       0   100
       25             0       0   100
       26             0       0   100
       27             0       0   100
       28             0       0   100
       29             0       0   100
       30             0       0   100
          inf         0       0   100

If neither number_of_bins, nor lowerbound nor bin_width are specified, the histogram will be autoscaled.

Histograms can be printed with their values, instead of bins. This is particularly useful for non numeric tallied values, like names of production stages. For example

Histogram of Status
duration           300

value                     duration     %
idle                        70      23.3 ******************
package                     42      14.0 ***********
prepare                     48      16   ************
stage A                     12       4   ***
stage B                     50      16.7 *************
stage C                     54      18   **************
stage D                     24       8   ******

Merging of monitors

Monitors can be merged, to create a new monitor, nearly always to collect aggregated data.

The method Monitor.merge() is used for that, like

mc = m0.merge(m1, m2)

Then we can just get the mean of the monitors m0, m1 and m2 combined by

mc.mean()

,but also directly with :

m0.merge(m1, m2).mean()

Alternatively, monitors can be merged with the + operator, like

mc = m0 + m1 + m2

And then get the mean of the aggregated monitors with

mc.mean()

, but also with

(m0 + m1 + m2).mean()

It is also possible to use the sum function to merge a number of monitors. So

print(sum((m0, m1, m2)).mean())

Finally, if ms = (m0, m1, m2), it is also possible to use

print(sum(ms).mean())

A practical example of this is the case where the list waitinglines contains a number of queues.

Then to get the aggregated statistics of the length of all these queues, use

sum(waitingline.length for waitingline in waitinglines).print_statistics()

For non level monitors, all of the tallied x-values are copied from the to be merged monitors. For level monitors, the x-values are summed, for all the periods where all the monitors were on. Periods where one or more monitors were disabled, are excluded. Note that the merge only takes place at creation of the (timestamped) monitor and not dynamically later.

Sample usage:

Suppose we have three types of products (a, b, c) and that each have a queue for processing, so a.processing, b.processing, c.processing. If we want to print the histogram of the combined (=summed) length of these queues

a.processing.length.merge(b.processing.length, c.processing.length, name='combined processing length')).print_histogram()

and to get the minimum of the length_of_stay for all queues

(a.processing.length_of_stay + b.processing.length_of_stay + c.processing.length_of_stay).minimum()

Note that it is possible to rename a merged monitor (particularly those created with + or sum) with the rename() method:

sum(waitingline.length for waitingline in waitinglines).rename('aggregated length of waitinglines').print_statistics()

Merged monitors are disabled and cannot be enabled again.

Slicing of monitors

It is possible to slice a monitor with Monitor.slice(), which has two applications:

  • to get statistics on a monitor with respect to a given time period, most likely a subrun

  • to get statistics on a monitor with respect to a recurring time period, like hour 0-1, hour 0-2, etc.

Examples

for i in range(10):
    start = i * 1000
    stop = (i+1) * 1000
    print(f'mean length of q in [{start},{stop})={q.length.slice(start,stop).mean()}'
    print(f'mean length of stay in [{start},{stop})={q.length_of_stay.slice(start,stop).mean()}'

for i in range(24):
    print(f'mean length of q in hour {i}={q.length.slice(i, i+1, 24).mean()}'
    print(f'mean length of stay of q in hour {i}={q.length_of_stay.slice(i, i+1, 24).mean()}'

Instead of slice(), a monitor can be sliced as well with the standard slice operator [], like

q.length[1000:2000].print_histogram()
q.length[2:3:24].print_histogram()
print(q.length[1000].mean())

Note that it is possible to rename a sliced monitor (particularly those created []) with the rename() method:

waitingline.length[1000:2000].rename('length of waitingline between t=1000 and t-2000').print_statistics()

Sliced monitors are disabled and cannot be enabled again.

Using monitored values in pandas

It is possible to save the t- and x- values of a monitor into a pandas dataframe, like

non_level_monitor.as_dataframe()

This results, for instance, in this dataframe:

    t  non level monitor.x
0   1.0                 11.0
1   2.0                 12.0
2   3.0                 13.0
3   4.5                 14.5
4   5.0                 15.0
5  10.0                 20.0

The same can be done for a level monitor. But it might be difficult to use this dataframe, as in most analyses, a uniform (equidistant) t-scale is required and that is not how salabim collects timeseries data.

That’s where Monitor.as_resampled_dataframe() is useful. Here, you specify the frequency (delta_t) of the resample. The smaller delta_t, the more accurate the dataframe will follow the collected data.

As an example, suppose we have a monitor like this:

_images/mon2.png

When we now resample with delta_t=1, we will see:

_images/mon3.png

, whereas delta_t=0.1, is much more accurate (at the expense of a bigger dataframe):

_images/mon4.png

The Monitor.as_resampled_dataframe method can be used to make a dataframe containing the x-values of several monitors.

If datetime0 (see also the Miscellaneous chapter) is specified, both Monitor.as_dataframe and Monitor.as_resampled_dataframe can emit the time as datetime.datatime values.

See the reference section for details.

Using monitored values in other packages, like matplotlib

For high quality, reproduction ready, graphs, it can be useful to use additional packages, most notably matplotlib.

The sampled values from a non level monitor can be retrieved with Monitor.x(). If the moment of the sample is required as well, either Monitor.xt() or Monitor.tx() can be used.

For level monitors, there is choice of :

  • Monitor.xt()

  • Monitor.tx()

  • Monitor.xduration()

To get a proper display of a level monitor, we advise something like

import matplotlib.pyplot as plt
...
plt.plot(*waitingline.length.tx(), drawstyle="steps-post")
plt.show()

, which might produces something like:

_images/mon1.png

Pickling a monitor

Monitor.freeze() returns a ‘frozen’ monitor that can be used to store the results not depending on the current environment.

This is particularly useful for pickling a monitor.

E.g. use

with open("mon.pickle", "wb") as f:
    pickle.dump(f, mon.freeze())

to save the monitor mon, and

with open("mon.pickle", "rb") as f:
    mon_retrieved = pickle.load(f)

to retrieve the monitor, later.

Both level and non level monitors are supported. Frozen monitors get the name of the original monitor padded with ‘.frozen’ unless specified differently.

Stats_only monitors

Monitors can optionally collect statistics only.

This minimizes memory usage as individual tallies will not be stored in that case. But beware that some important functionality is not available in stats_only monitors (see below).

You can define the stats_only status at creation time of the monitor, like

m = sim.Monitor('m', stats_only=True)

But, it is also possible to reset a monitor with a different stats_only value. This can be useful if you want a system monitor, like Component.mode or Component.status to not keep individual tallied values

class Car(sim.Component):
    def setup(self):
        self.mode.reset(stats_only=True)

When stats_only is active, values are always forced to numeric, with a fallback value of 0.

Monitor with stats_only=True support __call__/get (without arguments), base_name, deregister, duration, duration_zero, maximum, mean, minimum, monitor, name, number_of_entries, number_of_entries_zero, print_histogram, print_histograms, print_statistics, register, rename, reset, reset_monitors, sequence_number, setup, stats_only, std, t, tally, weight, weight_zero

Monitors with stats_only=True do NOT support (these will raise a NotImplementedError) __call__/get (with an argument), arithmeric operations, animate, bin_duration, bin_number_of_entries, bin_weight, freeze, histogram_autoscale, median, merge, multiply, percentile, slice, slicing, to_days, to_hours, to_microseconds, to_milliseconds, to_minutes, to_seconds, to_time_unit, to_weeks, to_years, tx, value_duration, value_number_of_entries, value_weight, values, x, xduration, xt, xweight

In line with the above, Queue.reset_monitors, Resource.reset_monitors, State.reset_monitors have a stats_only parameter, with which all monitors can set/reset the stats_only status. So, if you want all eight monitors belonging to a resource, but the requesters’ to be stats_only, you can write

r.reset_monitors(stats_only=True)
r.requesters.monitors(stats_only=False)

The current stats_only mode can be queried with Monitor.stats_only() .