Braitenberg Vehicles - PI day
My previous two commits say “Jitter is now probability based” and “Improve behaveJitter formatting”. Not much exciting there.
Before we look at the code, let me tell you some of the things I’ve been imagining for this little project:
- More “analog” style: Braitenberg’s articles think of the little guys as wired up in an analog style, with larger or smaller signals going to the wheels. My guys get numbers added and subtracted but it feels more digital to me.
- Connections mode: Braitenberg shows wiring diagrams and response curves. My program’s behaviors are written in Codea. Maybe there could be a wiring mode where different signal generators could be hooked together. That would be challenging at a few levels, I bet.
- Autonomous Vehicles: Chet and I are talking today about our deliver:Agile talk, and one possible topic is that teams don’t know the design of their systems. That lead us to think about the extent to which these little bugs would make a sensible architecture for autonomous vehicles.
Looking at the code … this always happens … I see some room for improvement, so I quickly do it.
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
No reason to do this in two steps, so let’s have this:
function BVehicle1:behaveSeekFood()
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 vec2(lw, rw)
end
And then this:
function BVehicle1:behaveSeekFood()
local food = vec2(food.x, food.y)
local distL = food:dist(self.eyeL)
local distR = food:dist(self.eyeR)
if distR > distL then
return vec2(0,1)
else
return vec2(1,0)
end
end
This is surely simpler. It’s worth nothing that this behavior will surely change in the future: there will probably be multiple occurrences of food, for example. That, we can foresee, will cause us to break out this function into one that somehow selects a food source to approach, and one, much like this one, that steers toward it. I could have decided to leave things as they are, because “we’re going to need” the breakout. I choose to leave things as simple as I can see to do, so long as they work.
This works: commit the code.
I found a function, computeWheelIncrement
, that wasn’t used. Deleted it. Commit.
This is just nasty:
function BVehicle1:avoidWall()
local minDist = 50
local wheels = vec2(0,0)
local distLeftEye, distRightEye
local adjLeft, adjRight
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(1000, self.position.y))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 0))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 1000))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
return wheels.x, wheels.y
end
Look at all that duplication! What we’re doing is getting the scaled power of each wall against each of our eyes, and adding it all up. We can extract some functions here. Let’s see. We go in with a vec2 consisting of the perpendicular distance from each of our eyes to a wall. We compute a vec2 adjustment to our wheels, and cumulate those adjustments. And it happens four times, which makes me at least imagine a loop wanting to show up. Let’s see about extracting a function to do the job. I’ll call it wallAdjustment for now. First pass:
function BVehicle1:wallAdjustment(point)
local minDist = 50
local distLeftEye, distRightEye = self:eyeDistancesFrom(point)
return vec2(-minDist/distRightEye, -minDist/distLeftEye)
end
function BVehicle1:avoidWall()
local minDist = 50
local wheels = vec2(0,0)
local distLeftEye, distRightEye
local adjLeft, adjRight
wheels = wheels + self:wallAdjustment(vec2(1000,self.position.y))
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 0))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 1000))
adjLeft = -minDist/distRightEye
adjRight = -minDist/distLeftEye
wheels = wheels + vec2(adjLeft, adjRight)
return wheels.x, wheels.y
end
Works. Commit. Then this:
function BVehicle1:wallAdjustment(point)
local minDist = 50
local distLeftEye, distRightEye = self:eyeDistancesFrom(point)
return vec2(-minDist/distRightEye, -minDist/distLeftEye)
end
function BVehicle1:avoidWall()
local wheels = vec2(0,0)
wheels = wheels + self:wallAdjustment(vec2(1000,self.position.y))
wheels = wheels + self:wallAdjustment(vec2(0,self.position.y))
wheels = wheels + self:wallAdjustment(vec2(self.position.x,0))
wheels = wheels + self:wallAdjustment(vec2(self.position.x,1000))
return wheels.x, wheels.y
end
That’s nicer … how about we try to get rid of that local and all the storing into it, and maybe just return a vector? The behaveWall
bit looks like this:
function BVehicle1:behaveAvoidWall()
return vec2(self:avoidWall())
end
I’m starting to think I refactored these behaviors too far. I wonder why. Anyway, let’s make this work, removing the cast here and putting it into the other method:
function BVehicle1:behaveAvoidWall()
return self:avoidWall()
end
function BVehicle1:avoidWall()
return self:wallAdjustment(vec2(1000,self.position.y))
+ self:wallAdjustment(vec2(0,self.position.y))
+ self:wallAdjustment(vec2(self.position.x,0))
+ self:wallAdjustment(vec2(self.position.x,1000))
end
Commit. Then roll avoid up into behave …
function BVehicle1:behaveAvoidWall()
return self:wallAdjustment(vec2(1000,self.position.y))
+ self:wallAdjustment(vec2(0,self.position.y))
+ self:wallAdjustment(vec2(self.position.x,0))
+ self:wallAdjustment(vec2(self.position.x,1000))
end
So this is nice and compact. Parts of me thinks “It’s less clear” but I think what I’m really saying is that with the code smushed down this far, it’s more visible that it was never clear. When it’s complicated, we allow lack of clarity because, well, it’s complicated. Now we have just a few lines so it’s more clear that it’s not clear enough. Right now, I don’t see how I’d improve it.
One possible confusion is that the behave
methods all return a vec2
adjustment to wheels, and that’s not expressed anywhere. For now, I’ve no real idea how to make it better.
Commit. Here’s the whole BVehicle1 class:
BVehicle1 = class()
function BVehicle1:init(behaviors, x,y, angle)
self.behaviors = behaviors
self.position = vec2(x,y)
self.angle = angle or 0
self.width = 10
end
function BVehicle1:partial(f, ...)
local a = {...}
return function(s)
return f(s,unpack(a))
end
end
function BVehicle1:behaveDefaultMotion(m1,m2)
return vec2(m1, m2)
end
function BVehicle1:behaveJitter(probability)
if math.random() >= probability then
return vec2(0,0)
else
return vec2(math.random()*10 - 5, math.random()*10 - 5)
end
end
function BVehicle1:behaveSeekFood()
local food = vec2(food.x, food.y)
local distL = food:dist(self.eyeL)
local distR = food:dist(self.eyeR)
if distR > distL then
return vec2(0,1)
else
return vec2(1,0)
end
end
function BVehicle1:behaveAvoidWall()
return self:wallAdjustment(vec2(1000,self.position.y))
+ self:wallAdjustment(vec2(0,self.position.y))
+ self:wallAdjustment(vec2(self.position.x,0))
+ self:wallAdjustment(vec2(self.position.x,1000))
end
function BVehicle1:calcAngle(wheels)
return math.atan(wheels.y-wheels.x, self.width)
end
function BVehicle1:determinePosition(wheels)
self.angle = self.angle + math.deg(self:calcAngle(wheels))
local step = vec2((wheels.x+wheels.y)/2,0)
local move = step:rotate(math.rad(self.angle))
self.position = self.position + move
end
function BVehicle1:wallAdjustment(point)
local minDist = 50
local distLeftEye, distRightEye = self:eyeDistancesFrom(point)
return vec2(-minDist/distRightEye, -minDist/distLeftEye)
end
function BVehicle1:eyeDistancesFrom(aPoint)
return self:eyeDistanceLimited(self.eyeL, aPoint, 100), self:eyeDistanceLimited(self.eyeR, aPoint, 100)
end
function BVehicle1:eyeDistanceLimited(eye, aPoint, limit)
local d = eye:dist(aPoint)
if d > limit then return 10000 end
return d
end
function BVehicle1:keepOnPage()
self.position.x = math.fmod(self.position.x + 1000, 1000)
self.position.y = math.fmod(self.position.y + 1000, 1000)
end
function BVehicle1:locateEyes()
local eyeR = vec2(20,0)
local eyeL = vec2(20,10)
self.eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
self.eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
end
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
function BVehicle1:draw()
pushStyle()
pushMatrix()
rectMode(CENTER)
stroke(255,255,0)
fill(255,255,0)
self:keepOnPage()
translate(self.position:unpack())
rotate(self.angle)
rect(0, 0, 20, 10)
local eyeSize = 8
local eyeR = vec2(10,-5) — changed
local eyeL = vec2(10,5) — changed
stroke(255,0,0)
fill(255,0,0)
ellipse(eyeR.x, eyeR.y, eyeSize)
ellipse(eyeL.x, eyeL.y, eyeSize)
popMatrix()
popStyle()
end
All this, including writing the article to this point, took about an hour, so it’s not like I’m wasting my life here. But I do think it’s time to work on some new behavior. Of the ideas above, two are quite grand, wiring diagrams and the application of these guys to autonomous vehicles in the real world. One idea was to consider whether the workings of our wheels are sufficiently analog, and I think they are. We add and subtract some pretty abstract values to the wheels, pushing them forward or back. If we think of those values as voltages, that’s fine. So I’m going to let that ride. Now let’s watch the guys run, and see what we might like to try next: