Подтвердить что ты не робот

Написание более быстрого симулятора физики Python

Я играл с написанием своего собственного движка физики в Python как упражнение в физике и программировании. Я начал, следуя приведенному ниже учебнику . Это получилось хорошо, но затем я нашел статью "Продвинутая физика персонажей" от thomas jakobsen, которая охватила интеграцию Верле для симуляций, которую я нашел захватывающей.

Я пытаюсь написать свой собственный базовый физический симулятор, используя вербальную интеграцию, но он оказывается немного сложнее, чем я ожидал раньше. Я просматривал, например, программы для чтения, и наткнулся на этот, написанный на Python, и я также нашел этот учебник, который использует Обработку.

Что впечатляет меня о версии обработки, так это то, как быстро она работает. Сама ткань имеет 2400 различных точек, имитируемых, и что не включая тела.

Пример python использует только 256 частиц для ткани, и он работает со скоростью около 30 кадров в секунду. Я попытался увеличить количество частиц до 2401 (он должен быть квадратным для работы этой программы), он работает со скоростью около 3 кадров в секунду.


Обе эти функции работают путем хранения экземпляров объекта частицы в списке, а затем итерации по списку, вызывая метод "обновления позиции" для каждой частицы. В качестве примера, это часть кода из эскиза обработки, который вычисляет новое положение каждой частицы:

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

EDIT: Вот код из предыдущей версии python:

"""
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 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 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 catch it.
                # I think it 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())

Поскольку обе программы работают примерно одинаково, но версия Python намного медленнее, меня заставляет задуматься:

  • Является ли эта разница в производительности частью природы Python?
  • Что мне делать иначе, чем выше, если я хочу получить лучшую производительность от своих собственных программ Python? Например, сохраните свойства всех частиц внутри массива вместо использования отдельных объектов и т.д.

ИЗМЕНИТЬ: Ответил!!

@Mr E связал PyCon в комментариях и @A. Ответ Розы со связанными ресурсами помог ENORMOUSLY лучше понять, как писать хороший, быстрый код python. Я теперь закладок этой страницы для справок в будущем: D

4b9b3361

Ответ 1

Существует статья Guido van Rossum, связанная в разделе Советы по производительности в вики Python. В своем заключении вы можете прочитать следующее предложение:

Если вы чувствуете потребность в скорости, переходите к встроенным функциям - вы не можете бить петлю, написанную на C.

В эссе продолжается список рекомендаций по оптимизации цикла. Я рекомендую оба ресурса, поскольку они дают конкретные и практические советы по оптимизации кода Python.

Существует также известная группа тестов в benchmarksgame.alioth.debian.org, где вы можете найти сравнения между различными программами и языками в разных машины. Как можно видеть, в игре много переменных, что делает невозможным состояние, столь же широкое, как Java, быстрее, чем Python. Это обычно суммируется в предложении "Языки не имеют скорости, а реализации -".

В вашем коде могут быть применены более питонические и более быстрые альтернативы с использованием встроенных функций. Например, существует несколько вложенных циклов (некоторые из них не требуют обработки всего списка), которые можно переписать с помощью imap или список понятий. PyPy - еще один интересный вариант для повышения производительности. Я не эксперт по оптимизации Python, но есть много полезных советов, которые чрезвычайно полезны (обратите внимание, что не писать Java в Python является одним из них!).

Ресурсы и другие связанные вопросы по SO:

Ответ 2

Если вы пишете Python, как вы пишете Java, конечно, это будет медленнее, идиоматический java не переводит хорошо на идиоматический питон.

Является ли эта разница в производительности частью природы Python? Что я должен делать иначе, чем выше, если я хочу получить лучшую производительность от своих собственных программ Python? Например, сохраните свойства всех частиц внутри массива вместо использования отдельных объектов и т.д.

Трудно сказать, не видя ваш код.

Ниже приведен неполный список различий между python и java, которые могут иногда влиять на производительность:

  • Обработка использует немедленный режим canvas, если вам нужна сопоставимая производительность в Python, вам также нужно использовать режим немедленного режима. Контейнеры в большинстве графических интерфейсов (включая холст Tkinter) - это режим сохранения, который проще в использовании, но по своей сути медленнее, чем немедленный. Вам нужно будет использовать режим немедленного режима, например, файл pygame, SDL или Pyglet.

  • Python - это динамический язык, который означает, что доступ к члену экземпляра, доступ к члену модуля и доступ к глобальной переменной разрешаются во время выполнения. Доступ к члену экземпляра, доступ члена модуля и глобальный доступ к переменной в python - это действительно доступ к словарю. В java они решаются во время компиляции и по своей природе намного быстрее. Кэш часто обращается к глобальным переменным, переменным модуля и атрибутам к локальной переменной.

  • В python 2.x range() создает конкретный список, в python, итерация, выполняемая с использованием iterator, for item in list, обычно быстрее, чем итерация с использованием переменной итерации, for n in range(len(list)). Вы должны почти всегда выполнять итерацию напрямую с помощью итератора вместо итерации с использованием диапазона (len (...)).

  • Номера Python неизменяемы, это означает, что любой арифметический расчет выделяет новый объект. Это одна из причин, почему простой python не очень подходит для расчетов низкого уровня; большинство людей, которые хотят писать низкоуровневые вычисления, не прибегая к написанию расширения C, обычно используют cython, psyco или numpy. Обычно это становится проблемой, когда у вас есть миллионы вычислений.

Это просто неполный, очень неполный список, есть много других причин, по которым перевод java на python приведет к получению субоптимального кода. Не видя своего кода, невозможно сказать, что вам нужно делать по-другому. Оптимизированный код python обычно отличается от оптимизированного java-кода.

Ответ 3

Я бы также предложил прочитать о других физических двигателях. Существует несколько двигателей с открытым исходным кодом, которые используют множество методов для вычисления "физики".

  • Динамика игры в Ньютоне
  • Бурундук
  • Пуля
  • Box2D
  • ODE (Open Dynamics Engine)

Есть также порты большинства двигателей:

  • Pymunk
  • PyBullet
  • PyBox2D
  • PyODE

Если вы прочитаете документацию этих движков, вы часто найдете утверждения о том, что они оптимизированы для скорости (30 кадров в секунду - 60 кадров в секунду). Но если вы думаете, что они могут это сделать при расчете "реальной" физики, вы ошибаетесь. Большинство двигателей вычисляют физику до такой степени, что обычный пользователь не может оптически различать "реальное" физическое поведение и "имитируемое" физическое поведение. Однако, если вы исследуете ошибку, это пренебрежимо, если вы хотите писать игры. Но если вы хотите заниматься физикой, все эти двигатели вам не нужны. Вот почему я бы сказал, что если вы делаете реальную физическую симуляцию, вы медленнее, чем эти двигатели по дизайну, и вы никогда не обойдете еще один физический движок.

Ответ 4

Физическое моделирование на основе частиц легко трансформируется в операции линейной алгебры, т.е. матричные операции. Numpy предлагает такие операции, которые реализованы в Fortran/C/С++ под капотом. Хорошо написанный код python/Numpy (в полной мере использующий язык и библиотеку) позволяет писать довольно быстрый код.