Animation

Animation is a powerful tool to debug, test and demonstrate simulations.

It is possible to show a number of shapes (lines, rectangles, circles, etc), texts as well (images) in a window. These objects can be dynamically updated. Monitors may be animated by showing the current value against the time. Furthermore the components in a queue may be shown in a highly customizable way. As text animation may be dynamically updated, it is even possible to show the current state, (monitor) statistics, etc. in the animation windows.

Salabim’s animation engine also allows some user input.

It is important to realize that animation calls can be still given when animation is actually off. In that case, there is hardly any impact on the performance.

Salabim animations can be

  • synchronized with the simulation clock and run in real time (synchronized)
  • advance per simulation event (non synchronized)

In synchronized mode, one time unit in the simulation can correspond to any period in real time, e.g.

  • 1 time unit in simulation time –> 1 second real time (speed = 1) (default)
  • 1 time unit in simulation time –> 4 seconds real time (speed = 0.25)
  • 4 time units in simulation time –> 1 second real time (speed = 4)

The most common way to start an animation is by calling `` env.animate(True)`` or with a call to animation_parameters.

Animations can be started en stopped during execution (i.e. run). When main is active, the animation is always stopped.

The animation uses a coordinate system that -by default- is in screen pixels. The lower left corner is (0,0). But, the user can change both the coordinate of the lower left corner (translation) as well as set the x-coordinate of the lower right hand corner (scaling). Note that x- and y-scaling are always the same.
Furthermore, it is possible to specify the colour of the background with animation_parameters.

Prior to version 2.3.0 there was actually just one animation object class: Animate. This interface is described later as the new animation classes are easier to use and even offer some additional functionality.

New style animation classes can be used to put texts, rectangles, polygon, lines, series of points, circles or images on the screen. All types can be connected to an optional text.

Here is a sample program to show of all the new style animation classes:

# Animate classes.py

"""
This program demonstrates the various animation classes available in salabim.
"""
import salabim as sim

env = sim.Environment(trace=False)
env.animate(True)
env.modelname("Demo animation classes")
env.background_color("20%gray")

sim.AnimatePolygon(spec=(100, 100, 300, 100, 200, 190), text="This is\na polygon")
sim.AnimateLine(spec=(100, 200, 300, 300), text="This is a line")
sim.AnimateRectangle(spec=(100, 10, 300, 30), text="This is a rectangle")
sim.AnimateCircle(radius=60, x=100, y=400, text="This is a cicle")
sim.AnimateCircle(radius=60, radius1=30, x=300, y=400, text="This is an ellipse")
sim.AnimatePoints(spec=(100, 500, 150, 550, 180, 570, 250, 500, 300, 500), text="These are points")
sim.AnimateText(text="This is a one-line text", x=100, y=600)
sim.AnimateText(
    text="""\
Multi line text
-----------------
Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute
irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla
pariatur.

Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
""",
    x=500,
    y=100,
)

sim.AnimateImage("Pas un pipe.jpg", x=500, y=400)
env.run(100)

Resulting in:

_images/Pic1.png

Animation of the components of a queue is accomplished with AnimateQueue(). It is possible to use the standard shape of components, which is a rectangle with the sequence number or define your own shape(s). The queue can be build up in west, east, north or south directions. It is possible to limit the number of component shown.

Monitors can be visualized dynamically with AnimateMonitor().

These features are demonstrated in Demo queue animation.py

import salabim as sim

'''
This us a demonstration of several ways to show queues dynamically and the corresponding statistics
The model simply generates components that enter a queue and leave after a certain time.

Note that the actual model code (in the process description of X does not contain any reference
to the animation!
'''


class X(sim.Component):
    def setup(self, i):
        self.i = i

    def animation_objects(self, id):
        '''
        the way the component is determined by the id, specified in AnimateQueue
        'text' means just the name
        any other value represents the colour
        '''
        if id == 'text':
            ao0 = sim.AnimateText(text=self.name(), textcolor='fg', text_anchor='nw')
            return 0, 16, ao0
        else:
            ao0 = sim.AnimateRectangle((-20, 0, 20, 20),
                text=self.name(), fillcolor=id, textcolor='white', arg=self)
            return 45, 0, ao0

    def process(self):
        while True:
            yield self.hold(sim.Uniform(0, 20)())
            self.enter(q)
            yield self.hold(sim.Uniform(0, 20)())
            self.leave()


env = sim.Environment(trace=False)
env.background_color('20%gray')

q = sim.Queue('queue')

qa0 = sim.AnimateQueue(q, x=100, y=50, title='queue, normal', direction='e', id='blue')
qa1 = sim.AnimateQueue(q, x=100, y=250, title='queue, maximum 6 components', direction='e', max_length=6, id='red')
qa2 = sim.AnimateQueue(q, x=100, y=150, title='queue, reversed', direction='e', reverse=True, id='green')
qa3 = sim.AnimateQueue(q, x=100, y=440, title='queue, text only', direction='s', id='text')

sim.AnimateMonitor(q.length, x=10, y=450, width=480, height=100, horizontal_scale=5, vertical_scale=5)

sim.AnimateMonitor(q.length_of_stay, x=10, y=570, width=480, height=100, horizontal_scale=5, vertical_scale=5)

sim.AnimateText(text=lambda: q.length.print_histogram(as_str=True), x=500, y=700,
    text_anchor='nw', font='narrow', fontsize=10)

sim.AnimateText(text=lambda: q.print_info(as_str=True), x=500, y=340,
    text_anchor='nw', font='narrow', fontsize=10)

[X(i=i) for i in range(15)]
env.animate(True)
env.modelname('Demo queue animation')
env.run()

Here is snapshot of this powerful, dynamics (including the histogram!):

_images/Pic2.png

Advanced

The various classes have a lot of parameters, like color, line width, font, etc.

These parameters can be given just as a scalar, like:

sim.AnimateText(text='Hello world', x=200, y=300, textcolor='red')

But each of these parameters may also be a:

  • function with zero arguments
  • function with one argument being the time t
  • function with two arguments being ‘arg’ and the time t
  • a method with instance ‘arg’ and the time t

The function or method is called at each animation frame update (maximum of 30 frames per second).

This makes it for instance possible to show dynamically the mean of monitor m, like in

sim.AnimateRectangle(spec=(10, 10, 200, 30), text=lambda: str(m.mean())

Class Animate

This class can be used to show:

  • line (if line0 is specified)
  • rectangle (if rectangle0 is specified)
  • polygon (if polygon0 is specified)
  • circle (if circle0 is specified)
  • text (if text is specified)
  • image (if image is specicified)

Note that only one type is allowed per instance of Animate.

Nearly all attributes of an Animate object are interpolated between time t0 and t1. If t0 is not specified, now() is assumed. If t1 is not specified inf is assumed, which means that the attribute will be the ‘0’ attribute.

E.g.:

Animate(x0=100,y0=100,rectangle0==(-10,-10,10,10)) will show a square around (100,100) for ever
Animate(x0=100,y0=100,x1=200,y1=0,rectangle0=(-10,-10,10,10)) will still show the same square around (100,100) as t1 is not specified
Animate(t1=env.now()+10,x0=100,y0=100,x1=200,y1=0,rectangle0=(-10,-10,10,10)) will show a square moving from (100,100) to (200,0) in 10 units of time.

It also possible to let the rectangle change shape over time:

Animate(t1=env.now(),x0=100,y0=100,x1=200,y1=0,rectangle0=(-10,-10,10,10),rectangle1=(-20,-20,20,20)) will show a moving and growing rectangle.

By default, the animation object will not change anymore after t1, but will remain visible. Alternatively, if keep=False is specified, the object will disappear at time t1.

Also, colors, fontsizes, angles can be changed in a linear way over time.

E.g.:

Animate(t1=env.now()+10,text='Test',textcolor0='red',textcolor1='blue',angle0=0,angle1=360) will show a rotating text changing from red to blue in 10 units of time.

The animation object can be updated with the update method. Here, once again, all the attributes can be specified to change over time. Note that the defaults for the ‘0’ values are the actual values at t=now().

Thus,

an=Animate(t0=0,t1=10,x0=0,x1=100,y0=0,circle0=(10,),circle1=(20,)) will show a horizontally moving, growing circle.

Now, at time t=5, we issue an.update(t1=10,y1=50,circle1=(10,)) Then x0 will be set 50 (halfway 0 an 100) and cicle0 to (15,) (halfway 10 and 20). Thus the circle will shrink to its original size and move vertically from (50,0) to (50,50). This concept is very useful for moving objects whose position and orientation are controlled by the simulation.

Here we explain how an attribute changes during time. We use x as an example. Normally, x=x0 at t=t0 and x=x1 at t>=t1. between t=t0 and t=t1, x is linearly interpolated. An application can however override the x method. The prefered way is to subclass the Animate class:

# Demo animate 1
import salabim as sim


class AnimateMovingText(sim.Animate):
    def __init__(self):
        sim.Animate.__init__(self, text="", x0=100, x1=1000, y0=100, t1=env.now() + 10)

    def x(self, t):
        return sim.interpolate(sim.interpolate(t, self.t0, self.t1, 0, 1) ** 2, 0, 1, self.x0, self.x1)

    def y(self, t):
        return int(t) * 50

    def text(self, t):
        return "{:0.1f}".format(t)


env = sim.Environment()

env.animate(True)

AnimateMovingText()

env.run(till=sim.inf)  # otherwise the simulation will end at t=0, because there are no events left

This code will show the current simulation time moving from left to right, uniformly accelerated. And the text will be shown a bit higher up, every second. It is not necessary to use t0, t1, x0, x1, but is a convenient way of setting attributes.

The following methods may be overridden:

method circle image line polygon rectangle text
anchor  
       
angle
circle
         
fillcolor
   
 
fontsize          
image  
       
layer
line    
     
linecolor
 
 
linewidth
 
 
max_lines          
offsetx
offsety
polygon      
   
rectangle        
 
text          
text_anchor          
textcolor          
visible
width  
       
x
xy_anchor
y

Dashboard animation

Here we present an example model where the simulation code is completely separated from the animation code. This makes communication and debugging and switching off animation much easier.

The example below generates 15 persons starting at time 0, 1, … . These persons enter a queue called q and stay there 15 time units.

The animation dashboard shows the first 10 persons in the queue q, along with the length of that q.

# Demo animate 2.py
import salabim as sim


class AnimateWaitSquare(sim.Animate):
    def __init__(self, i):
        self.i = i
        sim.Animate.__init__(
            self, rectangle0=(-10, -10, 10, 10), x0=300 - 30 * i, y0=100, fillcolor0="red", linewidth0=0
        )

    def visible(self, t):
        return q[self.i] is not None


class AnimateWaitText(sim.Animate):
    def __init__(self, i):
        self.i = i
        sim.Animate.__init__(self, text="", x0=300 - 30 * i, y0=100, textcolor0="white")

    def text(self, t):
        component_i = q[self.i]

        if component_i is None:
            return ""
        else:
            return component_i.name()


def do_animation():
    env.animation_parameters()
    for i in range(10):
        AnimateWaitSquare(i)
        AnimateWaitText(i)
    show_length = sim.Animate(text="", x0=330, y0=100, textcolor0="black", anchor="w")
    show_length.text = lambda t: "Length= " + str(len(q))


class Person(sim.Component):
    def process(self):
        self.enter(q)
        yield self.hold(15)
        self.leave(q)


env = sim.Environment(trace=True)

q = sim.Queue("q")
for i in range(15):
    Person(name="{:02d}".format(i), at=i)

do_animation()

env.run()

All animation initialization is in do_animation, where first 10 rectangle and text Animate objects are created. These are classes that are inherited from sim.Animate.

The AnimateWaitSquare defines a red rectangle at a specific position in the sim.Animate.__init__() call. Note that normally these squares should be displayed. But, here we have overridden the visible method. If there is no i-th component in the q, the square will be made invisible. Otherwise, it is visible.

The AnimateWaitText is more or less defined in a similar way. It defines a text in white at a specific position. Only the text method is overridden and will return the name of the i-th component in the queue, if any. Otherwise the null string will be returned.

The length of the queue q could be defined also by subclassing sim.Animate, but here we just make a direct instance of Animate with the null string as the text to be displayed. And then we immediately override the text method with a lambda function. Note that in this case, self is not available!

Using colours

When a colour has to be specified in one of the animation methods, salabim offers a choice of specification:

  • #rrggbb rr, gg, bb in hex, alpha=255
  • #rrggbbaa rr, gg, bb, aa in hex, alpha=aa
  • (r, g, b) r, g, b in 0-255, alpha=255
  • (r, g, b, a) r, g, b in 0-255, alpha=a
  • “fg” current foreground color
  • “bg” current background color
  • colorname alpha=255
  • colorname, a alpha=a

The colornames are defined as follows:

_images/Colornames.png

This output can be generated with the following program:

# Show colornames

import salabim as sim

env = sim.Environment()
names = sorted(sim.colornames().keys())
env.animation_parameters(modelname="show colornames", background_color="20%gray")
x = 10
y = env.height() - 110
sx = 165
sy = 21

for name in names:
    sim.Animate(rectangle0=(x, y, x + sx, y + sy), fillcolor0=name)
    sim.Animate(
        text=(name, "<null string>")[name == ""],
        x0=x + sx / 2,
        y0=y + sy / 2,
        anchor="c",
        textcolor0=("black", "white")[env.is_dark(name)],
        fontsize0=15,
    )
    x += sx + 4
    if x + sx > 1024:
        y -= sy + 4
        x = 10

env.run()

Video production and snapshots

An animation can be recorded as an .mp4 video by sprecifying video=filename in the call to animation_parameters. The effect is that 30 time per second (scaled animation time) a frame is written. In this case, the animation does not run synchronized with the wall clock anymore. Depending on the complexity of the animation, the simulation might run faster of slower than real time. Other than with an ordinary animation, frames are never skipped.

Once control is given back to main, the .mp4 file is closed.

Salabim also suppports taking a snapshot of an animated screen with Environment.snapshot().