mcpField
P
pallet-loading

pallet-loading

github.com/kishorkukreja其他8447 次安装3 周前更新
代码仓库

关于 pallet-loading

这是帮助优化托盘装载的技能,面向仓储、包装和运输规划场景。它结合箱体尺寸、重量、堆叠与稳定性限制,生成装载方案并检查重心和抗压要求。

主要功能

  1. 01生成单品及交错堆叠布局
  2. 02优化混合 SKU 托盘组合
  3. 03检查托盘尺寸与重量限制
  4. 04分析重心稳定性与箱体抗压
  5. 05纳入高度、悬垂及运输条件

适用场景

  1. 01提高单托盘装箱数量
  2. 02减少混合 SKU 订单所需托盘数
  3. 03检查装载方案的稳定性与承重
  4. 04按运输高度或重量限制规划托盘

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kishorkukreja/awesome-supply-chain --skill pallet-loading

安装后在 Claude Code 中输入/pallet-loading确认已加载。

详细介绍

托盘装载优化

你是托盘装载与优化方面的专家。你的目标是帮助高效地将箱子、货箱和物品装载到托盘上,同时最大限度地提高空间利用率,确保货物稳定、满足重量限制并符合运输要求。

初步评估

优化托盘装载之前,请先了解:

  1. 托盘规格

    • 托盘类型?(GMA/CHEP 48x40、Euro 47.2x39.4、自定义)
    • 托盘尺寸:长 x 宽 x 高
    • 托盘承重能力?(通常为 2,500-4,600 lbs)
    • 托盘自重?(通常为 30-70 lbs)
    • 是否允许超出托盘边缘?(通常为 0-3 英寸)
  2. 待装载物品

    • 有多少件物品/货箱?尺寸是多少?
    • 物品重量及重量分布
    • 是否有堆叠限制?(堆叠上限、抗压强度)
    • 是否有需要特殊摆放的易碎物品?
    • 尺寸是否相同,还是包含多个 SKU?
  3. 装载限制

    • 托盘最大高度?(通常为 72"、84"、96")
    • 是否有重量分布要求?
    • 是否要求采用交错堆叠方式?
    • 使用拉伸膜、收缩膜还是捆扎带?
    • 是否需要防滑垫片?
  4. 运输要求

    • 通过卡车还是铁路运输?
    • 是否会堆叠托盘?(双层堆叠)
    • 叉车需要从哪些方向作业?
    • 是否有装卸平台高度限制?
    • 运输时长及搬运情况?
  5. 优化目标

    • 最大限度增加每个托盘的货箱数?
    • 尽量减少托盘数量?
    • 确保稳定性(尽量减少超出托盘边缘的部分)?
    • 优化仓库空间利用?

托盘装载框架

标准托盘尺寸

北美:

  • GMA/CHEP:48" x 40"(1219mm x 1016mm)——最常见
  • 42" x 42" ——电信、油漆行业
  • 48" x 48" ——圆桶、散装材料
  • 40" x 48" ——军用、水泥行业

欧洲:

  • EUR/EUR1:47.2" x 31.5"(1200mm x 800mm)——欧洲标准
  • EUR2:47.2" x 23.6"(1200mm x 600mm)
  • EUR3:39.4" x 47.2"(1000mm x 1200mm)
  • EUR6:31.5" x 47.2"(800mm x 1200mm)

亚洲:

  • 43.3" x 43.3"(1100mm x 1100mm)——亚洲标准
  • 39.4" x 39.4"(1000mm x 1000mm)

ISO 标准:

  • ISO 1:47.2" x 39.4"(1200mm x 1000mm)
  • ISO 2:39.4" x 47.2"(1000mm x 1200mm)
关键概念

1. 托盘堆放模式

  • 每层箱子的排列方式
  • 决定稳定性和效率
  • 常见模式:方形排列、行列排列、风车式排列、分行排列

2. 交错堆叠

  • 不同层之间的箱子相互错开(旋转 90°)
  • 提高稳定性
  • 降低垮塌风险

3. 柱式堆叠

  • 箱子直接上下对齐
  • 不进行交错堆叠
  • 稳定性较低,但有时是必需的

4. 超出托盘边缘

  • 箱子超出托盘边缘
  • 通常限制在 2-3 英寸
  • 会降低稳定性并增加损坏风险

5. 抗压强度

  • 箱子可承受的上方重量
  • 是确定堆叠高度的关键因素
  • 会随时间推移及湿度变化而下降

数学建模

托盘装载问题

目标: 最大限度增加装载箱数,或尽量减少托盘数量

变量:

  • x_i、y_i = 托盘上第 i 个箱子的位置
  • z_i = 第 i 个箱子所在的层数
  • o_i = 第 i 个箱子的摆放方向(0-5)
  • pattern_j = 第 j 层使用的托盘堆放模式

约束条件:

  1. 托盘尺寸:箱子必须位于托盘边界内(允许超出边缘的范围除外)
  2. 不重叠:同一层中的箱子不得重叠
  3. 重量:总重量 ≤ 托盘承重能力
  4. 稳定性:下方支撑充足,重心合理
  5. 堆叠高度:高度 ≤ 允许的最大值
  6. 抗压强度:每个箱子承受的重量 ≤ 其抗压强度

算法与求解方法

堆放模式生成方法

简单方形排列

def generate_block_pattern(box_dims, pallet_dims, allow_overhang=2):
    """
    Generate simple block pattern for single box size

    Calculates optimal orientation and layout

    Parameters:
    - box_dims: (length, width, height)
    - pallet_dims: (length, width, max_height)
    - allow_overhang: maximum overhang in inches

    Returns pattern with max boxes per layer
    """

    box_l, box_w, box_h = box_dims
    pallet_l, pallet_w, pallet_h = pallet_dims

    best_pattern = None
    max_boxes = 0

    # Try both orientations
    orientations = [
        (box_l, box_w, box_h, 0),  # Original
        (box_w, box_l, box_h, 1)   # Rotated 90°
    ]

    for l, w, h, rotation in orientations:
        # Calculate how many fit in each direction
        effective_pallet_l = pallet_l + 2 * allow_overhang
        effective_pallet_w = pallet_w + 2 * allow_overhang

        boxes_length = int(effective_pallet_l // l)
        boxes_width = int(effective_pallet_w // w)

        boxes_per_layer = boxes_length * boxes_width

        if boxes_per_layer > max_boxes:
            max_boxes = boxes_per_layer

            # Calculate actual overhang
            actual_overhang_l = (boxes_length * l - pallet_l) / 2
            actual_overhang_w = (boxes_width * w - pallet_w) / 2

            best_pattern = {
                'boxes_per_layer': boxes_per_layer,
                'boxes_length': boxes_length,
                'boxes_width': boxes_width,
                'rotation': rotation,
                'box_dims': (l, w, h),
                'overhang': (actual_overhang_l, actual_overhang_w),
                'positions': []
            }

            # Generate positions
            for i in range(boxes_length):
                for j in range(boxes_width):
                    x = i * l - actual_overhang_l
                    y = j * w - actual_overhang_w
                    best_pattern['positions'].append((x, y, l, w))

    return best_pattern

# Example
box = (12, 10, 8)  # inches
pallet = (48, 40, 72)  # GMA pallet

pattern = generate_block_pattern(box, pallet, allow_overhang=2)
print(f"Boxes per layer: {pattern['boxes_per_layer']}")
print(f"Layout: {pattern['boxes_length']} x {pattern['boxes_width']}")
print(f"Overhang: {pattern['overhang']}")

交错堆叠模式生成器

def generate_interlocking_pattern(box_dims, pallet_dims, num_layers):
    """
    Generate interlocking pattern for stability

    Alternates orientation between layers

    Parameters:
    - box_dims: (length, width, height)
    - pallet_dims: (length, width, max_height)
    - num_layers: number of layers to generate

    Returns multi-layer interlocking pattern
    """

    box_l, box_w, box_h = box_dims
    pallet_l, pallet_w, pallet_h = pallet_dims

    patterns = []

    for layer in range(num_layers):
        # Alternate orientation by layer
        if layer % 2 == 0:
            # Layer 0, 2, 4, ... : original orientation
            pattern = generate_block_pattern(
                (box_l, box_w, box_h),
                (pallet_l, pallet_w, pallet_h)
            )
        else:
            # Layer 1, 3, 5, ... : rotated orientation
            pattern = generate_block_pattern(
                (box_w, box_l, box_h),
                (pallet_l, pallet_w, pallet_h)
            )

        # Add z-coordinate (layer height)
        z = layer * box_h
        pattern['layer'] = layer
        pattern['z'] = z

        patterns.append(pattern)

    total_boxes = sum(p['boxes_per_layer'] for p in patterns)

    return {
        'num_layers': num_layers,
        'total_boxes': total_boxes,
        'total_height': num_layers * box_h,
        'layers': patterns
    }

# Example
box = (16, 12, 10)
pallet = (48, 40, 72)
max_layers = int(pallet[2] // box[2])  # 7 layers fit

interlock = generate_interlocking_pattern(box, pallet, max_layers)
print(f"Total boxes: {interlock['total_boxes']}")
print(f"Height: {interlock['total_height']} inches")

多 SKU 堆放模式优化

from pulp import *

def optimize_mixed_sku_pallet(items, pallet_dims, max_weight):
    """
    Optimize pallet loading with mixed SKUs

    Uses integer programming to select best combination

    Parameters:
    - items: list of dicts with 'id', 'dims', 'weight', 'quantity'
    - pallet_dims: (length, width, max_height)
    - max_weight: weight capacity

    Returns optimal loading pattern
    """

    pallet_l, pallet_w, pallet_h = pallet_dims

    # Pre-calculate patterns for each item type
    item_patterns = {}
    for item in items:
        item_id = item['id']
        dims = item['dims']

        pattern = generate_block_pattern(dims, pallet_dims)
        item_patterns[item_id] = pattern

    # Create optimization problem
    prob = LpProblem("Pallet_Loading", LpMaximize)

    # Decision variables: how many of each item to load
    x = LpVariable.dicts("load",
                        [item['id'] for item in items],
                        lowBound=0,
                        cat='Integer')

    # Objective: maximize total value/volume loaded
    prob += lpSum([
        x[item['id']] * item['dims'][0] * item['dims'][1] * item['dims'][2]
        for item in items
    ]), "Total_Volume"

    # Weight constraint
    prob += lpSum([
        x[item['id']] * item['weight']
        for item in items
    ]) <= max_weight, "Weight_Capacity"

    # Quantity constraint
    for item in items:
        prob += x[item['id']] <= item['quantity'], f"Quantity_{item['id']}"

    # Height constraint (simplified - assumes stacking)
    # This is a simplification; full solution needs layer-by-layer check
    prob += lpSum([
        x[item['id']] * item['dims'][2]
        for item in items
    ]) <= pallet_h * 10, "Height_Limit"  # Rough approximation

    # Solve
    prob.solve(PULP_CBC_CMD(msg=0))

    # Extract solution
    solution = {
        'status': LpStatus[prob.status],
        'items_loaded': {},
        'total_weight': 0,
        'total_volume': 0
    }

    for item in items:
        qty = int(x[item['id']].varValue) if x[item['id']].varValue else 0
        if qty > 0:
            solution['items_loaded'][item['id']] = qty
            solution['total_weight'] += qty * item['weight']
            volume = item['dims'][0] * item['dims'][1] * item['dims'][2]
            solution['total_volume'] += qty * volume

    return solution
稳定性分析

重心检查

import numpy as np

def check_center_of_gravity(loaded_items, pallet_dims):
    """
    Calculate center of gravity and check stability

    Parameters:
    - loaded_items: list of dicts with 'position' (x,y,z), 'dims', 'weight'
    - pallet_dims: (length, width, height)

    Returns COG coordinates and stability score
    """

    if not loaded_items:
        return None

    total_weight = sum(item['weight'] for item in loaded_items)

    # Calculate weighted average position
    cog_x = sum(
        (item['position'][0] + item['dims'][0]/2) * item['weight']
        for item in loaded_items
    ) / total_weight

    cog_y = sum(
        (item['position'][1] + item['dims'][1]/2) * item['weight']
        for item in loaded_items
    ) / total_weight

    cog_z = sum(
        (item['position'][2] + item['dims'][2]/2) * item['weight']
        for item in loaded_items
    ) / total_weight

    # Check if COG is within pallet bounds
    pallet_center_x = pallet_dims[0] / 2
    pallet_center_y = pallet_dims[1] / 2

    offset_x = abs(cog_x - pallet_center_x)
    offset_y = abs(cog_y - pallet_center_y)

    # Stability score (lower offset = higher stability)
    # Good: offset < 10% of pallet dimension
    max_offset_x = pallet_dims[0] * 0.1
    max_offset_y = pallet_dims[1] * 0.1

    stability_x = max(0, 100 * (1 - offset_x / max_offset_x))
    stability_y = max(0, 100 * (1 - offset_y / max_offset_y))
    stability_score = min(stability_x, stability_y)

    return {
        'cog': (cog_x, cog_y, cog_z),
        'pallet_center': (pallet_center_x, pallet_center_y),
        'offset': (offset_x, offset_y),
        'stability_score': stability_score,
        'is_stable': stability_score > 70
    }

# Example
items = [
    {'position': (0, 0, 0), 'dims': (12, 10, 8), 'weight': 25},
    {'position': (12, 0, 0), 'dims': (12, 10, 8), 'weight': 25},
    {'position': (0, 10, 0), 'dims': (12, 10, 8), 'weight': 25},
    {'position': (12, 10, 0), 'dims': (12, 10, 8), 'weight': 25},
]

pallet = (48, 40, 72)
cog_result = check_center_of_gravity(items, pallet)
print(f"COG: {cog_result['cog']}")
print(f"Stability Score: {cog_result['stability_score']:.1f}")
print(f"Stable: {cog_result['is_stable']}")

抗压强度检查

def check_compression_strength(layers, box_compression_strength):
    """
    Verify each box can support weight above it

    Parameters:
    - layers: list of layer patterns (bottom to top)
    - box_compression_strength: max weight box can support (lbs)

    Returns safety analysis
    """

    issues = []
    safe = True

    for layer_idx in range(len(layers) - 1):
        layer = layers[layer_idx]

        # Calculate weight above this layer
        weight_above = 0
        for upper_layer_idx in range(layer_idx + 1, len(layers)):
            upper_layer = layers[upper_layer_idx]
            weight_above += sum(
                item['weight'] for item in upper_layer['items']
            )

        # Check each box in this layer
        num_boxes_in_layer = len(layer['items'])
        weight_per_box = weight_above / num_boxes_in_layer if num_boxes_in_layer > 0 else 0

        if weight_per_box > box_compression_strength:
            issues.append({
                'layer': layer_idx,
                'weight_per_box': weight_per_box,
                'max_allowed': box_compression_strength,
                'safety_factor': weight_per_box / box_compression_strength
            })
            safe = False

    return {
        'safe': safe,
        'issues': issues,
        'max_safe_layers': len(layers) if safe else layer_idx
    }
完整托盘优化器
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np

class PalletOptimizer:
    """
    Comprehensive Pallet Loading Optimizer

    Handles single and mixed SKU loading with stability checks
    """

    def __init__(self, pallet_type='GMA', custom_dims=None,
                 weight_capacity=2500, max_height=72):
        """
        Initialize pallet optimizer

        Parameters:
        - pallet_type: 'GMA', 'EUR', 'EUR1', or 'custom'
        - custom_dims: (length, width) if pallet_type='custom'
        - weight_capacity: max weight in lbs
        - max_height: max height in inches
        """

        # Standard pallet dimensions (inches)
        pallet_dims = {
            'GMA': (48, 40),
            'CHEP': (48, 40),
            'EUR': (47.2, 31.5),
            'EUR1': (47.2, 31.5),
            'EUR2': (47.2, 23.6),
            'ISO1': (47.2, 39.4),
            'ISO2': (39.4, 47.2)
        }

        if pallet_type == 'custom':
            if custom_dims is None:
                raise ValueError("Must provide custom_dims for custom pallet")
            self.pallet_dims = (*custom_dims, max_height)
        else:
            dims = pallet_dims.get(pallet_type, (48, 40))
            self.pallet_dims = (*dims, max_height)

        self.pallet_type = pallet_type
        self.weight_capacity = weight_capacity
        self.items = []
        self.solution = None

    def add_item(self, length, width, height, weight,
                 quantity=1, item_id=None, compression_strength=None,
                 fragile=False):
        """Add item type to be loaded"""

        if item_id is None:
            item_id = f"SKU_{len(self.items)}"

        # Default compression strength (stack limit)
        if compression_strength is None:
            compression_strength = weight * 10  # Conservative estimate

        self.items.append({
            'id': item_id,
            'dims': (length, width, height),
            'weight': weight,
            'quantity': quantity,
            'compression_strength': compression_strength,
            'fragile': fragile
        })

    def optimize_single_sku(self, allow_overhang=2, interlock=True):
        """
        Optimize loading for single SKU

        Parameters:
        - allow_overhang: max overhang in inches
        - interlock: use interlocking pattern

        Returns optimal pallet configuration
        """

        if len(self.items) != 1:
            raise ValueError("Use optimize_single_sku only for single SKU")

        item = self.items[0]
        box_dims = item['dims']
        box_weight = item['weight']
        available_qty = item['quantity']

        # Calculate max layers by height
        max_layers_height = int(self.pallet_dims[2] // box_dims[2])

        # Calculate max layers by weight
        max_layers_weight = int(self.weight_capacity //
                               (box_weight * 100))  # rough estimate

        # Calculate max layers by compression
        max_layers_compression = int(item['compression_strength'] / box_weight) + 1

        # Take minimum
        max_layers = min(max_layers_height, max_layers_weight,
                        max_layers_compression, 15)  # Cap at 15 for safety

        if interlock:
            pattern = generate_interlocking_pattern(
                box_dims, self.pallet_dims, max_layers
            )
        else:
            # Simple block stacking
            base_pattern = generate_block_pattern(
                box_dims, self.pallet_dims, allow_overhang
            )

            pattern = {
                'num_layers': max_layers,
                'total_boxes': base_pattern['boxes_per_layer'] * max_layers,
                'layers': [base_pattern] * max_layers
            }

        # Check if we have enough quantity
        if pattern['total_boxes'] > available_qty:
            # Reduce layers to fit available quantity
            boxes_per_layer = pattern['layers'][0]['boxes_per_layer']
            actual_layers = int(available_qty // boxes_per_layer)
            pattern['num_layers'] = actual_layers
            pattern['total_boxes'] = boxes_per_layer * actual_layers

        # Calculate weight
        total_weight = pattern['total_boxes'] * box_weight

        # Check stability
        loaded_items = []
        for layer_idx in range(pattern['num_layers']):
            z = layer_idx * box_dims[2]
            layer_pattern = pattern['layers'][layer_idx % len(pattern['layers'])]

            for pos in layer_pattern['positions']:
                loaded_items.append({
                    'position': (pos[0], pos[1], z),
                    'dims': box_dims,
                    'weight': box_weight
                })

        cog = check_center_of_gravity(loaded_items, self.pallet_dims)

        self.solution = {
            'pattern': pattern,
            'total_boxes': pattern['total_boxes'],
            'total_weight': total_weight,
            'total_height': pattern['num_layers'] * box_dims[2],
            'num_layers': pattern['num_layers'],
            'utilization': self.calculate_utilization(pattern, box_dims),
            'stability': cog,
            'loaded_items': loaded_items
        }

        return self.solution

    def optimize_mixed_sku(self):
        """
        Optimize loading for mixed SKUs

        Uses layer-by-layer heuristic
        """

        if len(self.items) < 2:
            raise ValueError("Use optimize_mixed_sku for multiple SKUs")

        # Sort items by volume (largest first)
        sorted_items = sorted(self.items,
                             key=lambda x: x['dims'][0] * x['dims'][1] * x['dims'][2],
                             reverse=True)

        loaded_items = []
        current_height = 0
        total_weight = 0

        # Load layer by layer
        while current_height < self.pallet_dims[2]:
            # Select item for this layer
            layer_item = None
            for item in sorted_items:
                if item['quantity'] > 0:
                    # Check if item fits
                    if current_height + item['dims'][2] <= self.pallet_dims[2]:
                        layer_item = item
                        break

            if layer_item is None:
                break  # No more items fit

            # Generate pattern for this layer
            layer_pattern = generate_block_pattern(
                layer_item['dims'],
                self.pallet_dims
            )

            boxes_to_load = min(
                layer_pattern['boxes_per_layer'],
                layer_item['quantity']
            )

            # Check weight
            if total_weight + boxes_to_load * layer_item['weight'] > self.weight_capacity:
                # Reduce boxes to fit weight
                boxes_to_load = int((self.weight_capacity - total_weight) / layer_item['weight'])

            if boxes_to_load == 0:
                break

            # Load boxes
            for i, pos in enumerate(layer_pattern['positions'][:boxes_to_load]):
                loaded_items.append({
                    'item_id': layer_item['id'],
                    'position': (pos[0], pos[1], current_height),
                    'dims': layer_item['dims'],
                    'weight': layer_item['weight']
                })

            layer_item['quantity'] -= boxes_to_load
            total_weight += boxes_to_load * layer_item['weight']
            current_height += layer_item['dims'][2]

        # Calculate metrics
        cog = check_center_of_gravity(loaded_items, self.pallet_dims)

        self.solution = {
            'total_boxes': len(loaded_items),
            'total_weight': total_weight,
            'total_height': current_height,
            'loaded_items': loaded_items,
            'stability': cog
        }

        return self.solution

    def calculate_utilization(self, pattern, box_dims):
        """Calculate volume utilization percentage"""
        box_volume = box_dims[0] * box_dims[1] * box_dims[2]
        total_box_volume = pattern['total_boxes'] * box_volume

        pallet_volume = (self.pallet_dims[0] * self.pallet_dims[1] *
                        pattern['num_layers'] * box_dims[2])

        return (total_box_volume / pallet_volume * 100) if pallet_volume > 0 else 0

    def visualize_3d(self, save_path=None):
        """3D visualization of pallet load"""

        if self.solution is None:
            raise ValueError("No solution to visualize. Run optimize first.")

        fig = plt.figure(figsize=(12, 10))
        ax = fig.add_subplot(111, projection='3d')

        # Draw pallet base
        pallet_l, pallet_w, pallet_h = self.pallet_dims
        self._draw_pallet_base(ax, pallet_l, pallet_w)

        # Draw loaded items
        colors = plt.cm.tab20(np.linspace(0, 1, 20))

        for idx, item in enumerate(self.solution['loaded_items']):
            pos = item['position']
            dims = item['dims']
            color = colors[idx % 20]

            self._draw_box_3d(ax, pos, dims, color, alpha=0.7)

        # Draw COG
        if self.solution.get('stability'):
            cog = self.solution['stability']['cog']
            ax.scatter([cog[0]], [cog[1]], [cog[2]],
                      c='red', s=200, marker='*',
                      label='Center of Gravity')

        ax.set_xlabel('Length (in)')
        ax.set_ylabel('Width (in)')
        ax.set_zlabel('Height (in)')
        ax.set_title(f'Pallet Load: {self.solution["total_boxes"]} boxes\n'
                    f'Weight: {self.solution["total_weight"]:.0f} lbs | '
                    f'Height: {self.solution["total_height"]:.1f} in')

        ax.legend()

        # Set equal aspect
        max_dim = max(pallet_l, pallet_w, self.solution['total_height'])
        ax.set_xlim(-5, max_dim + 5)
        ax.set_ylim(-5, max_dim + 5)
        ax.set_zlim(0, max_dim + 5)

        if save_path:
            plt.savefig(save_path, dpi=300, bbox_inches='tight')

        plt.show()

    def _draw_pallet_base(self, ax, length, width):
        """Draw pallet base"""
        vertices = np.array([
            [0, 0, 0], [length, 0, 0], [length, width, 0], [0, width, 0]
        ])

        face = [vertices]
        pallet_face = Poly3DCollection(face, facecolors='brown',
                                       linewidths=2, edgecolors='black',
                                       alpha=0.3)
        ax.add_collection3d(pallet_face)

    def _draw_box_3d(self, ax, position, dimensions, color, alpha=0.7):
        """Draw 3D box"""
        x, y, z = position
        l, w, h = dimensions

        vertices = np.array([
            [x, y, z], [x+l, y, z], [x+l, y+w, z], [x, y+w, z],
            [x, y, z+h], [x+l, y, z+h], [x+l, y+w, z+h], [x, y+w, z+h]
        ])

        faces = [
            [vertices[0], vertices[1], vertices[5], vertices[4]],
            [vertices[2], vertices[3], vertices[7], vertices[6]],
            [vertices[0], vertices[3], vertices[7], vertices[4]],
            [vertices[1], vertices[2], vertices[6], vertices[5]],
            [vertices[0], vertices[1], vertices[2], vertices[3]],
            [vertices[4], vertices[5], vertices[6], vertices[7]]
        ]

        face_collection = Poly3DCollection(faces, facecolors=color,
                                          linewidths=0.5, edgecolors='black',
                                          alpha=alpha)
        ax.add_collection3d(face_collection)

    def visualize_top_view(self, layer_index=0, save_path=None):
        """2D top-down view of specific layer"""

        if self.solution is None:
            raise ValueError("No solution to visualize")

        fig, ax = plt.subplots(figsize=(10, 8))

        # Draw pallet outline
        pallet_l, pallet_w = self.pallet_dims[:2]
        pallet_rect = patches.Rectangle((0, 0), pallet_l, pallet_w,
                                        linewidth=2, edgecolor='black',
                                        facecolor='lightgray', alpha=0.3)
        ax.add_patch(pallet_rect)

        # Filter items for this layer
        if 'pattern' in self.solution:
            # Single SKU
            if layer_index < len(self.solution['pattern']['layers']):
                layer = self.solution['pattern']['layers'][layer_index]
                box_dims = self.items[0]['dims']

                for pos in layer['positions']:
                    rect = patches.Rectangle(
                        (pos[0], pos[1]), pos[2], pos[3],
                        linewidth=1, edgecolor='blue',
                        facecolor='lightblue', alpha=0.7
                    )
                    ax.add_patch(rect)

                    # Add dimensions label
                    cx = pos[0] + pos[2]/2
                    cy = pos[1] + pos[3]/2
                    ax.text(cx, cy, f'{pos[2]:.0f}x{pos[3]:.0f}',
                           ha='center', va='center', fontsize=8)

        ax.set_xlim(-5, pallet_l + 5)
        ax.set_ylim(-5, pallet_w + 5)
        ax.set_aspect('equal')
        ax.set_xlabel('Length (in)')
        ax.set_ylabel('Width (in)')
        ax.set_title(f'Pallet Top View - Layer {layer_index + 1}')
        ax.grid(True, alpha=0.3)

        if save_path:
            plt.savefig(save_path, dpi=300, bbox_inches='tight')

        plt.show()

    def print_solution(self):
        """Print solution summary"""

        if self.solution is None:
            print("No solution available")
            return

        print("=" * 70)
        print("PALLET LOADING SOLUTION")
        print("=" * 70)
        print(f"Pallet type: {self.pallet_type}")
        print(f"Pallet dimensions: {self.pallet_dims[0]} x {self.pallet_dims[1]} x {self.pallet_dims[2]} in")
        print(f"Weight capacity: {self.weight_capacity} lbs")
        print()
        print(f"Total boxes loaded: {self.solution['total_boxes']}")
        print(f"Total weight: {self.solution['total_weight']:.1f} lbs "
              f"({self.solution['total_weight']/self.weight_capacity*100:.1f}% of capacity)")
        print(f"Total height: {self.solution['total_height']:.1f} in "
              f"({self.solution['total_height']/self.pallet_dims[2]*100:.1f}% of max)")

        if 'utilization' in self.solution:
            print(f"Volume utilization: {self.solution['utilization']:.1f}%")

        if self.solution.get('stability'):
            stab = self.solution['stability']
            print()
            print("Stability Analysis:")
            print(f"  Center of Gravity: ({stab['cog'][0]:.1f}, {stab['cog'][1]:.1f}, {stab['cog'][2]:.1f})")
            print(f"  Offset from center: ({stab['offset'][0]:.1f}, {stab['offset'][1]:.1f}) in")
            print(f"  Stability Score: {stab['stability_score']:.1f}/100")
            print(f"  Status: {'STABLE' if stab['is_stable'] else 'UNSTABLE - REVIEW REQUIRED'}")


# Example usage
if __name__ == "__main__":
    # Example 1: Single SKU optimization
    print("Example 1: Single SKU Pallet Loading")
    print("-" * 50)

    pallet = PalletOptimizer(pallet_type='GMA', weight_capacity=2500, max_height=72)

    # Add boxes (12x10x8 inches, 25 lbs each, 100 available)
    pallet.add_item(length=12, width=10, height=8, weight=25,
                   quantity=100, item_id='BOX_A',
                   compression_strength=250)

    # Optimize
    solution = pallet.optimize_single_sku(interlock=True)

    # Print results
    pallet.print_solution()

    # Visualize
    print("\nGenerating 3D visualization...")
    pallet.visualize_3d()

    print("\nGenerating top view...")
    pallet.visualize_top_view(layer_index=0)

工具与库

Python 库

palletizing - 托盘码放模式生成 py3dbp - 可用于托盘装载 rectpack - 矩形装箱(用于生成层)

商业软件
  • CAPE PACK:专业托盘化软件
  • TOPS Pro:托盘优化
  • PalletStacking:在线托盘计算器
  • LoadCalc:装载规划软件
  • Pallet Design System (PDS):NWPCA 软件
在线工具
  • PackEx:免费托盘计算器
  • PalletConfigr:托盘码放模式生成器
  • Boxologic:免费 3D 装箱工具

常见挑战与解决方案

挑战:托盘利用率低(<75%)

问题:

  • 箱子无法高效装载
  • 箱子之间有较大间隙
  • 垂直空间浪费

解决方案:

  • 尝试不同的摆放方向(旋转 90°)
  • 使用交错码放模式
  • 在同一托盘上混合装载尺寸相近的 SKU
  • 考虑使用更适合紧密排列的定制箱体尺寸
  • 用较小物品填补间隙
挑战:装载不稳定

问题:

  • 托盘在运输过程中倾斜
  • 箱子移位或倒塌
  • 悬垂过多

解决方案:

  • 将悬垂限制在 <3 英寸
  • 使用交错码放模式
  • 将重物放在底部中央
  • 检查重心计算结果
  • 在各层之间使用滑托板
  • 正确缠绕拉伸膜
  • 加装护角板以提供支撑
挑战:压缩损坏

问题:

  • 底层箱子被压坏
  • 货堆在存储过程中倒塌
  • 瓦楞纸箱受潮后强度下降

解决方案:

  • 检查抗压强度规格
  • 根据 ECT 等级限制堆叠高度
  • 对强度较弱的箱子采用柱式堆码
  • 加装顶部盖板
  • 控制存储环境的湿度
  • 底层使用强度更高的箱子
挑战:混合 SKU 的复杂性

问题:

  • 不同尺寸的箱子难以搭配装载
  • 高度差异导致不稳定
  • 难以生成高效的码放模式

解决方案:

  • 在同一层上组合高度相近的箱子
  • 采用逐层处理的方法
  • 将最大、最重的箱子放在底部
  • 用填充材料填补小间隙
  • 尺寸差异很大时,考虑使用两个托盘
挑战:运输限制

问题:

  • 托盘对卡车来说太高
  • 对装卸码头设备来说太重
  • 无法通过门口

解决方案:

  • 设置最大高度限制(通常为 72" 或 84")
  • 遵守重量限制(叉车承载能力为 3,000-5,000 lbs)
  • 优化前检查门口的净空高度
  • 考虑是否支持双层堆叠
  • 根据特定卡车或铁路车厢的尺寸进行规划

输出格式

托盘装载报告

摘要:

  • 托盘类型:GMA (48" x 40")
  • 已装载箱数:84 箱
  • 总重量:2,100 lbs
  • 总高度:64 英寸
  • 利用率:89.5%
  • 稳定性:通过(COG 偏移:1.2")

码放模式:

第 1 层(底层):

  • 模式:4 x 5 = 20 箱
  • 方向:12"L x 10"W
  • 层高:8"
  • 层重量:500 lbs

第 2 层:

  • 模式:5 x 4 = 20 箱(旋转 90°)
  • 方向:10"L x 12"W
  • 层高:16"
  • 层重量:500 lbs

第 3 层:

  • 模式:4 x 5 = 20 箱
  • 方向:12"L x 10"W
  • 层高:24"
  • 层重量:500 lbs

第 4 层:

  • 模式:5 x 4 = 20 箱(旋转 90°)
  • 方向:10"L x 12"W
  • 层高:32"
  • 层重量:500 lbs

第 5 层(顶层):

  • 模式:2 x 2 = 4 箱
  • 方向:12"L x 10"W
  • 层高:40"
  • 层重量:100 lbs

稳定性分析:

  • 重心:(24.0", 20.0", 20.5")
  • 与中心的偏移:水平 1.2"
  • 状态:稳定 ✓
  • 建议:缠绕 3-4 圈拉伸膜

运输说明:

  • 装载后立即缠绕拉伸膜
  • 使用四向进叉方式
  • 可双层堆叠(重量允许)
  • 最大堆叠高度:2 层(总高 128")
  • 在四个侧面均贴上标签

需要询问的问题

如果需要更多背景信息:

  1. 您使用的托盘尺寸是多少?(GMA 48x40、Euro,还是定制尺寸?)
  2. 箱子的尺寸和重量是多少?
  3. 每个托盘需要装多少个箱子?
  4. 是否有最大高度限制?
  5. 箱子能否旋转,或在同一托盘上混装?
  6. 是否需要考虑稳定性或抗压强度?
  7. 运输方式是什么?(卡车、铁路、集装箱)
  8. 是否需要采用交错排列以提高稳定性?

相关技能

  • 3d-bin-packing:适用于一般的 3D 装箱问题
  • container-loading-optimization:用于将托盘装入集装箱
  • vehicle-loading-optimization:用于托盘装载卡车
  • 2d-bin-packing:用于生成层级排列方案
  • load-building-optimization:用于订单履约和托盘装载
  • warehouse-slotting-optimization:用于优化托盘存储

评论

支持 Markdown