可变策略的拟人式三维装箱算法实现-开源

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

可变策略的拟人式三维装箱算法实现

问题

给定一个长方体容器和较多不同形态的长方体货物需确定装箱策略使货物尽可能多地装填到容器中。

假设与约束

  1. 货物可向上码放
  2. 货物必须完全包含在容器中
  3. 任意两个货物内的任意一点不可在空间中的同一位置
  4. 货物不可悬空放置即货物下方必须有其他货物或容器底部支撑
  5. 货物与容器平行放置即货物的边与容器的对应边平行
  6. 货物各个面都可以朝下放置没有上下左右前后的区别。

输入输出

输入为容器长宽高数据和货物数量及其各自的长宽高数据。

输出为以下三项

  1. 容器最终的利用率
  2. 容器中各个货物的位置和形态数据
  3. 结果示意图。

其中设容器的容积为 V V V共有 n n n个货物第 i i i个货物的体积为 s j s_j sj利用率 η \eta η的计算方式为

η = ∑ i = 1 n s i V . \eta = \frac{\sum_{i=1}^{n}s_i}{V}. η=Vi=1nsi.

程序目标与问题分析

程序需要在满足假设和约束条件的情况下尽可能提高 η \eta η的值。经分析不难发现利用率的高低取决于以下三个方面

  1. 货物装载的顺序
  2. 货物装载的位置
  3. 货物摆放的方式

所谓装载策略指的是分别说明上述三个方面的子算法。在本文中可变策略的是指本算法仅确定货物装载位置的算法–拟人式算法其他两个方面由算法复用者自行编写或使用遗传算法、模拟退火等启发式算法探索。当然可变策略的算法设计也会给具体的程序编码设计带来一定的麻烦。

装载顺序

装载顺序对最终的利用率影响很大。在其他策略一定的情况下不同的装载顺序会的到不同的结果。假设一个简单的情况有一空间有限的容器有一大一小两种货物。如果先装载大货物再装载小货物可能的一种情况是在大货物的空隙间填充入小货物然后得到较高的利用率。反之如果先装小货物可能的一种情况是装完小货物后的剩余空间的尺寸无法满足将大货物装入利用率低。

装载位置

在其他策略一定的情况下不同装载位置也会有不同的结果。这个是显而易见的可以通过调整小货物的位置提高利用率。再次考察“装载顺序”一节中先装小货物的情况将平铺于底面的货物堆码起来放置腾出放入大货物的空间提高了利用率。

摆放方式

摆放方式也会对结果造成很大的影响。由于题目中的货物为长方体而非正方体即使在约束 4、5、6 的约束下依然有种摆放方式。日常生活中对一个物体的摆放方式一般有“竖着”、“横着”、“躺着”、“立着”等模糊的形容。但在本算法中需要对货物的六种摆放方式有精确的描述。

一个货物的摆放方式是由空间中三个维度决定的前后、左右、上下。一个长方体有六个面这六个面可以根据尺寸分为三种。这三种面在三维空间中有 A 3 3 = 6 A_3^3=6 A33=6种的组合情况。这些组合情况的图示如下

图像来源[3]

数据结构

以下数据结构都是建立在三维坐标系的基础上设计的。一个基本的数据结构是坐标点它是由三个整型数有序组成的。第一到三个数分别表示某点在 x x x y y y z z z轴上面的投影值。一个点记为 ( d x , d y , d z ) (d_x,d_y,d_z) (dx,dy,dz)。货物和容器都是长方形的长、宽、高分别定义为平行于 x x x y y y z z z轴的边长。

容器箱

容器的长、宽、高由三个整型数有序组成 ( L , W , H ) (L,W,H) (L,W,H)。如图所示从正面以俯视角度看将底面的最左上方的点设为坐标原点 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)

除了基本的位置和尺寸信息外容器还需要存储其中货物的信息。货物信息由一个有序列表组成可对其进行增、删、查的操作。

货物

由于货物的摆放方式可变这就导致边长一样的货物会出现不同的表达即货物的长宽高是动态的。显然货物的尺寸是一定的不应该以动态的结构来表示故使用集合来表达一个货物的尺寸。一个货物的长宽高也由三个整型数有序组成 ( l , w , h ) (l,w,h) (l,w,h)但其尺寸信息由一个无需的三元集合来表示 { l , w , h } \{l,w,h\} {l,w,h}。为了方便更改摆放方式货物信息中不单独存储长、宽、高数据而是存储其尺寸数据和摆放标志。摆放标志的值域为 { 0 , 1 , 2 , 3 , 4 , 5 } \{0,1,2,3,4,5\} {0,1,2,3,4,5}分别表示上述的六种摆放方式。

同样的货物在空间中的位置由从正面以俯视角度看将底面的最左上方的点决定。设第 i i i个货物的在 ( x i , y i , z i ) (x_i,y_i,z_i) (xi,yi,zi)的位置相关信息如图所示。

其他的数据结构与题意不大相关都是由于算法需要和程序实现需要所建立的数据结构详情请查看后续算法和程序设计的相关内容。

算法设计

在日常砌墙时人们一般会先放置一块参考砖并以参考砖的高度作为基准规定每个物体的高度都不能超过参考砖的高度当物体不能放入时则提高参考砖的高度。受此思想的启发在三维装箱过程中在水平和垂直方向上同时引入参考砖来引导装填过程。本文使用记录可放置点的方法来查找装填位置引入水平参考线和垂直参考线来引导装填过程[1]。本文认为与其说为参考线不如称其为参考面更为严谨。

可放置点

可放置点指的是在向容器装入一个货物时可以用来参考货物放置位置的点。可放置点以有序列表的形式存储在容器的数据结构中称为可放置点表

虽然这个点称作可放置点但在放置之前仍然需要检测放入的货物是否满足上述的约束条件除约束 4。在初始的容器中只有一个可放置点即原点 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)。在放入货物 j j j后就需要更新可放置点将货物 j j j放置时参考的可放置点从可放置点表中删除然后再将新的可放置点添加到可放置点表中。新加的可放置点分别为货物 j j j前、上、右边的点即 ( x j + l j , y j , z j ) (x_j+l_j,y_j,z_j) (xj+lj,yj,zj) ( x j , y j + w j , z j ) (x_j,y_j+w_j,z_j) (xj,yj+wj,zj) ( x j , y j , z j + h j ) (x_j,y_j,z_j+h_j) (xj,yj,zj+hj)。如图所示可放置点在图中加粗

图像来源[1]。

可放置点表需要保持有序其中可放置点表的顺序为 y y y坐标从小到大排序 y y y坐标相同的按 x x x坐标从小到大 x x x y y y坐标都相同的按 z z z坐标从大到小[1]。其原因将在“参考面”一节解释。

参考面

容器内是一个三维空间使用两个参考面作为限制将三维装箱问题分治为一个维度。设置 x x x z z z轴上两个参考面 P h P_h Ph P v P_v Pv它们分别与 x y xy xy面平行 x y xy xy在考虑将一个货物装入容器时除了需要满足上述的约束条件除约束 4还需要满足摆放后不能超过参考面。故放置货物时选择可放置点的顺序应先从 y y y方向选择再考虑 x x x z z z方向。这个选择方式与“可放置点”一节中对可放置点表的排序相吻合。

初始状态下 P h P_h Ph P v P_v Pv的值均为 0。当一个货物装入容器但不满足参考面限制时需要调整参考面的值然后再重新装入货物。具体的调整方法详见“伪代码”部分。如果调整参考面后依然无法放置则认为该货物在当前容器状态下无法置入考虑下一个货物。

挪动

根据经验放置的箱子靠边时才能放入更多的箱子[1]。上述的所有算法步骤中均没有考虑到约束 4 的限制。为了考虑约束 4 并进一步提高空间利用率需要在算法中增加挪动这一步骤。在使用参考面引导货物载入时特别是在先装入小尺寸货物再装入大尺寸货物时会出现货物悬空没有贴边放置留有空隙的情况。为了消除空隙需要考虑挪动货物将货物尽量靠边。

在容器中挪动货物有向左 y y y、向后 x x x、向下 z z z三个方向。为了保证最终货物不是悬空的即满足约束 4须在向左 y y y、向后 x x x调整了货物位置后再调整垂直 z z z方向上的位置。挪动的结果是将货物尽量靠边所谓尽量是指货物挪动前后要满足约束和假设。

算法伪代码

初始化 容器;初始化 货物列表;
容器.水平参考面 = 0;
容器.垂直参考面 = 0;
容器.可放置点表 = [(0,0,0)];
while i = 1 until 货物列表.货物数量{
    初始化 flag = 未放置未更新;
    初始化 货物 = 货物列表中第 i 个货物;
    for 可放置点 in 容器.可放置点表{
        if (
            货物可以放置在可放置点 并且
            不与两个参考面冲突
        ){
            flag = 已放置;
            退出 可放置点 循环;
        }
    }
    if (flag 标记有 未放置){
        if (
            容器.水平参考面 == 0 或者
            容器.水平参考面 == 容器.长
        ){
            if (货物可以放置在 容器.垂直参考面 的原点){
                将货物放置在(0,0容器.垂直参考面);
                容器.垂直参考面 += 货物.高;
                容器.水平参考面 += 货物.长;
                flag = 已放置已更新;
            }else if (容器.垂直参考面 < 容器.高){
                容器.垂直参考面 = 货物.高;
                容器.水平参考面 = 货物.长;
                flag = 未放置已更新;
            }
        }else{
            for 可放置点 in 容器.可放置点表{
                if (
                    货物可以放置在可放置点 并且
                    货物 满足 容器.垂直参考线 的限制
                ){
                    容器.水平参考线 += 货物.长;
                    flag = 已放置已更新;
                    退出 可放置点 循环;
                }
            }
            if (flag 标记有 未放置){
                容器.水平参考线 = 容器.长;
                flag = 未放置已更新;
            }
        }
    }
    if (flag 标记有 已放置){
        容器.可放置点表.删除(货物.位置);
        货物.挪动;
        容器.可放置点表.并入(
            [根据新的 货物.位置 计算出的 3 个可放置点]
        );
        i++;
    }else if(flag 标记为 未放置已更新){
        i--;
    }else if(flag 标记为 未放置未更新){
        i++;
    }
}

碰撞检测

上述算法中的“某货物能否放置到某点”将在此节说明。直接在三维空间中确定两个实体是否冲突是有些困难的。本文将在二维平面上考虑三维空间中的实体的投影通过各个方向上的投影情况判断是否冲突。易得任意平行于长方体容器放置的长方体货物如果它们在任意方向上的投影没有重叠则两者就没有冲突。

而对于平面上的长方体而言相对在左边的长方体的右上角坐标如果小于相对右边的长方体的左下角坐标则两个长方形没有重叠。

Python 实现

代码结构

所有的算法实现将组织于一个名为Encase3D的包中。该包分为四个模块__init___cargo_containerdrawer。完整代码参阅文末 Gitee 上的开源仓库。

_cargo_container分别用于组织货物和容器的相关类代码drawer用于组织绘制 3D 效果图的相关函数代码。__init__部分用于初始化包并组织算法代码和可变策略类。

类设计

货物 Cargo 类及其相关类

需要描述三维状态首先需要建立坐标系首先设置三维点Point

class Point(object):
    def __init__(self, x: int, y: int, z: int) -> None:
        self.x = x
        self.y = y
        self.z = z

    def __repr__(self) -> str:
        return f"({self.x},{self.y},{self.z})"
    
    def __eq__(self, _o: object) -> bool:
        return self.x == _o.x and self.y == _o.y and self.z == _o.z

为了简化后续对三维点的操作在类中设置合法性属性和序列化属性

class Point(object):
    @property
    def is_valid(self) -> bool:
        return self.x >= 0 and self.y >=0 and self.z>= 0
    
    @property
    def tuple(self) -> tuple:
        return (self.x, self.y, self.z)

为了直观地描述货物的六种摆放方式抛弃抽象的 0、1、2… 记号。由于放入货物时货物的重心非常重要以货物的高将摆放方式分为三类。在高确定的情况下将货物根据从前面观察的宽窄分为两类。将六种情况根据上述分类命名后设计为枚举类

from enum import Enum

class CargoPose(Enum):
    tall_wide  = 0
    tall_thin  = 1
    mid_wide   = 2
    mid_thin   = 3
    short_wide = 4
    short_thin = 5

货物初始化时需要提供货物的尺寸信息

class Cargo(object):
    def __init__(self, length: int, width: int, height: int) -> None:
        self._point = Point(-1, -1, -1)
        self._shape = {length, width, height}
        self._pose = CargoPose.tall_thin

    def __repr__(self) -> str:
        return f"{self._point} {self.shape}"

货物的长宽高不仅由货物的尺寸决定还根据货物的摆放方式决定故分别设置长宽高属性根据其摆放方式返回正确的值。为了避免大量的 if-else使用 hash 字典重构

class Cargo(object):
    @property
    def pose(self) -> CargoPose:
        return self._pose

    @pose.setter
    def pose(self, new_pose: CargoPose):
        self._pose = new_pose

    @property
    def _shape_swiche(self) -> dict:
        edges = sorted(self._shape)
        return {
            CargoPose.tall_thin: (edges[1], edges[0], edges[-1]),
            CargoPose.tall_wide: (edges[0], edges[1], edges[-1]),
            CargoPose.mid_thin: (edges[-1], edges[0], edges[1]),
            CargoPose.mid_wide: (edges[0], edges[-1], edges[-1]),
            CargoPose.short_thin: (edges[-1], edges[1], edges[0]),
            CargoPose.short_wide: (edges[1], edges[-1], edges[0])
        }

    @property
    def shape(self) -> tuple:
        return self._shape_swiche[self._pose]

    @shape.setter
    def shape(self, length, width, height):
        self._shape = {length, width, height}

    @property
    def length(self) -> int:
        return self.shape[0] # 宽、高类似

货物的位置信息如果通过先访问其点来访问将会给后续的编码带来一定的麻烦故在此分别设置 x、y、z 属性以直接通过货物实例访问其位置

class Cargo(object):
    @property
    def point(self):
        return self._point

    @point.setter
    def point(self, new_point:Point):
        self._point = new_point

    @property
    def x(self) -> int:
        return self._point.x

    @x.setter
    def x(self, new_x: int):
        self._point = Point(new_x, self.y, self.z)
# y、z 类似

后续的算法中排序、利用率等需要用到提及信息设置只读的体积属性

class Cargo(object):
    @property
    def volume(self) -> int:
        reslut = 1
        for i in self._shape:
            reslut *= i
        return reslut

碰撞检测中还会用到投影信息设置投影方法需要传入目标投影面参数

class Cargo(object):
    def get_shadow_of(self, planar: str) -> tuple:
        if planar in ("xy", "yx"):
            x0, y0 = self.x, self.y
            x1, y1 = self.x + self.length, self.y + self.width
        elif planar in ("xz", "zx"):
            x0, y0 = self.x, self.z
            x1, y1 = self.x + self.length, self.z + self.height
        elif planar in ("yz", "zy"):
            x0, y0 = self.y, self.z
            x1, y1 = self.y + self.width, self.z + self.height
        return (x0, y0, x1, y1)

容器 Container 类

容器初始化需要长宽高信息。关于算法所需的可放置点表、已放置货物表、参考面信息使用另外的方法初始化以便使用同意箱子反复尝试装填

from typing import List
from Encase3D._cargo import *

class Container(object):
    def __init__(self, length: int, width: int, height: int) -> None:
        self._length = length
        self._width = width
        self._height = height
        self._refresh()

    def __repr__(self) -> str:
        return f"{self._length}, {self._width}, {self._height}"

    def _refresh(self):
        self._horizontal_planar = 0  # 水平放置参考面
        self._vertical_planar = 0  # 垂直放置参考面
        self._available_points = [Point(0, 0, 0)]  # 可放置点有序列表
        self._setted_cargos : List[Cargo] = []

同样的长宽高、提及等信息单独设置为只读属性

class Container(object):
    @property
    def length(self) -> int:
        return self._length
    # 宽、高类似

    @property
    def volume(self) -> int:
        return self.height * self.length * self.width

容器类中有部分的装箱算法逻辑在后续算法相关节中介绍。

拟人式三维装箱

将一个货物装入容器中的算法在容器类中实现。可变的策略和遍历货物集合的的算法在包初始化的中的方法实现详见“可变策略”一节。

可放置点表排序

可放置点表需要需要按照 y y y x x x z z z的顺序排列在容器类中使用以下方法实现

class Container(object):
        def _sort_available_points(self):
        self._available_points.sort(key=lambda x: x.z)
        self._available_points.sort(key=lambda x: x.x)
        self._available_points.sort(key=lambda x: x.y)

是否可放置

判断一个货物是否在一个点上可放置需要满足以下两个条件

  1. 不与容器内任一一个其他货物位置相冲突
  2. 完全在容器内。

对于第 1 个条件在当前模块中以“碰撞检测”的思想实现

def _is_cargos_collide(cargo0: Cargo, cargo1: Cargo) -> bool:
    return (
        _is_rectangles_overlap(cargo0.get_shadow_of("xy"), cargo1.get_shadow_of("xy")) and
        _is_rectangles_overlap(cargo0.get_shadow_of("yz"), cargo1.get_shadow_of("yz")) and
        _is_rectangles_overlap(cargo0.get_shadow_of(
            "xz"), cargo1.get_shadow_of("xz"))
    )

其中判断长方形是否冲突的方法也定义在当前模块中

def _is_rectangles_overlap(rec1:tuple, rec2:tuple) -> bool:
    return not (
        rec1[0] >= rec2[2] or rec1[1] >= rec2[3] or
        rec2[0] >= rec1[2] or rec2[1] >= rec1[3]
    )

由于第 1 个条件的检测需要遍历容器中的其他货物故先检测是否与容器冲突再检测是否与其他货物冲突。这些逻辑在容器实例中实现。需要注意的是为了避免检测过程中对原货物数据造成污染根据 Python 语言的特性需要深复制一个临时货物实例在其基础上操作。

from copy import deepcopy

class Container(object):
    def is_encasable(self, site: Point, cargo: Cargo) -> bool:
        encasable = True
        temp = deepcopy(cargo)
        temp.point = site
        if (
            temp.x + temp.length > self.length or
            temp.y + temp.width > self.width or
            temp.z + temp.height > self.height
        ):
            encasable = False
        for setted_cargo in self._setted_cargos:
            if _is_cargos_collide(temp, setted_cargo):
                encasable = False
        return encasable

挪动货物

挪动货物由于和上面同样的原因需要深复制后在进行操作。由于每个方向上的坐标的挪动逻辑是一样的故需要将坐标序列化后进行遍历操作避免代码的冗余重复。挪动完成后再将最终的放置点赋值回需要挪动的货物

class Container(object):
    def _adjust_setting_cargo(self, cargo: Cargo):
        site = cargo.point
        temp = deepcopy(cargo)
        if not self.is_encasable(site, cargo):
            return None
        xyz = [site.x, site.y, site.z] 
        # 序列化坐标以执行遍历递减操作, 减少冗余
        for i in range(3): # 012 分别表示 xyz
            is_continue = True
            while xyz[i] > 1 and is_continue:
                xyz[i] -= 1
                temp.point = Point(xyz[0], xyz[1], xyz[2])
                for setted_cargo in self._setted_cargos:
                    if not _is_cargos_collide(setted_cargo, temp):
                        continue
                    xyz[i] += 1
                    is_continue = False
                    break
        cargo.point = Point(xyz[0], xyz[1], xyz[2]) # 反序列化

标记

伪代码中的标记非常的理想化不仅可以记录是否放置的情况还可以记录容器参考面是否改变的情况。然而在实现中是否放置可以比较简单的实现但是否改变参考面需要有前后对比实现比较复杂。

标记本算法实现中设置为一个坐标点。如果成功放置则该标记就记录成功放置的点但没有成功放置时使用 (-1, -1, 0) 表示放置失败并改变了参考面(-1, -1, -1) 则表示单纯的放置失败。并在放置算法中专门设置一个子方法用于比较参考面是否发生改变

class Container(object):
    def _encase(self, cargo: Cargo) -> Point:
        # flag存储放置位置, (-1, -1, 0)放置失败并调整参考面, (-1, -1, -1)放置失败.
        flag = Point(-1, -1, -1)  
        # 用于记录执行前的参考面位置, 便于后续比较
        history = [self._horizontal_planar, self._vertical_planar]
        def __is_planar_changed() -> bool:
            return (
                not flag.is_valid and # 防止破坏已经确定可放置的点位, 即只能在(-1, -1, -1)基础上改
                self._horizontal_planar == history[0] and 
                self._vertical_planar == history[-1]
            ) 

单装箱实现

根据伪代码的逻辑最终的实现代码如下

class Container(object):
    def _encase(self, cargo: Cargo) -> Point:
        for point in self._available_points:
            if (
                self.is_encasable(point, cargo) and
                point.x + cargo.length < self._horizontal_planar and
                point.z + cargo.height < self._vertical_planar
            ):
                flag = point
                break
        if not flag.is_valid:
            if (
                self._horizontal_planar == 0 or
                self._horizontal_planar == self.length
            ):
                if self.is_encasable(Point(0, 0, self._vertical_planar), cargo):
                    flag = Point(0, 0, self._vertical_planar)
                    self._vertical_planar += cargo.height
                    self._horizontal_planar = cargo.length 
                    # 放置了货物 不检测参考面改变
                elif self._vertical_planar < self.height:
                    self._vertical_planar = self.height
                    self._horizontal_planar = self.length
                    if __is_planar_changed():
                        flag.z == 0 # 放置失败并调整参考面
            else:
                for point in self._available_points:
                    if (
                        point.x == self._horizontal_planar and
                        point.y == 0 and
                        self.is_encasable(point, cargo) and
                        point.z + cargo.height <= self._vertical_planar
                    ):
                        flag = point
                        self._horizontal_planar += cargo.length
                        break
                        # 放置了货物 不检测参考面改变
                if not flag.is_valid:
                    self._horizontal_planar = self.length
                    if __is_planar_changed():
                        flag.z == 0 # 放置失败并调整参考面
        if flag.is_valid:
            cargo.point = flag
            if flag in self._available_points:
                self._available_points.remove(flag)
            self._adjust_setting_cargo(cargo)
            self._setted_cargos.append(cargo)
            self._available_points.extend([
                Point(cargo.x + cargo.length, cargo.y, cargo.z),
                Point(cargo.x, cargo.y + cargo.width, cargo.z),
                Point(cargo.x, cargo.y, cargo.z + cargo.height)
            ])
            self._sort_available_points()
        return flag

可变策略

本来的想法是留有策略接口用户实现策略类来实现自定义可变策略。但 Python 中没有接口方法是第一类型可以直接作参数传递可为了两个策略的完整性故使用继承重写的方式来实现可变策略。

策略 Strategy 类

用户通过继承此类重写两个静态方法实现自定义两个装载策略装箱顺序和货物摆放方式。

from typing import Iterable, List
from Encase3D._cargo import *
from Encase3D._container import *

class Strategy(object):
    @staticmethod
    def encasement_sequence(cargos:Iterable) -> Iterable:
        return cargos

    @staticmethod
    def choose_cargo_poses(cargo:Cargo, container:Container) -> list:
        return list(CargoPose)

装箱实现

此部分实现的是伪代码中外层的循环以及伪代码中没有体现的摆放方式的选择。此外这部分代码顺便完成了空间利用率的输出。

def encase_cargos_into_container(
    cargos:Iterable, 
    container:Container, 
    strategy:type
) -> float:
    sorted_cargos:List[Cargo] = strategy.encasement_sequence(cargos)
    i = 0 # 记录发当前货物
    while i < len(sorted_cargos):
        j = 0 # 记录当前摆放方式
        cargo = sorted_cargos[i]
        poses = strategy.choose_cargo_poses(cargo, container)
        while j < len(poses):
            cargo.pose = poses[j]
            is_encased = container._encase(cargo)
            if is_encased.is_valid:
                break # 可以装入 不在考虑后续摆放方式
            j += 1  # 不可装入 查看下一个摆放方式
        if is_encased.is_valid:
            i += 1 # 成功放入 继续装箱
        elif is_encased == Point(-1,-1,0):
            continue # 没放进去但是修改了参考面位置 重装
        else :
            i += 1 # 纯纯没放进去 跳过看下一个箱子
    return sum(list(map(
            lambda cargo:cargo.volume,container._setted_cargos
        ))) / container.volume

输出与可视化

空间利用率的输出已在“可变策略”中实现这里具体讨论容器中各个货物的位置和形态数据和三维可视化示意图。

详细数据输出

详细数据为一个 csv 文件其中的表由七个字段组成

字段名含义
index序号
x货物放置点的 x 坐标
y货物放置点的 y 坐标
z货物放置点的 z 坐标
length货物当前摆放方式下的长
width货物当前摆放方式下的宽
height货物当前摆放方式下的高

具体在容器类中实现一个容器实例完成装载后可以调用该方法在主程序目录下输出 csv 文档文档名为调用保存时的时间戳组成。文档的最后一行会输出当前容器的数据。

from time import time

class Container(object):
    def save_encasement_as_file(self):
        file = open(f"{int(time())}_encasement.csv",'w',encoding='utf-8')
        file.write(f"index,x,y,z,length,width,height\n")
        i = 1
        for cargo in self._setted_cargos:
            file.write(f"{i},{cargo.x},{cargo.y},{cargo.z},")
            file.write(f"{cargo.length},{cargo.width},{cargo.height}\n")
            i += 1
        file.write(f"container,,,,{self}\n")
        file.close()

三维可视化

可视化的初始参数如下所示其中部分配置改编自[2]

from matplotlib import pyplot as plt
from matplotlib.figure import Figure
import numpy as np
from Encase3D._cargo import *
from Encase3D._container import *

plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.sans-serif'] = ['SimHei']
fig:Figure = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection='3d')
ax.view_init(elev=20, azim=40)
plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)

绘制六面颜色不同的立方体代码[2]为

def _plot_opaque_cube(x=10, y=20, z=30, dx=40, dy=50, dz=60):
    xx = np.linspace(x, x+dx, 2)
    yy = np.linspace(y, y+dy, 2)
    zz = np.linspace(z, z+dz, 2)
    xx2, yy2 = np.meshgrid(xx, yy)
    ax.plot_surface(xx2, yy2, np.full_like(xx2, z))
    ax.plot_surface(xx2, yy2, np.full_like(xx2, z+dz))
    yy2, zz2 = np.meshgrid(yy, zz)
    ax.plot_surface(np.full_like(yy2, x), yy2, zz2)
    ax.plot_surface(np.full_like(yy2, x+dx), yy2, zz2)
    xx2, zz2= np.meshgrid(xx, zz)
    ax.plot_surface(xx2, np.full_like(yy2, y), zz2)
    ax.plot_surface(xx2, np.full_like(yy2, y+dy), zz2)

绘制线框立方体代码[2]为

def _plot_linear_cube(x, y, z, dx, dy, dz, color='red'):
    xx = [x, x, x+dx, x+dx, x]
    yy = [y, y+dy, y+dy, y, y]
    kwargs = {'alpha': 1, 'color': color}
    ax.plot3D(xx, yy, [z]*5, **kwargs)
    ax.plot3D(xx, yy, [z+dz]*5, **kwargs)
    ax.plot3D([x, x], [y, y], [z, z+dz], **kwargs)
    ax.plot3D([x, x], [y+dy, y+dy], [z, z+dz], **kwargs)
    ax.plot3D([x+dx, x+dx], [y+dy, y+dy], [z, z+dz], **kwargs)
    ax.plot3D([x+dx, x+dx], [y, y], [z, z+dz], **kwargs)

具体可视化时首先初始化可视化配置然后根据容器尺寸调整坐标轴显示比例。绘制出容器线框后遍历容器中已放置的货物逐一绘制

def _draw_container(container:Container):
    _plot_linear_cube(
        0,0,0,
        container.length,
        container.width,
        container.height
    )

def _draw_cargo(cargo:Cargo):
    _plot_opaque_cube(
        cargo.x, cargo.y, cargo.z,
        cargo.length, cargo.width, cargo.height
    )

def draw_reslut(setted_container:Container):
    plt.gca().set_box_aspect((
        setted_container.length,
        setted_container.width,
        setted_container.height
    )) 
    _draw_container(setted_container)
    for cargo in setted_container._setted_cargos:
        _draw_cargo(cargo)
    plt.show()

实例

容器的长宽高分别为850、570、480。单位cm

需要装入的货物类型有

种类尺寸单位cm
1170*100*120
2100*80*90
3180*120*90
4210*120*100

每类货物各 30 个。

使用基于贪心思想的策略从大到小地考虑货物体积并逐一测试各个摆放方式直至成功置入

class VolumeGreedyStrategy(Strategy):
    @staticmethod
    def encasement_sequence(cargos:Iterable) -> Iterable:
        return sorted(cargos, key= lambda cargo:cargo.volume,reverse=1)

    @staticmethod
    def choose_cargo_poses(cargo:Cargo, container:Container) -> list:
        return list(CargoPose)

实现代码为

from Encase3D import *
from Encase3D import drawer

if __name__ == "__main__":
    cargos = [Cargo(170,100,120) for _ in range(30)]
    cargos.extend([Cargo(100,80,90) for _ in range(30)])
    cargos.extend([Cargo(150,120,90) for _ in range(30)])
    cargos.extend([Cargo(210,120,100) for _ in range(30)])
    case = Container(850,570,480)
    print(
        encase_cargos_into_container(cargos,case,VolumeGreedyStrategy)
    )
    case.save_encasement_as_file()
    drawer.draw_reslut(case)

输出的利用率为0.8622291021671826

输出的详细信息前后三行分别为

indexxyzlengthwidthheight
1000100120210
210000100120210
320000100120210
1092003603801008090
1103003603801008090
1114003603801008090
container850570480

输出的示意图为

存在的问题

时间消耗

由于 Python 语言的局限上述代码实现在货物数量超过 80 后时间消耗就比较多了。考察算法首先要遍历所有的货物进行装载在遍历货物时会遍历每个可放置点每个可放置点中需要检测是否有碰撞碰撞检测又会遍历已放置的点。经过大致的推算时间复杂度就在 O ( n 3 ) O(n^3) O(n3)数量级了。后续可以通过改进代码或更换语言减少时间复杂常量来改进时间消耗。另外也可以考虑改进算法碰撞检测时不遍历所有货物减少时间消耗。

实现 Python 重载与接口

Python 语言对面向对象程序设计支持地比较有个性许多特性有特殊的实现。笔者水平有限在编码过程中许多可以使用重载和接口的场景想不出来如何使用 Python 实现实现的不够优雅和 Pythonic。

例如确定一个货物的坐标时我希望通过方法重载实现以下功能输入一个点实例则直接赋值输入三个整型或三元元组则转化为点实例在赋值。可我除了 if-else 混合 type 函数想不出有什么别的更好的实现方法于是最终没有实现这功能。

启发式算法的邻域

笔者考虑过在拟人式装箱算法中引入模拟退火算法以获得更好的利用率。参考[1]的算法笔者不理解其中邻域的选择是怎么回事。对于模拟退火来说“温度”越高自变量随机变化的范围更大。但笔者理解的自变量应有一个从大到小的顺序参考[1]中自变量是装载顺序和摆放方式特别是摆放方式是一个无序集合域是如何通过模拟退火最终缩小邻域范围最终确定一个策略的呢

参考

[1] 张德富, 魏丽军, 陈青山 等. 三维装箱问题的组合启发式算法. 软件学报. 2007.
[http://www.jos.org.cn/1000-9825/18/2083.pdf]

[2] 晓东邪. 使用 matplotlib 绘制 3D 立方体图. CSDN 博客. 2018.
[https://blog.csdn.net/xiaodongxiexie/article/details/79817381]

[3]张游天, 金辰禹, 刘汉威 等. 基于混合启发式算法的货物空运装配策略. 物流技术. 2021.
[http://www.logisticstech.com/CN/Y2021/V40/I12/70]

开源地址[https://gitee.com/jaydencheng/3DEncasementAlgorithm]

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6