Pisanie szybszego symulatora fizyki Pythona

Bawiłem się pisaniem własnego silnika fizyki w Pythonie jako ćwiczenie z fizyki i programowania. Zacząłem od tutoriala znajdującego się tutaj. Poszło dobrze, ale potem znalazłem artykuł "Advanced character physics" Thomasa Jakobsena, który obejmował wykorzystanie integracji Verleta do symulacji, co uważam za fascynujące.

[2]}próbowałem napisać własny symulator fizyki bazowej z wykorzystaniem integracji verleta, ale okazało się, że być nieco trudniejszym, niż się spodziewałem. Przeglądałem na przykład programy do czytania i natknąłem się na Ten napisany w Pythonie {[4] } i znalazłem również ten tutorial , który wykorzystuje przetwarzanie.

To, co robi na mnie wrażenie w wersji przetwarzającej, to szybkość jej działania. Sama tkanina ma symulowane 2400 różnych punktów, nie wliczając ciał.

Przykład Pythona używa tylko 256 cząstek dla tkaniny i działa w około 30 klatkach na sekundę. Próbowałem zwiększyć liczbę cząstek do 2401 (musi być kwadratowy, aby ten program działał), działał z około 3 fps.


Oba te działania polegają na przechowywaniu instancji obiektu particle na liście, a następnie iteracji przez Listę, wywołując każdą cząstkę metodą "update position". Jako przykład, jest to część kodu ze szkicu przetwarzania, która oblicza nową pozycję każdej cząstki:

for (int i = 0; i < pointmasses.size(); i++) {
    PointMass pointmass = (PointMass) pointmasses.get(i);
    pointmass.updateInteractions();
    pointmass.updatePhysics(fixedDeltaTimeSeconds);
}

EDIT: Oto kod z Pythona wersja, którą linkowałem wcześniej:

"""
verletCloth01.py
Eric Pavey - 2010-07-03 - www.akeric.com

Riding on the shoulders of giants.
I wanted to learn now to do 'verlet cloth' in Python\Pygame.  I first ran across
this post \ source:
http://forums.overclockers.com.au/showthread.php?t=870396
http://dl.dropbox.com/u/3240460/cloth5.py

Which pointed to some good reference, that was a dead link.  After some searching,
I found it here:
http://www.gpgstudy.com/gpgiki/GDC%202001%3A%20Advanced%20Character%20Physics
Which is a 2001 SIGGRAPH paper by Thomas Jakobsen called:
"GDC 2001: Advanced Characer Physics".

This code is a Python\Pygame interpretation of that 2001 Siggraph paper.  I did
borrow some code from 'domlebo's source code, it was a great starting point.  But
I'd like to think I put my own flavor on it.
"""

#--------------
# Imports & Initis
import sys
from math import sqrt

# Vec2D comes from here: http://pygame.org/wiki/2DVectorClass
from vec2d import Vec2d
import pygame
from pygame.locals import *
pygame.init()

#--------------
# Constants
TITLE = "verletCloth01"
WIDTH = 600
HEIGHT = 600
FRAMERATE = 60
# How many iterations to run on our constraints per frame?
# This will 'tighten' the cloth, but slow the sim.
ITERATE = 2
GRAVITY = Vec2d(0.0,0.05)
TSTEP = 2.8

# How many pixels to position between each particle?
PSTEP = int(WIDTH*.03)
# Offset in pixels from the top left of screen to position grid:
OFFSET = int(.25*WIDTH)

#-------------
# Define helper functions, classes

class Particle(object):
    """
    Stores position, previous position, and where it is in the grid.
    """
    def __init__(self, screen, currentPos, gridIndex):
        # Current Position : m_x
        self.currentPos = Vec2d(currentPos)
        # Index [x][y] of Where it lives in the grid
        self.gridIndex = gridIndex
        # Previous Position : m_oldx
        self.oldPos = Vec2d(currentPos)
        # Force accumulators : m_a
        self.forces = GRAVITY
        # Should the particle be locked at its current position?
        self.locked = False
        self.followMouse = False

        self.colorUnlocked = Color('white')
        self.colorLocked = Color('green')
        self.screen = screen

    def __str__(self):
        return "Particle <%s, %s>"%(self.gridIndex[0], self.gridIndex[1])

    def draw(self):
        # Draw a circle at the given Particle.
        screenPos = (self.currentPos[0], self.currentPos[1])
        if self.locked:
            pygame.draw.circle(self.screen, self.colorLocked, (int(screenPos[0]),
                                                         int(screenPos[1])), 4, 0)
        else:
            pygame.draw.circle(self.screen, self.colorUnlocked, (int(screenPos[0]),
                                                         int(screenPos[1])), 1, 0)

class Constraint(object):
    """
    Stores 'constraint' data between two Particle objects.  Stores this data
    before the sim runs, to speed sim and draw operations.
    """
    def __init__(self, screen, particles):
        self.particles = sorted(particles)
        # Calculate restlength as the initial distance between the two particles:
        self.restLength = sqrt(abs(pow(self.particles[1].currentPos.x -
                                       self.particles[0].currentPos.x, 2) +
                                   pow(self.particles[1].currentPos.y -
                                       self.particles[0].currentPos.y, 2)))
        self.screen = screen
        self.color = Color('red')

    def __str__(self):
        return "Constraint <%s, %s>"%(self.particles[0], self.particles[1])

    def draw(self):
        # Draw line between the two particles.
        p1 = self.particles[0]
        p2 = self.particles[1]
        p1pos = (p1.currentPos[0],
                 p1.currentPos[1])
        p2pos = (p2.currentPos[0],
                 p2.currentPos[1])
        pygame.draw.aaline(self.screen, self.color,
                           (p1pos[0], p1pos[1]), (p2pos[0], p2pos[1]), 1)

class Grid(object):
    """
    Stores a grid of Particle objects.  Emulates a 2d container object.  Particle
    objects can be indexed by position:
        grid = Grid()
        particle = g[2][4]
    """
    def __init__(self, screen, rows, columns, step, offset):

        self.screen = screen
        self.rows = rows
        self.columns = columns
        self.step = step
        self.offset = offset

        # Make our internal grid:
        # _grid is a list of sublists.
        #    Each sublist is a 'column'.
        #        Each column holds a particle object per row:
        # _grid =
        # [[p00, [p10, [etc,
        #   p01,  p11,
        #   etc], etc],     ]]
        self._grid = []
        for x in range(columns):
            self._grid.append([])
            for y in range(rows):
                currentPos = (x*self.step+self.offset, y*self.step+self.offset)
                self._grid[x].append(Particle(self.screen, currentPos, (x,y)))

    def getNeighbors(self, gridIndex):
        """
        return a list of all neighbor particles to the particle at the given gridIndex:

        gridIndex = [x,x] : The particle index we're polling
        """
        possNeighbors = []
        possNeighbors.append([gridIndex[0]-1, gridIndex[1]])
        possNeighbors.append([gridIndex[0], gridIndex[1]-1])
        possNeighbors.append([gridIndex[0]+1, gridIndex[1]])
        possNeighbors.append([gridIndex[0], gridIndex[1]+1])

        neigh = []
        for coord in possNeighbors:
            if (coord[0] < 0) | (coord[0] > self.rows-1):
                pass
            elif (coord[1] < 0) | (coord[1] > self.columns-1):
                pass
            else:
                neigh.append(coord)

        finalNeighbors = []
        for point in neigh:
            finalNeighbors.append((point[0], point[1]))

        return finalNeighbors

    #--------------------------
    # Implement Container Type:

    def __len__(self):
        return len(self.rows * self.columns)

    def __getitem__(self, key):
        return self._grid[key]

    def __setitem__(self, key, value):
        self._grid[key] = value

    #def __delitem__(self, key):
        #del(self._grid[key])

    def __iter__(self):
        for x in self._grid:
            for y in x:
                yield y

    def __contains__(self, item):
        for x in self._grid:
            for y in x:
                if y is item:
                    return True
        return False


class ParticleSystem(Grid):
    """
    Implements the verlet particles physics on the encapsulated Grid object.
    """

    def __init__(self, screen, rows=49, columns=49, step=PSTEP, offset=OFFSET):
        super(ParticleSystem, self).__init__(screen, rows, columns, step, offset)

        # Generate our list of Constraint objects.  One is generated between
        # every particle connection.
        self.constraints = []
        for p in self:
            neighborIndices = self.getNeighbors(p.gridIndex)
            for ni in neighborIndices:
                # Get the neighbor Particle from the index:
                n = self[ni[0]][ni[1]]
                # Let's not add duplicate Constraints, which would be easy to do!
                new = True
                for con in self.constraints:
                    if n in con.particles and p in con.particles:
                        new = False
                if new:
                    self.constraints.append( Constraint(self.screen, (p,n)) )

        # Lock our top left and right particles by default:
        self[0][0].locked = True
        self[1][0].locked = True
        self[-2][0].locked = True
        self[-1][0].locked = True

    def verlet(self):
        # Verlet integration step:
        for p in self:
            if not p.locked:
                # make a copy of our current position
                temp = Vec2d(p.currentPos)
                p.currentPos += p.currentPos - p.oldPos + p.forces * TSTEP**2
                p.oldPos = temp
            elif p.followMouse:
                temp = Vec2d(p.currentPos)
                p.currentPos = Vec2d(pygame.mouse.get_pos())
                p.oldPos = temp

    def satisfyConstraints(self):
        # Keep particles together:
        for c in self.constraints:
            delta =  c.particles[0].currentPos - c.particles[1].currentPos
            deltaLength = sqrt(delta.dot(delta))
            try:
                # You can get a ZeroDivisionError here once, so let's catch it.
                # I think it's when particles sit on top of one another due to
                # being locked.
                diff = (deltaLength-c.restLength)/deltaLength
                if not c.particles[0].locked:
                    c.particles[0].currentPos -= delta*0.5*diff
                if not c.particles[1].locked:
                    c.particles[1].currentPos += delta*0.5*diff
            except ZeroDivisionError:
                pass

    def accumulateForces(self):
        # This doesn't do much right now, other than constantly reset the
        # particles 'forces' to be 'gravity'.  But this is where you'd implement
        # other things, like drag, wind, etc.
        for p in self:
            p.forces = GRAVITY

    def timeStep(self):
        # This executes the whole shebang:
        self.accumulateForces()
        self.verlet()
        for i in range(ITERATE):
            self.satisfyConstraints()

    def draw(self):
        """
        Draw constraint connections, and particle positions:
        """
        for c in self.constraints:
            c.draw()
        #for p in self:
        #    p.draw()

    def lockParticle(self):
        """
        If the mouse LMB is pressed for the first time on a particle, the particle
        will assume the mouse motion.  When it is pressed again, it will lock
        the particle in space.
        """
        mousePos = Vec2d(pygame.mouse.get_pos())
        for p in self:
            dist2mouse = sqrt(abs(pow(p.currentPos.x -
                                      mousePos.x, 2) +
                                  pow(p.currentPos.y -
                                      mousePos.y, 2)))
            if dist2mouse < 10:
                if not p.followMouse:
                    p.locked = True
                    p.followMouse = True
                    p.oldPos = Vec2d(p.currentPos)
                else:
                    p.followMouse = False

    def unlockParticle(self):
        """
        If the RMB is pressed on a particle, if the particle is currently
        locked or being moved by the mouse, it will be 'unlocked'/stop following
        the mouse.
        """
        mousePos = Vec2d(pygame.mouse.get_pos())
        for p in self:
            dist2mouse = sqrt(abs(pow(p.currentPos.x -
                                      mousePos.x, 2) +
                                  pow(p.currentPos.y -
                                      mousePos.y, 2)))
            if dist2mouse < 5:
                p.locked = False

#------------
# Main Program
def main():
    # Screen Setup
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    clock = pygame.time.Clock()

    # Create our grid of particles:
    particleSystem = ParticleSystem(screen)
    backgroundCol = Color('black')

    # main loop
    looping = True
    while looping:
        clock.tick(FRAMERATE)
        pygame.display.set_caption("%s -- www.AKEric.com -- LMB: move\lock - RMB: unlock - fps: %.2f"%(TITLE, clock.get_fps()) )
        screen.fill(backgroundCol)

        # Detect for events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                looping = False
            elif event.type == MOUSEBUTTONDOWN:
                if event.button == 1:
                    # See if we can make a particle follow the mouse and lock
                    # its position when done.
                    particleSystem.lockParticle()
                if event.button == 3:
                    # Try to unlock the current particles position:
                    particleSystem.unlockParticle()

        # Do stuff!
        particleSystem.timeStep()
        particleSystem.draw()

        # update our display:
        pygame.display.update()

#------------
# Execution from shell\icon:
if __name__ == "__main__":
    print "Running Python version:", sys.version
    print "Running PyGame version:", pygame.ver
    print "Running %s.py"%TITLE
    sys.exit(main())

Ponieważ oba programy działają mniej więcej tak samo, ale wersja Pythona jest o wiele wolniejsza, zastanawiam się:

  • czy ta różnica w wydajności jest częścią natury Pythona?
  • co powinienem zrobić inaczej niż powyżej, jeśli chcę uzyskać lepszą wydajność z własnych programów Pythona? Np. przechowuje właściwości wszystkich cząstek wewnątrz tablicy, zamiast używać pojedynczych obiektów, itp.

Edytuj: Odpowiedź!!

Podlinkowana dyskusja PyCon@Mr E w komentarzach i odpowiedź @ A. Rosa z linked resources pomogły ogromnie w lepszym zrozumieniu, jak pisać dobry, szybki kod Pythona. Jestem teraz bookmarking tej strony dla przyszłego odniesienia: d

Author: mooglinux, 2013-03-12

4 answers

Istnieje artykuł Guido van Rossuma podlinkowany w sekcji Wskazówki dotyczące wydajności Python Wiki. W jego konkluzji można przeczytać następujące zdanie:

Jeśli czujesz potrzebę szybkości, przejdź do wbudowanych funkcji - nie możesz pokonać pętli napisanej w C.

Esej jest kontynuacją listy wytycznych dotyczących optymalizacji pętli. Polecam oba zasoby, ponieważ dają konkretne i praktyczne porady dotyczące optymalizacji Pythona kod.

Istnieje również dobrze znana grupa benchmarków w benchmarksgame.alioth.debian.org , gdzie można znaleźć porównania pomiędzy różnymi programami i językami w różnych maszynach. Jak widać, istnieje wiele zmiennych w grze, które uniemożliwiają stan coś tak szerokiego jak Java jest szybsza niż Python . Jest to powszechnie podsumowane w zdaniu "języki nie mają prędkości; implementacje tak".

W kodzie można zastosować więcej pythonic i szybsze alternatywy za pomocą wbudowanych funkcji. Na przykład istnieje kilka zagnieżdżonych pętli (niektóre z nich nie wymagają przetwarzania całej listy), które można przepisać za pomocą imap lub składanie listy . PyPy jest również kolejną ciekawą opcją poprawy wydajności. Nie jestem ekspertem w optymalizacji Pythona, ale jest wiele wskazówek, które są niezwykle przydatne (zauważ, że nie pisz Javy w Pythonie jest jednym z oni!).

Zasoby i inne pytania związane z SO:

 8
Author: A. Rodas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:17:26

Jeśli piszesz Python tak jak piszesz Javę, oczywiście będzie wolniej, idiomatic java nie tłumaczy się dobrze na idiomatic python.

Czy ta różnica w wydajności jest częścią natury Pythona? Co powinienem zrobić inaczej niż powyżej, jeśli chcę uzyskać lepszą wydajność z własnych programów Pythona? Np. przechowuje właściwości wszystkich cząstek wewnątrz tablicy, zamiast używać pojedynczych obiektów, itp.

Trudno powiedzieć, nie widząc swojego kod.

Oto niekompletna lista różnic między Pythonem a Javą, które mogą czasami wpływać na wydajność:

  1. Przetwarzanie wykorzystuje płótno w trybie natychmiastowym, jeśli chcesz uzyskać porównywalną wydajność w Pythonie, musisz również użyć płótna w trybie natychmiastowym. Płótna w większości frameworków GUI (w tym Tkinter canvas) są trybem zachowywanym, który jest łatwiejszy w użyciu, ale z natury wolniejszy niż tryb natychmiastowy. Będziesz musiał użyć płótna trybu natychmiastowego, takiego jak te dostarczane przez pygame, SDL, albo Piglet.

  2. Python jest językiem dynamicznym, co oznacza, że dostęp do członków instancji, dostęp do członków modułów i globalny dostęp do zmiennych jest rozwiązywany w czasie wykonywania. Instancja member access, Module member access I global variable access w Pythonie to naprawdę dostęp do słownika. W Javie są one rozwiązywane w czasie kompilacji i ze względu na swoją naturę znacznie szybciej. Cache często dostępne globals, zmienne modułu i atrybuty do zmiennej lokalnej.

  3. W Pythonie 2.X, range() w Pythonie iteracja wykonywana przy użyciu iteratora, for item in list, jest zwykle szybsza niż iteracja wykonywana przy użyciu zmiennej iteracyjnej, for n in range(len(list)). Prawie zawsze należy iterać bezpośrednio używając iteratora zamiast iteracji używając range (len (...)).

  4. Liczby Pythona są niezmienne, co oznacza, że wszelkie obliczenia arytmetyczne przyporządkowują nowy obiekt. Jest to jeden z powodów, dla których prosty python nie jest zbyt odpowiedni do obliczeń niskiego poziomu; większość ludzi, którzy chcą być w stanie pisać niski poziom obliczenia bez konieczności uciekania się do pisania c rozszerzenie zazwyczaj używa cython, psyco, lub numpy. Zwykle staje się to problemem tylko wtedy, gdy masz miliony obliczeń.

Jest to tylko częściowa, bardzo niekompletna lista, istnieje wiele innych powodów, dla których tłumaczenie Javy na python mogłoby produkować nieoptymalny kod. Bez zobaczenia kodu nie można powiedzieć, co trzeba zrobić inaczej. Zoptymalizowany kod Pythona generalnie wygląda zupełnie inaczej niż zoptymalizowany kod Javy kod.

 5
Author: Lie Ryan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-13 00:19:21

Proponuję również poczytać o innych silnikach fizyki. Istnieje kilka silników open source, które używają różnych metod obliczania "fizyki".

  • Dynamika Gry Newtona
  • Wiewiórka
  • Bullet
  • Box2D
  • Oda (Open Dynamics Engine)

Są też porty większości silników:

  • Pymunk
  • PyBullet
  • PyBox2D
  • PyODE

Jeśli przeczytasz dokumentację tych silniki często znajdziesz stwierdzenia, że są one zoptymalizowane pod kątem prędkości (30fps - 60fps). Ale jeśli myślisz, że mogą to zrobić podczas obliczania" prawdziwej " fizyki, mylisz się. Większość silników oblicza fizykę do punktu, w którym normalny użytkownik nie może optycznie odróżnić "rzeczywistego" zachowania fizycznego od "symulowanego" zachowania fizycznego. Jednak jeśli zbadasz błąd, jest zaniedbany, jeśli chcesz pisać gry. Ale jeśli chcesz zrobić fizykę, wszystkie te silniki są bezużyteczne do ty. Dlatego powiedziałbym, że jeśli wykonujesz prawdziwą symulację fizyczną, jesteś wolniejszy od tych silników i nigdy nie wyprzedzisz innego silnika fizyki.

 5
Author: wagnerpeer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-13 08:26:28

Symulacja fizyki opartej na cząsteczkach łatwo przekłada się na operacje algebry liniowej tj. operacje matrycowe. Numpy oferuje takie operacje, które są zaimplementowane w Fortran/C / c++ pod maską. Dobrze napisany kod python / Numpy (w pełni wykorzystujący język i bibliotekę) pozwala na pisanie przyzwoicie szybkiego kodu.

 0
Author: Monkey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-28 01:40:09