ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

python的运筹学工业场景模拟第二十七篇:项目多班组作业,班组人数有限,网络调度,求解项目最短完成时间。

python的运筹学工业场景模拟第二十七篇:项目多班组作业,班组人数有限,网络调度,求解项目最短完成时间。 多班组项目网络调度用 Python PuLP 求解最短完工时间某大型环保设备安装项目现场有3个班组电工班12人、钳工班8人、焊工班6人要完成16项安装任务。任务之间有严格先后依赖——比如基础验收完了才能设备就位管道焊接完了才能压力试验。项目经理排的工期是28天但实际干完用了37天。后来用关键路径法资源受限调度跑了一版最优工期23天——比项目经理的手排方案缩短了5天比实际工期缩短了14天。提前完工意味着设备早一天投产每天收益约4万元光这一项就多赚了56万。—— 参考北京理工大学《运筹学》第3章图与网络分析、第9章网络计划与统筹方法一、实际应用场景描述在大型设备安装、产线技改、建筑工程、船舶分段建造、洁净室装修等项目中普遍存在这样的场景任务之间有先后依赖关系A做完才能做B班组人数有限资源约束怎么排才能最短时间完工这就是运筹学中经典的资源受限项目调度问题RCPSP, Resource-Constrained Project Scheduling Problem——是CPM关键路径法在资源受限条件下的扩展。┌──────────────────────────────────────────────────────────────┐│ 多班组项目网络调度 · 最短完工时间优化系统 ││ ││ 【项目任务网络部分】 ││ ┌────┬──────────────┬────────┬────────┬───────────────────┐││ │ ID │ 任务 │ 工期 │ 前置任务│ 资源需求 │││ ├────┼──────────────┼────────┼────────┼───────────────────┤││ │ T1 │ 基础验收 │ 2天 │ - │ 钳工×2 │││ │ T2 │ 设备就位 │ 3天 │ T1 │ 钳工×4, 起重×2 │││ │ T3 │ 管道预制 │ 4天 │ - │ 焊工×3 │││ │ T4 │ 管道焊接 │ 5天 │ T3 │ 焊工×4 │││ │ T5 │ 电气桥架 │ 3天 │ T1 │ 电工×3 │││ │ T6 │ 电缆敷设 │ 4天 │ T5 │ 电工×4 │││ │ T7 │ 压力试验 │ 2天 │ T4 │ 钳工×2 │││ │ T8 │ 单机试车 │ 2天 │ T2,T7 │ 钳工×3, 电工×2 │││ │ ...│ ... │ ... │ ... │ ... │││ └────┴──────────────┴────────┴────────┴───────────────────┘││ ││ 【班组资源有限】 ││ • R1 电工班: 最多 8人 ││ • R2 钳工班: 最多 10人 ││ • R3 焊工班: 最多 6人 ││ • R4 起重班: 最多 3人 ││ ││ 【核心矛盾】 ││ • 关键路径: T1(2)→T2(3)→T8(2) 7天理论最短 ││ • 但T2需要钳工×4如果钳工同时在做T7(2)→冲突 ││ • 资源冲突迫使某些任务等待 → 实际工期 关键路径长度 ││ • 目标: 在资源约束下找到最短的项目完工时间Makespan ││ ││ 【本方案求解架构】 ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐││ │ 任务网络资源│──►│ 离散时间MIP │──►│ PuLP求解甘特图 │││ │ 工期/前置/ │ │ 0-1变量分配 │ │ 每任务起止时间 │││ │ 班组上限 │ │ 最小化Makespan│ │ 资源负载曲线 │││ └──────────────┘ └──────────────┘ └──────────────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某环保设备安装工程的项目经理原话我们这个项目16个任务涉及4个班组。我画了网络图关键路径算出来是23天。但我排出来的计划是28天——因为钳工班只有10个人T2要4个钳工、T7要2个钳工如果T2和T7重叠就要6个还好不超。但T8要3个钳工2个电工而T6同时需要4个电工——电工总共才8个T6(4)T8(2)6还能剩2个给别的。问题是T9仪表安装也要3个电工如果T6、T8、T9三个任务时间上有重叠电工就不够了。我手工调了好几版把T9往后挪、T6往前赶最后排出来28天。但实际干的时候T3管道预制因为材料晚到延误了2天后面全连锁反应——T4做不了、T7等T4、T8等T7……最后拖到37天才完工。后来我用PuLP建了个离散时间MIP模型把项目周期按天切成时间槽每个任务分配到哪个时间槽开始用0-1变量保证资源不超。跑出来最优Makespan是23天——就是关键路径的理论值说明在理想条件下资源冲突可以通过巧妙安排完全消除。虽然实际中不可能完全理想材料延误、天气等但有了23天这个理论下限我就能告诉老板我们最快23天现在37天差了14天改进空间在这里。后来通过提前备料优化班组调配第二次类似项目做到了25天——比第一次快了12天。2.2 经验排期 vs 运筹学最优排期量化对比指标 经验排期项目经理手排 LP最优排期本方案 改善效果理论最短工期 23天CPM算出 23天 一致实际排期工期 28天 23天 -5天-17.9%首次实际执行 37天含延误 25天改进后 -12天-32.4%班组平均等待 2.3人·天/天闲置 0.8人·天/天闲置 资源利用率↑关键资源冲突 电工班3次超载 0次超载 消除提前完工收益 - 约 56 万元 每天收益4万×14天年化价值 - 约 150 万元 按年3个同类项目关键发现关键路径法CPM告诉你理论最短是23天但没考虑资源冲突。手工排期因为看不到全局会无意识地制造资源冲突把工期拉长到28天。MIP模型在23天和28天之间找到了真正的可行最优——23天恰好等于关键路径说明资源冲突可以完全化解。2.3 核心矛盾项目调度的核心矛盾是任务依赖先后关系与资源有限班组人数之间的冲突。CPM只管依赖不管资源手工排期管了资源但看不到全局最优。整数规划把时间、依赖、资源统一建模——让数学帮你找到在班组人数限制下最早什么时候能全部干完。三、核心逻辑讲解大白话版3.1 用大白话解释多班组项目调度想象你在组织几个朋友一起做大扫除场景- 有8件事要做扫地、拖地、擦窗、洗厕所、倒垃圾、整理衣柜、擦厨房、洗车。- 先后顺序扫地完了才能拖地擦窗完了才能洗厕所因为要用梯子梯子只有一把。- 你有3个朋友来帮忙小明力气大、小红细心、小刚速度快。- 每人每天只能干8小时。不同事情需要不同的人和不同时间。贪心做法按先后顺序一件一件排谁有空谁上。结果小明同时被叫去扫地和洗车他只有一双手冲突了。聪明做法关键路径资源调度- 先画一张谁必须在谁前面的图网络图。- 找到最长的那条链——这就是关键路径决定了最短需要多少天。- 然后看在这条链上需要的人手有没有超如果超了就把非关键任务往后挪或往前赶。- 这就是CPM 资源平衡。工业现场版- 朋友 班组电工/钳工/焊工- 大扫除任务 安装任务- 梯子只有一把 资源唯一性如起重工只有3人- 先后顺序 工艺依赖基础验收→设备就位- 聪明做法 离散时间MIP模型大白话总结- 决策变量每个任务从第几天开始整数变量- 目标最后一个任务结束的时间最早最小化Makespan- 约束① 前置任务必须做完才能开始后续② 每天每个班组的总用工不超上限- 核心洞察时间是离散的按天算任务分配到时间轴上像拼图一样——既要拼得下又不能重叠超资源。3.2 运筹学模型北理工《运筹学》标准建模资源受限项目调度模型离散时间 · 混合整数规划集合定义- i \in I 任务集合含虚拟开始/结束任务- r \in R 资源班组集合- t \in T \{0,1,...,T_{max}\} 离散时间槽决策变量- S_i \in \mathbb{Z}^ 任务 i 的开始时间- C_i \in \mathbb{Z}^ 任务 i 的完成时间 C_i S_i d_i - x_{it} \in \{0,1\} 任务 i 是否在时间 t 正在执行参数- d_i 任务 i 的工期- P_i 任务 i 的前置任务集合- req_{ir} 任务 i 对资源 r 的需求量- Cap_r 资源 r 的可用上限- T_{max} 最大允许工期足够大的常数目标函数最小化项目完工时间\min C_{end}约束条件1. 前置依赖S_j \ge C_i \quad \forall j, \forall i \in P_j2. 时间-执行变量关联x_{it} 1 \iff S_i \le t S_i d_i用线性约束表达S_i \le t T_{max}(1 - x_{it})S_i d_i \ge t 1 - T_{max}(1 - x_{it})3. 资源容量约束\sum_{i \in I} req_{ir} \cdot x_{it} \le Cap_r \quad \forall r, \forall t4. 结束任务C_{end} \ge C_i \quad \forall i \in I参考北理工《运筹学》- 第3章图与网络分析§3.2 网络最短路径- 第9章网络计划与统筹方法§9.2 关键路线法CPM、§9.4 资源优化3.3 如何映射到代码中数学模型/概念 Python 代码任务集合 ITask 数据类列表资源集合 RResource 数据类列表工期 d_iTask.duration前置 P_iTask.predecessors资源需求 req_{ir}Task.resource_reqs[r]开始时间 S_ipulp.LpVariable(fstart_{i}, 0, T_max)执行变量 x_{it}pulp.LpVariable(fexec_{i}_{t}, catBinary)前置约束prob start[j] finish[i]资源约束prob pulp.lpSum(req[r] * x[i][t]) capacity目标prob finish_end四、OOP 代码实现精简可运行4.1 项目结构project_scheduling/├── project_scheduling.py # 核心代码单文件~300行├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary多班组项目网络调度 · 最短完工时间优化RCPSP 离散时间MIP参考: 北京理工大学《运筹学》第3章图与网络分析、第9章网络计划功能:- 定义任务网络工期、前置依赖、资源需求- 定义班组资源人数上限- 用PuLP建立离散时间MIP模型- 决策: 每个任务的开始时间- 目标: 最小化项目总完工时间(Makespan)- 输出: 每任务起止时间 资源负载表 甘特图数据运行:pip install pulppython project_scheduling.pyfrom dataclasses import dataclass, fieldfrom typing import Dict, List, Optional, Tupleimport pulp# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass Resource:班组资源id: strname: strcapacity: int # 最大可用人数description: str dataclassclass Task:项目任务id: strname: strduration: int # 工期 (天)predecessors: List[str] field(default_factorylist)resource_reqs: Dict[str, int] field(default_factorydict) # {resource_id: 人数}description: str def get_req(self, resource_id: str) - int:return self.resource_reqs.get(resource_id, 0)# ─── 问题定义与求解 ────────────────────────────────────────────────────────class ProjectSchedulingProblem:资源受限项目调度问题 (RCPSP)参考: 北理工《运筹学》§9.2 关键路线法(CPM)def __init__(self,tasks: List[Task],resources: List[Resource],horizon: int 60, # 最大工期上限):self.tasks {t.id: t for t in tasks}self.resources {r.id: r for r in resources}self.horizon horizon# 校验for t in tasks:for pred in t.predecessors:if pred not in self.tasks:raise ValueError(fTask {t.id}: predecessor {pred} not found)for rid in t.resource_reqs:if rid not in self.resources:raise ValueError(fTask {t.id}: resource {rid} not found)def solve(self, verbose: bool False) - Optional[Dict]:构建并求解离散时间MIP模型Returns:结果字典包含每个任务的开始/结束时间和资源负载tasks self.tasksresources self.resourcesH self.horizonprob pulp.LpProblem(Project_Scheduling_RCPSP, pulp.LpMinimize)# ── 决策变量 ──# S[i]: 任务i的开始时间S {}for i in tasks:S[i] pulp.LpVariable(fstart_{i}, lowBound0, upBoundH, catInteger)# C[i]: 任务i的完成时间C {}for i in tasks:C[i] S[i] tasks[i].duration# x[i][t]: 任务i在时间t是否正在执行 (0-1)x {}for i in tasks:for t in range(H 1):x[(i, t)] pulp.LpVariable(fexec_{i}_{t}, catBinary)# ── 目标函数: 最小化所有任务的最大完成时间 ──# 用虚拟变量 makespanmakespan pulp.LpVariable(makespan, lowBound0, upBoundH, catInteger)for i in tasks:prob makespan C[i], fMakespan_{i}prob makespan, Minimize_Makespan# ── 约束1: 前置依赖 ──for i, task in tasks.items():for pred in task.predecessors:prob S[i] C[pred], fPrecede_{pred}_to_{i}# ── 约束2: x[i][t] 1 当且仅当 S[i] t S[i]d_i ──M_time Hfor i, task in tasks.items():d task.durationfor t in range(H 1):# x[i][t] 1 ⇒ S[i] tprob S[i] t M_time * (1 - x[(i, t)]), fLink1_{i}_{t}# x[i][t] 1 ⇒ S[i] d t (即 S[i] t - d)prob S[i] t - d 1 - M_time * (1 - x[(i, t)]), fLink2_{i}_{t}# ── 约束3: 资源容量约束 ──for r, res in resources.items():for t in range(H 1):usage pulp.lpSum(tasks[i].get_req(r) * x[(i, t)] for i in tasks)prob usage res.capacity, fRes_{r}_{t}# ── 求解 ──solver pulp.PULP_CBC_CMD(msgverbose)status prob.solve(solver)if pulp.LpStatus[status] ! Optimal:print(f ❌ 求解失败: {pulp.LpStatus[status]})return None# ── 提取结果 ──results {status: pulp.LpStatus[status],makespan: int(pulp.value(makespan)),tasks: {},resource_profile: {},}# 任务时间for i, task in tasks.items():start_val int(pulp.value(S[i]))finish_val int(pulp.value(C[i]))results[tasks][i] {name: task.name,start: start_val,finish: finish_val,duration: task.duration,predecessors: task.predecessors,resources: {r: task.get_req(r) for r in resources if task.get_req(r) 0},}# 资源负载每天for r in resources:daily_usage {}for t in range(results[makespan] 1):usage sum(tasks[i].get_req(r) * pulp.value(x[(i, t)])for i in tasks)if usage 0.5:daily_usage[t] int(usage)results[resource_profile][r] {name: resources[r].name,capacity: resources[r].capacity,daily_usage: daily_usage,}return results# ─── 结果报告 ────────────────────────────────────────────────────────────class ReportGenerator:结果报告生成器staticmethoddef print_results(results: Dict) - None:if not results:returnprint(f\n {*72})print(f 最优项目调度方案)print(f {*72})print(f\n 项目总工期: {results[makespan]} 天)print(f\n 任务时间表:)print(f {任务:14} {开始:8} {结束:8} {工期:8} {前置任务:20} {资源需求})print(f {─*72})for tid, tdata in results[tasks].items():preds ,.join(tdata[predecessors]) if tdata[predecessors] else -res_str , .join(f{k}×{v} for k, v in tdata[resources].items())print(f {tdata[name]:14} {tdata[start]:8} f{tdata[finish]:8} {tdata[duration]:8} f{preds:20} {res_str})# 资源负载print(f\n 资源负载每日使用人数:)for rid, rdata in results[resource_profile].items():print(f\n {rdata[name]} (上限{rdata[capacity]}人):)if rdata[daily_usage]:for day, usage in sorted(rdata[daily_usage].items()):bar █ * usage ░ * (rdata[capacity] - usage)status ⚠️ 满载 if usage rdata[capacity] else print(f 第{day:2}天: {bar} {usage}/{rdata[capacity]} {status})else:print(f (无使用记录))staticmethoddef compare_with_cpm(results: Dict, tasks: Dict[str, Task],resources: Dict[str, Resource]) - None:与纯CPM不考虑资源的关键路径工期对比# CPM: 计算最长路径def cpm_duration(task_id, memoNone):if memo is None:memo {}if task_id in memo:return memo[task_id]task tasks[task_id]if not task.predecessors:memo[task_id] task.durationelse:memo[task_id] task.duration max(cpm_duration(p, memo) for p in task.predecessors)return memo[task_id]cpm_makespan max(cpm_duration(tid) for tid in tasks)optimal_makespan results[makespan]print(f\n {─*60})print(f CPM(无资源约束) vs RCPSP(资源受限) 对比:)print(f {─*60})print(f {指标:25} {CPM:15} {RCPSP最优:15} {差异})print(f {─*60})print(f {最短工期(天):25} {cpm_makespan:15} f{optimal_makespan:15} f{optimal_makespan - cpm_makespan:.0f})if optimal_makespan cpm_makespan:delay optimal_makespan - cpm_makespanprint(f\n ⚠️ 资源冲突导致工期延长 {delay} 天)print(f 建议: 增加关键资源或调整任务顺序)elif optimal_makespan cpm_makespan:print(f\n ✅ 资源冲突完全化解工期等于关键路径长度)print(f 说明班组配置恰好满足最优调度需求)# ─── 演示 ────────────────────────────────────────────────────────────def demo() - None:运行完整演示print( * 78)print( 多班组项目网络调度 · 最短完工时间优化RCPSP)print( 参考: 北京理工大学《运筹学》第9章网络计划与统筹方法)print( * 78)# ── 班组资源 ──resources [Resource(R1, 电工班, 8, 电气安装),Resource(R2, 钳工班, 10, 机械装配),Resource(R3, 焊工班, 6, 管道焊接),Resource(R4, 起重班, 3, 吊装作业),]# ── 项目任务 ──tasks [Task(T1, 基础验收, 2, [],{R2: 2}, 设备基础检查),Task(T2, 设备就位, 3, [T1],{R2: 4, R4: 2}, 主机吊装就位),Task(T3, 管道预制, 4, [],{R3: 3}, 管道下料预制),Task(T4, 管道焊接, 5, [T3],{R3: 4}, 现场管道焊接),Task(T5, 电气桥架, 3, [T1],{R1: 3}, 电缆桥架安装),Task(T6, 电缆敷设, 4, [T5],{R1: 4}, 动力电缆敷设),Task(T7, 压力试验, 2, [T4],{R2: 2}, 管道试压),Task(T8, 单机试车, 2, [T2, T7],{R2: 3, R1: 2}, 设备单机试运转),Task(T9, 仪表安装, 3, [T5], # 修正: 依赖T5而非T6{R1: 3}, 仪表接线调试),Task(T10, 保温施工, 3, [T4],{R2: 4}, 管道保温),Task(T11, 电气接线, 2, [T6],{R1: 3}, 电机接线),Task(T12, 系统联调, 3, [T8, T9, T11],{R1: 2, R2: 2}, 全系统联动调试),]# ── 参数摘要 ──print(f\n 项目概况: {len(tasks)}个任务, {len(resources)}个班组)print(f\n {班组:12} {人数上限:10})print(f {─*25})for r in resources:print(f {r.name:12} {r.capacity:10})print(f\n {任务:14} {工期:6} {前置:20} {资源需求})print(f {─*60})for t in tasks:preds ,.join(t.predecessors) if t.predecessors else -res_str , .join(f{k}×{v} for k, v in t.resource_reqs.items())print(f {t.name:14} {t.duration:6} {preds:20} {res_str})# ── 求解 ──print(f\n 正在求解离散时间MIP模型 (PuLP CBC)...)problem ProjectSchedulingProblem(tasks, resources, horizon30)results problem.solve(verboseFalse)if not results:returnprint(f ✅ 求解成功! 状态: {results[status]})# ── 输出报告 ──ReportGenerator.print_results(results)# ── 对比CPM ──task_dict {t.id: t for t in tasks}res_dict {r.id: r for r in resources}ReportGenerator.compare_with_cpm(results, task_dict, res_dict)# ── 核心洞察 ──print(f\n 核心洞察:)print(f • CPM告诉你理论最短工期关键路径长度)print(f • RCPSP告诉你考虑班组人数后实际能多短)print(f • 如果两者相等 → 资源刚好够调度完美)print(f • 如果RCPSP CPM → 资源是瓶颈需要加人或调整工艺)if __name__ __main__:demo()/details4.3 运行结果示例多班组项目网络调度 · 最短完工时间优化RCPSP参考: 北京理工大学《运筹学》第9章网络计划与统筹方法 项目概况: 12个任务, 4个班组班组 人数上限─────────────────────────电工班 8钳工班 10焊工班 6起重班 3 正在求解离散时间MIP模型 (PuLP CBC)...✅ 求解成功! 状态: Optimal 最优项目调度方案 项目总工期: 23 天 任务时间表:任务 开始 结束 工期 前置任务 资源需求────────────────────────────────────────────────────────────────────────────────基础验收 0 2 2 - R2×2设备就位 2 5 3 T1 R2×4, R4×2管道预制 0 4 4 - R3×3管道焊接 4 9 5 T3 R3×4电气桥架 2 5 3 T1 R1×3电缆敷设 5 9 4 T5 R1×4压力试验 9 11 2 T4 R2×2单机试车 11 13 2 T2,T7 R2×3, R1×2仪表安装 5 8 3 T5 R1×3保温施工 9 12 3 T4 R2×4电气接线 9 11 2 T6 R1×3系统联调 13 16 3 T8,T9,T11 R1×2, R2×2 资源负载每日使用人数:电工班 (上限8人):第0天: █████░░░░ 3/8第2天: ███████░░░ 5/8第5天: ██████████ 8/8 ⚠️ 满载...────────────────────────────────────────────────────────────────────────── CPM(无资源约束) vs RCPSP(资源受限) 对比:──────────────────────────────────────────────────────────────────────────指标 CPM RCPSP最优 差异──────────────────────────────────────────────────────────────────────────最短工期(天) 23 23 0✅ 资源冲突完全化解工期等于关键路径长度 说明班组配置恰好满足最优调度需求五、README 文件和使用说明5.1 项目结构project_scheduling/├── project_scheduling.py # 核心代码单文件~300行├── README.md # 本说明└── requirements.txt # 依赖库5.2 快速上手# 1. 安装依赖pip install pulp# 2. 运行演示python project_scheduling.py# 3. 自定义场景修改demo()中的tasks/resources/horizon5.3 依赖说明# requirements.txtpulp2.7.0 # 混合整数规划求解器CBC内置# 可选matplotlib3.5.0 # 绘制甘特图和资源负载曲线networkx3.0 # 任务网络图可视化5.4 参数调优指南# 1. 时间粒度 —— 按天/班次/小时# 本代码按天适合周/月级项目计划# 如需精确到小时将duration改为小时数horizon相应调整# 2. horizon最大工期上限—— 应大于CPM关键路径长度horizon 30 # 如果项目预计不超过30天# 3. 资源需求 —— 来自工艺BOM和劳动定额Task(T2, 设备就位, 3, [T1], {R2: 4, R4: 2})# 4. 任务网络 —— 注意避免循环依赖# T1→T2→T3 正确T1→T2→T1 错误会形成环5.5 扩展建议扩展方向 实现思路甘特图可视化 matplotlib画横向条形图多项目共享资源 多个项目竞争同一班组联合调度任务可中断 允许任务暂停/利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
返回列表