Anonymous functions

In the previous tutorial, we created module, PyParticles, that allowed to us to recreate our particle simulation relatively easily. However, before we can use this module to create a range of different simulations, we need to be able to choose which behaviours the particles exhibit. In this tutorial, we will:

  • Introduce lambda for creating anonymous functions
  • Give the Environment class a dictionary of functions we can choose from

Like the previous tutorial, this one's not specific for Pygame. It introduces Python's anonymous functions, which are quite an advanced programming technique. It took a while to work out how and when to use them, but I think we now have a prefect example of how they can be useful. As usual, the final code is at the bottom of this post.

Particle functions

At present the Particle's move() function is responsible for changes in velocity due to gravity (or not if you commented it out) and drag. In the next tutorial, we'll model a cloud of gas in space, so we don't want to include drag or a gravitational force pulling all particles in the same direction (nor do we want the particle to bounce off the boundary of the screen). We therefore need to make these functions independent. To start with, move the code controlling gravity out of the move() function into a separate accelerate() function within the Particle class:

def accelerate(self, vector):
    (self.angle, self.speed) = addVectors((self.angle, self.speed), vector)

We don't necessarily need to move drag() into a separate function because by setting the mass_of_air variable to 0, drag becomes 1, so there's no reduction in speed. However, it's inefficient to multiply each particle's speed by 1 every tick of the simulation; it would be better if we could ignore drag unless specified otherwise. If we create a separate drag() function, then we could rewrite the Environment update() method like so:

def update(self):
    for particle in self.particles:
        particle.move()
        if self.mass_of_air != 0:
            particle.experienceDrag()
        if self.acceleration:
            particle.accelerate(self.acceleration)
        if self.hasBoundaries:
            self.bounce(particle)

This update() method tests whether to we should bother calculating drag. It also test whether to accelerate particles due to some constant force, such as gravity, which would be set as a vector belonging to the Environment class called self.acceleration. Finally it tests whether the particles should bounce off the walls or continue off into space. This is determined by a boolean (i.e. true or false), self.hasBoundaries.

The problem is that now we testing whether we should call these various functions for each of the particles, every tick of the simulation. If we have 100 particles, then that's 300 if statements every tick! We could improve the efficiency somewhat (to 3 if statements per tick) by putting each of the calls in a separate loop, with the test outside, like so:

for particle in self.particles:
    particle.move()

if self.mass_of_air != 0:
    for particle in self.particles:
        particle.experienceDrag()

if self.acceleration:
    for particle in self.particles:
        particle.accelerate(self.acceleration)

if self.hasBoundaries:
    for particle in self.particles:
        self.bounce(particle)

However, we still have to carry out each of the tests each tick of the simulation and the code is just inelegant. Ideally we should only have to carry out the test once at the start of the simulation.

Variable functions

My solution to this problem (and others probably exist), is to give the Environment object a list of the particle functions we want to use (move, drag, bounce etc.), and call each of them for each particle:

for particle in self.particles:
    for f in self.particle_functions:
        f(particle)

But how do we get our functions into a list? We can't just type:

self.particle_functions = [particle.experienceDrag, self.bounce(particle)]

We want to define the function list at the beginning of the simulation, before we've even defined particle. Even if we had defined the Environment object's list of particles, we need some way to refer each specific particle object. Furthermore, if we add self.bounce(particle)to the list, what we actual add is the result of calling self.bounce(particle), which is None because the function doesn't return anything.

The solution is to use the lamdba function to create anonymous functions. For convenience, I put the functions in a dictionary, so they can be referred to easily. To the Environment class's __init__() function, add:

self.particle_functions = []
self.function_dict = {
'move': lambda p: p.move(),
'drag': lambda p: p.experienceDrag(),
'bounce': lambda p: self.bounce(p),
'accelerate': lambda p: p.accelerate(self.acceleration)}

Lambda allows us to create a single line function without a name. Just as with normal functions they can take a parameter, which is given before the colon. For example, if we now type:

self.function_dict['move'](self.particles[0])

We will call lambda p: p.move() with the first particle in the Environment object's particle as the parameter. This will take the particle object (referred to as p in the lambda function) and call its move() function.

We can now give the Environment class a function to add specific functions to its particle_functions list.

def addFunctions(self, function_list):
    for f in function_list:
        if f in self.function_dict:
            self.particle_functions.append(self.function_dict[f])
        else:
            print "No such function: %s" % f

Now, when we start a new simulation, it's incredibly simple to define which functions to include:

env = PyParticles.Environment((width, height))
env.addFunctions(['move', 'accelerate', 'drag'])
env.acceleration = (math.pi, 0.002)

When the Environment's update() function is called, it will now call the move, accelerate and drag functions for each of its particles.

Two-particle functions

Sadly, the situation is not quite so simple, because we should also like to define which two-particle functions are called. For example, we might not want the particles to collide and in the next tutorial we'll want to add an attract() function which will take two particles as parameters. The following adaptation is required:

self.particle_functions1 = []
self.particle_functions2 = []
self.function_dict = {
'move': (1, lambda p: p.move()),
'drag': (1, lambda p: p.experienceDrag()),
'bounce': (1, lambda p: self.bounce(p)),
'accelerate': (1, lambda p: p.accelerate(self.acceleration)),
'collide': (2, lambda p1, p2: collide(p1, p2))}
        
def addFunctions(self, function_list):
    for func in function_list:
        (n, f) = self.function_dict.get(func, (-1, None))
        if n == 1:
            self.particle_functions1.append(f)
        elif n == 2:
            self.particle_functions2.append(f)
        else:
            print "No such function: %s" % func

Now the function dictionary values are a tuple that indicates whether the function takes 1 or 2 parameters, which we use to determine which of two function lists to add the function to. The Environment update() function should now be changed to:

def update(self):
    for i, particle in enumerate(self.particles):
        for f in self.particle_functions1:
            f(particle)
        for particle2 in self.particles[i+1:]:
            for f in self.particle_functions2:
                f(particle, particle2)

Now the collide() function can be included or ignored in the same way as for the other particle functions. The following tutorial will have an example of how our updated PyParticles module can be used.

Final note on efficiency

Note, our simulation still isn't as efficient as it could be since if we want to move the particles and make them experience drag, then we must make two function calls instead of one as before. Furthermore, we carry out the nested loop in the update() function regardless of whether we include any two-particle functions. This is a limitation of writing flexible code. If processing speed becomes an issue, then it's best to write a custom program. However, the PyParticles module is a good place to start prototyping a simulation and testing parameters; you can start weeding out further inefficiencies once you've got an interesting simulation running. PyParticles should be efficient enough to get you started.

These last two tutorial may have seemed like a lot of work to essentially get back where we started (and if you made it this far then I'm seriously impressed), but hopefully the following tutorials will demonstrate just how flexible our code now is. We can now use our moduel to create a range of apparently very different programs with relatively little effort. We'll start in the next tutorial, by making a simulation of a gas cloud collasping under its own gravity to form a solar system.

AttachmentSize
PyParticles2.txt5.89 KB

Comments

The signature of the findParticle function changed here from previous version from sepearate x and y parameters to a (x, y) tuple.  Not a big deal, but I would probably just update here or previous to just make it consistent for all remaining parts of tutorial.

Thanks - I've updated it.

I belive that the update function can have a better efficienfy if you swap these two lines:

for f in self.particle_functions2:       

for particle2 in self.particles[i+1:]:

 f(particle, particle2)

 

On the way it was before:

You are simulating N particles.

The number of comparsions was (only on the second half of the update for loop):

(sum from i to n, of (n - i)) * (n - 1)

Bottom line, You are testing every i+1 particle, if there is a function of 2 parameters to be executed. The simulation might be running without the collide function, and still, you are testing every tick, if the collide function is on the list or not.

By switching those two lines:

If there is no fuction that requires 2 parameters, you won't go into the second for loop (that goes through all i+1 particles).

Bottom line:

If there is a function with 2 parameters. Execute this function in all i+1 partcles.

If there isn't, move on, no need to test with all i+1 particles with there is something to be executed or not.

 

Correct me if I'm wrong, please.

Nikolas

 

 

Great write up and descriptions, most appreciated thank you.

For beginners, it might be clearer to highlight that using lamba isn't required per se, for storing functions in the dictionary, one can equally store named functions (or objects or class names).

eg. A menu example, which could be adapted 

    to a KEY input "call table",    see Wikipedia

    menu = {

        '1': (print_all, "Print Everyone"),

        '2': (print_one, "Print One person"),

        '3': (add_one, "Add Person"),

        '4': (filter_byrole2, "Filter by Role"),

        'q': (quit, "Quit"),

        }

 

    for k in sorted(menu.keys()): # will not be in key order !

        print k, menu[k][1]

 

    choice = raw_input('Select Item: ')

 

    if choice in menu:

        menu[choice][0]()

 

The reasons for using the lamba function are  :

Sometimes, you want to call a function on the object p.function_name()

othertimes you call a class function with the object as a parameter self.function_name(p) and with varying number of parameters.

 

More importantly though, I would attempt to avoid the one/two parameter 

issue in the first place.

 

By moving the collide() function into the Particle class, then call

 

    particle1.collide(particle2)

 

instead of

 

    collide(particle1, particle2)

 

A side effect of the two loop implimentation, is that the one parameter 

functions all get called before the two parameter functions.

 

ie maybe I want the order int the list to be significant ; Here's an idea :

 

particles = ['p1','p2','p3','p4','p5','p6']

# dict of funcs with number of params

dict_of_funcs = {

    'fa':(fa,1),

    'fb':(fb,1),

    'fc':(fc,2),

    'fd':(fd,2),

    'fe':(fe,1),

    'ff':(ff,1), }

 

ordered_funcs=[

    dict_of_funcs['fa'],

    dict_of_funcs['fd'],

    dict_of_funcs['fb'],

    dict_of_funcs['fc'],

    ]

 

for n,p in enumerate(particles):

    for fref, nparams in ordered_funcs:

        if (nparams == 1):

            fref(p)

        else:

            map(lambda q: fref(q, p), particles[:n])

 

Thanks for the thought provoking examples, I've  learned alot from studying them.

 

Very useful tutorial. 

I was looking for something to lean how to use pygame and classes, and this pretty much does the trick. 

There are some bugs I haven't been able to track down and fix though. I was hoping something has. At tutorial 10, the select and drag function works. Then in tutorial 11, and 14 this stops working. Clicking anywhere in the window (be it particle or background) the window actually closes. 

Any hints?

Post new comment

The content of this field is kept private and will not be shown publicly.