11

I am an aerospace student working on a school project for our python programming course. The assignment is create a program only using Pygame and numpy. I decided to create a wind tunnel simulation that simulates the airflow over a two dimensional wing. I was wondering if there is a more efficient way of doing the computation from a programming perspective. I will explain the program:

I have attached an image here: enter image description here

The (steady) flow field is modeled using the vortex panel method. Basically, I am using a grid of Nx times Ny points where at each point a velocity (u,v) vector is given. Then using Pygame I map these grid points as circles, so that they resemble an area of influence. The grid points are the grey circles in the following image:

enter image description here

I create N particles and determine their velocities by iterating as follows:

create a list of particles.
create a grid list.

for each gridpoint in grid list:
  for each particle in list of particles:
  if particle A is within the area of influence of grid point n (xn,yn):
   particle A its velocity = velocity at grid point n.

Visualize everything in Pygame.

this basic way was the only way I could think of visualizing the flow in Pygame. The simulation works pretty well, but If I increase the number of grid points(increase the accuracy of the flow field), the performance decreases. My question is if there is a more efficient way to do this just using pygame and numpy?

I have attached the code here:

import pygame,random,sys,numpy
from Flow import Compute
from pygame.locals import *
import random, math, sys
#from PIL import Image
pygame.init()

Surface = pygame.display.set_mode((1000,600))

#read the airfoil geometry from a dat file
with open ('./resources/naca0012.dat') as file_name:
    x, y = numpy.loadtxt(file_name, dtype=float, delimiter='\t', unpack=True)    

#parameters used to describe the flow

Nx=30# 30 column grid
Ny=10#10 row grid
N=20#number of panels
alpha=0#angle of attack
u_inf=1#freestream velocity

#compute the flow field
u,v,X,Y= Compute(x,y,N,alpha,u_inf,Nx,Ny)  

#The lists used for iteration
Circles = []
Particles= []
Velocities=[]

#Scaling factors used to properly map the potential flow datapoints into Pygame
magnitude=400
vmag=30
umag=30
panel_x= numpy.multiply(x,magnitude)+315
panel_y= numpy.multiply(-y,magnitude)+308


#build the grid suited for Pygame
grid_x= numpy.multiply(X,magnitude)+300
grid_y= numpy.multiply(Y,-1*magnitude)+300

grid_u =numpy.multiply(u,umag)
grid_v =numpy.multiply(v,-vmag)
panelcoordinates=  zip(panel_x, panel_y)

# a grid area
class Circle:
    def __init__(self,xpos,ypos,vx,vy):

        self.radius=16

        self.x = xpos
        self.y = ypos
        self.speedx = 0
        self.speedy = 0

#create the grid list
for i in range(Ny):
    for s in range(Nx):
        Circles.append(Circle(int(grid_x[i][s]),int(grid_y[i][s]),grid_u[i][s],grid_v[i][s]))
        Velocities.append((grid_u[i][s],grid_v[i][s]))

#a particle
class Particle:
    def __init__(self,xpos,ypos,vx,vy):
        self.image = pygame.Surface([10, 10])
        self.image.fill((150,0,0))
        self.rect = self.image.get_rect()
        self.width=4
        self.height=4
        self.radius =2
        self.x = xpos
        self.y = ypos
        self.speedx = 30
        self.speedy = 0

#change particle velocity if collision with grid point
def CircleCollide(Circle,Particle):
    Particle.speedx = int(Velocities[Circles.index((Circle))][0])
    Particle.speedy = int(Velocities[Circles.index((Circle))][1])

#movement of particles
def Move():
    for Particle in Particles:
        Particle.x += Particle.speedx
        Particle.y += Particle.speedy
#create particle streak 
def Spawn(number_of_particles):
    for i in range(number_of_particles):
            i=i*(300/number_of_particles)        
            Particles.append(Particle(0, 160+i,1,0))

#create particles again if particles are out of wake
def Respawn(number_of_particles):
    for Particle in Particles:
        if Particle.x >1100:
            Particles.remove(Particle)
    if Particles==[]:
            Spawn(number_of_particles)

#Collsion detection using pythagoras and distance formula

def CollisionDetect():

   for Circle in Circles:  
       for Particle in Particles:
           if Particle.y >430 or Particle.y<160:
               Particles.remove(Particle)
           if math.sqrt( ((Circle.x-Particle.x)**2)  +  ((Circle.y-Particle.y)**2)  ) <= (Circle.radius+Particle.radius):       
               CircleCollide(Circle,Particle)

#draw everything
def Draw():
    Surface.fill((255,255,255))
    #Surface.blit(bg,(-300,-83))
    for Circle in Circles:
        pygame.draw.circle(Surface,(245,245,245),(Circle.x,Circle.y),Circle.radius)

    for Particle in Particles:
        pygame.draw.rect(Surface,(150,0,0),(Particle.x,Particle.y,Particle.width,Particle.height),0)


        #pygame.draw.rect(Surface,(245,245,245),(Circle.x,Circle.y,1,16),0)

    for i in range(len(panelcoordinates)-1):
        pygame.draw.line(Surface,(0,0,0),panelcoordinates[i],panelcoordinates[i+1],3)

    pygame.display.flip()


def GetInput():
    keystate = pygame.key.get_pressed()
    for event in pygame.event.get():
        if event.type == QUIT or keystate[K_ESCAPE]:
            pygame.quit();sys.exit()


def main():

    #bg = pygame.image.load("pressure.png")
    #bg = pygame.transform.scale(bg,(1600,800))
    #thesize= bg.get_rect()
    #bg= bg.convert()
    number_of_particles=10
    Spawn(number_of_particles)
    clock = pygame.time.Clock()

    while True:
        ticks = clock.tick(60)
        GetInput()
        CollisionDetect()
        Move()
        Respawn(number_of_particles)
        Draw()
if __name__ == '__main__': main()

The code requires another script that computes the flow field itself. It also reads datapoints from a textfile to get the geometry of the wing. I have not provided these two files, but I can add them if necessary. Thank you in advance.

Sami
  • 115
  • 8
  • So your only question is how to make it more efficient? – OneCricketeer Mar 01 '16 at 13:56
  • Welcome to the world of CFD. There are various ways of speeding things up but generally speaking: they involve a lot of recoding. If this is what you want for your student project, go for it, it's worth it! Have a look at other frameworks (OpenFOAM, for instance). Especially the computation coupled directly with the visualization will not work with more computationally intensive simulations. But great job! – jhoepken Mar 01 '16 at 13:57
  • cricket_007 yes, but only using Pygame and numpy its functionalities. Jens Höpken, thank you. I read about OpenFOAM just recently . I will definitely have a look. – Sami Mar 01 '16 at 14:47
  • I'm unfamiliar with the `Flow` package can you supply details. I may have to install that before I can be of much help. – Colin Dickie Mar 01 '16 at 15:35

2 Answers2

5

One bottleneck in your code is likely collision detection. CollisionDetect() computes the distance between each particle and each circle. Then, if a collision is detected, CircleCollide() finds the index of the circle in Circles (a linear search), so that the velocities can be retrieved from the same index in Velocities. Clearly this is ripe for improvement.

First, the Circle class already has the velocities in the speedx/speedy attributes, so Velocities can be eliminated .

Second, because the circles are at fixed locations, you can calculate which circle is closest to any given particle from the position of the particle.

# You may already have these values from creating grid_x etc.
# if not, you only need to calculated them once, because the
# circles don't move
circle_spacing_x = Circles[1].x - Circles[0].x
circle_spacing_y = Circles[Nx].y - Circles[0].y

circle_first_x = Circles[0].x - circle_spacing_x / 2
circle_first_y = Circles[0].y - circle_spacing_y / 2

Then CollisionDetect() becomes:

def CollisionDetect():

    for particle in Particles:
        if particle.y >430 or particle.y<160:
           Particles.remove(particle)
           continue

        c = (particle.x - circle_first_x) // circle_spacing_x
        r = (particle.y - circle_first_y) // circle_spacing_y
        circle = Circles[r*Nx + c]

        if ((circle.x - particle.x)**2 + (circle.y - particle.y)**2
            <= (circle.radius+particle.radius)**2):       
                particle.speedx = int(circle.speedx)
                particle.speedy = int(circle.speedy)
RootTwo
  • 3,810
  • 1
  • 8
  • 12
  • 1
    Excellent, a great solution!! I applied your solution it is an amazing improvement. I can easily triple the amount of grid points(to make the flow more accurate) while increasing the amount of particles to 100 without observing any performance drop. The old code did terrible in comparison! Colin Dickie I tried your code and did not observe any significant improvement, but thanks for your input. – Sami Mar 25 '16 at 11:59
1

I've tidied up your code and made a few changes, namely adding scope to your classes and introducing a couple more. Without further knowledge of Flow I cannot test this fully, but if you could get back to me I can do some more. I'm assuming here that the 'flow field' can be simulated by the numpy.meshgrid function.

import pygame,numpy,sys
import pygame.locals
import math

class Particle:
    def __init__(self,xpos,ypos,vx,vy):
        self.size = numpy.array([4,4])
        self.radius =2
        self.pos = numpy.array([xpos,ypos])
        self.speed = numpy.array([30,0])
        self.rectangle = numpy.hstack((self.pos,self.size))
    def move(self):
        self.pos += self.speed
        self.rectangle = numpy.hstack((self.pos,self.size))
    def distance(self,circle1):
        return math.sqrt(numpy.sum((circle1.pos - self.pos)**2))
    def collision(self,circle1):
        result = False
        if self.pos[1] >430 or self.pos[1]<160:
            result = True
        if  self.distance(circle1) <= (circle1.radius+self.radius):
            self.speed = circle1.speed
        return result

class Particles:
    def __init__(self,num_particles):
        self.num = num_particles
        self.particles =[]
        self.spawn()
    def spawn(self):
        for i in range(self.num):
            i=i*(300/self.num)        
            self.particles.append(Particle(0, 160+i,1,0))
    def move(self):
        for particle in self.particles:
            particle.move()
            if particle.pos[0] >1100:
                self.particles.remove(particle)
        if not self.particles: self.spawn()
    def draw(self):
        for particle in self.particles:
            pygame.draw.rect(Surface,(150,0,0),particle.rectangle,0)
    def collisiondetect(self,circle1):
        for particle in self.particles:
           if particle.collision(circle1):
               self.particles.remove(particle)

def GetInput():
    keystate = pygame.key.get_pressed()
    for event in pygame.event.get():
        if event.type == pygame.locals.QUIT or keystate[pygame.locals.K_ESCAPE]:
            pygame.quit()
            sys.exit()

#draw everything
def Draw(sw,cir):
    Surface.fill((255,255,255))
    cir.draw()
    for i in range(panelcoordinates.shape[1]):
        pygame.draw.line(Surface,(0,0,0),panelcoordinates[0,i-1],panelcoordinates[0,i],3)
    sw.draw()
    pygame.display.flip()

# a grid area
class Circle:
    def __init__(self,xpos,ypos,vx,vy):
        self.radius=16
        self.pos = numpy.array([xpos,ypos])
        self.speed = numpy.array([vx,vy])

class Circles:
    def __init__(self,columns,rows):
        self.list = []
        grid_x,grid_y = numpy.meshgrid(numpy.linspace(0,1000,columns),numpy.linspace(200,400,rows))
        grid_u,grid_v = numpy.meshgrid(numpy.linspace(20,20,columns),numpy.linspace(-1,1,rows))
        for y in range(rows):
            for x in range(columns):
                c1= Circle(int(grid_x[y,x]),int(grid_y[y,x]),grid_u[y,x],grid_v[y,x])
                self.list.append(c1)
    def draw(self):
        for circle in self.list:
            pygame.draw.circle(Surface,(245,245,245),circle.pos,circle.radius)
    def detectcollision(self,parts):
        for circle in self.list:
            parts.collisiondetect(circle)


if __name__ == '__main__':
    #initialise variables
    number_of_particles=10
    Nx=30
    Ny=10

    #circles and particles
    circles1 = Circles(Nx,Ny)
    particles1 = Particles(number_of_particles)

    #read the airfoil geometry
    panel_x = numpy.array([400,425,450,500,600,500,450,425,400])
    panel_y = numpy.array([300,325,330,320,300,280,270,275,300])
    panelcoordinates=  numpy.dstack((panel_x,panel_y))

    #initialise PyGame
    pygame.init()
    clock = pygame.time.Clock()
    Surface = pygame.display.set_mode((1000,600))

    while True:
        ticks = clock.tick(6)
        GetInput()
        circles1.detectcollision(particles1)
        particles1.move()
        Draw(particles1,circles1)

I've also made some a crude stab at drawing a aerofoil, as again I don't have the knowledge of the data file with the coordinates.

Colin Dickie
  • 874
  • 4
  • 8