Skip to content

Instantly share code, notes, and snippets.

@villares
Created November 19, 2023 19:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save villares/40df109503f4237256ef378dbd97920f to your computer and use it in GitHub Desktop.
Save villares/40df109503f4237256ef378dbd97920f to your computer and use it in GitHub Desktop.
# -----------------------------------------------------------------------------
# From Pytnon to Numpy
# Copyright (2017) Nicolas P. Rougier - BSD license
# More information at https://github.com/rougier/numpy-book
# -----------------------------------------------------------------------------
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.animation import FuncAnimation
from matplotlib.collections import PathCollection
class MarkerCollection:
"""
Marker collection
"""
def __init__(self, n=100):
v = np.array([(-0.25, -0.25), (+0.0, +0.5), (+0.25, -0.25), (0, 0)])
c = np.array([Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
self._base_vertices = np.tile(v.reshape(-1), n).reshape(n, len(v), 2)
self._vertices = np.tile(v.reshape(-1), n).reshape(n, len(v), 2)
self._codes = np.tile(c.reshape(-1), n)
self._scale = np.ones(n)
self._translate = np.zeros((n, 2))
self._rotate = np.zeros(n)
self._path = Path(vertices=self._vertices.reshape(n*len(v), 2),
codes=self._codes)
self._collection = PathCollection([self._path], linewidth=0.5,
facecolor="k", edgecolor="w")
def update(self):
n = len(self._base_vertices)
self._vertices[...] = self._base_vertices * self._scale
cos_rotate, sin_rotate = np.cos(self._rotate), np.sin(self._rotate)
R = np.empty((n, 2, 2))
R[:, 0, 0] = cos_rotate
R[:, 1, 0] = sin_rotate
R[:, 0, 1] = -sin_rotate
R[:, 1, 1] = cos_rotate
self._vertices[...] = np.einsum('ijk,ilk->ijl', self._vertices, R)
self._vertices += self._translate.reshape(n, 1, 2)
class Flock:
def __init__(self, count=500, width=640, height=360):
self.width = width
self.height = height
self.min_velocity = 0.5
self.max_velocity = 2.0
self.max_acceleration = 0.03
self.velocity = np.zeros((count, 2), dtype=np.float32)
self.position = np.zeros((count, 2), dtype=np.float32)
angle = np.random.uniform(0, 2*np.pi, count)
self.velocity[:, 0] = np.cos(angle)
self.velocity[:, 1] = np.sin(angle)
angle = np.random.uniform(0, 2*np.pi, count)
radius = min(width, height)/2*np.random.uniform(0, 1, count)
self.position[:, 0] = width/2 + np.cos(angle)*radius
self.position[:, 1] = height/2 + np.sin(angle)*radius
def run(self):
position = self.position
velocity = self.velocity
min_velocity = self.min_velocity
max_velocity = self.max_velocity
max_acceleration = self.max_acceleration
n = len(position)
dx = np.subtract.outer(position[:, 0], position[:, 0])
dy = np.subtract.outer(position[:, 1], position[:, 1])
distance = np.hypot(dx, dy)
# Compute common distance masks
mask_0 = (distance > 0)
mask_1 = (distance < 25)
mask_2 = (distance < 50)
mask_1 *= mask_0
mask_2 *= mask_0
mask_3 = mask_2
mask_1_count = np.maximum(mask_1.sum(axis=1), 1)
mask_2_count = np.maximum(mask_2.sum(axis=1), 1)
mask_3_count = mask_2_count
# Separation
mask, count = mask_1, mask_1_count
target = np.dstack((dx, dy))
target = np.divide(target, distance.reshape(n, n, 1)**2, out=target,
where=distance.reshape(n, n, 1) != 0)
steer = (target*mask.reshape(n, n, 1)).sum(axis=1)/count.reshape(n, 1)
norm = np.sqrt((steer*steer).sum(axis=1)).reshape(n, 1)
steer = max_velocity*np.divide(steer, norm, out=steer,
where=norm != 0)
steer -= velocity
# Limit acceleration
norm = np.sqrt((steer*steer).sum(axis=1)).reshape(n, 1)
steer = np.multiply(steer, max_acceleration/norm, out=steer,
where=norm > max_acceleration)
separation = steer
# Alignment
# ---------------------------------------------------------------------
# Compute target
mask, count = mask_2, mask_2_count
target = np.dot(mask, velocity)/count.reshape(n, 1)
# Compute steering
norm = np.sqrt((target*target).sum(axis=1)).reshape(n, 1)
target = max_velocity * np.divide(target, norm, out=target,
where=norm != 0)
steer = target - velocity
# Limit acceleration
norm = np.sqrt((steer*steer).sum(axis=1)).reshape(n, 1)
steer = np.multiply(steer, max_acceleration/norm, out=steer,
where=norm > max_acceleration)
alignment = steer
# Cohesion
# ---------------------------------------------------------------------
# Compute target
mask, count = mask_3, mask_3_count
target = np.dot(mask, position)/count.reshape(n, 1)
# Compute steering
desired = target - position
norm = np.sqrt((desired*desired).sum(axis=1)).reshape(n, 1)
desired *= max_velocity / norm
steer = desired - velocity
# Limit acceleration
norm = np.sqrt((steer*steer).sum(axis=1)).reshape(n, 1)
steer = np.multiply(steer, max_acceleration/norm, out=steer,
where=norm > max_acceleration)
cohesion = steer
# ---------------------------------------------------------------------
acceleration = 1.5 * separation + alignment + cohesion
velocity += acceleration
norm = np.sqrt((velocity*velocity).sum(axis=1)).reshape(n, 1)
velocity = np.multiply(velocity, max_velocity/norm, out=velocity,
where=norm > max_velocity)
velocity = np.multiply(velocity, min_velocity/norm, out=velocity,
where=norm < min_velocity)
position += velocity
# Wraparound
position += (self.width, self.height)
position %= (self.width, self.height)
def update(*args):
#global flock, collection,
global trace
# Flock updating
flock.run()
collection._scale = 10
collection._translate = flock.position
collection._rotate = -np.pi/2 + np.arctan2(flock.velocity[:, 1],
flock.velocity[:, 0])
collection.update()
# Trace updating
if trace is not None:
P = flock.position.astype(int)
trace[height-1-P[:, 1], P[:, 0]] = .75
trace *= .99
im.set_array(trace)
# -----------------------------------------------------------------------------
if __name__ == '__main__':
n = 500
width, height = 640, 360
flock = Flock(n)
fig = plt.figure(figsize=(10, 10*height/width), facecolor="white")
ax = fig.add_axes([0.0, 0.0, 1.0, 1.0], aspect=1, frameon=False)
collection = MarkerCollection(n)
ax.add_collection(collection._collection)
ax.set_xlim(0, width)
ax.set_ylim(0, height)
ax.set_xticks([])
ax.set_yticks([])
# Trace
trace = None
if 0:
trace = np.zeros((height, width))
im = ax.imshow(trace, extent=[0, width, 0, height], vmin=0, vmax=1,
interpolation="nearest", cmap=plt.cm.gray_r)
animation = FuncAnimation(fig, update, interval=10, frames=1000)
animation.save('boid.mp4', fps=40, dpi=80, bitrate=-1, codec="libx264",
extra_args=['-pix_fmt', 'yuv420p'],
metadata={'artist': 'Nicolas P. Rougier'})
plt.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment