Miscellaneous

Run control

Normally, a simulation is run for a given duration with

env.run(duration=100)
If you do not specify a till or duration parameter, like ::

env.run()

, the simulation will run till there are no events left, or otherwise infinitely.

If it is required that the simulation does not stop when there are no more events, which can be useful for animation, issue

env.run(till=sim.inf)

Finally, it is possible to return control to ‘main’ from a component with

env.main().activate()

For instance, if we want to stop a simulation after 50 ships are created

class ShipGenerator(sim.Component):
    def process(self):
        for _ in range(50):
            self.hold(iat)
            Ship()
        env.main().activate

Or, if you want to terminate a run based upon a condition

class RunChecker(sim.Component):
    def process(self):
        while True:
            if len(q0) + len(q1) > 10:
                env.main.activate()
            self.standby()

It is perfectly possible and sometimes very useful to continue a simulation after a run statement, like

env.run(100)
q.reset_statistics()
env.run(1000)
q.print_statistics()

The salabim time (now) can be reset to 0 (or another time) with

env.reset_now()

Please note that in this case, user time values has to be corrected accordingly.

Time units

By default, salabim time does not have a specific dimension, which means that is up to the modeller what time unit is used, be it seconds, hours, days or whatever.

It can be useful to work in specific time unit, as this opens the possibility to specify times and durations in another time unit.

In order to use time unit, the environment has to be initialized with a time_unit parameter, like

env = sim.Environment(time_unit='hours')

From then on, the simulation runs in hours. Standard output is in then in hours and for instance

self.enter(q)
self.hold(48)
print(env.now() - self.queuetime())

means hold for 48 (hours) and 48 will be printed.

But, now we also specify a time in another time unit and get times in a specific time unit

self.enter(q)
self.hold(env.days(2))
print(env.to_minutes(env.now() - self.queuetime()))

means hold for 2 days = 48 hours and 2880 (48 * 60) will be printed.

With this, it is possible to set the speed of the animation. For instance if we want one second of real time to correspond to 5 minutes

env.speed(sim.minutes(5))

The following time units are available:

  • ‘years’

  • ‘weeks’

  • ‘days’

  • ‘hours’

  • ‘minutes’

  • ‘seconds’

  • ‘milliseconds’

  • ‘microseconds’

  • ‘n/a’ which means nothing is assigned and conversions are not supported

For conversion from a given time unit to the simulation time unit, the following calls are available:

  • years()

  • weeks()

  • days()

  • hours()

  • minutes()

  • seconds()

  • milliseconds()

  • microseconds()

For conversion from the simulation time unit to a given time unit, the following calls are available:

  • to_years()

  • to_weeks()

  • to_days()

  • to_hours()

  • to_minutes()

  • to_seconds()

  • to_milliseconds()

  • to_microseconds()

  • to_time_unit()

Distributions (apart from IntUniform, Poisson and Beta) can also specify the time unit, like

env = sim.Environment(time_unit='seconds')
processingtime_dis = sim.Uniform(10, 20, 'minutes')
dryingtime_dis = sim.Normal(2, 0.1, 'hours')

Note that distribution keep the time unit information and therefore salabim is able to present information on the distribution in pretty format: So

processingtime_dis.print_info()
dryingtime_dis.print_info()

will print

Uniform distribution 0x2bfb4d14c50
  lowerbound=10 minutes
  upperbound=20 minutes
  randomstream=0x2bfb2cc0c78
Normal distribution 0x2bfb42ce630
  mean=2 hours
  standard_deviation=0.1 hours
  coefficient_of_variation=0.05
  randomstream=0x2bfb2cc0c78

It is possible to scale a non level monitor (particularly length_of_stay of a queue) to another time unit, which is particularly useful for print_histogram. For example

waitingline.length_of_stay.to_minutes().print_histogram()

will use minutes as the time unit.

This is equivalent to

waitingline.length_of_stay.to_time_unit('minutes').print_histogram()

And if the environment’s time unit is seconds, equivalent to

(waitingline.length_of_stay * 60).print_histogram()

Working with datetimes and timedeltas

Salabim always uses a float based time scale.

In some instances it might be useful to deal with datetimes or timedelta, e.g. when getting input from an external source (file, API, …_ or when writing out times in a more human readable format.

In order to work with datetimes, datetime0 should be specified at creation of an environment or later with a call to Environment.datetime0.

env = sim.Environment(datetime0=True)

If we now have a file in the form

2022-04-30 12:00:04 100 red
2022-04-30 12:05:02 200 blue
2022-04-30 13:56:12 200 red

we could use the following code to read and process this information

class WorkLoadGenerator(sim.Component):
    def process(self):
        with open("workload.txt", "r") as f:
            for line in f.readlines():
                workload_date_str = line[:19]
                workload_datetime = datetime.datetime.strptime(workload_date_str, "%Y-%m-%d %H:%M:%S")
                self.hold(till=env.datetime_to_t(workload_datetime))
                # generate workload

env = sim.Environment(datetime0=True, trace=True)
WorkLoadGenerator()
env.run()

This generates the following output

line#                     time current component    action                               information
------ ----------------------- -------------------- -----------------------------------  ------------------------------------------------
                                                    line numbers refers to               salabim_exp.py
   43                                               default environment initialize
   43                                               main create
   43  Thu 1970-01-01 00:00:00 main                 current
   44                                               workloadgenerator.0 create
   44                                               workloadgenerator.0 activate         scheduled for Thu 1970-01-01 00:00:00 @   35  process=process
   45                                               main run                             ends on no events left   @   45+
   35  Thu 1970-01-01 00:00:00 workloadgenerator.0  current
   40                                               workloadgenerator.0 hold +19112 12:00:04 scheduled for Sat 2022-04-30 12:00:04 @   40+
   40+ Sat 2022-04-30 12:00:04 workloadgenerator.0  current
   40                                               workloadgenerator.0 hold +00:04:58   scheduled for Sat 2022-04-30 12:05:02 @   40+
   40+ Sat 2022-04-30 12:05:02 workloadgenerator.0  current
   40                                               workloadgenerator.0 hold +01:51:10   scheduled for Sat 2022-04-30 13:56:12 @   40+
   40+ Sat 2022-04-30 13:56:12 workloadgenerator.0  current
   40+                                              workloadgenerator.0 ended
   45+                                              run ended                            no events left
   45+ Sat 2022-04-30 13:56:12 main                 current

As you can see, the simulation starts on 1 January 1970.

This can be changed by specifying the start datettime

env = sim.Environment(datetime0=datetime.datetime(2022, 4, 29), trace=True)

Once a datetime0 is specified, the moments and durations in the trace will be like shown above. Note that the time in the upper right hand corner of an animation is also presented like this.

If no time unit is specified and datetime0 is given, salabim will assume that the time unit is seconds. But, it is also possible to work in any time unit combined with datetime0

env = sim.Environment(time_unit="days", datetime0=datetime.datetime(2022, 4, 29))
env.trace(True)
env.run(1)  # this is one day

gives

49                                               main run +1 00:00:00                 scheduled for Sat 2022-04-30 00:00:00 @   49+
49+ Sat 2022-04-30 00:00:00 main                 current

There are a some additional methods connected to datetime0:

  • datetime_to_t

  • timedelta_to_duration

  • Environment.t_to_datetime

  • Environment.duration_to_timedelta

  • Environment.datetime0

Refer to the reference sections for details.

Usage of the the trace facility

Control

Tracing can be turned on at time of creating an environment

env = sim.Environment(trace=True)

and can be turned on during a simulation run with env.trace(True) and likewise turned off with env.trace(False). The current status can be queried with env.trace(False).

It is possible to temporarily turn off trace with

with env.suppress_trace():
    ... # here trace is off
# here trace is the same as before entering the context manager.

It is also possible to direct the trace output to file, by using

with open('output.txt', 'w') as out:
    env = sim.Environment(trace=out)

or (not prefered)

out = open('output.txt', 'w')
env.trace(out)
...
out.close()

Interpretation of the trace

A trace ouput looks like

line#         time current component    action                               information
-----   ---------- -------------------- -----------------------------------  ------------------------------------------------
                                        line numbers refers to               Example - basic.py
   11                                   default environment initialize
   11                                   main create
   11        0.000 main                 current
   12                                   car.0 create
   12                                   car.0 activate                       scheduled for      0.000 @    6  process=process
   13                                   main run                             scheduled for      5.000 @   13+
    6        0.000 car.0                current
    8                                   car.0 hold                           scheduled for      1.000 @    8+
    8+       1.000 car.0                current
    8                                   car.0 hold                           scheduled for      2.000 @    8+
    8+       2.000 car.0                current
    8                                   car.0 hold                           scheduled for      3.000 @    8+
    8+       3.000 car.0                current
    8                                   car.0 hold                           scheduled for      4.000 @    8+
    8+       4.000 car.0                current
    8                                   car.0 hold                           scheduled for      5.000 @    8+
   13+       5.000 main                 current

The texts are pretty self explanatory. If a mode is given, that will be shown as well.

Note that line numbers sometimes has an added +, which means that the activation is actually the statement following the given line number. When there is more than one source file involved, the line number may be preceded by a letter. In that case, the trace will contain an information line to which file that letter refers to.

The text ‘process=’ refers to the activation process, which is quite often just process.

When a time is followed by an exclamation mark (!), it means that the component is scheduled urgent, i.e. before all other events for the same moment.

Suppressing components from being shown in the trace

It is possible to suppress the trace when a specific component becomes or is current. This can be either indicated at creation of a component with

c = sim.Component(suppress_trace=True)

or later with

c.suppress_trace(True)

Note that this suppresses all trace output during the time a component is current.

Showing standby components in the trace

By default standby components are (apart from when they become non standby) suppressed from the trace. With env.suppress_trace_standby(False) standby components are fully traced.

Changing the format of times and durations

By default, times and durations are printed as 10.3f.

But it is possible to use other formats by overriding the Environment.time_to_str and Environment,duration_to_str methods.

E.g.

sim.Environment.time_to_str = lambda self, t: f"{t:10.1f}

or

class MyEnvironment(sim.Environment):
    def time_to_str(self, t):
        return datetime.datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")

...

env = sim.MyEnvironment(trace=True)

In general, it is recommended to use datetime0 fumctionality in this case (see the section “Working with datetimes and timedeltas”).

Note that the method time_to_str should return a string with the same length, regardless of the value of t.

Adding lines to the trace output

A model can add additional information to the trace with the Environment.print_trace() method. This methods accepts up to five parameters to be show on one line. When trace is False, nothing will be displayed.

Example

env.print_trace('', '**ALERT**', 'Houston, we have a problem with', c.name())

Refer to the reference section for details.

Suppressing line numbers in the trace

Particularly when the trace output is written to a file, it may be useful to suppress line numbers in the trace. This can result in dramatic (up to 50 times!) performance increase, because the calculation of line numbers is very demanding.

The method Environment.suppress_trace_linenumbers() can be used to disable/enable line number in the trace

env = sim.Environment(trace=True)
env.suppress_trace_linenumbers(True)

By default, line numbers are not suppressed in the trace. The current status can be queried with

print(env.suppress_trace_linenumbers())

Redirecting standard output

Normally, all salabim output, like the trace, is written to stdout.

However, if the trace parameter of Environment() or Environment.trace() is a file handle (open for write), then the trace output will go to the specified file. Note that it is required to close the file handle at the end, so a context manager (‘with’) is recommended. Example

with open('output.txt', 'w') as out:
    env = sim.Environment(trace=out)
    ...
    env.run()

Alternatively, stdout can be set to a file, in which case ALL salabim output will be redirected. This can be done like

save_stdout = sys.stdout
sys.stdout = open('output.txt', 'w')

If required, it is possible to revert to the original stdout

sys.stdout.close()
sys.stdout = save_stdout

Capturing stdout

It is also possible to use a context manager to capture stdout.

We can do that with sim.capture_stdout().

Unless include_print=False is given, the output goes also to the console (as usual).

For example, with

with sim.capture_stdout():
    env = sim.Environment(trace=True)

all trace output will printed AND be captured.

With

with sim.capture_stdout(include_print=False):
    env = sim.Environment(trace=True)

, trace output will not be printed, but still be captured.

The captured output can be retrieved with

  • sim.captured_stdout_as_list() to retrieve it as a list of strings (per line)

  • sim.captured_stdout_as_str() to retrieve it as a string

  • sim.captured_stdout_as_file() to retrieve it in a file (given as a str, Path or file handle)

The first is particularly useful for Python in Excel.

It is possible to clear the captured_stdout information with sim.clear_captured_stdout() .