小言_互联网的博客

运筹优化在物流行业应用demo案例,Gurobi求解

554人阅读  评论(0)

作者 | xulu1352   目前在一家互联网公司从事推荐算法工作(知乎:xulu1352)

编辑 | auroral-L

本篇文章已获得作者的独家授权,全文共4650字,预计阅读时间20分钟。

缘起

最近在清理笔记本硬盘文件,看到一个压箱底文件夹(本人早已脱离靠运筹模型恰饭的行业,进了推荐算法的坑),此文件是当年本人在老东家(某快递)做的一个组内分享gurobi求解数学建模的资料。国内互联网上关于gurobi这种土豪级专业运筹求解器的学习资料并不多见,今天就把当年做的demo案例分享给有需要的人,相关资料已经做了脱敏处理。

问题描述

在航空货运领域,使用货机运输快件。与干线(陆运)最大的不同是,飞机装载货物是通过不同型号的板箱,且飞机有固定的航线。

已知的条件:

1.目前除了全勤的货航飞机(下称全货机)以外,还有一些市场上的客机行李舱(下称散航)可以租用;

2.全货机通常是物流公司的固定资产,因此成本远低于散航;

3.路由,这里指航线,其输入组成:全货机-全货机;全货机-散航;散航直达;全货机直达;

4.全货机的板箱有多种类型:AKE、FC、DQF、PAG等,受飞机内部结构影响做成的不同形状的板箱,其中PAG板是规则的板,可以在飞机转运过程中适用于各种机型,因此PAG板在人工经验下一般装载相同目的地的货物,以便于转运时装卸,从而提高时效;

5.名词解释:od指从始发地到目的地的一票件,为缩小计算规模,通常会按半小时汇总一次;

6.全货机与散航都有机型的区别,载重与装载率限制,货物可以按票计量也可以按重计量;

下述模型解决的问题:如何在货航运输中最有效地利用PAG板,使得整个航空运输网络的时效最大化,时效一致的情况下选择成本更低的航线;

Problem Description

目标函数 :优先全网时效最大化,时效相同的情况下选择成本较低的航线

约束条件 :

(1):整板中转路由的板数限制

(2):混板中转/直发路由的板数限制

(3):货量百分比<=1

(4):全货机板数的限制

(5):散航可获取舱位的限制

(6):转板最低装载率的限制

(7):转运的板箱PAG数量相等的限制

Sets:

 :OD集合(d)

 :全货机集合(c)

 :散航集合(b)

 :路由集合(r)

 :全货机整板中转路由集合(rt,RT⊆CR)

 :全货机直发路由/混板中转集合(rs,RS⊆CR)

 :满载率系数 0.95

 :最低装载率系数 0.5

 :惩罚系数

Parameter:

 :OD pair d的货量

 :OD pair d 的票量

 :散航b的容量

 :全货机c的容量

 :全货机c可承载的PAG板箱个数(针对需要中转的路由)

 :全货机c除了PAG外剩余舱位

 :全货机PAG板箱的容量,可以容纳的OD重量

 :路由r的派送达成率(包括次晨和次日)

 :路由r的单公斤成本

 :全货机直发路由/混板中转路由rs是否包含全货机c,0-1

 :全货机整板中转路由rt是否包含全货机c,0-1

 :路由r是否包含此散航b,0-1

Decision Variable:

 : [0,1],d货量分配在路由r上的百分比,连续变量

 :全货机c在全货机整板中转路由rt中分配的PAG个数,整数变量

 :全货机c在全货机直发/混板中转路由rs中分配的PAG个数,整数变量

加载数据


   
  1. from itertools import groupby
  2. from itertools import combinations
  3. import pandas as pd
  4. import numpy as np
  5. import pickle
  6. # from docplex.mp.model import Model
  7. # from docplex.util.environment import get_environment
  8. import gurobipy
  9. #od集合
  10. with open( './data/od', 'rb') as f:
  11. od = pickle.load(f)
  12. #路由集合
  13. with open( './data/route', 'rb') as f:
  14. route = pickle.load(f)
  15. #散航集合
  16. with open( './data/cargo_set_data', 'rb') as f:
  17. cargo_set_data = pickle.load(f)
  18. #全货机集合
  19. with open( './data/cargo_route_data', 'rb') as f:
  20. cargo_route_data = pickle.load(f)
  21. #全货机路由映射表
  22. with open( './data/cargo_trans_data', 'rb') as f:
  23. cargo_trans_data = pickle.load(f)
  24. #全货机直发/混板中转路由映射表
  25. with open( './data/cargo_straight_data', 'rb') as f:
  26. cargo_straight_data = pickle.load(f)
  27. # 全货机整板中转路由映射表
  28. with open( './data/passenger_map_data', 'rb') as f:
  29. passenger_map_data = pickle.load(f)
  30. # 散航映射表
  31. with open( './data/passenger_data', 'rb') as f:
  32. passenger_data = pickle.load(f)
  33. #全货机直发/混板中转路由与OD
  34. route_data = route[[ 'od_id', 'route_id']].copy()
  35. DCS_df = pd.merge(cargo_straight_data,route_data, on=[ 'route_id'],how= 'left')[[ 'od_id', 'route_id', 'cargo_straight_id']].copy()
  36. DCS_df.rename( columns={ 'cargo_straight_id': 'move_id'},inplace= True)
  37. #全货机中转路由与OD
  38. DCT_df = route[(route[ 'aircraft_type'] == 'cargo_aircraft')&(route[ 'is_trans'] == '1')][[ 'od_id', 'route_id', 'move_id']].copy()
  39. #散航路由与OD
  40. DBS_df = route[(route[ 'aircraft_type'] == 'passenger_aircraft')][[ 'od_id', 'route_id', 'move_id']].copy()
  41. DCS = list( map(tuple, DCS_df.values))
  42. DCT = list( map(tuple, DCT_df.values))
  43. DBS = list( map(tuple, DBS_df.values))
  44. DBS = sorted(DBS, key=lambda x:x[ 2])
  45. DBS_iter = groupby(DBS, lambda x:x[ 2])
  46. DBS_dict = dict([( key, list( group)) for key, group in DBS_iter])
  47. DCT = sorted(DCT, key=lambda x:x[ 2])
  48. DCT_iter = groupby(DCT, lambda x:x[ 2])
  49. DCT_dict = dict([( key, list( group)) for key, group in DCT_iter])
  50. DCS = sorted(DCS, key=lambda x:x[ 2])
  51. DCS_iter = groupby(DCS, lambda x:x[ 2])
  52. DCS_dict = dict([( key, list( group)) for key, group in DCS_iter])

创建模型及变量

创建模型


   
  1. model = gurobipy.Model()
  2. #Compute Server job ID: aadf4435-ae7e-4a24-b4d5-917e0a5acdc5
  3. #Capacity available on '*.*.*.*:80' - connecting...
  4. #Established HTTP unencrypted connection

Z变量 ([0,1],d货量分配在路由r上的百分比)


   
  1. DR = list(map(tuple, route[[ 'od_id', 'route_id']].values))
  2. DR = sorted(DR, key=lambda x:x[ 0])
  3. DR_iter = groupby(DR, lambda x:x[ 0])
  4. DR_dict = dict([(key,list(group)) for key, group in DR_iter])
  5. Z = model.addVars(DR, lb= 0.0, ub= 1, vtype=gurobipy.GRB.CONTINUOUS, name= "Z")
  6. # Z[('010R#020W#2019-09-17 18:30:00#T4', 29819)]

XT变量 (全货机c在全货机整板中转路由rt中分配的PAG个数,整数变量)


   
  1. #整板中转路由与全货机映射关系
  2. RT_C = cargo_trans_data[['cargo_transportation_id', 'flight_id']].copy()
  3. RT_C.drop_duplicates(inplace=True)
  4. RT_C_MAP = list(map(tuple, RT_C.values)) #全货机中转路由和全货机映射关系
  5. RT_C_MAP = sorted(RT_C_MAP, key=lambda x:x[0])
  6. RT_C_iter = groupby(RT_C_MAP, lambda x:x[0])
  7. RT_C_dict = dict([(key,list(group)) for key, group in RT_C_iter])
  8. RT = RT_C.cargo_transportation_id.unique()
  9. XT = model.addVars(RT_C_MAP, lb=0, ub=25, vtype=gurobipy.GRB.INTEGER, name= "XT")
  10. model.update()
  11. # XT[('CAN#O36979#WUX&WUX#O36902#CGO', 'CAN#O36979#WUX')]

XS变量 (全货机c在全货机直发/混板中转路由rs中分配的PAG个数,整数变量)


   
  1. RS_C = cargo_straight_data[['cargo_straight_id','flight_id']].copy()
  2. RS_C.drop_duplicates(inplace=True)
  3. RS_C_MAP = list(map(tuple, RS_C.values)) #全货机直发路由和全货机映射关系
  4. # model.XS = model.integer_var_dict(RS_C_MAP,lb=0, ub=25, name=lambda xs: 'XS_{}_{}'.format(xs[1], xs[0]))
  5. XS = model.addVars(RS_C_MAP, lb=0, ub=25, vtype=gurobipy.GRB.INTEGER, name= "XS")
  6. model.update()
  7. # XS[('SZX#O36879#CGO', 'SZX#O36879#CGO')]

参数定义

集合


   
  1. #系数
  2. alpha = 0.95 #满载
  3. beta = 0.5 #最低装载
  4. theta = 100 #时效激励系数
  5. gamma = 1 #成本惩罚系数
  6. omega = 80 #无路由惩罚系数
  7. # Parameter
  8. #路由集合
  9. R = route[['route_id', 'rate','route_cost']].set_index('route_id').to_dict()
  10. # R_id = list(route['route_id'].unique())
  11. Tr = R['rate'] #路由时效
  12. Lr = R['route_cost'] #路由成本
  13. #od集合
  14. D = od[['od_id', 'package_qty','real_weight']].set_index('od_id').to_dict()
  15. D_id = list(od['od_id'].unique())
  16. Nd = D['package_qty'] #od票数
  17. Wd = D['real_weight'] #od重量
  18. #全货机集合
  19. C = cargo_set_data[['flight_id','capacity','PAG_qty','non_PAG_capacity','PAG_capacity']].set_index('flight_id').to_dict()
  20. C_id = list(cargo_set_data['flight_id'].values) #全货机id
  21. Mc = C['capacity'] #全货机业载
  22. Pc = C['PAG_qty'] #全货机c可承载的PAG板箱个数
  23. Qc = C['non_PAG_capacity'] #全货机c除了PAG外剩余舱位
  24. # V = C['PAG_capacity'] #全货机PAG板箱的容量
  25. #散航集合
  26. B = passenger_data[['move_id','capacity']].set_index('move_id').to_dict()
  27. B_id = list(passenger_data['move_id'].values) #散航id
  28. Mb = B['capacity'] #散航可获取舱位
  29. # 路有单个PAG容量
  30. V = cargo_route_data[['cargo_route_id','PAG_capacity']].drop_duplicates().set_index(
  31.     'cargo_route_id').to_dict()['PAG_capacity']

约束条件


   
  1. #整板中转路由 全货机分配的PAG
  2. model.addConstrs(gurobipy.quicksum(Z[(dr[0],dr[1])] * Wd[dr[0]] for dr in DCT_dict[rt_c[0]])
  3. <= alpha * XT[rt_c] * V[rt_c[0]] for rt_c in RT_C_MAP)
  4. # 直发路由 全货机分配的货量
  5. model.addConstrs(gurobipy.quicksum(Z[(dr[0],dr[1])] * Wd[dr[0]] for dr in DCS_dict[rs_c[0]])
  6. <= alpha * XS[rs_c] * V[rs_c[0]] + Qc[rs_c[1]] for rs_c in RS_C_MAP)
  7. # Z变量约束
  8. model.addConstrs(gurobipy.quicksum(Z[dr] for dr in DR_dict[d]) <= 1 for d in D_id)
  9. # 全货机PAG板约束
  10. model.addConstrs(gurobipy.quicksum(XT[rt_c] for rt_c in RT_C_MAP if rt_c[1] == c)
  11. + gurobipy.quicksum(XS[rs_c] for rs_c in RS_C_MAP if rs_c[1] == c) <= Pc[c] for c in C_id)
  12. #散航可获取舱位约束
  13. model.addConstrs(gurobipy.quicksum(Z[(dr[0],dr[1])] * Wd[dr[0]] for dr in DBS_dict[b])
  14. <= alpha * Mb[b] for b in B_id)
  15. #转板最低装载率的限制
  16. model.addConstrs(gurobipy.quicksum(Z[(dr[0],dr[1])] * Wd[dr[0]] for dr in DCT_dict[rt_c[0]])
  17. >= alpha * (XT[rt_c] - 1) * V[rt_c[0]] + beta * V[rt_c[0]] for rt_c in RT_C_MAP)
  18. # 整板中转路由PAG数相等的限制
  19. for rt in RT:
  20. rtc = RT_C_dict[rt]
  21. rtc_comb = list(combinations(rtc, 2))
  22. for rtc_c in rtc_comb:
  23. model.addConstr(XT[rtc_c[0]] == XT[rtc_c[1]])
  24. model.update()

目标函数


   
  1. model.setObjective(gurobipy.quicksum(theta * Z[dr] * Tr[dr[1]] * Nd[dr[0]] - gamma * Lr[dr[1]] * Z[dr] * Wd[dr[0]] for dr in DR) -
  2. gurobipy.quicksum(omega * (1 - gurobipy.quicksum(Z[dr] for dr in DR_dict[d])) * Nd[d] for d in D_id),
  3. gurobipy.GRB.MAXIMIZE)
  4. # model.Params.LogToConsole=True # 显示求解过程
  5. # model.Params.outputFlag = 0
  6. logfile = open( 'cb.log', 'w')
  7. model._logfile = logfile
  8. # model.Params.MIPGap=0.0000001 # 百分比界差
  9. model.optimize()
  10. logfile.close()

求解日志


   
  1. Optimize a model with 128960 rows, 605647 columns and 1379343 nonzeros
  2. Variable types: 604159 continuous, 1488 integer (0 binary)
  3. Coefficient statistics:
  4. Matrix range [4e-01, 6e+03]
  5. Objective range [5e-02, 7e+05]
  6. Bounds range [1e+00, 2e+01]
  7. RHS range [1e+00, 1e+04]
  8. Found heuristic solution: objective 6.374111e+07
  9. Presolve removed 86364 rows and 394040 columns (presolve time = 6s) ...
  10. Presolve removed 103872 rows and 467534 columns (presolve time = 12s) ...
  11. Presolve removed 103872 rows and 467534 columns
  12. Presolve time: 11.97s
  13. Presolved: 25088 rows, 138113 columns, 294625 nonzeros
  14. Found heuristic solution: objective 8.552618e+07
  15. Variable types: 137781 continuous, 332 integer (125 binary)
  16. Deterministic concurrent LP optimizer: primal and dual simplex
  17. Showing first log only...
  18. Root simplex log...
  19. Iteration Objective Primal Inf. Dual Inf. Time
  20. 0 7.9463817e+06 0.000000e+00 6.256257e+07 16s
  21. Concurrent spin time: 0.04s
  22. Solved with dual simplex
  23. Root relaxation: objective 1.855799e+08, 3937 iterations, 3.93 seconds
  24. Nodes | Current Node | Objective Bounds | Work
  25. Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
  26. 0 0 1.8558e+08 0 176 8.5526e+07 1.8558e+08 117% - 21s
  27. H 0 0 1.848550e+08 1.8558e+08 0.39% - 22s
  28. H 0 0 1.849884e+08 1.8558e+08 0.32% - 27s
  29. 0 0 1.8543e+08 0 123 1.8499e+08 1.8543e+08 0.24% - 29s
  30. H 0 0 1.853229e+08 1.8543e+08 0.06% - 31s
  31. 0 0 1.8542e+08 0 111 1.8532e+08 1.8542e+08 0.05% - 32s
  32. 0 0 1.8542e+08 0 111 1.8532e+08 1.8542e+08 0.05% - 33s
  33. 0 0 1.8541e+08 0 69 1.8532e+08 1.8541e+08 0.04% - 35s
  34. H 0 0 1.853492e+08 1.8541e+08 0.03% - 36s
  35. 0 0 1.8540e+08 0 69 1.8535e+08 1.8540e+08 0.03% - 39s
  36. 0 0 1.8540e+08 0 69 1.8535e+08 1.8540e+08 0.03% - 39s
  37. 0 0 1.8540e+08 0 69 1.8535e+08 1.8540e+08 0.03% - 40s
  38. 0 0 1.8540e+08 0 52 1.8535e+08 1.8540e+08 0.03% - 41s
  39. H 0 0 1.853721e+08 1.8540e+08 0.02% - 43s
  40. 0 0 1.8540e+08 0 49 1.8537e+08 1.8540e+08 0.02% - 44s
  41. 0 0 1.8540e+08 0 49 1.8537e+08 1.8540e+08 0.02% - 44s
  42. 0 0 1.8540e+08 0 50 1.8537e+08 1.8540e+08 0.02% - 46s
  43. H 0 0 1.853783e+08 1.8540e+08 0.01% - 47s
  44. 0 0 1.8540e+08 0 48 1.8538e+08 1.8540e+08 0.01% - 49s
  45. 0 0 1.8540e+08 0 48 1.8538e+08 1.8540e+08 0.01% - 50s
  46. 0 0 1.8540e+08 0 48 1.8538e+08 1.8540e+08 0.01% - 51s
  47. 0 0 1.8540e+08 0 41 1.8538e+08 1.8540e+08 0.01% - 53s
  48. H 0 0 1.853855e+08 1.8540e+08 0.01% - 55s
  49. Cutting planes:
  50. Gomory: 60
  51. Implied bound: 810
  52. Clique: 1
  53. MIR: 255
  54. Flow cover: 13
  55. Explored 1 nodes (7317 simplex iterations) in 55.71 seconds
  56. Thread count was 8 (of 8 available processors)
  57. Solution count 9: 1.85385e+08 1.85378e+08 1.85372e+08 ... 8.51746e+07
  58. Optimal solution found (tolerance 1.00e-04)
  59. Best objective 1.853854947260e+08, best bound 1.854012555127e+08, gap 0.0085%

解析结果


   
  1. df_xt = pd.DataFrame([[rt_c[ 0], rt_c[ 1], XT[rt_c].X] for rt_c in RT_C_MAP], columns=[ 'cargo_transportation_id', 'flight_id', 'PAG'])
  2. df_xs = pd.DataFrame([[rs_c[ 0], rs_c[ 1], XS[rs_c].X] for rs_c in RS_C_MAP], columns=[ 'cargo_straight_id', 'flight_id', 'PAG'])
  3. df_z = pd.DataFrame([[dr[ 0], dr[ 1], Z[dr].X] for dr in DR], columns=[ 'od_id', 'route_id', 'rate'])


转载:https://blog.csdn.net/qq_43045873/article/details/114312658
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场