排产排程问题也可以用数学规划的方式进行建模,调用MindOpt优化求解器进行求解。
行业背景
排产排程、原料采购、仓储存放等是制造业降本增效的关键问题。如何合理安排采购量和生产计划,让成本降低,或者利润更高。这个优化问题也可以运用数学规划的方法来建模和求解。
例如:某香皂制造厂要对未来半年内的香皂生产和原料采买制定计划。原料的部分需要提前预留,但是存储也有成本,需要尽量合理安排,使得原料够用,又能满足使用需要。
业务调研、数据量化、数学建模
在使用优化技术的时候,需要更详细的调研业务的需求,整理相关的业务逻辑和数据,并量化表示它。然后采用数学规划的方法进行数学建模。
此部分细节较多,可在案例排产排程03中查阅细节,此处我们仅列出数学公式如下,参数部分请点击案例链接查看:
集合O 是油脂,包含O1和O2, M是月份。
以上公式对应的约束有:
假设从油脂到香皂的转化过程中,油脂没有任何浪费,生产过程中重量守恒
假设从油脂到香皂的制造过程中,硬度的转化满足线性关系
任意油脂上个月的储存量与本月购买量的总和要等于本月的使用量与该月储存量的总和,需要考虑一月与其他月份,其中是1月份时,各种油脂的初始储备量
植物油脂与动物油脂购买量的限制
六月末每种油脂的储存数量需要与储存储备量一致
源代码
MindOpt支持多种编程语言或者建模语言来调用。此处仅列出一种供参考:
MindOpt APL建模语言调用
MindOpt APL建模语言的源代码(可在排产排程03线上试运行):
##====MindOpt APL 建模语言版本代码====
clear model;#清除model,多次run的时候使用
option modelname manufacture_03_soap2;
#---------建模-----------------
# manufacture_03_soap2.mapl
# 声明集合
set O1 := { "VEG1", "VEG2" };
set O2 := {"OIL1", "OIL2", "OIL3"};
set O := O1 + O2;
set M := {1, 2, 3, 4, 5, 6};
set N := {"Buy", "Use", "Store"};
# 声明参数
param cost[O * M] :=
| 1, 2, 3, 4, 5, 6 |
|"VEG1"| 110, 130, 110, 120, 100, 90 |
|"VEG2"| 120, 130, 140, 110, 120, 100 |
|"OIL1"| 130, 110, 130, 120, 150, 140 |
|"OIL2"| 110, 90, 100, 120, 110, 80 |
|"OIL3"| 115, 115, 95, 125, 105, 135 |;
param hardness[O] := <"VEG1"> 8.0, <"VEG2"> 6.0,
<"OIL1"> 2.0, <"OIL2"> 4.0, <"OIL3"> 5.0;
param r := 150;
param b1 := 200;
param b2 := 250;
param l := 3;
param u := 6;
param s := 500;
param d := 5;
# 声明变量
var x[O * M * N] >= 0;
var y[M] >= 0;
# 声明目标
maximize Reward: sum {<m> in M}(
r * y[m]
- sum {<j> in O} cost[j, m] * x[j, m, "Buy"]
- d * sum{<j> in O} x[j, m, "Store"]
);
# 声明约束
subto Weight:
forall { <m> in M }
sum {<j> in O} x[j, m, "Use"] == y[m];
subto Hardness1:
forall { <m> in M }
sum {<j> in O} hardness[j] * x[j, m, "Use"] >= y[m] * l;
subto Hardness2:
forall {<m> in M }
sum {<j> in O} hardness[j] * x[j, m, "Use"] <= y[m] * u;
subto VEGBound:
forall {<m> in M }
sum {<j> in O1} x[j, m, "Use"] <= b1;
subto OILBound:
forall { <m> in M }
sum {<j> in O2} x[j, m, "Use"] <= b2;
subto Link_1:
forall {<j, 1> in O * M }
s + x[j,1,"Buy"] == x[j,1,"Use"] + x[j,1,"Store"];
subto Link_2to6:
forall {<j, m> in O * M with m > 1 }
x[j,m-1,"Store"] + x[j,m,"Buy"] == x[j,m,"Use"] + x[j,m,"Store"];
subto Store_June:
forall { <j> in O }
x[j,6,"Store"] == s;
#------------------------------
print "-----------------用MindOpt求解---------------";
option solver mindopt; # 指定求解用MindOpt求解器
solve; # 求解
#display; #打印求解变量值,比较长,请删除注释#后进行打印
print "-----------------结果---------------";
print "最大利润 = ", sum {<m> in M}(
r * y[m]
- sum {<j> in O} cost[j, m] * x[j, m, "Buy"]
- d * sum{<j> in O} x[j, m, "Store"]
);
结果和结果用法
不同代码日志打印不一样。部分结果日志打印如下所示:
...
Model summary.
- Num. variables : 96
- Num. constraints : 60
- Num. nonzeros : 253
- Bound range : [2.0e+02,5.0e+02]
- Objective range : [5.0e+00,1.5e+02]
- Matrix range : [1.0e+00,8.0e+00]
...
Simplex method terminated. Time : 0.002s
...
OPTIMAL; objective 108250.00
...
-----------------结果---------------
最大利润 = 108250
目标的最优解是:108250。更多解的细节,如决策变量的取值,可以通过print
命令打印来显示,也可以从程序运行写的.sol
文件里面获取,还可以调用display
获取所有变量的。
如运行如下指令,验证“六月末每种油脂的储存数量需要等于一月初的初始储存数量”这个约束:
forall {<j> in O} print '六月末(',j,')的储存数量= ', x[j,6,"Store"];
输出结果是500,与1月初需求相同:
六月末(VEG1)的储存数量= 500
六月末(VEG2)的储存数量= 500
六月末(OIL1)的储存数量= 500
六月末(OIL2)的储存数量= 500
六月末(OIL3)的储存数量= 500
运行如下代码,将输出打印为csv格式,作为排产排程问题的解决方案:
print "{}, {}, {}, {}" % "油脂", "月份", "处理方案","数量" : "每月油脂处理方式.csv";
close "每月油脂处理方式.csv";
forall {<j,m,n> in O*M*N}
print "{}, {}, {}, {}" % j, m, n, x[j,m,n] >> "每月油脂的处理方式.csv";
close "每月油脂处理方式.csv";
print "{}, {}" % "月份", "生产计划" : "Results_.csv";
close "Results_.csv";
forall {<m> in M}
print "{}, {}" % m,y[m] >> "Results_.csv";
close "Results_.csv";
部分结果如下:
即,一月采购油脂VEG1的计划为0,使用计划为100吨,储存计划为400吨、二月的采购计划为0,使用和储存计划各为200吨。