关于 nesting-optimization
nesting-optimization 是一项用于规划不规则多边形板材排样、减少材料浪费的技能。它帮助制造和切割规划团队梳理约束,并参考启发式与几何算法方法。
主要功能
- 01不规则二维形状排样
- 02板材与间距约束
- 03旋转和纹理方向对齐
- 04BL、遗传算法及 NFP 方法
- 05材料利用与切割目标
适用场景
- 01规划金属板或织物排样
- 02考虑纹理方向和边缘余量
- 03比较复杂多边形的排样方法
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill nesting-optimization安装后在 Claude Code 中输入/nesting-optimization确认已加载。
详细介绍
排样优化
你是异形形状排样优化和多边形装箱方面的专家。你的目标是帮助减少从板材(如金属板、皮革、织物、木材或复合材料)上切割不规则非矩形零件时的材料浪费。
初步评估
在解决排样问题之前,请先了解以下信息:
-
形状特征
- 有哪些形状类型?(简单多边形、复杂曲线、带孔形状)
- 如何定义形状?(坐标、CAD 文件、DXF、SVG)
- 有多少种不同的零件类型?
- 需要排样的零件总数是多少?
- 零件的复杂程度如何?(顶点/曲线数量)
-
材料规格
- 使用什么材料?(金属板、皮革、织物、木材、复合材料)
- 板材尺寸(宽 × 高)是多少?
- 是单一板材尺寸还是有多种可选尺寸?
- 每张板材或每单位面积的材料成本是多少?
- 是否需要考虑材料纹理方向?
- 是否存在需要避开的材料缺陷或区域?
-
排样约束
- 零件是否可以旋转?(任意角度或 90°、180° 等离散角度)
- 零件之间的最小间距是多少?
- 零件与板材边缘之间的最小距离是多少?
- 零件是否必须与材料纹理方向对齐?
- 是否有必须组合在一起的零件?
- 每张板材最多可放置多少个零件?
-
切割技术
- 使用手工切割、CNC router、激光、水刀还是等离子切割?
- 是否需要优化切割路径?
- 是否需要连接桥/定位片来固定零件?
- 是否对起点/终点位置有约束?
-
优化目标
- 尽量减少板材数量?
- 尽量减少材料总面积/成本?
- 尽可能提高板材利用率?
- 尽量缩短切割时间/路径长度?
- 如何平衡多个目标?
排样问题框架
问题分类
1. 异形排样(二维装箱)
- 将任意多边形/形状排放到板材上
- 尽量减少使用的板材或材料浪费
- 最通用、最复杂的变体
- NP 难问题
2. 异形条带排样
- 宽度固定,尽量减少高度
- 使用单条长材料
- 常见于织物/纺织品切割
- 以最小化高度为目标
3. 受约束排样
- 其他约束条件:
- 固定方向
- 与材料纹理方向对齐
- 零件组合要求
- 材料上的质量区域
4. 在线排样
- 零件随时间动态到达
- 无法重新调整已放置的零件
- 实时决策
- 与生产调度集成
5. 多材料排样
- 使用成本不同的多种材料
- 将零件分配给合适的材料
- 尽量降低材料总成本
数学建模
基本排样问题
已知:
- S = 尺寸为 W × H 的板材
- P = {p₁, p₂, ..., pₙ} = 零件(多边形)集合
- d_i = 零件 p_i 的需求量
决策变量:
- x_i, y_i = 零件 i 的位置(参考点)
- θ_i = 零件 i 的旋转角度
- s_j = 若使用板材 j,则为 1;否则为 0
目标: 最小化 Σ s_j(尽量减少板材数量)
或:最小化所用材料的总面积
约束:
-
不重叠: 零件不能相互重叠
- 对所有零件对 (i, k):φ(p_i, p_k, x_i, y_i, θ_i, x_k, y_k, θ_k) = 0
- 其中 φ 为重叠函数
-
边界包含: 零件必须位于板材边界内
- 对所有 i,p_i(x_i, y_i, θ_i) ⊆ S
-
间距: 零件之间的最小距离
- 对所有零件对 (i, k),d(p_i, p_k) ≥ d_min
-
需求量: 满足数量要求
- 每种零件的放置数量必须正确
复杂度:
- 强 NP 难
- 对一般情况不存在高效的精确算法
- 需要采用元启发式方法
算法与求解方法
方法 1:自下而上、从左至右(BL)启发式
import numpy as np
from shapely.geometry import Polygon, Point
from shapely.affinity import translate, rotate
import matplotlib.pyplot as plt
class BottomLeftNesting:
"""
Bottom-Left (BL) Heuristic for Polygon Nesting
Classic nesting heuristic:
1. Place each part at bottom-left-most feasible position
2. Check overlaps and containment
3. Move until valid position found
Fast but not optimal - good starting solution
"""
def __init__(self, sheet_width, sheet_height, spacing=0):
"""
Initialize nesting solver
Parameters:
- sheet_width: width of sheet
- sheet_height: height of sheet
- spacing: minimum spacing between parts
"""
self.sheet_width = sheet_width
self.sheet_height = sheet_height
self.spacing = spacing
self.parts = []
self.placed_parts = []
def add_part(self, polygon_coords, quantity=1, part_id=None, rotation_allowed=True):
"""
Add part to nest
Parameters:
- polygon_coords: list of (x,y) coordinates defining polygon
- quantity: number of this part needed
- part_id: identifier
- rotation_allowed: can part be rotated?
"""
if part_id is None:
part_id = f"Part_{len(self.parts)}"
polygon = Polygon(polygon_coords)
for i in range(quantity):
self.parts.append({
'id': f"{part_id}_{i}" if quantity > 1 else part_id,
'polygon': polygon,
'original_polygon': polygon,
'rotation_allowed': rotation_allowed,
'area': polygon.area
})
def find_bottom_left_position(self, part_polygon, placed_polygons):
"""
Find bottom-left position for a part
Strategy:
1. Start at (0, 0)
2. Move right until no overlap
3. Move down if possible
4. Repeat until valid position found
"""
# Try different positions in a grid search
step = 10 # mm grid resolution
for y in range(0, int(self.sheet_height), step):
for x in range(0, int(self.sheet_width), step):
# Try this position
test_polygon = translate(part_polygon, xoff=x, yoff=y)
# Check if fits in sheet
if not self._fits_in_sheet(test_polygon):
continue
# Check overlaps with placed parts
if self._has_overlap(test_polygon, placed_polygons):
continue
# Valid position found
return x, y, test_polygon
return None, None, None
def _fits_in_sheet(self, polygon):
"""Check if polygon fits within sheet boundaries"""
bounds = polygon.bounds # (minx, miny, maxx, maxy)
return (bounds[0] >= 0 and
bounds[1] >= 0 and
bounds[2] <= self.sheet_width and
bounds[3] <= self.sheet_height)
def _has_overlap(self, polygon, placed_polygons):
"""Check if polygon overlaps with any placed polygons"""
for placed in placed_polygons:
# Buffer for spacing
if self.spacing > 0:
buffered_placed = placed.buffer(self.spacing / 2)
buffered_polygon = polygon.buffer(self.spacing / 2)
if buffered_polygon.intersects(buffered_placed):
return True
else:
if polygon.intersects(placed):
return True
return False
def nest(self, rotation_angles=[0, 90, 180, 270]):
"""
Perform bottom-left nesting
Parameters:
- rotation_angles: list of angles to try (degrees)
Returns: nesting solution
"""
# Sort parts by area (largest first)
sorted_parts = sorted(self.parts, key=lambda p: p['area'], reverse=True)
sheets = []
current_sheet_parts = []
for part in sorted_parts:
placed = False
# Try rotations if allowed
angles_to_try = rotation_angles if part['rotation_allowed'] else [0]
best_position = None
best_polygon = None
best_angle = 0
for angle in angles_to_try:
# Rotate part
rotated_polygon = rotate(part['polygon'], angle, origin='centroid')
# Find position
x, y, positioned_polygon = self.find_bottom_left_position(
rotated_polygon, current_sheet_parts
)
if x is not None:
# Check if this is better (more bottom-left)
if best_position is None or (y < best_position[1] or
(y == best_position[1] and x < best_position[0])):
best_position = (x, y)
best_polygon = positioned_polygon
best_angle = angle
if best_position is not None:
# Place part
current_sheet_parts.append(best_polygon)
self.placed_parts.append({
'id': part['id'],
'polygon': best_polygon,
'position': best_position,
'angle': best_angle,
'sheet': len(sheets)
})
placed = True
if not placed:
# Start new sheet
if current_sheet_parts:
sheets.append(current_sheet_parts)
current_sheet_parts = []
# Place on new sheet
for angle in angles_to_try:
rotated_polygon = rotate(part['polygon'], angle, origin='centroid')
x, y, positioned_polygon = self.find_bottom_left_position(
rotated_polygon, []
)
if x is not None:
current_sheet_parts.append(positioned_polygon)
self.placed_parts.append({
'id': part['id'],
'polygon': positioned_polygon,
'position': (x, y),
'angle': angle,
'sheet': len(sheets)
})
break
# Add last sheet
if current_sheet_parts:
sheets.append(current_sheet_parts)
# Calculate statistics
total_part_area = sum(p['polygon'].area for p in self.placed_parts)
total_sheet_area = len(sheets) * self.sheet_width * self.sheet_height
utilization = (total_part_area / total_sheet_area * 100) if total_sheet_area > 0 else 0
return {
'num_sheets': len(sheets),
'sheets': sheets,
'placed_parts': self.placed_parts,
'utilization': utilization,
'waste': total_sheet_area - total_part_area
}
def visualize(self, sheet_index=0, save_path=None):
"""
Visualize nesting for a specific sheet
Parameters:
- sheet_index: which sheet to visualize
- save_path: path to save figure
"""
if not self.placed_parts:
raise ValueError("No nesting solution. Run nest() first.")
# Get parts for this sheet
sheet_parts = [p for p in self.placed_parts if p['sheet'] == sheet_index]
if not sheet_parts:
raise ValueError(f"Sheet {sheet_index} has no parts")
fig, ax = plt.subplots(figsize=(12, 10))
# Draw sheet boundary
ax.add_patch(plt.Rectangle(
(0, 0), self.sheet_width, self.sheet_height,
fill=False, edgecolor='black', linewidth=2
))
# Draw parts
colors = plt.cm.tab20(np.linspace(0, 1, 20))
for idx, part in enumerate(sheet_parts):
polygon = part['polygon']
color = colors[idx % 20]
# Draw polygon
x, y = polygon.exterior.xy
ax.fill(x, y, facecolor=color, edgecolor='black',
linewidth=1.5, alpha=0.6)
# Add label
centroid = polygon.centroid
ax.text(centroid.x, centroid.y, part['id'],
ha='center', va='center',
fontsize=9, fontweight='bold',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
# Add rotation indicator if rotated
if part['angle'] != 0:
ax.text(centroid.x, centroid.y - 20, f"∠{part['angle']}°",
ha='center', va='top',
fontsize=7, style='italic')
ax.set_xlim(-50, self.sheet_width + 50)
ax.set_ylim(-50, self.sheet_height + 50)
ax.set_aspect('equal')
ax.set_xlabel('Width (mm)', fontsize=12)
ax.set_ylabel('Height (mm)', fontsize=12)
ax.set_title(
f'Nesting Solution - Sheet {sheet_index + 1}\n'
f'Parts: {len(sheet_parts)} | Sheet: {self.sheet_width}×{self.sheet_height}mm',
fontsize=14, fontweight='bold'
)
ax.grid(True, alpha=0.3)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
# Example usage
def example_bottom_left_nesting():
"""Example: Nesting irregular shapes"""
nester = BottomLeftNesting(sheet_width=3000, sheet_height=1500, spacing=5)
# Add some irregular parts (simplified polygons)
# L-shaped part
l_shape = [(0, 0), (100, 0), (100, 200), (50, 200), (50, 50), (0, 50)]
nester.add_part(l_shape, quantity=5, part_id='L_Bracket')
# T-shaped part
t_shape = [(0, 0), (150, 0), (150, 30), (90, 30), (90, 100), (60, 100), (60, 30), (0, 30)]
nester.add_part(t_shape, quantity=8, part_id='T_Bracket')
# Rectangular part
rect = [(0, 0), (80, 0), (80, 120), (0, 120)]
nester.add_part(rect, quantity=10, part_id='Rectangle')
# Nest
print("Nesting parts...")
solution = nester.nest(rotation_angles=[0, 90, 180, 270])
print(f"\nSolution:")
print(f"Sheets needed: {solution['num_sheets']}")
print(f"Utilization: {solution['utilization']:.2f}%")
print(f"Waste: {solution['waste']:.0f} mm²")
# Visualize
nester.visualize(sheet_index=0)
return solution
方法 2:用于排样的遗传算法
import random
import numpy as np
from shapely.geometry import Polygon
from shapely.affinity import translate, rotate
class GeneticAlgorithmNesting:
"""
Genetic Algorithm for Polygon Nesting
Chromosome encoding:
- Sequence of parts (permutation)
- Rotation angles for each part
Fitness: Minimizes sheets used and waste
"""
def __init__(self, sheet_width, sheet_height, spacing=0,
population_size=50, generations=100,
mutation_rate=0.1, crossover_rate=0.8):
"""
Initialize GA nesting solver
Parameters:
- sheet_width, sheet_height: sheet dimensions
- spacing: minimum spacing between parts
- population_size: GA population size
- generations: number of generations
- mutation_rate: probability of mutation
- crossover_rate: probability of crossover
"""
self.sheet_width = sheet_width
self.sheet_height = sheet_height
self.spacing = spacing
self.population_size = population_size
self.generations = generations
self.mutation_rate = mutation_rate
self.crossover_rate = crossover_rate
self.parts = []
self.n_parts = 0
self.best_solution = None
self.best_fitness = float('inf')
def add_part(self, polygon_coords, quantity=1, part_id=None,
rotation_angles=[0, 90, 180, 270]):
"""
Add part to nest
Parameters:
- polygon_coords: list of (x,y) coordinates
- quantity: number of parts
- part_id: identifier
- rotation_angles: allowed rotation angles
"""
if part_id is None:
part_id = f"Part_{len(self.parts)}"
polygon = Polygon(polygon_coords)
for i in range(quantity):
self.parts.append({
'id': f"{part_id}_{i}" if quantity > 1 else part_id,
'polygon': polygon,
'rotation_angles': rotation_angles,
'area': polygon.area
})
self.n_parts = len(self.parts)
def create_chromosome(self):
"""
Create random chromosome
Chromosome = (permutation, rotation_angles)
- permutation: order to place parts
- rotation_angles: angle choice for each part
"""
permutation = list(np.random.permutation(self.n_parts))
rotation_indices = [random.randint(0, len(self.parts[i]['rotation_angles']) - 1)
for i in range(self.n_parts)]
return {
'permutation': permutation,
'rotation_indices': rotation_indices
}
def decode_chromosome(self, chromosome):
"""
Decode chromosome to nesting solution
Use Bottom-Left placement with specified order and rotations
"""
permutation = chromosome['permutation']
rotation_indices = chromosome['rotation_indices']
sheets = [[]] # List of lists of polygons
placed_parts = []
for idx in permutation:
part = self.parts[idx]
angle_idx = rotation_indices[idx]
angle = part['rotation_angles'][angle_idx]
# Rotate part
rotated_polygon = rotate(part['polygon'], angle, origin='centroid')
# Try to place on current sheet
placed = False
for sheet_idx, sheet in enumerate(sheets):
# Try bottom-left placement
position = self._find_position_in_sheet(rotated_polygon, sheet)
if position is not None:
x, y = position
placed_polygon = translate(rotated_polygon, xoff=x, yoff=y)
sheet.append(placed_polygon)
placed_parts.append({
'id': part['id'],
'polygon': placed_polygon,
'sheet': sheet_idx,
'angle': angle
})
placed = True
break
if not placed:
# Create new sheet
position = self._find_position_in_sheet(rotated_polygon, [])
if position:
x, y = position
placed_polygon = translate(rotated_polygon, xoff=x, yoff=y)
sheets.append([placed_polygon])
placed_parts.append({
'id': part['id'],
'polygon': placed_polygon,
'sheet': len(sheets) - 1,
'angle': angle
})
return {
'sheets': sheets,
'placed_parts': placed_parts,
'num_sheets': len(sheets)
}
def _find_position_in_sheet(self, polygon, placed_polygons):
"""Find valid position for polygon in sheet"""
# Simplified: try grid positions
step = 20
for y in range(0, int(self.sheet_height), step):
for x in range(0, int(self.sheet_width), step):
test_polygon = translate(polygon, xoff=x, yoff=y)
# Check bounds
bounds = test_polygon.bounds
if (bounds[0] < 0 or bounds[1] < 0 or
bounds[2] > self.sheet_width or bounds[3] > self.sheet_height):
continue
# Check overlaps
valid = True
for placed in placed_polygons:
if self.spacing > 0:
if test_polygon.buffer(self.spacing/2).intersects(
placed.buffer(self.spacing/2)):
valid = False
break
else:
if test_polygon.intersects(placed):
valid = False
break
if valid:
return (x, y)
return None
def fitness(self, chromosome):
"""
Calculate fitness of chromosome
Fitness = num_sheets + waste_penalty
Lower is better
"""
solution = self.decode_chromosome(chromosome)
num_sheets = solution['num_sheets']
# Calculate total waste
total_part_area = sum(p['polygon'].area for p in solution['placed_parts'])
total_sheet_area = num_sheets * self.sheet_width * self.sheet_height
waste = total_sheet_area - total_part_area
# Fitness function
fitness = num_sheets * 10000 + waste
return fitness
def crossover(self, parent1, parent2):
"""
Order Crossover for permutation
Uniform crossover for rotation angles
"""
if random.random() > self.crossover_rate:
return parent1.copy(), parent2.copy()
# Order crossover for permutation
size = self.n_parts
cx_point1 = random.randint(0, size - 2)
cx_point2 = random.randint(cx_point1 + 1, size - 1)
# Create children
child1_perm = [-1] * size
child2_perm = [-1] * size
# Copy segments
child1_perm[cx_point1:cx_point2] = parent1['permutation'][cx_point1:cx_point2]
child2_perm[cx_point1:cx_point2] = parent2['permutation'][cx_point1:cx_point2]
# Fill remaining
self._fill_permutation(child1_perm, parent2['permutation'], cx_point2)
self._fill_permutation(child2_perm, parent1['permutation'], cx_point2)
# Uniform crossover for rotation indices
child1_rot = []
child2_rot = []
for i in range(size):
if random.random() < 0.5:
child1_rot.append(parent1['rotation_indices'][i])
child2_rot.append(parent2['rotation_indices'][i])
else:
child1_rot.append(parent2['rotation_indices'][i])
child2_rot.append(parent1['rotation_indices'][i])
child1 = {'permutation': child1_perm, 'rotation_indices': child1_rot}
child2 = {'permutation': child2_perm, 'rotation_indices': child2_rot}
return child1, child2
def _fill_permutation(self, child, parent, start_pos):
"""Fill remaining positions in child permutation"""
child_set = set([x for x in child if x != -1])
pos = start_pos
for item in parent[start_pos:] + parent[:start_pos]:
if item not in child_set:
while child[pos % len(child)] != -1:
pos += 1
child[pos % len(child)] = item
child_set.add(item)
def mutate(self, chromosome):
"""
Mutation: swap two positions in permutation
Random change in rotation angles
"""
# Mutate permutation
if random.random() < self.mutation_rate:
i, j = random.sample(range(self.n_parts), 2)
chromosome['permutation'][i], chromosome['permutation'][j] = \
chromosome['permutation'][j], chromosome['permutation'][i]
# Mutate rotation angles
if random.random() < self.mutation_rate:
idx = random.randint(0, self.n_parts - 1)
part = self.parts[chromosome['permutation'][idx]]
chromosome['rotation_indices'][idx] = random.randint(
0, len(part['rotation_angles']) - 1
)
return chromosome
def tournament_selection(self, population, fitnesses, tournament_size=3):
"""Tournament selection"""
tournament_idx = random.sample(range(len(population)), tournament_size)
tournament_fit = [fitnesses[i] for i in tournament_idx]
winner_idx = tournament_idx[tournament_fit.index(min(tournament_fit))]
return population[winner_idx]
def solve(self):
"""
Run genetic algorithm
Returns: best nesting solution found
"""
print("Starting Genetic Algorithm Nesting...")
print(f"Parts: {self.n_parts}")
print(f"Population: {self.population_size}")
print(f"Generations: {self.generations}")
print()
# Initialize population
population = [self.create_chromosome() for _ in range(self.population_size)]
for generation in range(self.generations):
# Evaluate fitness
fitnesses = [self.fitness(chrom) for chrom in population]
# Track best
min_fit_idx = fitnesses.index(min(fitnesses))
if fitnesses[min_fit_idx] < self.best_fitness:
self.best_fitness = fitnesses[min_fit_idx]
self.best_solution = self.decode_chromosome(population[min_fit_idx])
print(f"Generation {generation}: Best fitness = {self.best_fitness:.0f}, "
f"Sheets = {self.best_solution['num_sheets']}")
# Create new population
new_population = []
# Elitism
new_population.append(population[min_fit_idx])
# Generate offspring
while len(new_population) < self.population_size:
parent1 = self.tournament_selection(population, fitnesses)
parent2 = self.tournament_selection(population, fitnesses)
child1, child2 = self.crossover(parent1, parent2)
child1 = self.mutate(child1)
child2 = self.mutate(child2)
new_population.extend([child1, child2])
population = new_population[:self.population_size]
print(f"\nFinal solution:")
print(f"Sheets: {self.best_solution['num_sheets']}")
return self.best_solution
# Example usage
def example_genetic_nesting():
"""Example: GA-based nesting"""
ga = GeneticAlgorithmNesting(
sheet_width=3000,
sheet_height=1500,
spacing=5,
population_size=30,
generations=50
)
# Add parts
l_shape = [(0, 0), (100, 0), (100, 200), (50, 200), (50, 50), (0, 50)]
ga.add_part(l_shape, quantity=8, part_id='L_Bracket')
t_shape = [(0, 0), (150, 0), (150, 30), (90, 30), (90, 100), (60, 100), (60, 30), (0, 30)]
ga.add_part(t_shape, quantity=6, part_id='T_Bracket')
solution = ga.solve()
return solution
方法 3:基于无适配多边形(NFP)的排样
class NoFitPolygonNesting:
"""
No-Fit Polygon (NFP) Based Nesting
NFP is a geometric construct that represents all positions
where polygon A can be placed relative to polygon B such that
A just touches but doesn't overlap with B.
This is the state-of-the-art for industrial nesting but
computationally complex.
Simplified implementation for demonstration.
"""
def __init__(self, sheet_width, sheet_height):
self.sheet_width = sheet_width
self.sheet_height = sheet_height
def compute_nfp(self, polygon_a, polygon_b):
"""
Compute No-Fit Polygon for polygon_a relative to polygon_b
The NFP represents all positions where polygon_a touches
but doesn't overlap with polygon_b
This is a simplified placeholder - full NFP computation
is complex and typically uses Minkowski sum algorithms
"""
# In practice, use libraries like:
# - pyclipper for Minkowski operations
# - shapely for polygon operations
# - SVGNest algorithms
# Simplified: just return boundary approximation
from shapely.ops import unary_union
# Buffer approach (approximation)
nfp = polygon_b.buffer(0.01).boundary
return nfp
def nest_with_nfp(self, parts):
"""
Nest parts using NFP-based placement
This is a conceptual outline - full implementation
requires sophisticated NFP algorithms
"""
# This would implement:
# 1. Compute NFPs for all part pairs
# 2. Use NFPs to find valid placement positions
# 3. Optimize placement order and positions
# 4. Handle rotations using rotated NFPs
pass # Placeholder for full implementation
完整排样求解器
class ComprehensiveNestingSolver:
"""
Comprehensive Nesting Solver
Supports:
- Multiple algorithms (BL, GA)
- Visualization
- Export to DXF/SVG
"""
def __init__(self, sheet_width, sheet_height, spacing=0):
self.sheet_width = sheet_width
self.sheet_height = sheet_height
self.spacing = spacing
self.parts = []
self.solution = None
def add_part_from_coordinates(self, coords, quantity=1, part_id=None, rotatable=True):
"""Add part from coordinate list"""
self.parts.append({
'coords': coords,
'quantity': quantity,
'id': part_id or f"Part_{len(self.parts)}",
'rotatable': rotatable
})
def solve(self, method='bottom_left', **kwargs):
"""
Solve nesting problem
Methods:
- 'bottom_left': Bottom-left heuristic (fast)
- 'genetic': Genetic algorithm (better quality)
"""
if method == 'bottom_left':
solver = BottomLeftNesting(self.sheet_width, self.sheet_height, self.spacing)
for part in self.parts:
solver.add_part(part['coords'], part['quantity'], part['id'], part['rotatable'])
self.solution = solver.nest()
elif method == 'genetic':
solver = GeneticAlgorithmNesting(self.sheet_width, self.sheet_height, self.spacing)
for part in self.parts:
solver.add_part(part['coords'], part['quantity'], part['id'])
self.solution = solver.solve()
return self.solution
def print_summary(self):
"""Print solution summary"""
if not self.solution:
print("No solution available")
return
print("="*70)
print("NESTING SOLUTION")
print("="*70)
print(f"Sheets used: {self.solution['num_sheets']}")
print(f"Utilization: {self.solution.get('utilization', 0):.2f}%")
print(f"Sheet size: {self.sheet_width} × {self.sheet_height}")
工具与库
Python 库
- shapely:多边形运算和几何处理
- pyclipper:多边形裁剪和偏移
- SVGNest:JavaScript 排样(可从 Python 调用)
- nestable:Python 排样库
商业软件
- SigmaNEST:面向制造业的专业排样软件
- TruTops:钣金排样(Trumpf)
- Lantek:CNC 排样和切割
- Alma CAM:适用于多个行业的排样软件
- DeepNest:开源的 Web 排样软件
常见挑战与解决方案
挑战:复杂的不规则形状
**解决方案:**使用基于 NFP 的算法或高级元启发式算法
挑战:计算耗时
**解决方案:**采用分层排样、并行处理和限时搜索
挑战:材料纹理方向
**解决方案:**为放置算法添加方向约束
输出格式
排样报告:
- 板材:12
- 利用率:84.5%
- 零件:145
- 废料:15.5%
要询问的问题
- 需要排样哪些形状?
- 板材尺寸是多少?
- 零件可以旋转吗?
- 最小间距是多少?
- 有哪些材料限制?
相关技能
- 2d-cutting-stock:用于矩形切割
- guillotine-cutting:用于满足闸刀式切割约束
- trim-loss-minimization:用于最大限度减少废料
- optimization-modeling:用于通用优化
评论
支持 Markdown