⚠️ 前置说明:本篇是“网络流问题(第 7 章)”的韧性工程篇。核心目标是:在最大流算完之后,模拟传送带/管路/通信链路随机失效,观察最大流怎么掉、掉多少、掉在哪,输出一份“系统抗打击能力”报告。程序基于 NetworkX 最大流 + 随机删边迭代,无深度学习依赖,可直接运行。
动态物料分配与通道失效韧性分析:一条滚筒线坏了,产能掉多少?
“最大流 420 箱/小时很漂亮。但现场不会永远漂亮——滚筒堵料、气动阀卡死、AGV 巷道光幕误触发,通道说没就没。老板真正想问的是:‘坏一条路,产能掉几成?坏两条会不会直接瘫痪?’ 这不是最大流问题,是韧性(resilience)问题。”
—— 参考北京邮电大学 《图论及其应用》第 2 章“图的概念”、第 7 章“网络流 / 最大流最小割”**
一、实际应用场景描述
动态物料流韧性仿真器(MaterialFlowResilienceSimulator)解决的是“计划图在故障下还扛不扛得住”:
角色 图论含义 工业含义
有向边 物料通道 传送带 / 溜槽 / 管路 / 通信链路
"capacity" 通道上限 节拍上限 / 通径 / 带宽
源点
"S" 供料总入口 原料缓存区汇总
汇点
"T" 产线汇总 包装/出库总入口
删边 通道失效 堵料、停机、光幕误触发、阀门卡死
最大流衰减 产能损失 每小时少过多少件/吨
韧性曲线 抗打击能力 “坏 k 条还能保住多少产能”
为什么这篇重要:
- 第 46 篇:静态最大流;
- 第 53 篇:负载率预警;
- 本篇:把“时间”和“故障”加进来——动态图 + 蒙特卡洛式失效仿真,这是 MES/APS 做“应急预案”“冗余配置”“关键通道识别”的底层工具。
┌──────────────────────────────────────────────────────────────┐
│ 动态物料分配与通道失效韧性分析 │
│ │
│ 基准图 G0(V,E,capacity) │
│ │ │
│ ├─ MaxFlowBaseline :算原始最大流 F0 │
│ │ │
│ ▼ │
│ for trial in range(N): │
│ G = G0.copy() │
│ failed = random.sample(edges, k=fail_count) │
│ G.remove_edges_from(failed) │
│ F = max_flow(G, S, T) │
│ drop = F0 - F │
│ record(trial, failed, F, drop_ratio) │
│ │
│ 输出: │
│ - 平均衰减 / 最大衰减 / 归零概率 │
│ - 关键通道(删了最疼) │
│ - 韧性曲线:失效数 k → 剩余最大流 │
└──────────────────────────────────────────────────────────────┘
二、现场痛点(含量化对比)
2.1 现场原话(叙事)
某家电装配车间物流工程师:
“周五上午 B1→L1 滚筒堵料,PLC 报了,但 MES 还在按 420 排产。实际产线只吃到 260——因为 W→B2→L1 那条备用路本来就有,但没人知道它‘救回来 260’。等维修完,已经少装了 160 台。后来我做了失效仿真:随机坏 1 条,平均剩 320;随机坏 2 条,有 18% 概率直接归零。这张表比任何 PPT 都管用——主管当场批了 B2→L1 的第二条平行滚筒。”
2.2 三种认知对比(程序可复现)
视角 结论 问题
只看静态最大流 “420,够用” ❌ 不考虑故障
只看单点故障 “坏 B1→L1 还有 320” ❌ 不考虑并发故障
随机失效韧性仿真(本篇) 坏1条均值320 / 坏2条 18% 归零 / 关键边排名 ✅ 给冗余投资依据
注:场景与数值为教学示例;删边后重算最大流、衰减率、归零概率、关键边统计为 NetworkX 实测能力。
三、核心逻辑讲解(大白话)
3.1 大白话版
把工厂物料网想成“水管网”:
- 平时:水厂(S)往小区(T)送水,最大 420 吨/小时;
- 某天:B 路段爆管(删边),水改走别的管,还能送 320;
- 再某天:B 和 C 同时爆管,小区直接断水(最大流=0);
- 韧性 = “坏得越狠、剩得越多、归零概率越低”越好。
工程师要的不是“最好情况”,而是:
“随机坏 k 条,我的产能分布长什么样?”
这就是本篇程序在算的东西。
3.2 《图论》模型(北邮教材映射)
教材章节 本程序
第 2 章 图的概念 有向图、边集、删边操作
第 7 章 网络流 最大流、流量守恒、最小割
第 7 章延伸 边活力(edge vitality)= 删边后最大流下降量
(可靠性工程) 蒙特卡洛失效、韧性指标
数学式子:
F_0 = \max f(G_0),\quad F_k = \max f(G_0 \setminus E_{\text{fail}})
\text{drop} = \frac{F_0 - F_k}{F_0},\quad
\text{resilience}(k) = \mathbb{E}[F_k]
边 e 的“关键度”:
\text{criticality}(e) = \mathbb{E}[F_0 - f(G\setminus\{e\})]
3.3 代码映射
图论概念 代码
基准图
"MaterialFlowNetworkBuilder"
最大流
"nx.maximum_flow_value(G, S, T, capacity="capacity")"
随机失效
"random.sample(list(G.edges()), k)"
衰减率
"(F0 - F) / F0"
韧性报告
"ResilienceReport"
关键通道
"CriticalEdgeAnalyzer"
可视化 韧性曲线 + 关键边柱状图
四、OOP 代码实现
4.1 项目结构
material_flow_resilience/
├── material_flow_resilience.py # 核心:动态失效 + 最大流衰减 + 韧性报告
├── test_material_flow_resilience.py # 9 项单元测试(9/9 通过)
├── visualize.py # 韧性曲线 + 关键边图
├── resilience_curve.png
├── README.md
└── pack.py / material_flow_resilience.zip
4.2 核心源码
<details>
<summary></summary>
"""
动态物料分配与通道失效韧性分析
=============================
图建模:有向容量图
核心:模拟通道随机失效 → 重算最大流 → 统计衰减 → 输出韧性报告
参考:北邮《图论及其应用》第 2、7 章(最大流 / 最小割 / 边活力)
"""
import random
from dataclasses import dataclass, field
from typing import Dict, List, Tuple
import networkx as nx
import matplotlib.pyplot as plt
@dataclass
class FailureTrial:
"""一次失效仿真结果。"""
trial_id: int
failed_edges: List[Tuple[str, str]]
max_flow: float
base_flow: float
@property
def drop(self) -> float:
return max(0.0, self.base_flow - self.max_flow)
@property
def drop_ratio(self) -> float:
if self.base_flow <= 0:
return 0.0
return self.drop / self.base_flow
def summary(self) -> str:
failed = ",".join(f"{u}->{v}" for u, v in self.failed_edges)
return (f"trial#{self.trial_id}: failed=[{failed}] "
f"flow={self.max_flow:.1f}/{self.base_flow:.1f} "
f"drop={self.drop:.1f} ({self.drop_ratio*100:.1f}%)")
@dataclass
class ResilienceReport:
base_flow: float
trials: List[FailureTrial] = field(default_factory=list)
edge_criticality: Dict[Tuple[str, str], float] = field(default_factory=dict)
@property
def avg_drop_ratio(self) -> float:
if not self.trials:
return 0.0
return sum(t.drop_ratio for t in self.trials) / len(self.trials)
@property
def max_drop_ratio(self) -> float:
return max((t.drop_ratio for t in self.trials), default=0.0)
@property
def zero_flow_prob(self) -> float:
if not self.trials:
return 0.0
return sum(1 for t in self.trials if t.max_flow <= 1e-9) / len(self.trials)
def summary(self) -> str:
lines = [
"===== 物料流韧性报告 =====",
f"基准最大流 : {self.base_flow:.1f}",
f"仿真次数 : {len(self.trials)}",
f"平均衰减率 : {self.avg_drop_ratio*100:.1f}%",
f"最大衰减率 : {self.max_drop_ratio*100:.1f}%",
f"产能归零概率 : {self.zero_flow_prob*100:.1f}%",
"",
"--- 关键通道(删了最疼,按关键度降序)---",
]
if self.edge_criticality:
for (u, v), crit in sorted(self.edge_criticality.items(),
key=lambda x: -x[1])[:5]:
lines.append(f" {u}->{v} : criticality={crit:.2f}")
else:
lines.append(" 无(未做单边关键度分析)")
return "\n".join(lines)
class MaterialFlowNetworkBuilder:
"""构建 S→...→T 的物料流网络。"""
def __init__(self, source: str = "S", sink: str = "T"):
self.source = source
self.sink = sink
self._edges: List[Tuple[str, str, float]] = []
def add_channel(self, u: str, v: str, capacity: float):
if capacity < 0:
raise ValueError("容量不能为负")
self._edges.append((u, v, float(capacity)))
return self
def build(self) -> nx.DiGraph:
g = nx.DiGraph()
g.add_node(self.source)
g.add_node(self.sink)
for u, v, cap in self._edges:
g.add_edge(u, v, capacity=cap)
return g
class MaxFlowEngine:
"""最大流计算封装。"""
@staticmethod
def max_flow(g: nx.DiGraph, s: str, t: str) -> float:
if s == t:
return 0.0
if not nx.has_path(g, s, t):
return 0.0
return nx.maximum_flow_value(g, s, t, capacity="capacity")
class FailureSimulator:
"""随机删边 + 重算最大流。"""
def __init__(self, builder: MaterialFlowNetworkBuilder, seed: int = 42):
self.builder = builder
self.seed = seed
self.base_graph = builder.build()
self.base_flow = MaxFlowEngine.max_flow(
self.base_graph, builder.source, builder.sink)
def run_trial(self, trial_id: int, fail_count: int) -> FailureTrial:
rng = random.Random(self.seed + trial_id)
g = self.base_graph.copy()
edges = list(g.edges())
k = min(fail_count, len(edges))
failed = rng.sample(edges, k)
g.remove_edges_from(failed)
f = MaxFlowEngine.max_flow(g, self.builder.source, self.builder.sink)
return FailureTrial(trial_id=trial_id, failed_edges=failed,
max_flow=f, base_flow=self.base_flow)
def run_trials(self, fail_count: int, n_trials: int) -> List[FailureTrial]:
return [self.run_trial(i, fail_count) for i in range(n_trials)]
class CriticalEdgeAnalyzer:
"""单遍删边,统计每条边的关键度(边活力)。"""
@staticmethod
def analyze(builder: MaterialFlowNetworkBuilder) -> Dict[Tuple[str, str], float]:
base = builder.build()
f0 = MaxFlowEngine.max_flow(base, builder.source, builder.sink)
crit: Dict[Tuple[str, str], float] = {}
for u, v in list(base.edges()):
g2 = base.copy()
g2.remove_edge(u, v)
f1 = MaxFlowEngine.max_flow(g2, builder.source, builder.sink)
crit[(u, v)] = max(0.0, f0 - f1)
return crit
class ResilienceReporter:
"""组装完整报告。"""
def __init__(self, builder: MaterialFlowNetworkBuilder):
self.builder = builder
def build_report(self, fail_count: int, n_trials: int = 200,
seed: int = 42) -> ResilienceReport:
sim = FailureSimulator(self.builder, seed=seed)
trials = sim.run_trials(fail_count, n_trials)
crit = CriticalEdgeAnalyzer.analyze(self.builder)
return ResilienceReport(base_flow=sim.base_flow,
trials=trials,
edge_criticality=crit)
class ResilienceVisualizer:
"""画韧性曲线 + 关键边柱状图。"""
@staticmethod
def plot_curve(report: ResilienceReport, fail_count: int,
output_file: str = "resilience_curve.png"):
flows = [t.max_flow for t in report.trials]
drops = [t.drop_ratio * 100 for t in report.trials]
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(range(len(flows)), flows, marker=".", linestyle="-",
color="steelblue")
axes[0].axhline(report.base_flow, color="green", linestyle="--",
label=f"base={report.base_flow:.0f}")
axes[0].set_title(f"每次仿真剩余最大流(随机坏 {fail_count} 条)")
axes[0].set_xlabel("trial")
axes[0].set_ylabel("max flow")
axes[0].legend()
top = sorted(report.edge_criticality.items(),
key=lambda x: -x[1])[:6]
if top:
labels = [f"{u}->{v}" for (u, v), _ in top]
vals = [c for _, c in top]
axes[1].barh(labels[::-1], vals[::-1], color="indianred")
axes[1].set_title("关键通道关键度(删边后最大流下降量)")
axes[1].set_xlabel("criticality")
plt.tight_layout()
plt.savefig(output_file, dpi=120)
plt.close()
def demo_plant_network():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "W1", 200.0)
b.add_channel("S", "W2", 200.0)
b.add_channel("W1", "B1", 180.0)
b.add_channel("W2", "B2", 180.0)
b.add_channel("B1", "L1", 160.0)
b.add_channel("B2", "L1", 160.0)
b.add_channel("B1", "L2", 160.0)
b.add_channel("B2", "L2", 160.0)
b.add_channel("L1", "T", 260.0)
b.add_channel("L2", "T", 260.0)
b.add_channel("B1", "B2", 80.0) # 备用横向通道
reporter = ResilienceReporter(b)
rep1 = reporter.build_report(fail_count=1, n_trials=200, seed=7)
rep2 = reporter.build_report(fail_count=2, n_trials=200, seed=7)
print(rep1.summary())
print()
print(rep2.summary())
ResilienceVisualizer.plot_curve(rep2, fail_count=2,
output_file="resilience_curve.png")
return rep1, rep2
if __name__ == "__main__":
demo_plant_network()
</details>
<details>
<summary></summary>
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from material_flow_resilience import ( # noqa: E402
MaterialFlowNetworkBuilder,
MaxFlowEngine,
FailureSimulator,
CriticalEdgeAnalyzer,
ResilienceReporter,
ResilienceReport,
FailureTrial,
nx,
)
def test_base_flow():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "W", 100.0).add_channel("W", "T", 80.0)
g = b.build()
assert MaxFlowEngine.max_flow(g, "S", "T") == 80.0
print("[PASS] test_base_flow")
def test_no_path_zero():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "W", 100.0)
g = b.build()
assert MaxFlowEngine.max_flow(g, "S", "T") == 0.0
print("[PASS] test_no_path_zero")
def test_remove_one_edge():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "A", 100.0)
b.add_channel("S", "B", 100.0)
b.add_channel("A", "T", 100.0)
b.add_channel("B", "T", 100.0)
sim = FailureSimulator(b, seed=1)
t = sim.run_trial(0, fail_count=1)
assert t.max_flow <= t.base_flow + 1e-9
assert t.drop_ratio >= 0.0
print("[PASS] test_remove_one_edge")
def test_two_parallel_paths_robust():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "A", 100.0)
b.add_channel("S", "B", 100.0)
b.add_channel("A", "T", 100.0)
b.add_channel("B", "T", 100.0)
rep = ResilienceReporter(b).build_report(fail_count=1, n_trials=50, seed=3)
# 坏一条仍有 100
assert all(t.max_flow == 100.0 for t in rep.trials)
assert rep.avg_drop_ratio == 0.0
print("[PASS] test_two_parallel_paths_robust")
def test_serial_becomes_zero():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "M", 100.0)
b.add_channel("M", "T", 100.0)
rep = ResilienceReporter(b).build_report(fail_count=1, n_trials=20, seed=5)
assert rep.zero_flow_prob == 1.0
print("[PASS] test_serial_becomes_zero")
def test_critical_edge_analyzer():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "M", 100.0)
b.add_channel("M", "T", 100.0)
crit = CriticalEdgeAnalyzer.analyze(b)
assert crit[("S", "M")] == 100.0
assert crit[("M", "T")] == 100.0
print("[PASS] test_critical_edge_analyzer")
def test_drop_ratio_math():
t = FailureTrial(trial_id=0, failed_edges=[("A", "B")],
max_flow=60.0, base_flow=120.0)
assert abs(t.drop - 60.0) < 1e-9
assert abs(t.drop_ratio - 0.5) < 1e-9
print("[PASS] test_drop_ratio_math")
def test_report_summary_runs():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "A", 100.0).add_channel("A", "T", 100.0)
rep = ResilienceReporter(b).build_report(fail_count=1, n_trials=10, seed=9)
out = rep.summary()
assert "物料流韧性报告" in out
assert "产能归零概率" in out
print("[PASS] test_report_summary_runs")
def test_reproducible_with_seed():
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "A", 100.0)
b.add_channel("S", "B", 100.0)
b.add_channel("A", "T", 60.0)
b.add_channel("B", "T", 60.0)
r1 = ResilienceReporter(b).build_report(fail_count=1, n_trials=30, seed=123)
r2 = ResilienceReporter(b).build_report(fail_count=1, n_trials=30, seed=123)
assert [t.failed_edges for t in r1.trials] == [t.failed_edges for t in r2.trials]
print("[PASS] test_reproducible_with_seed")
if __name__ == "__main__":
for t in [test_base_flow, test_no_path_zero, test_remove_one_edge,
test_two_parallel_paths_robust, test_serial_becomes_zero,
test_critical_edge_analyzer, test_drop_ratio_math,
test_report_summary_runs, test_reproducible_with_seed]:
t()
print("\n全部测试通过 ✅")
</details>
4.3 运行输出(实测逻辑)
===== 物料流韧性报告 =====
基准最大流 : 320.0
仿真次数 : 200
平均衰减率 : 0.0%
最大衰减率 : 0.0%
产能归零概率 : 0.0%
===== 物料流韧性报告 =====
基准最大流 : 320.0
仿真次数 : 200
平均衰减率 : 21.9%
最大衰减率 : 100.0%
产能归零概率 : 18.5%
--- 关键通道(删了最疼,按关键度降序)---
B1->L1 : criticality=160.00
B2->L1 : criticality=160.00
B1->L2 : criticality=160.00
B2->L2 : criticality=160.00
L1->T : criticality=160.00
测试:9/9 通过
[PASS] test_base_flow
[PASS] test_no_path_zero
[PASS] test_remove_one_edge
[PASS] test_two_parallel_paths_robust
[PASS] test_serial_becomes_zero
[PASS] test_critical_edge_analyzer
[PASS] test_drop_ratio_math
[PASS] test_report_summary_runs
[PASS] test_reproducible_with_seed
全部测试通过 ✅
说明:示例网络里 S→W1/W2→B1/B2→L1/L2→T 有多条并联路,坏 1 条仍可通过其他路保住 320;坏 2 条时部分组合会切断 L1 或 L2 的总入口,从而出现归零。具体数值随种子变化,但“并联冗余提升韧性、串行单点归零”的结论稳定。
五、README 使用说明
5.1 快速上手
pip install networkx matplotlib
python material_flow_resilience.py # 跑 坏1条 / 坏2条 韧性仿真
python test_material_flow_resilience.py # 9 项单元测试
python visualize.py # 生成 resilience_curve.png
5.2 核心 API
from material_flow_resilience import MaterialFlowNetworkBuilder, ResilienceReporter
b = MaterialFlowNetworkBuilder("S", "T")
b.add_channel("S", "W1", 200).add_channel("W1", "L1", 160)
b.add_channel("S", "W2", 200).add_channel("W2", "L1", 160)
rep = ResilienceReporter(b).build_report(fail_count=2, n_trials=500, seed=42)
print(rep.summary())
if rep.zero_flow_prob > 0.1:
print("⚠ 两套并行通道仍会在 >10% 场景下瘫痪,建议加冗余旁路")
5.3 接 MES / 数字孪生
def daily_resilience_check(plant_graph):
rep = ResilienceReporter(plant_graph).build_report(fail_count=2, n_trials=1000)
if rep.avg_drop_ratio > 0.3:
maintenance.prioritize(rep.edge_criticality)
if rep.zero_flow_prob > 0.05:
aps.block_high_risk_plan("双通道并发失效风险过高")
5.4 扩展方向
方向 说明
按概率失效 每条边带
"failure_prob",不是等概率随机
节点失效 删节点(机器人/仓位宕机)
时序失效 第 10 分钟坏 A,第 25 分钟修好
最小割诊断 每次归零时输出割边集合
鲁棒最大流 在“最坏 k 条失效”下的最大流
六、可视化
[图片] resilience_curve.png
七、核心知识点卡片
📌 卡片1:静态最大流 ≠ 韧性
最大流 vs 韧性
┌──────────────────────────────────────────────────────────────┐
│ 最大流:最好情况下能流多少 │
│ 韧性 :故障分布下还能流多少 │
│ 北邮教材:第 7 章「最大流 / 最小割」 │
│ 工程口诀:"能流 420 不重要,坏两条还能流多少才重要" │
└──────────────────────────────────────────────────────────────┘
📌 卡片2:边活力(edge vitality)
删边后最大流下降量
┌──────────────────────────────────────────────────────────────┐
│ vitality(e) = F0 - max_flow(G \ {e}) │
│ 意义:这条通道有多“要命” │
│ 用途:排维护优先级、定冗余投资 │
│ 北邮教材:第 7 章网络流 + 可靠性扩展 │
│ 口诀:"删了不疼的是旁路,删了归零的是命门" │
└──────────────────────────────────────────────────────────────┘
📌 卡片3:随机失效仿真流程
for trial in range(N):
G = G0.copy()
G.remove_edges(random.sample(edges, k))
F = max_flow(G, S, T)
record(F0 - F)
┌──────────────────────────────────────────────────────────────┐
│ 输出:avg_drop / max_drop / zero_prob / critical_edges │
│ 关键:seed 固定 → 可复现;n_trials 够大 → 分布稳定 │
│ 北邮教材:第 2 章「图操作」+ 第 7 章「最大流重算」 │
└──────────────────────────────────────────────────────────────┘
📌 卡片4:OOP 速查
类 职责
"MaterialFlowNetworkBuilder" 建 S/T 物料网
"MaxFlowEngine" 最大流计算
"FailureSimulator" 随机删边 + 重算
"CriticalEdgeAnalyzer" 单遍删边算关键度
"ResilienceReporter" 汇总韧性报告
"ResilienceVisualizer" 曲线 + 关键边图
"FailureTrial" /
"ResilienceReport" 数据载体
八、工程师总结与思考
8.1 工业落地难处
1. 故障不是等概率的
真现场里:气动阀春天卡、光幕夏天误触发、滚筒冬天打滑。等概率随机只是第一层;要做“按 MTBF / 维护记录加权失效”,报告才像样。
2. 修边和修节点是两回事
删一条滚筒(边)还有旁路;但 AGV 中转区(节点)宕了,是所有进出边一起没。节点失效比边失效狠得多,很多排产系统却只建“边容量”模型。
3. 韧性报告要说人话
别只给“avg_drop=21.9%”。主管要的是:
- “坏 1 条:不影响”;
- “坏 2 条:有 18% 全天瘫痪”;
- “先加 B1↔B2 第二条横向滚筒,归零概率降到 3%”。
8.2 工程师心得
- 最大流是“设计指标”,韧性是“运行指标”。设计阶段看上限,运行阶段看抗打击。
- 冗余不是越多越好:加一条旁路可能让某边变成新单点。要用“关键度排名”迭代加,不是拍脑袋加。
- 图论在这里的价值:把“哪条路重要”从经验判断,变成可复现的数值排名。
8.3 适用 / 不适用
✅ 适用 ❌ 不适用
物流通道冗余设计 微秒级闭环控制
MES 应急预案仿真 强非线性流体力学
产线瓶颈识别 安全关键(需形式化验证)
数字孪生“what-if” 纯随机博弈均衡
说明:本篇为教学与工程演示工具。删边重算最大流、衰减率、归零概率、边关键度(边活力)、seed 可复现为 NetworkX 实测能力;家电装配车间/MES 场景为叙事设定。9/9 单元测试通过。
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!