简单遗传算法 + 最低水平线算法求解二维排样问题

简介: 简单遗传算法 + 最低水平线算法求解二维排样问题

前言

笔者之前有两篇文章分别介绍了最低水平线算法(点击传送门)和简单遗传算法点击传送门),

本文在此基础上将最低水平线算法和简单遗传算法相结合来求解二维排样问题。

编码

由于板材和零件都是矩形,为了使矩形排放时板材的利用率尽可能高,每个零件在具体排放时只能有横放和竖放两种方式。由于每个基因的编码可以为正或负,在本文中统一规定矩形的编号为正表示矩形不进行在所排平面内的90度旋转,编号为负表示矩形在排样时要在所排平面内做90度旋转,

如染色体 (-2,1,5,3,4) 表示2号矩形需要做旋转,而其他矩形则正常排放,如下图。

设要排放的矩形总数为n,n个矩形的编号构成了一个染色体。种群中每条染色体的长度与待排矩形零件总数相同,染色体中每个基因的编码对应相应矩形的编号,所有基因编码的一个排列顺序构成了一个染色体。

适应度

本文选择的适应度是排样序列经过最低水平线算法排样后的利用率,利用率越高,个体适应度越好

交叉操作

本文采用的染色体交叉方法具体如下:

先选一个固定的交叉位置,在该点以前的基因不变,在该点以后的基因部分进行交叉。将两条染色体的不交叉的基因部分进行比较,在去掉两个基因片断中相同的基因后,将染色体不交叉部分剩下的基因按原来的顺序分别放入两个数组p和q中。在对染色体交叉部分的基因片断进行操作时,对不等于数组p或q中的基因直接进行交换。对于与数组中的基因相同的基因,则先换成对应的数组p或q中的基因后再进行交换。

例如要进行交叉的两个染色体分别为A={3,2,4,5,9,8,7,6,1,10},B={9,1,10,3,2,5,4,6,7,8},染色体交叉的具体步骤如下:

  1. 先确定一个交叉位置,假如选择第五个位置(每条染色体最左边的基因,其位置规定为1,从左向右,位置编号依次增加)为交叉点,图中箭头所在位置代表交叉点;
  2. 对比A染色体和B染色体的不变部分,两者都有的基因位是3、2、9,将A染色体不变部分的基因片断去掉3、2、9,将剩下的基因部分按原来的顺序放入数组p={4,5},同理将B染色体不变部分的基因片断中去掉3、2、9,将剩下的基因部分按原来的顺序放入数组q={1,10};
  3. 对比A染色体和B染色体的交叉部分,两者不相同的部分:A染色体为1、10;B为5、4,分别替换为对应的数组p或q中的基因,这时A染色体的交叉部分变为{8,7,6,4,5},B染色体交叉部分变成{10,1,6,7,8 };
  4. 经过步骤3,这时的A染色体和B染色体分别变为:

对两条染色体的后半部分进行交换,得到交叉操作后最终形成的两条新染色体;

变异操作

本文变异操作选择交换变异,具体步骤:产生[1,n]之间的一个随机数 Num,将该数和紧随其后的数相互交换;如果该数是染色体中的最后一个数,则将该数与染色体中的第一个数相交换。

代码及运行结果(笔者运行环境为python3.7)

import random
import copy
import numpy as np
import matplotlib.pyplot as plt
import math
# 个体长度(待排样箱体个数)
CHROM_LEN = 26
# 种群大小
POP_SIZE = 40
# 最大遗传代数
MAX_GENERATION = 100
# 交叉概率
PC = 0.7
# 变异概率
PM = 0.05
# 水平线类(起始x位置,终止x位置,高度)
class OutLine:
    def __init__(self, origin, end, height):
        self.origin = origin
        self.end = end
        self.height = height
    def __str__(self):
        return "OutLine:origin:{}, end:{}, height:{}".format(self.origin, self.end, self.height)
# 矩形物品类(宽度,高度,编号)
class Product:
    def __init__(self, w, h, num=0):
        self.w = w
        self.h = h
        self.num = num
    def __str__(self):
        return "product:w:{}, h:{}, num:{}".format(self.w, self.h, self.num)
    # 旋转90度并返回新对象
    def rotate_new(self):
        return Product(self.h, self.w, self.num)
# 布局类
class RectLayout:
    def __init__(self, width, line_list=[]):
        # 容器宽度
        self.width = width
        # 水平线集合(起始x位置,终止x位置,高度)
        self.line_list = line_list
        # 水平线(起始x位置,终止x位置,高度)
        self.lowest_line = None
        # 水平线索引(起始x位置,终止x位置,高度)
        self.lowest_line_idx = 0
        # 最终位置结果[[矩形件编号,左下角横坐标,左下角纵坐标,矩形件宽度,矩形件高度], ...]
        self.result_pos = []
        # 板材利用率
        self.ratio = 0.0
    # 初始化水平线集合(起始x位置,终止x位置,高度)
    def init_line_list(self, origin, end, height):
        init_line = OutLine(origin, end, height)
        self.line_list = [init_line]
        self.lowest_line = init_line
        self.lowest_line_idx = 0
    # 提升最低水平线
    def enhance_line(self, index):
        if len(self.line_list) > 1:
            # 获取高度较低的相邻水平线索引,并更新水平线集
            neighbor_idx = 0
            if index == 0:
                neighbor_idx = 1
            elif index + 1 == len(self.line_list):
                neighbor_idx = index - 1
            else:
                # 左边相邻水平线
                left_neighbor = self.line_list[index - 1]
                # 右边相邻水平线
                right_neighbor = self.line_list[index + 1]
                # 选择高度较低的相邻水平线,左右相邻水平线高度相同时,选择左边相邻的水平线
                if left_neighbor.height < right_neighbor.height:
                    neighbor_idx = index - 1
                elif left_neighbor.height == right_neighbor.height:
                    if left_neighbor.origin < right_neighbor.origin:
                        neighbor_idx = index - 1
                    else:
                        neighbor_idx = index + 1
                else:
                    neighbor_idx = index + 1
            # 选中的高度较低的相邻水平线
            old = self.line_list[neighbor_idx]
            # 更新相邻水平线
            if neighbor_idx < index:
                self.line_list[neighbor_idx] = OutLine(old.origin, old.end + self.line_width(index), old.height)
            else:
                self.line_list[neighbor_idx] = OutLine(old.origin - self.line_width(index), old.end, old.height)
            # 删除当前水平线
            del self.line_list[index]
    # 按位置更新水平线
    def update_line_list(self, index, new_line):
        self.line_list[index] = new_line
    # 按位置插入水平线(插在某索引位置后面)
    def insert_line_list(self, index, new_line):
        new_lists = []
        if len(self.line_list) == index + 1:
            new_lists = self.line_list + [new_line]
        else:
            new_lists = self.line_list[:index + 1] + [new_line] + self.line_list[index + 1:]
        self.line_list = new_lists
    # 计算水平线宽度
    def line_width(self, index):
        line = self.line_list[index]
        return line.end - line.origin
    # 找出最低水平线(如果最低水平线不止一条则选取最左边的那条)
    def find_lowest_line(self):
        # 最低高度
        lowest = min([_l.height for _l in self.line_list])
        # 最低高度时,最小开始横坐标
        origin = min([_l.origin for _l in self.line_list if _l.height == lowest])
        for _idx, _line in enumerate(self.line_list):
            if _line.height == lowest and _line.origin == origin:
                self.lowest_line_idx = _idx
                self.lowest_line = _line
    # 清空水平线集合
    def empty_line_list(self):
        self.line_list.clear()
    # 计算最高水平线高度,即所用板材最大高度
    def cal_high_line(self):
        max_height = max([ll.height for ll in self.line_list])
        return max_height
    # 将矩形物品排样
    def packing(self, _pro: Product):
        # 最低水平线宽度
        lowest_line_width = self.line_width(self.lowest_line_idx)
        # 对矩形件排样
        self.result_pos.append([_pro.num, self.lowest_line.origin, self.lowest_line.height, _pro.w, _pro.h])
        # 更新水平线集
        new_line1 = OutLine(self.lowest_line.origin, self.lowest_line.origin + _pro.w, self.lowest_line.height + _pro.h)
        new_line2 = OutLine(self.lowest_line.origin + _pro.w, self.lowest_line.origin + lowest_line_width, self.lowest_line.height)
        self.update_line_list(self.lowest_line_idx, new_line1)
        if lowest_line_width - _pro.w > 0:
            self.insert_line_list(self.lowest_line_idx, new_line2)
    # 计算板材利用率
    def cal_used_ratio(self):
        # 计算板材利用率
        used_area = 0
        for _p in self.result_pos:
            used_area += _p[3] * _p[4]
        # 板材使用最大高度
        max_high = self.cal_high_line()
        # 利用率
        if max_high > 0:
            self.ratio = round((used_area * 100) / (self.width * max_high), 3)
        return used_area, self.ratio
# 排样主算法
def packing_main(seq, container_width, products):
    # 初始化布局类
    layout = RectLayout(width=container_width)
    layout.empty_line_list()
    layout.result_pos.clear()
    # 初始化水平线集
    layout.init_line_list(0, container_width, 0)
    # 循环直到把所有物品都排完
    while seq:
        candidate_idx = seq[0]
        _idx = int(math.fabs(int(candidate_idx)))
        # 候选物品
        pro = products[_idx]
        # 最低水平线及其索引
        layout.find_lowest_line()
        # 可用长度
        available_width = layout.line_width(layout.lowest_line_idx)
        # 对应的装箱编号为负数时,对矩形件进行旋转
        if int(candidate_idx) < 0:
            pro = pro.rotate_new()
        if available_width >= pro.w:
            # 将候选物品排样
            layout.packing(pro)
            # 剔除已经排样的物品
            seq.remove(candidate_idx)
        else:
            # 最低水平线宽度小于要排样矩形宽度,提升最低水平线
            layout.enhance_line(layout.lowest_line_idx)
    # 计算板材利用率
    _area, _ratio = layout.cal_used_ratio()
    # 计算最高水平线高度,即所用板材最大高度
    container_height = layout.cal_high_line()
    return layout, _ratio, container_height
# 对排样序列洗牌,并随机对物品进行旋转
def shuffle_seq(seq):
    # 洗牌
    random.shuffle(seq)
    # 新的序列
    new_seq = []
    for s in seq:
        # 随机数
        r = np.random.rand()
        # 随机对物品进行旋转
        if r < 0.5:
            new_seq.append(0 - s)
        else:
            new_seq.append(s)
    return new_seq
# 遗传算法个体类
class Individual:
    def __init__(self, chrom: str):
        # 基因序列
        self.chrom = chrom
        # 适应度
        self.fitness = 0
    # 计算个体适应度
    def get_fitness(self, container_width, products):
        _, r, _ = packing_main(copy.copy(self.chrom.split(",")), container_width, products)
        self.fitness = r
        return self.fitness
    def __str__(self):
        return "chrom:{}, fitness:{}".format(self.chrom, self.fitness)
# 获得当代最佳和最差个体索引
def best_and_worst(population):
    # 最佳个体索引
    best_idx = 0
    # 最差个体索引
    worst_idx = 0
    for _idx, p in enumerate(population):
        if p.fitness > population[best_idx].fitness:
            best_idx = _idx
        elif p.fitness < population[worst_idx].fitness:
            worst_idx = _idx
    return best_idx, worst_idx
# 选择(复制)操作
def select(population):
    # 新种群
    new_pop = []
    # 当代个体适应度总和
    fitness_sum = max(sum([i.fitness for i in population]), 0.0001)
    # 当代个体累计适应度占比
    cfitness = []
    # 计算相对适应度占比
    for j in range(POP_SIZE):
        cfitness.append(population[j].fitness / fitness_sum)
    # 计算累计适应度占比
    for j in range(POP_SIZE):
        if j == 0:
            continue
        cfitness[j] = cfitness[j-1] + cfitness[j]
    # 依据累计适应度占比进行选择复制,随机数大于对应的累计适应度占比,则进行复制
    for k in range(POP_SIZE):
        index = 0
        while random.random() > cfitness[index]:
            index += 1
            # 若无法找到要复制的其他个体,则沿用当前个体
            if index >= POP_SIZE:
                index = k
                break
        new_pop.append(copy.deepcopy(population[index]))
    return new_pop
# 两个个体进行交叉操作
def crossover_inner(individual1: Individual, individual2: Individual, position: int):
    # 两个个体染色体相同,则不进行交叉
    if (np.array(individual1.chrom.split(",")) == np.array(individual2.chrom.split(","))).all():
        return
    # 两个个体基因型列表
    _chrom1 = individual1.chrom.split(",")
    _chrom2 = individual2.chrom.split(",")
    # 两个个体各自基因型列表中元素对应的正负号    
    flag_table1 = {int(math.fabs(int(c))): 1 if int(c) >= 0 else -1 for c in _chrom1}
    flag_table2 = {int(math.fabs(int(c))): 1 if int(c) >= 0 else -1 for c in _chrom2}
    # 两个个体去掉正负号的排样序列
    chrom1 = [int(math.fabs(int(ch))) for ch in _chrom1]
    chrom2 = [int(math.fabs(int(ch))) for ch in _chrom2]
    # 不交叉部分排样物品序号的交集
    intersection1 = list(set(chrom1[:position]).intersection(set(chrom2[:position])))
    # 不交叉部分中,个体1对于个体2的物品序号差集
    diff1 = []
    # 不交叉部分中,个体2对于个体1的物品序号差集
    diff2 = []
    # 不交叉部分排样物品序号均相同
    if len(intersection1) == position:
        # 直接交换
        cross_gene1 = _chrom1[position:]
        cross_gene2 = _chrom2[position:]
        individual1.chrom = ",".join(_chrom1[:position] + cross_gene2)
        individual2.chrom = ",".join(_chrom2[:position] + cross_gene1)
        return
    # 不交叉部分排样物品序号均不相同
    elif len(intersection1) == 0:
        diff1 = chrom1[:position]
        diff2 = chrom2[:position]
    else:
        diff1 = [c for c in chrom1[:position] if c not in intersection1]
        diff2 = [c for c in chrom2[:position] if c not in intersection1]
    # 物品序号转换表
    table = dict()
    for _idx, d in enumerate(diff1):
        table[d] = diff2[_idx]
    for _idx, d in enumerate(diff2):
        table[d] = diff1[_idx]
    # 交叉部分排样物品序号的交集
    intersection2 = list(set(chrom1[position:]).intersection(set(chrom2[position:])))
    # 转换排样序号,使最终交叉后的基因型合法
    for i, v in enumerate(chrom1):
        if i >= position and v not in intersection2 and v in table:
            _chrom1[i] = table[v] * flag_table2[table[v]]
    for i, v in enumerate(chrom2):
        if i >= position and v not in intersection2 and v in table:
            _chrom2[i] = table[v] * flag_table1[table[v]]
    # 需要交换的基因
    cross_gene1 = _chrom1[position:]
    cross_gene2 = _chrom2[position:]
    individual1.chrom = ",".join([str(c) for c in _chrom1[:position] + cross_gene2])
    individual2.chrom = ",".join([str(c) for c in _chrom2[:position] + cross_gene1])
# 交叉操作
def crossover(population):
    # 随机产生个体配对索引,类似于洗牌的效果
    index = [i for i in range(POP_SIZE)]
    for i in range(POP_SIZE):
        point = random.randint(0, POP_SIZE - i - 1)
        temp = index[i]
        index[i] = index[point + i]
        index[point + i] = temp
    for i in range(0, POP_SIZE, 2):
        if random.random() > PC:
            # 随机选择交叉开始位置
            cross_start = random.randint(0, CHROM_LEN - 2) + 1
            # 两个个体染色体交叉
            crossover_inner(population[index[i]], population[index[i + 1]], cross_start)
# 变异操作
def mutation(population):
    for individual in population:
        # 新染色体
        new_chrom_ch = individual.chrom.split(",")
        for i in range(CHROM_LEN):
            # 随机数小于变异概率,则进行变异操作
            if random.random() < PM:
                # 变异位置
                p = random.randint(0, CHROM_LEN-1)
                # 交换变异
                next = 0 if p == CHROM_LEN-1 else p+1
                temp = new_chrom_ch[p]
                new_chrom_ch[p] = new_chrom_ch[next]
                new_chrom_ch[next] = temp
                # 随机产生旋转效果
                if np.random.rand() < 0.5:
                    new_chrom_ch[p] = str(0 - int(new_chrom_ch[p]))
        # 更新染色体
        individual.chrom = ",".join(new_chrom_ch)
# 绘制排样结果
def draw_packing_result(layout: RectLayout, container_width, container_height):
    # 绘制排样布局
    fig = plt.figure()
    plt.xlim(0, container_width)
    plt.ylim(0, container_height)
    plt.axis("off")
    ax = fig.add_subplot(111, aspect='equal')
    ax.set_xlim(0, container_width)
    ax.set_ylim(0, container_height)
    for pos in layout.result_pos:
        ax.add_patch(
            plt.Rectangle(
                (pos[1], pos[2]),  # 矩形左下角
                pos[3],  # 长
                pos[4],  # 宽
                alpha=1,
                edgecolor='black',
                linewidth=1
            )
        )
        # 物品编号
        ax.text(pos[1] + pos[3] / 2 - 0.5, pos[2] + pos[4] / 2 - 0.3, "{}".format(pos[0]), transform=ax.transData)
    # plt.show()
    plt.savefig('lowest_horizontal_line_sga1.png', dpi=800)
# 绘制进化过程
def draw_evolution(evolution):
    x = [i for i in range(len(evolution))]
    # 清空画布
    plt.clf()
    plt.plot(x, evolution)
    # plt.show()
    plt.savefig('lowest_horizontal_line_sga2.png', dpi=800)
# 主方法
def main():
    # 板材宽度
    container_width = 10
    # 初始化矩形物品尺寸,也可以随机生成
    # item_sizes = np.random.randint(1, 8, size=(CHROM_LEN, 2)).tolist()
    item_sizes = [[3, 1], [4, 4], [1, 1], [2, 3], [2, 4], [3, 4], [1, 4], [2, 2], [3, 3], [3, 1], [4, 2], [3, 1],
                  [3, 1], [3, 2], [4, 2], [1, 2], [1, 3], [3, 4], [2, 3], [1, 1], [2, 1], [3, 2], [4, 3], [3, 2],
                  [4, 3], [4, 3]]
    # 物品id(仅作为显示使用)
    ran = [i + 1 for i in range(len(item_sizes))]
    # 矩形物品列表
    products = []
    for idx in range(CHROM_LEN):
        products.append(Product(item_sizes[idx][0], item_sizes[idx][1], ran[idx]))
    # 种群
    population = []
    # 初始化种群
    seq_seed = [i for i in range(len(item_sizes))]
    temp = copy.copy(seq_seed)
    for _ in range(POP_SIZE):
        population.append(Individual(",".join([str(t) for t in temp])))
        # 打乱初始排样顺序
        temp = shuffle_seq(temp)
    # 计算初始种群适应度
    for individual in population:
        individual.get_fitness(container_width, products)
    # 初始种群最佳和最差个体
    best_idx, worst_idx = best_and_worst(population)
    # 历史最佳个体
    current_best = population[best_idx]
    # 进化过程,每一代最佳个体对应的利用率
    evolution = []
    # 循环直到最大代数
    for generation in range(MAX_GENERATION):
        # 选择复制
        population = select(population)
        # 交叉
        crossover(population)
        # 变异
        mutation(population)
        # 重新计算适应度
        for individual in population:
            individual.get_fitness(container_width, products)
        # 当代种群最佳和最差个体索引
        best_idx, worst_idx = best_and_worst(population)
        # 利用精英模型执行进化操作,用历史最佳个体代替当代的最差个体
        if population[best_idx].fitness > current_best.fitness:
            current_best = population[best_idx]
        else:
            population[worst_idx] = current_best
        # 更新进化过程
        _, _f, _ = packing_main(copy.copy(current_best.chrom.split(",")), container_width, products)
        evolution.append(_f)
    # 打印最佳个体信息
    print(current_best)
    best_layout, _, best_container_height = packing_main(copy.copy(current_best.chrom.split(",")), container_width, products)
    # 绘制排样布局
    draw_packing_result(best_layout, container_width, best_container_height)
    # 绘制进化过程
    draw_evolution(evolution)
if __name__ == "__main__":
    main()

绘制的排样结果图如下(由于初始种群的随机性,每一次执行产生的结果可能会不同):

每一代最优解的进化过程如下图所示(由于初始种群的随机性,每一次执行产生的结果可能会不同):

本文理论部分摘自广西师范大学的硕士论文《矩形件下料优化排样的遗传算法》,论文作者和导师都很牛,读者可以自行百度获取论文原文😏。

笔者水平有限,若有不对或可以优化的地方欢迎评论留言。

相关文章
|
6月前
|
算法 数据安全/隐私保护 计算机视觉
基于二维CS-SCHT变换和LABS方法的水印嵌入和提取算法matlab仿真
该内容包括一个算法的运行展示和详细步骤,使用了MATLAB2022a。算法涉及水印嵌入和提取,利用LAB色彩空间可能用于隐藏水印。水印通过二维CS-SCHT变换、低频系数处理和特定解码策略来提取。代码段展示了水印置乱、图像处理(如噪声、旋转、剪切等攻击)以及水印的逆置乱和提取过程。最后,计算并保存了比特率,用于评估水印的稳健性。
|
5月前
|
算法 调度 Python
【调度算法】并行机调度问题遗传算法
【调度算法】并行机调度问题遗传算法
75 2
|
4月前
|
机器学习/深度学习 人工智能 分布式计算
算法金 | 最难的来了:超参数网格搜索、贝叶斯优化、遗传算法、模型特异化、Hyperopt、Optuna、多目标优化、异步并行优化
机器学习中的超参数调优是提升模型性能的关键步骤,包括网格搜索、随机搜索、贝叶斯优化和遗传算法等方法。网格搜索通过穷举所有可能的超参数组合找到最优,但计算成本高;随机搜索则在预设范围内随机采样,降低计算成本;贝叶斯优化使用代理模型智能选择超参数,效率高且适应性强;遗传算法模拟生物进化,全局搜索能力强。此外,还有多目标优化、异步并行优化等高级技术,以及Hyperopt、Optuna等优化库来提升调优效率。实践中,应结合模型类型、数据规模和计算资源选择合适的调优策略。
175 0
算法金 | 最难的来了:超参数网格搜索、贝叶斯优化、遗传算法、模型特异化、Hyperopt、Optuna、多目标优化、异步并行优化
|
5月前
|
算法 调度 Python
【调度算法】开放车间调度问题遗传算法
【调度算法】开放车间调度问题遗传算法
48 1
|
6月前
|
算法 数据安全/隐私保护 C++
基于二维CS-SCHT变换和扩频方法的彩色图像水印嵌入和提取算法matlab仿真
该内容是关于一个图像水印算法的描述。在MATLAB2022a中运行,算法包括水印的嵌入和提取。首先,RGB图像转换为YUV格式,然后水印通过特定规则嵌入到Y分量中,并经过Arnold置乱增强安全性。水印提取时,经过逆过程恢复,使用了二维CS-SCHT变换和噪声对比度(NC)计算来评估水印的鲁棒性。代码中展示了从RGB到YUV的转换、水印嵌入、JPEG压缩攻击模拟以及水印提取的步骤。
|
5月前
|
算法 Python
二维矩形件排样算法之最低水平线搜索算法实现
二维矩形件排样算法之最低水平线搜索算法实现
167 0
|
5月前
|
机器学习/深度学习 移动开发 算法
二维矩形件排样算法之最低水平线算法实现
二维矩形件排样算法之最低水平线算法实现
98 0
|
5月前
|
算法 调度 Python
【调度算法】单机调度问题遗传算法
【调度算法】单机调度问题遗传算法
64 0
|
6月前
|
机器学习/深度学习 存储 算法
数据结构与算法 动态规划(启发式搜索、遗传算法、强化学习待完善)
数据结构与算法 动态规划(启发式搜索、遗传算法、强化学习待完善)
97 1
|
24天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。