Решение задачи раскроя арматуры¶

Из рабочей документации на фундамент извлекаем данные по длине отрезков арматуры (деталей) и их количестве

In [1]:
parts = ([8160]*42+[3960]*84+[3145]*165+[2855]*126)*2+([2965]*16+[2665]*20+[2005]*4+[1990]*12+[3530]*6)*7+ \
([2365]*28+[1990]*12+[1995]*4+[3530]*6)*11
parts.sort(reverse=True)  # Сортируем по уменьшению длины
len(parts)  # Количество деталей
Out[1]:
1790

Создаём список длин деталей и сортируем его по убыванию

In [2]:
parts_uniq = list(set(parts))
parts_uniq.sort(reverse=True)  # Сортируем по уменьшению длины
parts_uniq
Out[2]:
[8160, 3960, 3530, 3145, 2965, 2855, 2665, 2365, 2005, 1995, 1990]

Формируем спецификацию деталей (длина отрезка - количество отрезков)

In [3]:
from collections import Counter
counts = dict(Counter(parts))
counts
Out[3]:
{8160: 84,
 3960: 168,
 3530: 108,
 3145: 330,
 2965: 112,
 2855: 252,
 2665: 140,
 2365: 308,
 2005: 28,
 1995: 44,
 1990: 216}
In [4]:
# Поставочная длина прута арматуры
rodLength = 11700  # мм

Алгоритм одномерной упаковки Best-Fit Decreasing с использованием классов¶

In [5]:
# Класс Прут
class Rod:
    def __init__(self, length):
        self.parts = []  # список отрезков, сделанных из данного прута
        self.residue = length  # остаток
    
    def add_part(self, part):  # отрезание от прута отрезка
        self.parts.append(part)
        self.residue -= part

rods = [Rod(rodLength)]
for part in parts:
    best_rod = None
    min_residue = rodLength + 1
    
    for rod in rods:
        if rod.residue >= part and rod.residue < min_residue:
            min_residue = rod.residue
            best_rod = rod
    
    if best_rod:
        best_rod.add_part(part)
    else:
        new_rod = Rod(rodLength)
        new_rod.add_part(part)
        rods.append(new_rod)
In [6]:
# Требуемое количество прутов арматуры 11,7 м 
len(rods)
Out[6]:
486
In [7]:
# Класс схемы раскроя
class Scheme:
    def __init__(self, parts, residue, count):
        self.parts = parts  # список отрезков
        self.residue = residue  # остаток
        self.count = count  # сколько раз применить
In [8]:
# Набор схем раскроя
schemes = []
schemes.append(Scheme(rods[0].parts, rods[0].residue, 1))
for rod in rods[1:]:
    newScheme = True
    for scheme in schemes:
        if rod.parts == scheme.parts:
            scheme.count += 1
            newScheme = False
            break
    if newScheme:
        schemes.append(Scheme(rod.parts, rod.residue, 1))
In [9]:
# Сортируем схемы раскроя 1) по убыванию частоты применения, 2) по убыванию количества деталей в схеме
schemes.sort(key=lambda x: (-x.count, -len(x.parts)))
[(scheme.parts,  scheme.residue, scheme.count) for scheme in schemes]
Out[9]:
[([3145, 3145, 3145, 1990], 275, 90),
 ([8160, 3530], 10, 84),
 ([2855, 2855, 2855, 2855], 280, 62),
 ([3960, 3960, 3145], 635, 60),
 ([2365, 2365, 2365, 2365, 1995], 245, 44),
 ([2965, 2965, 2965, 2665], 140, 37),
 ([2365, 2365, 2365, 2365, 2005], 235, 28),
 ([2665, 2665, 2665, 2665], 1040, 25),
 ([1990, 1990, 1990, 1990, 1990], 1750, 24),
 ([3960, 3960, 3530], 250, 24),
 ([2365, 2365, 2365, 2365, 1990], 250, 5),
 ([2965, 2855, 2855, 2855], 170, 1),
 ([2855, 2665, 2665, 2665], 850, 1),
 ([1990], 9710, 1)]

Формируем данные для csv файла для отображения набора схем раскроя в табличном виде.

In [10]:
result = "; ".join([str(x) for x in parts_uniq]) + '; Остаток' + '; Количество'
print('N; ' + result)
for i, scheme in enumerate(schemes):
    print(i+1, end='')
    for part in parts_uniq:
        print(';', scheme.parts.count(part), end='')
    print(';', scheme.residue, ';', scheme.count)
N; 8160; 3960; 3530; 3145; 2965; 2855; 2665; 2365; 2005; 1995; 1990; Остаток; Количество
1; 0; 0; 0; 3; 0; 0; 0; 0; 0; 0; 1; 275 ; 90
2; 1; 0; 1; 0; 0; 0; 0; 0; 0; 0; 0; 10 ; 84
3; 0; 0; 0; 0; 0; 4; 0; 0; 0; 0; 0; 280 ; 62
4; 0; 2; 0; 1; 0; 0; 0; 0; 0; 0; 0; 635 ; 60
5; 0; 0; 0; 0; 0; 0; 0; 4; 0; 1; 0; 245 ; 44
6; 0; 0; 0; 0; 3; 0; 1; 0; 0; 0; 0; 140 ; 37
7; 0; 0; 0; 0; 0; 0; 0; 4; 1; 0; 0; 235 ; 28
8; 0; 0; 0; 0; 0; 0; 4; 0; 0; 0; 0; 1040 ; 25
9; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 5; 1750 ; 24
10; 0; 2; 1; 0; 0; 0; 0; 0; 0; 0; 0; 250 ; 24
11; 0; 0; 0; 0; 0; 0; 0; 4; 0; 0; 1; 250 ; 5
12; 0; 0; 0; 0; 1; 3; 0; 0; 0; 0; 0; 170 ; 1
13; 0; 0; 0; 0; 0; 1; 3; 0; 0; 0; 0; 850 ; 1
14; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 1; 9710 ; 1

Программа для решения задачи раскроя¶

In [11]:
from ortools.linear_solver import pywraplp
import math

def generate_patterns(stock_length, part_lengths):
    """Генерирует допустимые шаблоны раскроя с помощью динамического программирования"""
    patterns = []
    max_counts = [stock_length // length for length in part_lengths]
    
    # Рекурсивная генерация шаблонов
    def generate_recursive(idx, current_pattern, current_length):
        if idx == len(part_lengths):
            patterns.append(current_pattern[:])
            return
        
        max_count = min(max_counts[idx], (stock_length - current_length) // part_lengths[idx])
        for count in range(max_count + 1):
            new_length = current_length + count * part_lengths[idx]
            if new_length <= stock_length:
                current_pattern[idx] = count
                generate_recursive(idx + 1, current_pattern, new_length)
            current_pattern[idx] = 0  # Reset
    
    generate_recursive(0, [0] * len(part_lengths), 0)
    return patterns

def solve_cutting_stock(stock_length, part_types):
    # Преобразуем данные в списки
    part_lengths = list(part_types.keys())
    demands = [part_types[length] for length in part_lengths]
    n_types = len(part_lengths)
    
    # 1. Генерируем все возможные шаблоны раскроя
    patterns = generate_patterns(stock_length, part_lengths)
    print(f"Сгенерировано шаблонов раскроя: {len(patterns)}")
    
    # 2. Создаем решатель
    solver = pywraplp.Solver.CreateSolver('SCIP')
    
    # 3. Создаем переменные: сколько раз использовать каждый шаблон
    x = [solver.IntVar(0, sum(demands), f'x_{i}') for i in range(len(patterns))]
    
    # 4. Ограничения: удовлетворить спрос по всем типам деталей
    constraints = []
    for j in range(n_types):
        constraint = solver.RowConstraint(demands[j], demands[j], f'demand_{j}')
        for i, pattern in enumerate(patterns):
            constraint.SetCoefficient(x[i], pattern[j])
    
    # 5. Цель: минимизировать количество использованных прутьев
    objective = solver.Objective()
    for i in range(len(patterns)):
        objective.SetCoefficient(x[i], 1)
    objective.SetMinimization()
    
    # 6. Решаем задачу
    status = solver.Solve()
    
    # 7. Обработка результатов
    if status == pywraplp.Solver.OPTIMAL:
        solution = []
        total_stocks = 0
        
        # Собираем решение
        for i, var in enumerate(x):
            count = int(var.solution_value() + 0.5)  # Округление для целочисленных значений
            if count > 0:
                total_stocks += count
                pattern_info = []
                for j, length in enumerate(part_lengths):
                    if patterns[i][j] > 0:
                        pattern_info.append(f"{patterns[i][j]}x{length}мм")
                solution.append({
                    'pattern': pattern_info,
                    'count': count,
                    'waste': stock_length - sum(patterns[i][j] * part_lengths[j] for j in range(n_types))
                })
        
        # Рассчитываем эффективность
        total_waste = sum(item['waste'] * item['count'] for item in solution)
        total_length = sum(length * count for length, count in part_types.items())
        efficiency = (1 - total_waste / (total_stocks * stock_length)) * 100
        
        return {
            'status': 'OPTIMAL',
            'total_stocks': total_stocks,
            'total_waste': total_waste,
            'efficiency': efficiency,
            'solution': solution
        }
    else:
        return {'status': 'NOT_SOLVED', 'message': 'Оптимальное решение не найдено'}

# Ваши данные
rod_length = 11700  # мм
part_types = {
    8160: 42*2,
    3960: 84*2,
    3145: 165*2,
    2855: 126*2,
    2965: 16*7,
    2665: 20*7,
    2005: 4*7,
    1990: 12*7 + 12*11,
    3530: 6*7 + 6*11,
    2365: 28*11,
    1995: 4*11
}

# Решаем задачу
result = solve_cutting_stock(rod_length, part_types)

# Выводим результаты
print("\nРезультаты раскроя:")
print(f"Статус решения: {result['status']}")
if result['status'] == 'OPTIMAL':
    print(f"Минимальное количество прутьев: {result['total_stocks']}")
    print(f"Общие отходы: {result['total_waste']} мм")
    print(f"Эффективность раскроя: {result['efficiency']:.2f}%")
    
    print("\nОптимальные шаблоны раскроя:")
    for i, item in enumerate(result['solution']):
        print(f"Шаблон {i+1}:")
        print(f"  - Используется {item['count']} раз")
        print(f"  - Детали: {', '.join(item['pattern'])}")
        print(f"  - Отходы на прут: {item['waste']} мм")
        print(f"  - Общие отходы: {item['waste'] * item['count']} мм")
        print()
    
    # Проверяем покрытие спроса
    demand_fulfilled = {length: 0 for length in part_types}
    for item in result['solution']:
        for part_str in item['pattern']:
            count, length = part_str.split('x')
            length = int(length.replace('мм', ''))
            demand_fulfilled[length] += int(count) * item['count']
    
    print("\nПроверка покрытия спроса:")
    for length, demand in part_types.items():
        fulfilled = demand_fulfilled[length]
        status = "OK" if fulfilled == demand else "ОШИБКА"
        print(f"Детали {length}мм: {fulfilled}/{demand} ({status})")
Сгенерировано шаблонов раскроя: 1015

Результаты раскроя:
Статус решения: OPTIMAL
Минимальное количество прутьев: 474
Общие отходы: 49170 мм
Эффективность раскроя: 99.11%

Оптимальные шаблоны раскроя:
Шаблон 1:
  - Используется 64 раз
  - Детали: 2x2665мм, 2x1990мм, 1x2365мм
  - Отходы на прут: 25 мм
  - Общие отходы: 1600 мм

Шаблон 2:
  - Используется 1 раз
  - Детали: 1x2855мм
  - Отходы на прут: 8845 мм
  - Общие отходы: 8845 мм

Шаблон 3:
  - Используется 2 раз
  - Детали: 1x2855мм, 2x2005мм, 2x2365мм
  - Отходы на прут: 105 мм
  - Общие отходы: 210 мм

Шаблон 4:
  - Используется 1 раз
  - Детали: 1x2855мм, 1x2965мм, 1x3530мм
  - Отходы на прут: 2350 мм
  - Общие отходы: 2350 мм

Шаблон 5:
  - Используется 23 раз
  - Детали: 1x3145мм, 1x2965мм, 1x2005мм, 1x3530мм
  - Отходы на прут: 55 мм
  - Общие отходы: 1265 мм

Шаблон 6:
  - Используется 15 раз
  - Детали: 2x3145мм, 1x2965мм, 1x2365мм
  - Отходы на прут: 80 мм
  - Общие отходы: 1200 мм

Шаблон 7:
  - Используется 72 раз
  - Детали: 2x3145мм, 1x2855мм, 1x2365мм
  - Отходы на прут: 190 мм
  - Общие отходы: 13680 мм

Шаблон 8:
  - Используется 44 раз
  - Детали: 3x3145мм, 1x1995мм
  - Отходы на прут: 270 мм
  - Общие отходы: 11880 мм

Шаблон 9:
  - Используется 6 раз
  - Детали: 1x3960мм, 2x2665мм, 1x2365мм
  - Отходы на прут: 45 мм
  - Общие отходы: 270 мм

Шаблон 10:
  - Используется 73 раз
  - Детали: 1x3960мм, 1x2965мм, 2x2365мм
  - Отходы на прут: 45 мм
  - Общие отходы: 3285 мм

Шаблон 11:
  - Используется 88 раз
  - Детали: 1x3960мм, 2x2855мм, 1x1990мм
  - Отходы на прут: 40 мм
  - Общие отходы: 3520 мм

Шаблон 12:
  - Используется 1 раз
  - Детали: 1x3960мм, 1x3145мм, 1x2005мм, 1x2365мм
  - Отходы на прут: 225 мм
  - Общие отходы: 225 мм

Шаблон 13:
  - Используется 84 раз
  - Детали: 1x8160мм, 1x3530мм
  - Отходы на прут: 10 мм
  - Общие отходы: 840 мм


Проверка покрытия спроса:
Детали 8160мм: 84/84 (OK)
Детали 3960мм: 168/168 (OK)
Детали 3145мм: 330/330 (OK)
Детали 2855мм: 252/252 (OK)
Детали 2965мм: 112/112 (OK)
Детали 2665мм: 140/140 (OK)
Детали 2005мм: 28/28 (OK)
Детали 1990мм: 216/216 (OK)
Детали 3530мм: 108/108 (OK)
Детали 2365мм: 308/308 (OK)
Детали 1995мм: 44/44 (OK)
In [12]:
# Результат работы программы
result['solution']
Out[12]:
[{'pattern': ['2x2665мм', '2x1990мм', '1x2365мм'], 'count': 64, 'waste': 25},
 {'pattern': ['1x2855мм'], 'count': 1, 'waste': 8845},
 {'pattern': ['1x2855мм', '2x2005мм', '2x2365мм'], 'count': 2, 'waste': 105},
 {'pattern': ['1x2855мм', '1x2965мм', '1x3530мм'], 'count': 1, 'waste': 2350},
 {'pattern': ['1x3145мм', '1x2965мм', '1x2005мм', '1x3530мм'],
  'count': 23,
  'waste': 55},
 {'pattern': ['2x3145мм', '1x2965мм', '1x2365мм'], 'count': 15, 'waste': 80},
 {'pattern': ['2x3145мм', '1x2855мм', '1x2365мм'], 'count': 72, 'waste': 190},
 {'pattern': ['3x3145мм', '1x1995мм'], 'count': 44, 'waste': 270},
 {'pattern': ['1x3960мм', '2x2665мм', '1x2365мм'], 'count': 6, 'waste': 45},
 {'pattern': ['1x3960мм', '1x2965мм', '2x2365мм'], 'count': 73, 'waste': 45},
 {'pattern': ['1x3960мм', '2x2855мм', '1x1990мм'], 'count': 88, 'waste': 40},
 {'pattern': ['1x3960мм', '1x3145мм', '1x2005мм', '1x2365мм'],
  'count': 1,
  'waste': 225},
 {'pattern': ['1x8160мм', '1x3530мм'], 'count': 84, 'waste': 10}]

Переведём возвращаемый результат в список схем раскроя

In [13]:
schemes =[]
for elem in result['solution']:
    original_list = elem['pattern']
    numbers_list = []
    for item in original_list:
        count, rest = item.split('x')  # Разделяем по 'x'
        num = int(rest.replace('мм', ''))  # Удаляем 'мм' и преобразуем в число
        numbers_list.extend([num] * int(count))  # Добавляем число `count` раз
    schemes.append(Scheme(numbers_list, elem['waste'], elem['count']))
In [14]:
[scheme.parts for scheme in schemes]
Out[14]:
[[2665, 2665, 1990, 1990, 2365],
 [2855],
 [2855, 2005, 2005, 2365, 2365],
 [2855, 2965, 3530],
 [3145, 2965, 2005, 3530],
 [3145, 3145, 2965, 2365],
 [3145, 3145, 2855, 2365],
 [3145, 3145, 3145, 1995],
 [3960, 2665, 2665, 2365],
 [3960, 2965, 2365, 2365],
 [3960, 2855, 2855, 1990],
 [3960, 3145, 2005, 2365],
 [8160, 3530]]

Отсортируем схемы по частоте применения

In [15]:
schemes.sort(key=lambda x: (-x.count, -len(x.parts)))
[(scheme.parts,  scheme.residue, scheme.count) for scheme in schemes]
Out[15]:
[([3960, 2855, 2855, 1990], 40, 88),
 ([8160, 3530], 10, 84),
 ([3960, 2965, 2365, 2365], 45, 73),
 ([3145, 3145, 2855, 2365], 190, 72),
 ([2665, 2665, 1990, 1990, 2365], 25, 64),
 ([3145, 3145, 3145, 1995], 270, 44),
 ([3145, 2965, 2005, 3530], 55, 23),
 ([3145, 3145, 2965, 2365], 80, 15),
 ([3960, 2665, 2665, 2365], 45, 6),
 ([2855, 2005, 2005, 2365, 2365], 105, 2),
 ([3960, 3145, 2005, 2365], 225, 1),
 ([2855, 2965, 3530], 2350, 1),
 ([2855], 8845, 1)]

Формируем данные для csv файла для отображения набора схем раскроя в табличном виде.

In [16]:
result = "; ".join([str(x) for x in parts_uniq]) + '; Остаток' + '; Количество'
print('N; ' + result)
for i, scheme in enumerate(schemes):
    print(i+1, end='')
    for part in parts_uniq:
        print(';', scheme.parts.count(part), end='')
    print(';', scheme.residue, ';', scheme.count)
N; 8160; 3960; 3530; 3145; 2965; 2855; 2665; 2365; 2005; 1995; 1990; Остаток; Количество
1; 0; 1; 0; 0; 0; 2; 0; 0; 0; 0; 1; 40 ; 88
2; 1; 0; 1; 0; 0; 0; 0; 0; 0; 0; 0; 10 ; 84
3; 0; 1; 0; 0; 1; 0; 0; 2; 0; 0; 0; 45 ; 73
4; 0; 0; 0; 2; 0; 1; 0; 1; 0; 0; 0; 190 ; 72
5; 0; 0; 0; 0; 0; 0; 2; 1; 0; 0; 2; 25 ; 64
6; 0; 0; 0; 3; 0; 0; 0; 0; 0; 1; 0; 270 ; 44
7; 0; 0; 1; 1; 1; 0; 0; 0; 1; 0; 0; 55 ; 23
8; 0; 0; 0; 2; 1; 0; 0; 1; 0; 0; 0; 80 ; 15
9; 0; 1; 0; 0; 0; 0; 2; 1; 0; 0; 0; 45 ; 6
10; 0; 0; 0; 0; 0; 1; 0; 2; 2; 0; 0; 105 ; 2
11; 0; 1; 0; 1; 0; 0; 0; 1; 1; 0; 0; 225 ; 1
12; 0; 0; 1; 0; 1; 1; 0; 0; 0; 0; 0; 2350 ; 1
13; 0; 0; 0; 0; 0; 1; 0; 0; 0; 0; 0; 8845 ; 1

Инженерные расчёты на Python, С.В. Медведев, 2020-2025
Использование Python и Jupyter Notebook для инженерных расчётов, С.В. Медведев, 2020-2025