Контролируемая упаковка кругов с помощью Python

Я пытаюсь перенести на Python алгоритм «Контролируемая упаковка круга с обработкой», который я нашел здесь:

http://www.codeplastic.com/2017/09/09/controlled-circle-packing-with-processing/?replytocom=22#respond

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

Пока вот что у меня есть:

#!/usr/bin/python
# coding: utf-8

import numpy as np
import matplotlib.pyplot as plt
from random import uniform


class Ball:

    def __init__(self, x, y, radius):

        self.r = radius

        self.acceleration = np.array([0, 0])

        self.velocity = np.array([uniform(0, 1),
                                  uniform(0, 1)])

        self.position = np.array([x, y])


    @property
    def x(self):
        return self.position[0]

    @property
    def y(self):
        return self.position[1]


    def applyForce(self, force):

        self.acceleration = np.add(self.acceleration, force)


    def update(self):

        self.velocity = np.add(self.velocity, self.acceleration)
        self.position = np.add(self.position, self.velocity)
        self.acceleration *= 0


class Pack:

    def __init__(self, radius, list_balls):

        self.list_balls = list_balls
        self.r = radius
        self.list_separate_forces = [np.array([0, 0])] * len(self.list_balls)
        self.list_near_balls = [0] * len(self.list_balls)


    def _normalize(self, v):

        norm = np.linalg.norm(v)
        if norm == 0:
            return v
        return v / norm


    def run(self):

        for i in range(300):
            print(i)
            for ball in self.list_balls:
                self.checkBorders(ball)
                self.checkBallPositions(ball)
                self.applySeparationForcesToBall(ball)

    def checkBorders(self, ball):

        if (ball.x - ball.r) < - self.r or (ball.x + ball.r) > self.r:
            ball.velocity[0] *= -1
            ball.update()
        if (ball.y - ball.r) < -self.r or (ball.y + ball.r) > self.r:
            ball.velocity[1] *= -1
            ball.update()


    def checkBallPositions(self, ball):

        list_neighbours = [e for e in self.list_balls if e is not ball]

        for neighbour in list_neighbours:

            d = self._distanceBalls(ball, neighbour)

            if d < (ball.r + neighbour.r):
                return

        ball.velocity[0] = 0
        ball.velocity[1] = 0


    def getSeparationForce(self, c1, c2):

        steer = np.array([0, 0])

        d = self._distanceBalls(c1, c2)

        if d > 0 and d < (c1.r + c2.r):
            diff = np.subtract(c1.position, c2.position)
            diff = self._normalize(diff)
            diff = np.divide(diff, d)
            steer = np.add(steer, diff)

        return steer


    def _distanceBalls(self, c1, c2):

        x1, y1 = c1.x, c1.y
        x2, y2 = c2.x, c2.y

        dist = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)

        return dist


    def applySeparationForcesToBall(self, ball):

        i = self.list_balls.index(ball)

        list_neighbours = [e for e in self.list_balls if e is not ball]

        for neighbour in list_neighbours:
            j = self.list_balls.index(neighbour)
            forceij = self.getSeparationForce(ball, neighbour)

            if np.linalg.norm(forceij) > 0:
                self.list_separate_forces[i] = np.add(self.list_separate_forces[i], forceij)
                self.list_separate_forces[j] = np.subtract(self.list_separate_forces[j], forceij)
                self.list_near_balls[i] += 1
                self.list_near_balls[j] += 1

        if self.list_near_balls[i] > 0:
            self.list_separate_forces[i] = np.divide(self.list_separate_forces[i], self.list_near_balls[i])


        if np.linalg.norm(self.list_separate_forces[i]) > 0:
            self.list_separate_forces[i] = self._normalize(self.list_separate_forces[i])
            self.list_separate_forces[i] = np.subtract(self.list_separate_forces[i], ball.velocity)
            self.list_separate_forces[i] = np.clip(self.list_separate_forces[i], a_min=0, a_max=np.array([1]))

        separation = self.list_separate_forces[i]
        ball.applyForce(separation)
        ball.update()


list_balls = list()

for i in range(10):
    b = Ball(0, 0, 7)
    list_balls.append(b)


p = Pack(30, list_balls)
p.run()

plt.axes()

# Big container
circle = plt.Circle((0, 0), radius=30, fc='none', ec='k')
plt.gca().add_patch(circle)

for c in list_balls:
    ball = plt.Circle((c.x, c.y), radius=c.r, picker=True, fc='none', ec='k')
    plt.gca().add_patch(ball)

plt.axis('scaled')
plt.show()

Первоначально код был написан с помощью Processing, я старался использовать вместо этого numpy. Я не совсем уверен в своем checkBallPosition, исходный автор использует переменную count, которая мне кажется бесполезной. Мне также интересно, почему вектор steer в исходном коде имеет размерность 3.

Вот что дает мой код:

введите здесь описание изображения

Круги (мне пришлось переименовать их в шары, чтобы не конфликтовать с Circle из matplotlib) перекрываются и, похоже, не уходят друг от друга. Я не думаю, что я очень далеко, но мне понадобится небольшая помощь, чтобы найти, что не так с моим кодом. Не могли бы вы мне помочь, пожалуйста ?

РЕДАКТИРОВАТЬ: я понимаю, что мне, вероятно, нужно сделать несколько проходов. Может быть, пакет обработки (язык?) Запускает функцию run несколько раз. Для меня это действительно имеет смысл, эта проблема очень похожа на оптимизацию молекулярной механики, и это повторяющийся процесс.

введите здесь описание изображения

Мой вопрос теперь может быть немного более конкретным: похоже, функция checkBorders не выполняет свою работу должным образом и не восстанавливает круги должным образом. Но, учитывая его простоту, я бы сказал, что ошибка в applySeparationForcesToBall, я, вероятно, неправильно приложу усилия.


person JPFrancoia    schedule 26.03.2018    source источник
comment
Задайте, пожалуйста, более конкретный вопрос. Я вижу, что вы потратили некоторое время на то, чтобы написать хороший пост, но на самом деле вопрос, в конце концов, просто просит помощи в отладке вашего кода. Постарайтесь определить, какая часть работает не так, как вы ожидаете, и сформулируйте конкретные вопросы (например, правильно ли, когда я использую 2-норму в X для вычисления Y), на которые можно дать конкретный ответ.   -  person allo    schedule 26.03.2018
comment
Я не пытался понять ваш код, но мне кажется странным, что вы делите r (радиус) в некоторых местах на 2.   -  person Arndt Jonasson    schedule 26.03.2018
comment
Кроме того, если создать два круга, перекрывающих друг друга, не будут ли они просто подпрыгивать взад и вперед?   -  person Arndt Jonasson    schedule 26.03.2018
comment
r/2 мне тоже кажется странным. Я одного не понимаю. Что касается вашего второго комментария, я так не думаю, если они перекрываются, checkBallPositions не обнуляет их скорости (которые инициализируются случайным образом), поэтому в какой-то момент они не будут перекрываться.   -  person JPFrancoia    schedule 26.03.2018
comment
Вы пробовали отлаживать свой код? Какая строка кода ведет себя не так, как вы ожидали?   -  person Kevin Workman    schedule 26.03.2018
comment
Да, конечно, пробовал. Но я не могу найти строку, которая ведет себя странно, даже если я уверен, что она хотя бы одна. Я обновил свой вопрос, чтобы было немного понятнее. Я понял, что нужно сделать несколько итераций. Вероятно, проблема в applySeparationForcesToBall, теперь круги выбрасываются из контейнера.   -  person JPFrancoia    schedule 26.03.2018
comment
Код обработки, о котором вы говорите, представляет собой анимацию, которая обновляется 60 раз в секунду и не содержит никакой логики для нахождения внутри прямоугольника. Вам повезет больше, если вы просто реализуете алгоритм с нуля, вместо того, чтобы заставлять существующую программу делать что-то еще. Я собираюсь удалить тег processing, потому что он не содержит вопросов об обработке.   -  person Kevin Workman    schedule 27.03.2018


Ответы (1)


Хорошо, после нескольких дней возни, мне удалось это сделать:

введите описание изображения здесь

Вот полный код:

#!/usr/bin/python
# coding: utf-8

"""
http://www.codeplastic.com/2017/09/09/controlled-circle-packing-with-processing/
https://stackoverflow.com/questions/573084/how-to-calculate-bounce-angle/573206#573206
https://stackoverflow.com/questions/4613345/python-pygame-ball-collision-with-interior-of-circle
"""

import numpy as np
import matplotlib.pyplot as plt
from random import randint
from random import uniform
from matplotlib import animation


class Ball:

    def __init__(self, x, y, radius):

        self.r = radius

        self.acceleration = np.array([0, 0])

        self.velocity = np.array([uniform(0, 1),
                                  uniform(0, 1)])

        self.position = np.array([x, y])


    @property
    def x(self):
        return self.position[0]

    @property
    def y(self):
        return self.position[1]


    def applyForce(self, force):

        self.acceleration = np.add(self.acceleration, force)

    def _normalize(self, v):

        norm = np.linalg.norm(v)
        if norm == 0:
            return v
        return v / norm


    def update(self):

        self.velocity = np.add(self.velocity, self.acceleration)
        self.position = np.add(self.position, self.velocity)
        self.acceleration *= 0


class Pack:

    def __init__(self, radius, list_balls):

        self.iter = 0
        self.list_balls = list_balls
        self.r = radius
        self.list_separate_forces = [np.array([0, 0])] * len(self.list_balls)
        self.list_near_balls = [0] * len(self.list_balls)
        self.wait = True


    def _normalize(self, v):

        norm = np.linalg.norm(v)
        if norm == 0:
            return v
        return v / norm


    def run(self):

        self.iter += 1
        for ball in self.list_balls:
            self.checkBorders(ball)
            self.checkBallPositions(ball)
            self.applySeparationForcesToBall(ball)
            print(ball.position)

        print("\n")


    def checkBorders(self, ball):

        d = np.sqrt(ball.x**2 + ball.y**2)

        if d >= self.r - ball.r:

            vr = self._normalize(ball.velocity) * ball.r

            # P1 is collision point between circle and container
            P1x = ball.x + vr[0]
            P1y = ball.y + vr[1]
            P1 = np.array([P1x, P1y])

            # Normal vector
            n_v = -1 * self._normalize(P1)

            u = np.dot(ball.velocity, n_v) * n_v
            w = np.subtract(ball.velocity, u)

            ball.velocity = np.subtract(w, u)

            ball.update()



    def checkBallPositions(self, ball):

        i = self.list_balls.index(ball)

        # for neighbour in list_neighbours:
        # ot a full loop; if we had two full loops, we'd compare every
        # particle to every other particle twice over (and compare each
        # particle to itself)
        for neighbour in self.list_balls[i + 1:]:

            d = self._distanceBalls(ball, neighbour)

            if d < (ball.r + neighbour.r):
                return

        ball.velocity[0] = 0
        ball.velocity[1] = 0


    def getSeparationForce(self, c1, c2):

        steer = np.array([0, 0])

        d = self._distanceBalls(c1, c2)

        if d > 0 and d < (c1.r + c2.r):
            diff = np.subtract(c1.position, c2.position)
            diff = self._normalize(diff)
            diff = np.divide(diff, 1 / d**2)
            steer = np.add(steer, diff)

        return steer


    def _distanceBalls(self, c1, c2):

        x1, y1 = c1.x, c1.y
        x2, y2 = c2.x, c2.y

        dist = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)

        return dist


    def applySeparationForcesToBall(self, ball):

        i = self.list_balls.index(ball)

        for neighbour in self.list_balls[i + 1:]:
            j = self.list_balls.index(neighbour)
            forceij = self.getSeparationForce(ball, neighbour)

            if np.linalg.norm(forceij) > 0:
                self.list_separate_forces[i] = np.add(self.list_separate_forces[i], forceij)
                self.list_separate_forces[j] = np.subtract(self.list_separate_forces[j], forceij)
                self.list_near_balls[i] += 1
                self.list_near_balls[j] += 1

        if np.linalg.norm(self.list_separate_forces[i]) > 0:
            self.list_separate_forces[i] = np.subtract(self.list_separate_forces[i], ball.velocity)

        if self.list_near_balls[i] > 0:
            self.list_separate_forces[i] = np.divide(self.list_separate_forces[i], self.list_near_balls[i])


        separation = self.list_separate_forces[i]
        ball.applyForce(separation)
        ball.update()


list_balls = list()

for i in range(25):
    # b = Ball(randint(-15, 15), randint(-15, 15), 5)
    b = Ball(0, 0, 5)
    list_balls.append(b)


p = Pack(30, list_balls)

fig = plt.figure()

circle = plt.Circle((0, 0), radius=30, fc='none', ec='k')
plt.gca().add_patch(circle)
plt.axis('scaled')
plt.axes().set_xlim(-50, 50)
plt.axes().set_ylim(-50, 50)


def draw(i):

    patches = []

    p.run()
    fig.clf()
    circle = plt.Circle((0, 0), radius=30, fc='none', ec='k')
    plt.gca().add_patch(circle)
    plt.axis('scaled')
    plt.axes().set_xlim(-50, 50)
    plt.axes().set_ylim(-50, 50)

    for c in list_balls:
        ball = plt.Circle((c.x, c.y), radius=c.r, picker=True, fc='none', ec='k')
        patches.append(plt.gca().add_patch(ball))

    return patches


co = False
anim = animation.FuncAnimation(fig, draw,
                               frames=500, interval=2, blit=True)


# plt.show()


anim.save('line2.gif', dpi=80, writer='imagemagick')

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

person JPFrancoia    schedule 28.03.2018