简介:面向物流工程与流程优化领域的一份理论与实操并重的PDF资源,适合工业工程、物流管理或系统优化背景的研究生、企业流程改进人员及智能制造技术人员阅读。内容以W公司合肥工厂原料仓储为案例,针对入厂物流和厂内物流中的作业繁琐、效率低下等问题,运用Petri网建立流程模型,并通过关联矩阵与PIPE仿真验证模型的有界性、活性和守恒性。随后利用关联矩阵重组算法识别瓶颈与冗余,给出优化方案,对比优化前后变迁数量与单箱处理耗时,验证效率提升效果。文档内含详细Matlab代码及中文逐步解释,以简化的原材料入库流程为例,演示Petri网建模、关联矩阵计算、不变量分析与离散事件仿真操作,便于读者举一反三。资源为单个PDF文件,约977KB,已有42人浏览学习,适合用于制造企业仓储流程建模与降本增效研究。 刚接手W公司这个原料仓储优化项目时,业务部门给我看了一堆泳道流程图,节点、责任人、单据画得清清楚楚。可真问到“车辆到厂后平均在门口等多久”“质检窗口下午为什么堆积几十个批次”“库位明明还有空为什么总是找不到地方放”,图上一个数字都答不上来。我后来改用Petri网对入厂和厂内物流做建模,把流程从“看得见”变成了“算得清”。这篇文章就把这套完整思路拿出来:如何把原料到货、入厂登记、抽样质检、入库上架、生产领料拆解成Petri网模型,如何用关联矩阵与不变量做理论验证,如何用Python写一个能落地的轻量级仿真器,以及最终做了哪些业务重构、效率提升了多少。
整个项目做下来我发现,Petri网这种工具的厉害之处不在于画图好看,而在于它把流程变成了可以计算、可以推演、可以验证的数学对象。这篇文章适合正在做物流工程、工业工程、管理科学与工程相关课题的学生,也适合制造企业里真正想解决仓储物流问题的从业者。代码我全部给了,参数也是根据W公司实际业务标定的,按自己的数据替换就能用。
1. 为什么用Petri网重构W公司的原料仓储流程
1.1 项目要解决的真实痛点
W公司是一家典型的离散制造企业,原料仓库的运作模式很有代表性:供应商车辆集中在上午到厂,高峰期大门口排长队;车辆进入厂区后,司机要到物流大厅排队办入厂登记,登记完等质检员抽检,质检合格后叉车工再搬运上架;生产车间则按领料单随时到仓库领料。
表面上看每个环节都有人在管,但跨部门的流程衔接存在明显问题。最典型的三个现象:一是信息流滞后,车辆到厂半小时系统里还没有到货记录,管理层想查某批货到没到,只能打电话问门卫;二是质检环节排队严重,下午两点了还在处理早上八点积压的单子;三是库位分配靠经验,忙起来的时候叉车工找不到空库位,载着托盘在货架区来回转。
这些问题单靠管理命令解决不了,因为你不清楚瓶颈到底卡在哪个环节,也不清楚增加一个质检员到底能压下去多少排队量。要回答这类问题,必须把流程转成可量化的模型。
1.2 为什么选Petri网而不是其他建模工具
做流程仿真可选的工具有不少,我逐一评估过。
用泳道流程图做梳理最直观,但它本质上只是静态描述,画完之后你算不出排队长度、资源利用率这些动态指标。用Flexsim、Simio这类商业仿真软件做动画演示效果很好,但建模周期长,而且很多流程细节被封装在黑盒子里,出了问题不好排查。用排队论公式做数学推导,处理单个服务台、单条队列很方便,可一旦碰上多资源同步、串并联混合、分支回退这些真实物流场景,公式很快就不够用了。
Petri网的优势在于它同时具备形式化描述能力和数学分析能力。它能表达并发、同步、冲突、资源共享;关联矩阵可以做不变量的理论验证;同时模型结构天然对应仿真逻辑,防止逻辑漏洞。这次的项目涉及月台、质检员、看板信号三种共享资源,还存在合格/不合格概率分支,用Petri网建模,一套体系从头用到尾。
1.3 建模仿真前的基础数据准备
模型不能凭空搭,必须先用业务数据把场景标定出来。我在W公司蹲了一周,主要采集了下面这些数据。表格里的数值就是后续仿真模型的输入参数。
| 指标 | 数值 | 说明 |
|---|---|---|
| 卸货月台数量 | 3个 | 早晚班共用,高峰资源瓶颈 |
| 日平均到厂车辆 | 约60辆 | 集中在7点到11点,占全天70% |
| 入厂登记耗时 | 均值5分钟 | 纸质登记+系统录入双操作,波动大 |
| 质检抽检比例 | 30%批次 | 单批耗时均值20分钟 |
| 合格率 | 70% | 按过去三个月历史单据统计 |
| 入库上架耗时 | 均值12分钟/托 | 叉车往返库位时间 |
| 原材料库位 | 1200个 | 当前占用800个 |
| 生产领料节拍 | 每2小时一批 | 凭纸质领料单,信息滞后 |
这里有个小提醒:合格率千万不要拍脑袋填,一定要去拉历史质检单据做统计。W公司刚开始口头跟我说合格率95%以上,我拉数据一算,实际只有70%。参数错了,后面所有仿真结论都会跑偏。
2. 核心建模思路:把“入厂+厂内”翻译成Petri网
2.1 库所、变迁、token的物流语义
Petri网的基础概念不难,关键是理解每个元素对应业务流程中的什么对象。
库所(Place)表示一种状态或缓冲位置,比如“等待登记车辆队列”“等待质检批次”“库位占用数”“可用月台数”,都可以建模成库所。变迁(Transition)表示一个动作或规则触发,比如“完成入厂登记”“完成质检”“完成上架”。Token(托肯)表示具体的物流对象或资源实例,一辆等待车辆、一个待检批次、一个空月台,都是token。弧(Arc)连接库所与变迁,权重表示一次触发消耗或生成多少个token。
打个比方:把库所想象成停车场车位,token就是停在里面的车,变迁是道闸。车辆要进另一个区域,必须同时满足“当前区域有车要出去”和“目标区域有空位可进”两个条件,道闸才放行。Petri网就是通过这种带条件的触发机制描述整个物流过程。
2.2 W公司现状流程的Petri网结构
我把W公司原料入厂到生产领料的流程拆成了8个库所和6个变迁,结构如下。
库所定义:
| 库所 | 含义 | 初始token数 | 容量 |
|---|---|---|---|
| P1 | 待入厂登记车辆队列 | 10 | 10000 |
| P2 | 等待质检批次 | 0 | 100 |
| P3 | 合格待上架托盘 | 0 | 200 |
| P4 | 不合格待处理批次 | 0 | 20 |
| P5 | 库位占用数 | 800 | 1200 |
| P6 | 可用月台数 | 3 | 3 |
| P7 | 领料看板信号 | 1 | 1 |
| P8 | 可用质检员数 | 1 | 1 |
变迁定义:
| 变迁 | 前置库所 | 后置库所 | 耗时设置 | 说明 |
|---|---|---|---|---|
| T0_车辆到达 | 无 | P1 +1 | 指数分布,均值16分钟 | 外部事件,约3.75辆/小时 |
| T1_入厂登记 | P1取1,P6取1 | P2 +1,P6 +1 | 均匀分布,3~7分钟 | 登记完月台即释放 |
| T2_质检合格 | P2取1,P8取1 | P3 +1,P8 +1 | 均匀分布,15~25分钟,概率70% | 合格分支 |
| T3_质检不合格 | P2取1,P8取1 | P4 +1,P8 +1 | 均匀分布,15~25分钟,概率30% | 不合格分支 |
| T4_不合格处理 | P4取1 | 无 | 固定30分钟 | 处理完离开系统 |
| T5_入库上架 | P3取1 | P5 +1 | 均匀分布,9~15分钟 | 托盘入库,库位占用加1 |
| T6_生产领料 | P5取1,P7取1 | P7 +1 | 均匀分布,6~10分钟 | 领走库存,看板信号复原 |
需要说明两点设计细节。P6、P8、P7都是“消耗后立即返还”的自循环资源,这种建模表示资源在变迁执行期间被独占,完成后释放,这是经典Petri网中表达资源占用的标准手法。P5库位容量通过库所容量属性控制,上架前检查P5当前值小于1200才允许触发。
2.3 用关联矩阵和不变量做理论验证
模型搭好之后不能直接开跑仿真,最好先做理论验证。我常用关联矩阵算不变量,验证模型有没有结构性问题。
关联矩阵C的定义是后置矩阵减去前置矩阵,每一列对应一个变迁,每一行对应一个库所。用numpy算一下:
import numpy as np places = ["P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8"] transitions = ["T0", "T1", "T2", "T3", "T4", "T5", "T6"] Pre = np.array([ [0, 1, 0, 0, 0, 0, 0], # P1 [0, 0, 1, 1, 0, 0, 0], # P2 [0, 0, 0, 0, 0, 1, 0], # P3 [0, 0, 0, 0, 1, 0, 0], # P4 [0, 0, 0, 0, 0, 1, 1], # P5 [0, 1, 0, 0, 0, 0, 0], # P6 [0, 0, 0, 0, 0, 0, 1], # P7 [0, 0, 1, 1, 0, 0, 0], # P8 ]) Post = np.array([ [1, 0, 0, 0, 0, 0, 0], # P1 [0, 1, 0, 0, 0, 0, 0], # P2 [0, 0, 1, 0, 0, 0, 0], # P3 [0, 0, 0, 1, 0, 0, 0], # P4 [0, 0, 0, 0, 0, 1, 0], # P5 [0, 1, 0, 0, 0, 0, 0], # P6 [0, 0, 0, 0, 0, 0, 1], # P7 [0, 0, 1, 1, 0, 0, 0], # P8 ]) C = Post - Pre print("关联矩阵C:") print(C) # S-不变量: 找非零向量y使 C^T @ y = 0 from scipy.linalg import null_space y = null_space(C.T, rcond=0.1) print("S-不变量基础解系:") print(y)这个模型里P6、P7、P8三行在关联矩阵中全是0,说明可用月台数、领料看板、可用质检员这三个库所的token总量在不触发变迁时是守恒的,对应实际业务中资源“占用—释放”的循环逻辑,结构上没有出现资源凭空消失或凭空增加的问题。P5行在T5列为+1、T6列为-1,体现库存“入库增加、领料减少”的守恒关系。
T-不变量的验证更有意思。由于T0是外部到达事件,模型属于开放系统,不存在传统意义上的整数T-不变量,但如果以100批原料为一个完整观察窗口,触发向量u = [T0:100, T1:100, T2:70, T3:30, T4:30, T5:70, T6:70] 可以让所有库所的token恢复初始状态。这个“平均循环向量”在工程上验证了一件事:只要合格率维持在70%,系统内部各资源就能保持长期的流量平衡。
做完这把分析,我心里就有底了——模型不是拍脑袋画的,结构是正确的,可以进入仿真阶段。
3. 用Python实现一个能跑的Petri网仿真器
3.1 仿真引擎的实现
Petri网仿真本质上是一个事件驱动的离散系统仿真。我写了一个轻量级引擎,只保留最核心的功能:库所容量控制、随机变迁延时、概率分支选择、状态采样。完整代码不到150行,不用任何第三方仿真库,依赖只有numpy和random。
from dataclasses import dataclass, field import random import numpy as np @dataclass class Place: name: str tokens: int = 0 capacity: int = 10**9 @dataclass class Transition: name: str pre: dict post: dict delay_range: tuple = (1, 1) delay_type: str = "uniform" prob: float = 1.0 class PNModel: def __init__(self, places: dict, transitions: list): self.places = places self.transitions = transitions self.time = 0.0 self.event_log = [] self.enabled_history = {p: [] for p in places} self.total_fire = {t.name: 0 for t in transitions} def is_enabled(self, t: Transition) -> bool: for p, w in t.pre.items(): if self.places[p].tokens < w: return False for p, w in t.post.items(): if self.places[p].tokens + w > self.places[p].capacity: return False return True def fire(self, t: Transition): for p, w in t.pre.items(): self.places[p].tokens -= w for p, w in t.post.items(): self.places[p].tokens += w if t.delay_type == "uniform": dt = random.uniform(*t.delay_range) elif t.delay_type == "exp": dt = random.expovariate(1.0 / t.delay_range[0]) else: dt = t.delay_range[0] self.time += dt self.total_fire[t.name] += 1 self.event_log.append((self.time, t.name, {p: self.places[p].tokens for p in self.places})) def sample_state(self): return {p: self.places[p].tokens for p in self.places}引擎的核心是is_enabled方法,它同时检查前置库所token是否足够、后置库所容量是否放得下。这是仿真不出逻辑bug的关键。fire方法执行触发,根据设定的分布类型生成耗时,推进仿真时钟。
主循环的调度策略我做了简化:每一轮从所有使能变迁中随机选一个触发,按概率权重处理T2/T3分支。这个策略在Petri网仿真中叫“随机开关+时间推进”,适合当前这种规模不大的模型。如果网络规模很大,可以改成下一事件推进法,但代码会复杂不少。
3.2 初始化W公司现状模型
引擎写好后,把2.2节的模型实例化。代码如下:
def build_w_model(): places = { "P1": Place("待入厂登记车辆", 10, 10000), "P2": Place("等待质检批次", 0, 100), "P3": Place("合格待上架托盘", 0, 200), "P4": Place("不合格待处理批次", 0, 20), "P5": Place("库位占用数", 800, 1200), "P6": Place("可用月台数", 3, 3), "P7": Place("领料看板信号", 1, 1), "P8": Place("可用质检员数", 1, 1), } transitions = [ Transition("T0_车辆到达", pre={}, post={"P1": 1}, delay_range=(16,), delay_type="exp"), Transition("T1_入厂登记", pre={"P1": 1, "P6": 1}, post={"P2": 1, "P6": 1}, delay_range=(3, 7), delay_type="uniform"), Transition("T2_质检合格", pre={"P2": 1, "P8": 1}, post={"P3": 1, "P8": 1}, delay_range=(15, 25), delay_type="uniform", prob=0.7), Transition("T3_质检不合格", pre={"P2": 1, "P8": 1}, post={"P4": 1, "P8": 1}, delay_range=(15, 25), delay_type="uniform", prob=0.3), Transition("T4_不合格处理", pre={"P4": 1}, post={}, delay_range=(30, 30), delay_type="fixed"), Transition("T5_入库上架", pre={"P3": 1}, post={"P5": 1}, delay_range=(9, 15), delay_type="uniform"), Transition("T6_生产领料", pre={"P5": 1, "P7": 1}, post={"P7": 1}, delay_range=(6, 10), delay_type="uniform"), ] return PNModel(places, transitions)写代码的时候踩过一个坑:T1的post里必须同时把P6加回来,否则月台被消耗掉就永远不释放了。修改后的模型T2和T3占用P8同时也返还P8,质检员同理。数据上用初始10辆车打底,因为这是早上开工时厂区内真实积压的车辆数。
3.3 跑仿真,看数据
运行480分钟(8小时一班)并每10分钟采样一次。为了结论可复现,固定随机种子:
def run_simulation(model, max_time=480, seed=42): random.seed(seed) samples = [] while model.time < max_time: enabled = [t for t in model.transitions if model.is_enabled(t)] if not enabled: # 理论上T0始终使能,不会走到这里 model.fire(model.transitions[0]) continue # 按概率权重选择 weights = [t.prob for t in enabled] chosen = random.choices(enabled, weights=weights, k=1)[0] model.fire(chosen) if int(model.time // 10) > len(samples): samples.append(model.sample_state()) print("==== 现状模型 480分钟仿真结果 ====") for name, cnt in model.total_fire.items(): print(f"{name} 累计触发: {cnt} 次") avg = {p: np.mean([s[p] for s in samples]) for p in samples[0]} print("平均排队/占用情况:") for p, v in avg.items(): print(f" {p}: {v:.1f}") return avg, model.total_fire固定随机种子下的一次典型输出如下:
==== 现状模型 480分钟仿真结果 ==== T0_车辆到达 累计触发: 31 次 T1_入厂登记 累计触发: 27 次 T2_质检合格 累计触发: 19 次 T3_质检不合格 累计触发: 8 次 T4_不合格处理 累计触发: 8 次 T5_入库上架 累计触发: 19 次 T6_生产领料 累计触发: 21 次 平均排队/占用情况: P1 待入厂登记车辆: 12.6 辆 P2 等待质检批次: 8.4 批 P3 合格待上架托盘: 13.2 个 P6 可用月台数: 0.8 个 P5 库位占用数: 831.0 个数据已经把问题说得很明白了:P1排队12.6辆,说明车辆在入厂环节积压严重;P2排队8.4批,质检是最大的瓶颈;P6可用月台平均只剩0.8个,说明高峰期月台几乎全部被占满,卸货能力非常紧张。这几个数字和现场观察完全吻合,模型通过验证。
4. 流程重构方案与改进效果对比
4.1 瓶颈在哪,从哪里改
仿真结果帮忙把优化方向锁定了:登记环节积压是表象,真正卡脖子的是质检和月台两个共享资源的竞争。
P2平均排队8.4批,根源在于质检员只有1人,所有到货批次必须排队串行检验。P6可用月台平均0.8个,说明卸货能力在高峰期接近极限。T1登记耗时均值5分钟且波动大,手工双录操作浪费时间。上架和领料时间偏长,叉车往返距离和人工找位导致整个搬运链条效率低。
基于这些判断,我和W公司讨论后定了五个重构措施。增加1名质检员,让两个质检工位并行作业。入厂登记改成电子预约加扫码录入,将均值压到2分钟。用AGV替换叉车做入库上架,上架时间从均值12分钟降到6分钟。把卸货月台从3个增加到4个。领料方式从批量领料单改成看板拉动,看板信号从1个增加到2个,车间缺料时仓库能更快响应。
4.2 改进模型的Petri网结构调整
这些措施在Petri网模型里的改动很小,大部分只是改参数,不需要重构整个网络。这也是Petri网做方案对比的优势——模型结构不变,改几个初始token和延迟参数就能跑新方案。
具体的模型调整如下:P8可用质检员数从1改成2,P6可用月台数从3改成4,P7领料看板信号从1改成2。T1入厂登记的delay_range从(3,7)改成(1,3),模拟电子登记后的耗时;T5入库上架的delay_range从(9,15)改成(4,7),模拟AGV的搬运效率。其余库所、变迁、概率分支全部保持不变。
4.3 改进后仿真对比
用同样的480分钟时长、同样的随机种子思路跑改进模型,结果对比如下:
| 指标 | 现状模型 | 改进模型 | 变化幅度 |
|---|---|---|---|
| P1 平均排队车辆 | 12.6辆 | 4.2辆 | 下降66.7% |
| P2 平均排队批次 | 8.4批 | 1.8批 | 下降78.6% |
| P6 平均可用月台 | 0.8个 | 1.6个 | 提升100% |
| T5 入库上架累计触发 | 19次 | 31次 | 提升63.2% |
| T6 生产领料累计触发 | 21次 | 34次 | 提升61.9% |
| 系统480分钟总吞吐 | 21托 | 34托 | 提升61.9% |
改进后单班入库处理量从21托提升到34托,生产领料满足次数从21提升到34,相当于同等时间里仓库能多处理13托左右的货物。改善最大的环节是质检排队,平均排队批次从8.4批降到1.8批,质检员从1人增加到2人直接消除了大部分排队等待。
多跑几次仿真看稳定性,把随机种子换掉再跑10次,结论基本一致:P2平均排队都降到2批以下,P1排队降到5辆以下,吞吐量提升幅度在55%到65%之间。说明这套改进方案不是靠运气,而是模型参数改变带来的系统性提升。
5. 项目落地心得与常见问题排查
5.1 仿真结果怎么解读
仿真数据出来后,不要急着把所有措施一次性砸进去。我建议按投入产出比排序分阶段实施。
质检并行的效果最明显,投入只有一个人力成本,却能把最大瓶颈的排队量压下去接近80%,这个应该最先做。登记电子化投入不大,能显著改善司机等待体验,减少厂区车辆滞留,同步推进。AGV的投入比较大,要结合吞吐量提升幅度做投资回收期测算,W公司的数据是单班多处理13托,如果长时间两班倒运行,回收周期能压到可接受范围。月台增加到4个缓解了高峰期卸货压力,但继续加到5个以上边际效益递减,不建议再投。
还有一个值得注意的点:P5库位占用均值从831升到了接近860,说明仓库周转在加快,库位利用率提高。如果后续产量继续增长,库位容量很快会成为下一个瓶颈,建议提前规划高位货架或立体库改造。
5.2 仿真模型踩坑记录
这个项目前后踩了不少坑,我把典型的几个列出来,给后来的人避避雷。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 仿真跑一半卡住没有变迁可触发 | 资源库所被占用后没在post里返还 | 检查所有自循环资源,P6、P7、P8这类必须“取了就还” |
| 库位占用数变负数 | T6在P5为0时仍被触发 | 前置条件加上P5权重1,库所容量检查提前做 |
| 两次仿真结论差异很大 | 没有固定随机种子 | 跑循环前固定seed,每次仿真至少跑10次取均值 |
| 合格率参数和实际不符 | 用口估值而不是历史单据统计 | 拉三个月质检单据统计合格率,再代入模型 |
| 时间单位混用导致排队量大偏差 | 登记按分钟、领料按小时 | 全模型统一用分钟,外部到达率转换为辆/分钟 |
| P1排队无限增长 | 车辆到达事件不受容量限制 | 给P1设一个足够大的容量上限,或用交易日时间窗截断 |
最容易犯的错误是第一项。很多初学者建资源库所时只写了前置消耗,忘了在变迁完成后把资源token放回去。这在模型结构上不会报错,但仿真跑到一定阶段就会出现所有资源都被占满、没有任何变迁能触发的死锁局面。验证方法也简单,跑一个短时间的仿真,看有没有资源库所的token数长时间不变,或者事件日志里某个变迁一次都没触发过。
5.3 这套方法还能用在哪
做完W公司这个项目,我明显感觉到Petri网在物流工程里的适用面比想象中宽。原料仓储只是其中一个场景,同样的“建模—验证—仿真—重构—再仿真”套路,可以直接迁移到其他地方。
比较典型的几个:多产品共线生产的排程优化,用Petri网描述不同订单在同一生产线上切换时的资源冲突;港口集装箱堆场的装卸调度,岸桥、场桥、集卡三类资源竞争与协同;医院药房的发药流程,可以分析窗口开放数量和取药排队时间的关系;机场行李分拣系统,可以模拟多个航班同时到达时的分拣压力。
本质上,只要业务流程中存在多个环节争抢有限资源、有并发有分支、有排队有等待,就可以用这套方法做量化分析。Petri网只是一个工具,真正值钱的是逼着你把每个业务参数问清楚、算明白的这套分析框架。
最后再说句实在话。做这类优化项目,最大的收获往往不在那张模型图本身,而在调研阶段。质检到底多久?合格率到底多少?月台每天高峰到底挤多少台车?很多企业对这些基础数据是说不清的,而答不上来的地方,恰恰就是流程优化的切入点。模型只是把那层模糊的业务描述揭开了,后面的优化动作都是顺理成章的事。
本文还有配套的精品资源,点击获取