Как стать автором
Обновить

Упаковка N кругов различных диаметров на X листов (прямоугольников), заданных габаритов

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров3.2K

Другими словами мы имеем частный случай задачи по раскрою материала.

Немного предыстории: в одном из своих проектов у меня появилась задача по расчету необходимого количества листов металла для производства деталей круглой формы. В моем случае это листы металла стандартного габарита 6000x1500 миллиметров.

Поискав готовые решения этой задачи, я решил написать свой вариант.

Не судите строго это все родилось за несколько часов :)

Постановка задачи

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

Описание алгоритма

Из полученного на входе словаря, содержащего радиусы кругов и их количества мы формируем массив, вида:

# Словарь на входе от пользователя
user_circles = {
    830 : 1,
    730 : 2,
    490 : 4,
    360 : 6,
    280 : 8,
    150 : 12,
    100 : 16,
    50 : 18,
}
# Массив упорядоченных по убыванию радиуса объектов-кругов
self.circles = [
  Circle(830),
  Circle(730),
  Circle(730),
  Circle(490),
  Circle(490),
  Circle(490),
  Circle(490),
  ...
]

Далее мы создаем новый лист (прямоугольник) заданных пользователем размеров:

self.sheets.append(Sheet(self.sheet_w, self.sheet_h))

Организуем цикл по массиву с упорядоченными объектами-кругами и для каждого круга используем метод поиска наилучшей координаты Y:

for i in self.circles:
  i.cx, i.cy = self.find_best_packing_start_point(current_sheet, i)

Вот небольшая иллюстрация этого алгоритма:

Для каждого следующего круга ищется наилучшая координата Y
Для каждого следующего круга ищется наилучшая координата Y

Если очередной круг был успешно помещен на текущий лист (прямоугольник) то из словаря кругов, заданного пользователей вычитается круг данного радиуса:

self.user_circles[i.r] -= 1

Если текущий круг не входит в лист (прямоугольник) по габаритам, то он добавляется в переменную:

self.circles_excluded[i.r] += 1

Когда для текущего листа (прямоугольника) мы попробовали разместить все круги из массива self.circles мы генерируем заново массив self.circles из оставшихся для размещения кругов:

# Убираем круги, которые уже размещены
  def clean_circles(self):
    circles_cleaned = []
    for i in self.user_circles.keys():
      for k in range(self.user_circles[i]):
        circles_cleaned.append(Circle(i))
    self.circles = circles_cleaned

Далее мы добавляем следующий лист и повторяем алгоритм пока не останется кругов для размещения.

Исходный код на Python

import svgwrite
import math

class Sheet:

  def __init__(self, w, h):
    self.w = w
    self.h = h
    self.circles = []


class Circle:

  def __init__(self, r, cx=None, cy=None):
    self.cx = cx
    self.cy = cy
    self.r = r
  
  
  @staticmethod
  def two_circle_intersections(c1, c2):
    x0, y0, r0 = c1.cx, c1.cy, c1.r
    x1, y1, r1 = c2.cx, c2.cy, c2.r
    d = math.sqrt((x1-x0)**2 + (y1-y0)**2)
    if d > r0 + r1 :
        return False
    if d < abs(r0-r1):
        return False
    if d == 0 and r0 == r1:
        return False
    else:
      return True


class CirclePacking:

  ACCURACY = 20 # На сколько частей делим высоту для поиска наилучшей позиции по вертикали для текущего круга
  
  def __init__(self, sheet_w, sheet_h, circles, cut_border):
    self.sheet_w = sheet_w
    self.sheet_h = sheet_h
    self.user_circles = circles
    self.circles = []
    self.sheets = []
    self.circles_excluded = {}
    self.cut_border = cut_border


  def create_circles_sorted(self):
    self.circles = []
    for i in reversed(sorted(self.user_circles.keys())):
      for k in range(self.user_circles[i]):
        self.circles.append(Circle(i))


  # Убираем круги, которые уже размещены
  def clean_circles(self):
    circles_cleaned = []
    for i in self.user_circles.keys():
      for k in range(self.user_circles[i]):
        circles_cleaned.append(Circle(i))
    self.circles = circles_cleaned


  def packing(self):
    self.create_circles_sorted()
    while sum(self.user_circles.values()) > 0:
      #print(self.user_circles, self.circles)
      self.sheets.append(Sheet(self.sheet_w, self.sheet_h))
      current_sheet = self.sheets[-1]
      for i in self.circles:
        i.cx, i.cy = self.find_best_packing_start_point(current_sheet, i)
        if i.cx is None and i.cy is None:
          if i.r not in self.circles_excluded:
            self.circles_excluded[i.r] = 1
          else:
            self.circles_excluded[i.r] += 1
          self.user_circles[i.r] -= 1
        else:
          # Проверка что мы не вышли за длину листа
          if i.cx > current_sheet.w - i.r - self.cut_border:
            pass
          else:
            self.user_circles[i.r] -= 1
            current_sheet.circles.append(i)
      self.clean_circles()


  # Ищем наилучшую позицию по вертикали для размещения круга 
  def find_best_packing_start_point(self, s, c):
    try_results = {}
    N = CirclePacking.ACCURACY
    step = int((s.h -c.r) / N)
    # Формируем массив координат по вертикали для попыток
    try_y = [c.r, s.h-c.r, s.h/2]
    for i in range(int((s.h - c.r) / step)):
      try_y.append(c.r + step * i)
    # Пробуем все координаты из массива попыток
    for i in try_y:
      c.cx = s.w + c.r
      c.cy = i
      self.move_circle(s, c)
      # Проверяем что мы вошли на лист
      if c.cy >= c.r + self.cut_border and c.cy <= s.h - c.r - self.cut_border:
        try_results[c.cx] = c.cy
    if try_results == {}:
      cx = cy = None
    else:
      cx = sorted(try_results.keys())[0]
      cy = try_results[cx]
    return cx, cy


  def move_circle(self, s, c):
    while c.cx > c.r + self.cut_border:
      if self.check_intersections(s, c):
        return
      c.cx -= 1


  def check_intersections(self, s, c):
    for i in s.circles:
      i.r += self.cut_border
      if Circle.two_circle_intersections(i, c):
        i.r -= self.cut_border
        return True
      i.r -= self.cut_border
    return False


  def draw_sheet_with_circles(self, sheet_idx, scale, filename):
    sheet = self.sheets[sheet_idx]
    w = sheet.w * scale
    h = sheet.h * scale
    border_x = 150 * scale
    border_y = 150 * scale
    svg = svgwrite.Drawing(filename = filename, size = (str(w+border_x*2)+'px', str(h+border_y*2)+'px'))
    svg.add(svg.line(stroke='red', start=(str(0+border_x)+'px', str(0+border_y)+'px'), end=(str(w+border_x)+'px', str(0+border_y)+'px')))
    svg.add(svg.line(stroke='red', start=(str(0+border_x)+'px', str(0+border_y)+'px'), end=(str(0+border_x)+'px', str(h+border_y)+'px')))
    svg.add(svg.line(stroke='red', start=(str(0+border_x)+'px', str(h+border_y)+'px'), end=(str(w+border_x)+'px', str(h+border_y)+'px')))
    line1 = svg.add(svg.line(stroke='red', start=(str(w+border_x)+'px', str(0+border_y)+'px'), end=(str(w+border_x)+'px', str(h+border_y)+'px')))
    line1.dasharray([5, 10])
    for i in sheet.circles:
      cx = i.cx * scale
      cy = i.cy * scale
      r = i.r * scale
      svg.add(svg.circle(center=(str(cx+border_x)+'px', str(cy+border_x)+'px'), r=str(r)+'px', stroke='black', fill='white', stroke_width=1))
    svg.save()

Пример работы алгоритма

Ниже приведен пример работы алгоритма:

circles = {
    830 : 1,
    730 : 2,
    490 : 4,
    360 : 6,
    280 : 8,
    150 : 12,
    100 : 16,
    50 : 18,
}

cp = CirclePacking(sheet_w=6000, sheet_h=1500, circles=circles, cut_border=20)
cp.packing()
Пример упаковки кругов на листы (прямоугольники)
Пример упаковки кругов на листы (прямоугольники)

Обратите внимание на то, что круг с радиусом 830 не вошел на листы 6000x1500 и он помещен в атрибут: cp.circles_excluded:

cp.circles_excluded
#  {830: 1}

Оставшиеся вопросы:

  • Как ускорить работу алгоритма не выходя за возможности стандартного Python (не использовать библиотеки ускорения кода Python)?

  • Как еще повысить плотность заполнения листов?

Ссылки

Github: https://github.com/tau15/python_circle_packing_in_rectangle

Google Colab: https://colab.research.google.com/drive/1e-RoNHStyqdyROZNPHga_vvrIKyRy3ZR?usp=sharing

Вопросы и предложения на алгоритму пишите мне: @TAU15

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А на каком бы языке Вы стали решать подобную задачу?
18.52% Python5
11.11% Java3
11.11% C/С++3
3.7% C#1
7.41% Haskell2
11.11% Go3
37.04% Другой10
Проголосовали 27 пользователей. Воздержались 6 пользователей.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 3: ↑2 и ↓1+1
Комментарии10

Публикации

Истории

Работа

Python разработчик
119 вакансий
Data Scientist
78 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань