Braitenberg Vehicles - 8
As I look at the current code, after over a month away from it, I’m wondering whether the behaviors should be, as they are now, methods on the vehicle class, BVehicle1, or whether they should be more free-standing functions. If they were, where would we keep them? Perhaps in another class called Behavior? Perhaps just in a table indexed by the behavior name?
I lean toward the former, but that could be my long OO background talking. And maybe there’s some approach that I haven’t even considered.
With some experimentation, I learned a few things. First, some better names for the behavior sets, and for the vehicles list (rename objects to vehicles, the code got a little clearer.
However, there’s this anomaly, for example
b1 = {}
table.insert(b1, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1))
table.insert(b1, BVehicle1.behaveJitter)
table.insert(b1, BVehicle1.behaveAvoidWall)
Note that we init the behaveDefaultMotion
behavior with partial
, and not the other two. Furthermore, trying to use partial with those causes errors that look as if the function, such as behaveJitter
, no longer has the right value for self
. That tells me that I don’t really understand my partial binding trick code as well as I thought.
So we’ll revisit that for next time. Meanwhile, I’ve reverted the code, making today’s experiment vanish as if it had never been.
Thursday
Well, I’ve done the tax prep work I needed to do this morning, so let’s take a look at the partial function stuff.
The basic idea, as I recall it from over a month ago, is that each vehicle has a list of various “behaviors” that control what it does. So ideas like “seek food” or “avoid walls” are behaviors. A behavior might in principle do anything but in practice what they do now is adjust the speed of rotation of one or both of the wheels of the vehicle. When we update a vehicle before drawing it, we use this code:
function BVehicle1:update()
self:locateEyes()
local wheels = vec2(0,0)
for i,b in pairs(self.behaviors) do
wheels = wheels + b(self)
end
self:determinePosition(wheels)
end
The update
method runs each behavior, which is expected to return a vec2
that is added into wheels
, which is added to the current position to figure out the new position.
So each behavior is expected to return a vec2
. Here’s a typical one:
function BVehicle1:behaveSeekFood()
return vec2(self:seekFood())
end
function BVehicle1:seekFood()
local lw = 0
local rw = 0
local food = vec2(food.x, food.y)
local distL = food:dist(self.eyeL)
local distR = food:dist(self.eyeR)
local adj
if distR > distL then
rw = 1
else
lw = 1
end
return lw, rw
end
You might ask why that’s divided up that way, and the answer is history. It probably shouldn’t be, and that can be on our list of things to fix. Be that as it may, all the behaveSeekFood
does is add one to the wheel that will turn the vehicle toward sensed food. You can see in the videos that the vehicle does get there. It jitters around, however, because there’s also a behaveJitter
function. It sets a random addendum into the wheels:
function BVehicle1:behaveJitter()
self.jitterCount = self.jitterCount + 1
if math.fmod(self.jitterCount, 10) == 0 then
return vec2(math.random()*10 - 5, math.random()*10 - 5)
else
return vec2(0,0)
end
end
So that’s all good. But there’s more. I wanted to give each vehicle a default movement amount, and I wanted that to be different for each wheel. That’s one way of getting a vehicle to go around in a circle. So I wanted the behavior for default motion to take two parameters, and set those into the wheels:
function BVehicle1:behaveDefaultMotion(m1,m2)
return vec2(m1, m2)
end
The question was, given that behaveDefaultMotion
is a method, how could I set it up so that I could provide the parameters at vehicle definition time, and have them remembered and used at run time? My setup looks like this:
— define behaviors
bAvoidWalls = {}
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1))
table.insert(bAvoidWalls, BVehicle1.behaveJitter)
table.insert(bAvoidWalls, BVehicle1.behaveAvoidWall)
bSeekFood = {}
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
table.insert(bSeekFood, BVehicle1.behaveJitter)
table.insert(bSeekFood, BVehicle1.behaveSeekFood)
Here I’m defining two behavior tables, containing pointers to methods (functions) that the Vehicle will understand. Note that the last two lines there say BVehicle1 DOT methodName
, not COLON
. So BVehicle.behaveJitter
is the function pointer itself, not a call. In the update method shown above we say b(self)
, which calls that function/method.
The tables are used in creating vehicles, like this:
table.insert(objects, BVehicle1(bAvoidWalls, 400,200,0))
table.insert(objects, BVehicle1(bSeekFood, 400,400, 0))
Vehicle creation expects a behavior table, x and y coordinates, and a rotation:
function BVehicle1:init(behaviors, x,y, angle)
self.behaviors = behaviors
self.position = vec2(x,y)
self.angle = angle or 0
self.width = 10
self.jitterCount = 0
end
For this to work, the behaveDefaultMotion
in the table can’t be a raw call to that function, because we have to bind in the parameters, 1 and 1, or 1 and 1.1, in our two cases. So we want, not a function pointer to behaveDefaultMotion
, but instead a pointer to a function that will call behaveDefaultMotion
with the two parameters, as needed. This is called, variously, a “partial application” of the function, or a “closure”. We take a multi-parameter function and fix the values of some of its parameters, and return that as a new function. We’re not fixing all the parameters, because each vehicle method expects self
as its first parameter. This is automatically provided when you use object:method(blah)
, and we provide self’ explicitly in our
update method, when we call with
b(self)`.
To deal with this case, as shown in the setup above, I use the method partial
, which looks like this:
function BVehicle1:partial(f, arg1, arg2)
return function(...)
return f(self, arg1, arg2,...)
end
end
So partial
accepts two arguments arg1
and arg2
, and returns a new function with arbitrary parameters, which calls the provided function (behaveDefaultMotion
in our case), providing as its parameters, first the two arguments provided, and then all the rest.
The miraculous thing is that this works. If you don’t quite understand it, don’t feel bad: neither do I. In fact, I think it is wrong. Note that we return a function that will call the provided one with self, arg1, arg2, ...
. What’s self here? It’s whatever it was when we called
partial`! But we wrote this:
BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1))
So at that moment, self
is surely the class BVehicle1, isn’t it? So as it happens, our default motion method doesn’t reference self
, but if it did, I think it wouldn’t be our specific vehicle at all.
I’m going to test that. I do it by printing BVehicle1
at the beginning, and self
plus the two parameters inside behaveDefaultMotion
. Sure enough, behaveDefaultMotion1 prints the same table name every time, the class table
BVehicle1`. I try to fix that this way:
function BVehicle1:partial(f, arg1, arg2)
return function(s, ...)
return f(s, arg1, arg2,...)
end
end
That works. We know that when we call the function in update
, we’ll call it with the correct self
, which will bind to s in the inner function and be used as self
in the actual call. So that’s better.
Are you confused? I am, though a bit less so, and I’m right here and I wrote this. And we aren’t even to the concern I really had, which was “Why don’t we always use partial
, instead of just when the behavior method expects parameters.
Now one more thing comes to mind: in the partial
above, I’m assuming that there are additional parameters that are going to be passed to the newly created function. But there aren’t: by design we only pass in the appropriate self
object. So let’s see if we can remove the ...
and make it a bit simpler.
function BVehicle1:partial(f, arg1, arg2)
return function(s)
return f(s, arg1, arg2)
end
end
That continues to work and makes a bit more sense to me. Now I wonder if we can’t use … to allow partial to accept any number of arg, not just two.
After some tries and some googling, I found that this works:
function BVehicle1:partial(f, ...)
local a = {...}
return function(s)
return f(s,unpack(a))
end
end
Putting the ...
directly where unpack(a)
is now wouldn’t compile, with no useful error message. Saving the parameters as a table and then unpacking the table back into the call to f
works, and our little guys are still getting the right parameters. I was briefly concerned that they might all get the same
a, but they don’t. The reason, of course, is that on every call to
partial we get a new
a` and bind it into the new function that we create.
So this is an improvement, though I have no use at present for allowing other than two arguments, and that was a violation of YAGNI. Still, I’ve learned something and improved the code a bit in, I hope, both clarity and function. That will do for now.
Friday
Since my new partial
seems to work correctly and to be more likely to actually be right, I tried using it in all the setup operations:
function setup()
print(“Braitenberg Vehicle Experiment”)
objects = {}
— define behaviors
bAvoidWalls = {}
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1))
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveJitter))
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveAvoidWall))
bSeekFood = {}
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveJitter))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveSeekFood))
— define vehicles
table.insert(objects, BVehicle1(bAvoidWalls, 600,400,0))
table.insert(objects, BVehicle1(bAvoidWalls, 400,600,0))
table.insert(objects, BVehicle1(bAvoidWalls, 800,400,0))
table.insert(objects, BVehicle1(bAvoidWalls, 400,800,0))
table.insert(objects, BVehicle1(bAvoidWalls, 100,500,0))
table.insert(objects, BVehicle1(bAvoidWalls, 200,700,0))
table.insert(objects, BVehicle1(bAvoidWalls, 300,300,0))
table.insert(objects, BVehicle1(bAvoidWalls, 400,200,0))
table.insert(objects, BVehicle1(bSeekFood, 400,400, 0))
food = Food(300,900)
end
This does seem to work correctly throughout. Now, we have some very nice duplication to remove, left for next time.
Tune in then!