''' 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 '''