'''
Created on Apr 2, 2015
First Sprint: 4-2-15 to 4-15-15
@author: Brett Paufler
Copyright Brett Paufler (c) 2015
All Rights Reserved
NOTE: If this is pulled from my website, it's saved as a .txt file, because...
Well, it's not worth dealing with the headaches of loading a .py to my server.
Change it to a .py file in notepad++ and it should work.
Or copy paste to your favorite IDE:
that's what I would do,
whenever I find interesting code on the web...
Creates a Pixalated Gif animation of Boids in 'Flight'
run_multi_boids_switchboard()
(located at bottom of file) is the intended control function
class Boid
feeds to class Flock
feeds to class Create_Image
which runs from run_multi_boids_switchboard()
(or in reverse)
run_multi_boids_switchboard()
loads class Create_Image
which loads class Flock
which Loads class Boid
to make a GIF image file
# # # SO, IF SEEKING UNDERSTANDING, # # #
# # # START AT THE BOTTOM # # #
# # # AND WORK YOUR WAY UP # # #
Most external calls should be reasonable self explanatory, but...
numpy arrays are used through for ease of vector math
b1.pos = b2.pos - b3.pos, being essentially equal to
np.array([5.0, 3.0]) = np.array([10.0, 10.0]) - np.array([5.0, 7.0])
np.linalg.norm(a) returns scalar length of a vector
so, np.linalg.norm(a-b) is the distance between the two points
(using the two points as a coordinate system)
whereas, a-b, returns a vector:
the vector that when added to a will get one to b
The following (in some degree of psuedo-code) creates the gif
with imgList being a valid numpy image array,
clip = mpy.ImageSequenceClip(imgList, fps=5)
clip.write_gif('saveGifAs.gif')
Craig Reynold's: Boid (The Algorithm)
Rule #1: Boids Seek Each Other (Boids flock towards their center of gravity)
Rule #2: Boids Seek Same Heading (Boids tend to match velocity of nearby Boids)
Rule #3: Boids Avoid Collision (Boids tend to fly away from nearby Boids)
The exact implementation is a thing of art and esthetic beauty,
meaning it resides in the eye of the beholder
or the hand of the coder
Beyond Boids (tricking it out):
FLOOR: there is a ground, beyond which, boids may not fall (they land, instead)
GOAL: arbitrary positional goals (like Rule #1, but centered in space)
NOT IMPLEMENTED: but this could be another flock of birds (Hawks & Doves)
WIND: arbitrary vector inputs (like Rule #2, but geographic in nature)
Once again, this is NOT IMPLEMENTED
Limitations of Implementation:
limited to 2D
Boids represented as dot-pixels, only
TODO:
As noted above:
WIND
HAWKS & DOVES
TOO_DAMN_CLOSE: (if boid_one.pos == boid_two.pos and vel == vel,
boids will never separate (random, veer off, to implement)
I am going on vacation and taking the summer off,
so any of that will have to await my return,
and continued interest at some later date...
Brett Paufler, 4-18-15
Though in some sense the following was coded from the top down,
I have a hunch it may be easier to understand from the bottom up...
'''
import math
import copy
import random
import numpy as np
import moviepy.editor as mpy
class Boid():
'''A Boid instance (being very much like a gameplay object, I would imagine)
'''
def __init__(self, pos=[0.0,0.0]):
'''pos = np.array, current position
vel = np.array, current velocity
delta = np.array, current proposed change to velocity
floor = if 'floor' in mode and boid falls to bottom of image,
boid 'lands' and stays motionless for a random amount of time
Boids are called from Flock and the passed value for pos
is the center of the image (flock.size/2) by default
'''
self.pos = np.array(pos)
self.vel = np.array([0.0, 0.0])
self.delta = np.array([0.0, 0.0])
self.floor = 0
def __repr__(self):
'''Has code been adequately documented if the __repr__ doesn't contain a doc string?
Yes, my friends! These are matters of coding-importance!!!
'''
return "{Boid Instance: posx:%.2f, posy:%.2f, velx:%.2f, vely:%.2f}" % (
self.pos[0], self.pos[1], self.vel[0], self.vel[1])
def distance(self,b2):
'''scalar distance between two boids, self and b2
'''
dis = np.linalg.norm(self.pos - b2.pos)
if dis < 0.1: #avoids divide by zero problems elsewhere
dis = 0.1
return dis
def add_to_delta(self, vector, max_val):
'''increments delta in direction of vector up to max_val
'''
if np.linalg.norm(vector) > max_val:
vector = vector / np.linalg.norm(vector) * max_val
self.delta = self.delta + vector
def norm_delta(self, max_val=1.0):
'''trims delta to 1.0 or passed value,
the passed value is typically flock.max_delta
'''
if np.linalg.norm(self.delta) > max_val:
self.delta = self.delta / np.linalg.norm(self.delta) * max_val
def delta_to_vel(self, max_val):
'''applies delta to vel, clears delta, and reduces vel to max_val.
max_val is typically the passed value of flock.max_vel (max_velocity)
'''
self.vel += self.delta
self.delta = np.array([0.0,0.0])
def floor_decrement(self):
'''counts down the floor marker
and voids out all other motion in the interim
'floor' is a mode option that sets a lower bound on boid movement
'''
if self.floor > 0:
self.floor -= 1
self.delta = np.array([0.0,0.0])
self.vel = np.array([0.0,0.0])
class Flock():
'''A Flock contains a group of Boids and controls the Motion Logic therewith
'''
def __init__(self, size, number=50, mode=['pos_cross' ],
goal=[], anti_goal=[], max_vel=5.0, max_acc=0.25):
'''Flock variables are typically passed in from a Create_Image instance
via the run_multi_boids_switchboard() function.
number: number of boids in this flock
mode: a list of special conditions
most control structures are of the form
if 'this_string' in flock.mode:
then do something
keyword_strings (starting positions):
'pos_rand': boids are given random positions
'pos_cross': boids align to create a centered cross
'pos_ver_line': boids form an evenly spaced vertical line
'pos_hor_line': boids form an evenly spaced horizontal line
'pos_checker': boids form a grid pattern (left over in the center)
keyword strings (starting velocities):
vel_rand: starting velocity is random from -max to +max
vel_rand_one: as above limted to -1.0 to 1.0
vel_up, vel_down, vel_right, vel_left: vel=flock.max_vel in indicated direction
vel_up_one, vel_down_one, vel_right_one, vel_left_one: vel at one, not max
keyword strings (special options)':
'floor': boids cannot progress beyond bottom of image size (they land)
'wind': TODO: probably will be a fluctuating random addition to pos
ex: mode = ['pos_cross', 'floor', 'vel_rand']
ex: mode = ['pos_checker', 'vel_up', 'vel_left']
goal: all boids in flock are attracted to this locale (default is [], none):
ex: goal =[np.array([25,25])]
ex: goal =[np.array([75,75]), np.array([25,75]), np.array([75,25])]
anti_goal = all boids in flock are repelled from this locale:
format is the same as per goal
ex: anti_goal = [np.array([50,50])]
max_vel = is the maximum speed of any one boid relative to the static background
max_acc = is the maximum change in velocity/turn
max_delta = is derived from max_vel and max_acc
ex: if max_vel=5.0, max_acc=0.1, max_delta=0.5,
so, vel may shift by 0.5(max)/turnv
boids: is a list of boid objects, self.number long
'''
self.size = np.array(size)
self.number = number
self.mode = mode
self.goal = goal
self.anti_goal = anti_goal
self.max_vel = max_vel
self.max_acc = max_acc
self.max_delta = self.max_vel * self.max_acc
self.boids = [Boid(pos=self.size/2.0) for _ in range(self.number)]
print self
def __repr__(self):
'''Messy formatting, but it works. Feel free to clean up for me...
'''
a = "\nFLOCK\n"
b = "\tNumber: %s\n" % self.number
c = "\tmax_vel: %.2f,\t max_acc: %.2f, \t max_delta: %.2f \n" % (self.max_vel, self.max_acc, self.max_delta)
d = '\tgaol: %s\n' % self.goal
e = '\tanti_gaol: %s\n' % self.anti_goal
f = '\tmode: %s\n\n' % self.mode
return a + b + c +d + e + f
def start_pos(self):
'''changes the starting boid.pos (position) from np.array([self.size/2.0)]
(the center) to:
if key_word in self.mode (so, may be able to apply more than one):
'pos_rand': random over self.size
'pos_cross': centered cross (half vert line, half hor line)
'pos_ver_line': vertical line, centered horizontally
'pos_hor_line': horizon line, centered vertically
'pos_checker': a grid layout
'''
if 'pos_rand' in self.mode:
for b in self.boids:
for d in range(len(self.size)):
b.pos[d] = random.random() * self.size[d]
elif 'pos_cross' in self.mode:
hN = int(self.number)/2
vN = self.number - hN
hor = list(np.linspace(0, self.size[0],
hN, dtype=float)) + [self.size[0]/2.0]*vN
ver = [self.size[1]/2.0]*hN + list(np.linspace(0,
self.size[1], vN, dtype=float))
for b,h,v in zip(self.boids,hor,ver):
b.pos = np.array([h, v])
elif 'pos_ver_line' in self.mode:
ver = list(np.linspace(0, self.size[1], self.number, dtype=float))
for b,v in zip(self.boids,ver):
b.pos[0] = self.size[0]/2.0
b.pos[1] = v
elif 'pos_hor_line' in self.mode:
hor = list(np.linspace(0, self.size[0], self.number, dtype=float))
for b,h in zip(self.boids,hor):
b.pos[0] = h
b.pos[1] = self.size[1]/2.0
elif 'pos_checker' in self.mode:
s = int(math.floor(math.sqrt(self.number)))
x = self.size[0] / float(s)
y = self.size[1] / float(s)
hor = list(np.linspace(0+x, self.size[0]-x, s, dtype=float))
ver = list(np.linspace(0+y, self.size[1]-y, s, dtype=float))
c = 0
for h in hor:
for v in ver:
self.boids[c].pos[0] = h
self.boids[c].pos[1] = v
c += 1
def start_vel(self):
'''changes boid.vel (velocity) staring value from np.array([0.0, 0.0]) to:
if keyword_string in Boid.mode (so, can fire more than one):
vel_rand: starting velocity is random from -max to +max
vel_rand_one: as above limted to -1.0 to 1.0
vel_up, vel_down, vel_right, vel_left: vel=flock.max_vel in indicated direction
vel_up_one, vel_down_one, vel_right_one, vel_left_one: vel at one, not max
'''
for b in self.boids:
if 'vel_rand' in self.mode:
b.vel[0] += (random.random()*2.0 - 1.0) * self.max_vel
b.vel[1] += (random.random()*2.0 - 1.0) * self.max_vel
if 'vel_rand_one' in self.mode:
b.vel[0] += (random.random()*2.0 - 1.0)
b.vel[1] += (random.random()*2.0 - 1.0)
if 'vel_down' in self.mode:
b.vel[1] += self.max_vel
if 'vel_down_one' in self.mode:
b.vel[1] += 1
if 'vel_up' in self.mode:
b.vel[1] += - self.max_vel
if 'vel_up_one' in self.mode:
b.vel[1] += -1
if 'vel_right' in self.mode:
b.vel[0] += self.max_vel
if 'vel_right_one' in self.mode:
b.vel[0] += 1
if 'vel_left' in self.mode:
b.vel[0] += - self.max_vel
if 'vel_left_one' in self.mode:
b.vel[0] += -1
def vel_flock_pos(self):
'''distance weighted increment of self.delta,
based on POSITION of other boids in flock
(in essence, each boid tries to move toward the center of nearby boids)
Rule #1 Implementation
vel_flock_pos = + vector / distance
'''
for b1 in self.boids:
ave = np.array([0.0,0.0])
for b2 in (b2 for b2 in self.boids if b2 != b1):
ave -= (b1.pos - b2.pos) / b1.distance(b2)
b1.add_to_delta(ave, self.max_delta)
def vel_flock_vel(self):
'''distance weighted increment of self.delta,
based on VELOCITY of other boids in flock
(in essense, each boid tries to mimic the behavior of nearby boids)
Rule #2 Implementation
vel_flock_vel = + vector / distance**2
Note: pow(x,2)
'''
for b1 in self.boids:
ave = np.array([0.0,0.0])
for b2 in (b2 for b2 in self.boids if b2 != b1):
ave -= b2.vel / pow(b1.distance(b2),2)
b1.add_to_delta(ave, self.max_delta)
def vel_flock_self_avoidance(self):
'''distance weighted decrement of self.delta,
based on POSITION of other boids in flock
(in essence, each boid tries to avoid nearby boids)
Rule #3 Implementation
vel_flock_self_avoidance = - vector / distance**3
Note: the minus
Note: pow(x,3)
'''
ave = np.array([0.0,0.0])
for b1 in self.boids:
for b2 in (b2 for b2 in self.boids if b2 != b1):
ave += (b1.pos - b2.pos) / pow(b1.distance(b2), 3)
b1.add_to_delta(ave, self.max_delta)
def vel_flock_too_damn_close(self):
'''if two boids line up on both pos and vel (pos1=pos2 & vel1=vel2),
the boids will visually converge and flow together (disappear into one)
In order to force the two boids apart, need some sort of function here
'''
pass
#TODO: NOT IMPLEMENTED
#NEED TO ADD TOO DAMN CLOSE
#Thinking something like
#self.add_to_delta(random in range (-self.max_delta to + self.max_delta)
def vel_goal(self):
'''a fixed target version of vel_flock_pos
boids move toward goal
boids move away from anti_goal
TODO: HAWK and DOVE implementation,
flocks which have other flocks as their goal
'''
for b in self.boids:
ave = np.array([0.0,0.0])
for g in self.goal:
g = Boid(pos=g)
ave -= (b.pos - g.pos) / b.distance(g)
for g in self.anti_goal:
g = Boid(pos=g)
ave += (b.pos - g.pos) / b.distance(g)
b.add_to_delta(ave, self.max_delta)
def apply_deltas(self):
'''delta reduced to max_val, delta added to vel, delta reset to zero
'''
for b in self.boids:
b.norm_delta(max_val=self.max_delta)
b.delta_to_vel(max_val=self.max_vel)
def move(self):
'''moves a turn, increments the flock state by one turn
'''
#Base Craig Reynold's Boid (Delta Increment)
self.vel_flock_pos()
self.vel_flock_vel()
self.vel_flock_self_avoidance()
#Advanced Options: Goals and Floor (Delta Increment)
if self.goal or self.anti_goal:
self.vel_goal()
if 'floor' in self.mode:
for b in self.boids:
b.floor_decrement()
#vel+=delta, pos+=vel, delta=0
self.apply_deltas()
for b in self.boids:
b.pos += b.vel
#The FLoor Check
#Yeah, this could stand to be refactored.
#But have I mentioned my upcoming vacation, yet?
if 'floor' in self.mode:
for b in self.boids:
if b.pos[1] > self.size[1] - 1:
b.pos[1] = self.size[1] - 1
b.vel = np.array([0.0, 0.0])
b.floor = random.randint(1,10)
def flock_to_img(self):
'''returns an img array,
Boids are signified by a value of 255,
which will be converted to an image dot by mpy write_gif
Boids that are 'out of bounds' boids are ignored
Note: In my naming convention:
img: stands for an img like array
image: stands for an actual image file
So, clearly, we're returning an img array
'''
img = np.zeros((self.size[1], self.size[0]), dtype=int)
for b in self.boids:
x = math.floor(b.pos[0])
y = math.floor(b.pos[1])
if (x>= 0) & (x= 0) & (y Flock > Boid
Create_Image will call Flock, which will call Boid
run_multi_boids_switchboard() is the hook
Oh, and just as a bye-the-bye, as a work flow,
I got an image to screen
Got it working sort of right (but mostly wrong)
Maybe created a Boid class
Got the whole shebang working better (still some flaws)
Reformatted to include a Flock Class (a major endeavor)
Got the Flock class tricked out
Reformatted to include a Create_Image class,
by which time I was pretty much done.
Refactored.
Killed dead code.
Added almost all of the comments (based on previous notes)
Reread and proofread.
Call it forty hours, maybe more, I don't keep track.
After that, presumably, I'll start on the webpage.
So, maybe, sixty hours for the project is closer to the truth...
'''
def __init__(self, turns=1, blank=0, size=np.array([100,100]),
flocks=[], sN="boid_test_gif_needs_name.gif"):
'''An Object to hold the values needed to make an intricate gif image
turns = length of gif (including blank & first page),
so, one gives a single shot of the starting position
blank = number of leading blank screens with which to start the gif image
helpful for making it clear when the loop restarts
size = (width x height) output image size
flock = [], a list of dictionaries containing Flock(argValues)
one dictionary for each flock
see, run_multi_boids_switchboard() for more info on this
sN = is my naming convention for saveName
I do a lot of image work.
It's convenient to use consistent variable names,
from project to project.
'''
self.turns = turns
self.blank = blank
self.size = np.array(size)
self.flocks = flocks #see: run_multi_boids_switchboard()
self.sN = sN
print self
def __repr__(self):
'''In the words of the Daleks:
'Must Comment Code!'
'Must Comment Code!!'
'Must Comment Code!!!'
'''
a = '\nImage_Scene:\n'
b = '\tTurns: %d, \tBlank: %d, \tSize: %s\n' % (self.turns, self.blank, self.size)
c = '\tFlocks: %d' % len(self.flocks)
return a + b + c
def run(self, flockDict=[{'mode':['pos_checker']}]):
'''Makes a GIF!!!
The only meaningful method in the Create_Image class.
See: run_multi_boids_switchboard() for more information on flockDict
Note: I keep on saying this, so there must be a reason...
'''
#Holding array for the img lists
#When full, this will sort of look like:
#imgList = [[img,img,img,...], [img,img,img,...],...]
imgList = []
#Runs Each Flock Seperately
#So, in order to implement Hawk & Dove,
#This will need to be refactored
#Or more accurately, torn down and rebuilt completely
for flock in self.flocks:
flock = Flock(size=self.size, **flock)
if flock.mode:
flock.start_pos()
flock.start_vel()
#Each layer (one per flock) will turn into a seperate GIF and/or color layer
imgLayer = []
#Adds zero arrays, which come out as black screens
for _ in range(self.blank):
imgLayer.append(np.zeros((self.size[1], self.size[0]), dtype=int))
#An img of the starting position ('pos_cross', etc.) added to the layer
imgLayer.append(flock.flock_to_img())
#A Turn (move) is made and appended to the layer
for n in range(self.turns - self.blank - 1):
print "Boids: Turn %d" % n
flock.move()
imgLayer.append(flock.flock_to_img())
imgList.append(imgLayer)
#OUTPUT THE GIF
#There are only two tested/value lenghths for flock (one or three)
#Others might work, but these are the only ones tested for
#For the three layer condition, the first three layers are zipped together
if len(imgList) >= 3:
imgList.append([np.dstack((a,b,c)) for a,b,c in zip(imgList[0], imgList[1], imgList[2])])
#And each layer is outputed as a GIF
#The first three layers seperately in black and white
#The forth in a single RGB GIF (Red, Green, Blue layers in order)
for i, img in enumerate(imgList):
sN = self.sN[:-4] + '_%d' % i + self.sN[-4:]
print sN
clip = mpy.ImageSequenceClip(img, fps=5)
clip.write_gif(sN)
def run_multi_boids_switchboard():
'''Runs the works, outputing images as appropriate
If you just want to make a picture and don't really care about the code,
this is all you need to understand
Code is tested for one or three flocks
One Flock: Yields a single black and white gif
Three Flocks: Yields three black and white gifs
And a color composite overlapping the three
'''
#Image Wide Variables, Reference the Create_Image class if these aren't clear
image_variables = {'turns': 250, #reduce this to like 5 for debugging
'blank': 2,
'size': np.array([400,200]),
'flocks': [],
'sN':"07_boids_colorized.gif"
}
img = Create_Image(**image_variables)
#Variables for a flock, this can be copied wholesale and modified for each flock
#Or amended piecemeal, as I do below
#The commented out values to the right are values recently used
#I work in my code (inside my IDE), so THIS IS my interface
sample_flock = {'number': 50,
'mode': ['pos_checker'], ##, 'floor', 'vel_down', ],
'max_vel': 5.0,
'max_acc': 0.25,
'goal':[], #[np.array([0,50])], #[np.array([25,25]), np.array([75,75]), np.array([25,75]), np.array([75,25])]
'anti_goal': [], #[np.array([0,50])],
}
#No copy.deepcopy and you might find all the dictionaries look alike
f1 = copy.deepcopy(sample_flock)
f1['mode'] = ['pos_cross', 'floor', 'vel_right']
f1['goal'] = [np.array([50,50])]
f1['number'] = 25
f2 = copy.deepcopy(sample_flock)
f2['mode'] = ['pos_checker', 'floor', 'vel_up_one']
f2['goal'] = [np.array([200,150])]
f2['number'] = 100
f3 = copy.deepcopy(sample_flock)
f3['mode'] = ['pos_rand', 'floor', 'vel_left']
f3['goal'] = [np.array([350, 50])]
f3['number'] = 50
#Lump together, save to the working Create_Image instance, and run
flocks = [f1,f2,f3]
img.flocks = flocks
img.run()
if __name__ == '__main__':
'''Actually, my name = 'Brett Paufler',
And I hope you've enjoyed.
Copyright Brett Paufler 4-18-15
'''
print 'BOIDS: RUNNING AS MAIN'
#Obviously, this is the hook that runs it all
run_multi_boids_switchboard()
print "Multi Boid SwitchBoard: RUN OVER"
#TODO - PERHAPS ADD WIND
#TODO - Hawk and Dove, Interaction Between Flocks
'''
Copyright Brett Paufler 4-18-15 All Rights Reserved
See Terms of Service for Complete Details at www.paufler.net
Brett@Paufler.net
'''