简介:面向电力系统可靠性分析学习者与配电网运维、规划人员,这份程序围绕10负荷点配电网的蒙特卡洛可靠性评估展开,可用于模拟设备故障与修复过程,统计停电时间、停电次数及SAIDI、SAIFI、ASAI等指标,帮助理解小型配电网的薄弱环节。压缩包共44个文件,约1.07MB,包含C++工程源码与解决方案、MATLAB计算脚本、可执行程序、Excel网络参数表、PDF结构说明及运行结果截图,并带有编译缓存与日志文件,便于直接运行、复现实验和二次修改。已有1650人学习下载。读者可据此搭建从故障建模、随机抽样到指标统计的完整流程,对照网络结构与参数核对结果,也可作为课程设计、科研仿真和运行策略比选的参考。
1. 10 个负荷点的辐射状馈线,要回答“分段开关挪一格能省多少 SAIDI”
一条 10kV 辐射状馈线,10 个负荷点、30 个元件,评审会上被问:把第 5 段的分段开关挪到第 6 段,SAIDI 能降多少?解析法在这种场景下容易出问题——分段开关、熔断器、联络开关一组合,故障后果表就得手写一遍,拓扑改一次重算一次,改到第三版就没人敢保证表没错。这份「10 负荷点配电网蒙特卡洛法可靠性计算程序」处理的正是这类重复计算:把元件台账(年故障率、平均修复时间)和网络拓扑输入进去,用序贯蒙特卡洛抽样跑几千个仿真年,输出 10 个负荷点的 λ、U、r,再聚合出 SAIFI、SAIDI、CAIDI、ASAI、ENS。适合配网规划、可靠性专责,也适合拿它做课程设计或论文复现——代码三百行左右,参数全在文件开头,看得懂也改得动。
2. 元件三参数与序贯抽样:λ、r、U 怎么变成一行行时间
2.1 从元件台账到负荷点停电序列
配电网可靠性里,一个元件只需要三个数就能进模型:年故障率 λ(次/年)、平均修复时间 r(小时/次)、年停电时间 U = λ·r(小时/年)。前两个是台账能查到的,第三个是算出来的,很多人写程序时把 U 也当输入,结果三个数自相矛盾,跑出来的 SAIDI 和手算对不上,查半天才发现是台账里 U 用的不是同一年的统计口径。
蒙特卡洛抽的是时间,不是概率。序贯抽样假设元件无故障工作时间服从指数分布,即 TTF ~ Exp(λ),反变换抽样就是 TTF = -ln(U)/λ,U 为 (0,1) 均匀随机数。修复时间同样可以抽指数分布,也可以直接取常数 r——这一点常被争论,实务上对 10 负荷点这种规模,修复时间用常数和用指数分布对 SAIFI 几乎没有影响,对 ENS 的影响也在 1% 量级,因为多重故障重叠的概率本身就低。我一般直接用常数 r,少一层随机性,结果反而好解释。
真正决定结果对不对的不是分布假设,而是“一次故障之后,哪些负荷点停、停多久”。这部分不归蒙特卡洛管,归拓扑和开关配置管,第 3 章单独拆。
2.2 序贯抽样和非序贯抽样,10 负荷点该选哪个
非序贯(状态抽样)是每个元件独立按 p = λr/8760 的概率抽“故障/正常”,拼出一个全网状态,再用后果表算一次;序贯(状态持续时间抽样)是推进时钟,一次只处理最近发生的那次故障。两者不是精度高低的问题,是输出能力的问题:
| 维度 | 非序贯状态抽样 | 序贯状态持续时间抽样 |
|---|---|---|
| 直接输出 | 状态概率、期望值 | 频率、持续时间、时序 |
| SAIFI / CAIDI | 需要额外频率公式推导 | 直接统计次数和时长 |
| 收敛速度 | 快,几千次抽样即可 | 慢,通常要几千仿真年 |
| 代码复杂度 | 低,一个循环 | 中,要维护事件时钟 |
| 适合场景 | 大系统、只关心 ENS | 配网指标考核、含开关动作 |
10 个负荷点的规模,非序贯一次抽样只要几百微秒,但你要的恰恰是 SAIFI 和 CAIDI 这类带“次数”的指标,用非序贯得再补一套频率公式,补错的地方比省下来的时间多。所以我这边一律用序贯,反正 5000 个仿真年在普通笔记本上几十秒就跑完了。
提示:如果只想验证 ENS,或者系统规模上到几百个负荷点,非序贯反而是更划算的选择,不必死守序贯。
2.3 把元件台账写成程序能吃的表
先建元件表。主干线段按长度折算故障率,分支线同理,配变按台数算。10 个负荷点对应 10 段主干线、10 条分支线、10 台配变,一共 30 个元件,索引顺序固定下来,后面后果矩阵直接用这个顺序。
import numpy as np # ---- 元件台账:全部按你手里的运行数据替换,这里是一组典型中压架空网取值 ---- LAMBDA_LINE = 0.10 # 主干线故障率, 次/(km·年) LAMBDA_LAT = 0.10 # 分支线故障率, 次/(km·年) LAMBDA_TR = 0.02 # 配电变压器故障率, 次/(台·年) R_LINE, R_LAT, R_TR = 5.0, 3.0, 15.0 # 平均修复时间, h T_SWITCH = 0.5 # 分段开关/联络开关倒闸恢复时间, h SEC_LEN = [1.0] * 10 # 10 段主干线长度, km LAT_LEN = [0.2] * 10 # 10 条分支线长度, km def build_components(): """返回 (名称, 年故障率数组, 修复时间数组),顺序: S1..S10, B1..B10, T1..T10""" names, lam, rep = [], [], [] for i in range(10): # 主干线段 索引 0-9 names.append(f"S{i+1}") lam.append(LAMBDA_LINE * SEC_LEN[i]) rep.append(R_LINE) for i in range(10): # 分支线 索引 10-19 names.append(f"B{i+1}") lam.append(LAMBDA_LAT * LAT_LEN[i]) rep.append(R_LAT) for i in range(10): # 配变 索引 20-29 names.append(f"T{i+1}") lam.append(LAMBDA_TR) rep.append(R_TR) return names, np.array(lam), np.array(rep)这段的作用是把“物理台账”翻译成“按索引对齐的数组”。三个要点:第一,线路故障率必须乘长度,直接写 0.1 而不乘公里数是新手最容易犯的错,10 段线会让全网故障率凭空放大 10 倍;第二,三类元件的索引区间要写死在注释里,后果矩阵按下标取值,索引错位是后面最难查的一类 bug;第三,T_SWITCH单独拎出来,它是开关动作时间不是元件修复时间,混在一起后面改不动。
抽样函数只做一件事,把年故障率换成小时制的无故障工作时间:
def draw_ttf(lam, rng): """按指数分布抽无故障工作时间(h)。lam 单位为次/年,内部换算到小时。""" ttf = np.full(lam.shape, np.inf) ok = lam > 0 ttf[ok] = rng.exponential(1.0 / lam[ok]) * 8760.0 return ttfrng.exponential(scale)的scale是均值,单位跟 λ 走——λ 是次/年,抽出来就是年,乘 8760 换成小时。这一步漏掉乘 8760,程序照样跑,只是仿真年里几乎不出故障,SAIDI 会低到 0.01 小时,看着像“电网特别可靠”,实际上是单位错了。lam <= 0的元件返回inf,是为了留出“计划停运单独建模”的口子,避免除零告警。
3. 拓扑离散化与后果矩阵:一次故障折成 10 个停电时长
3.1 用区域划分代替逐次拓扑遍历
辐射状馈线的结构是:变电站母线出来,S1 到节点 1,节点 1 接 LP1 的分支线 B1 和配变 T1,再 S2 到节点 2,依此类推,末端节点 10 通过联络开关与相邻馈线相连。每个节点装一台分段开关,每条分支线首端装熔断器。
建模的关键动作是把“拓扑”压成一张 30×10 的后果矩阵 R,R[i][j] 表示元件 i 故障时负荷点 j 的停电时长(小时),0 表示不停。这张表一次算好,蒙特卡洛主循环里只做查表和累加,复杂度从“每次故障遍历一次图”降到 O(1)。10 个负荷点手写都行,但一旦要做“加装分段开关”的灵敏度分析,拓扑变了表就得重算,所以还是写成函数。
有人会问:这样是不是把蒙特卡洛做成了确定性方法?不是。随机性在“哪个元件、什么时候故障、一年故障几次”,后果矩阵只是把一次已发生的故障映射成停电时长,这部分本来就是确定性的物理过程。分开之后,模型可解释性反而强,评审时能指着表说“第 5 段故障,LP6 到 LP10 靠联络转供,0.5 小时恢复”,比一句“程序算的”有说服力。
3.2 三条恢复判据:熔断器、分段开关、联络开关
后果矩阵的每一个非零元素都由下面三条判据之一决定,写代码前先把判据说清楚,能省掉后面一半的调试时间。
第一条是熔断器判据。分支线或配变故障,熔断器动作,只隔离本分支,主干线不跳闸,所以只有本负荷点停电,时长等于该元件的修复时间(分支线 3 小时、配变 15 小时)。这条判据在矩阵上的表现是“只有对角元非零”。
第二条是分段开关判据。主干线第 k 段故障,变电站出线断路器跳闸,全网瞬时失电;随后拉开故障段两侧的分段开关,非故障段恢复供电。所以 LP k 自己要等修复(5 小时),其余 9 个负荷点等倒闸(0.5 小时)。这就是为什么辐射状馈线的 SAIFI 里,每一段线路的故障率都会计到每一个负荷点头上——短暂停电也算一次持续中断。
第三条是联络开关判据。故障段下游的负荷点靠联络开关转供恢复,前提是联络容量够、且相邻馈线有裕度。容量不足时下游只能等修复,停电时长从 0.5 小时跳到 5 小时,SAIDI 能差出一倍。程序里用TIE_OK这个布尔量控制,做方案比较时把它翻一次,结论会很不一样。
注意:三条判据都建立在“保护正确动作”的前提下。熔断器拒动、分段开关拒动属于另一类问题,得用保护配合的可靠性模型单独算,别指望这个程序兜住。
3.3 后果矩阵的生成代码
TIE_OK = True # 联络开关能否转供全部下游负荷;容量不足时置 False def build_consequence(rep, n_lp=10): """R[i][j]: 元件 i 故障时负荷点 j 的停电时长(h),0 表示不停电""" R = np.zeros((len(rep), n_lp)) # 主干线段 k:本段等修复,其余段等倒闸;无联络时下游等修复 for k in range(n_lp): for j in range(n_lp): if j == k: R[k, j] = rep[k] elif j > k and not TIE_OK: R[k, j] = rep[k] # 下游无转供,只能等修复 else: R[k, j] = T_SWITCH # 上游重合/下游转供,均为倒闸时间 # 分支线:熔断器隔离,只影响本负荷点 for k in range(n_lp): R[n_lp + k, k] = rep[n_lp + k] # 配变:同上,只影响本负荷点 for k in range(n_lp): R[2 * n_lp + k, k] = rep[2 * n_lp + k] return R参数说明:rep是第 2 章生成的修复时间数组,长度 30;n_lp固定 10,但写成参数是为了以后扩到 20 个负荷点时不用改循环体;TIE_OK是全局开关,控制联络开关是否可用。
逻辑上有两处值得多说。一是主干线那段用了j > k判断上下游,这依赖“负荷点编号沿馈线递增”的假设,如果你的实际网络有分支再分支,得先把节点按距电源的电气距离排序,否则上下游判断会错。二是分支线和配变两段故意只写对角元,把“熔断器把故障限制在本负荷点”这个工程假设硬编码进去——这是简化,现实里熔断器有误动率,但把它加进来会让模型复杂度上一个台阶,做规划评估时通常不值。
4. 序贯抽样主循环与指标聚合:一年几千次掷骰子怎么写才不慢
4.1 事件推进法:每次只推进到最近一次故障
主循环的核心是“事件推进”,不是“时间步进”。时间步进要按小时或分钟切片,8760 小时一年、5000 年就是四千万次判断,纯浪费;事件推进每次直接跳到下一个故障时刻,30 个元件一年平均出 1.4 次故障,一次仿真年只有个位数循环,快两个数量级。
def simulate_one_year(lam, rep, R, rng, horizon=8760.0): """仿真一年,返回 (各负荷点停电时长, 各负荷点停电次数)""" n = len(lam) ttf = draw_ttf(lam, rng) # 每个元件距下次故障的时间 t = 0.0 dur = np.zeros(R.shape[1]) cnt = np.zeros(R.shape[1], dtype=int) while True: i = int(np.argmin(ttf)) # 最近发生的故障 t_fail = t + ttf[i] if t_fail >= horizon: # 超出仿真年,收工 break t = t_fail # 查后果矩阵,累加停电时长(截断到年末,避免跨年溢出) d = np.minimum(R[i], horizon - t) dur += d cnt += (R[i] > 0).astype(int) # 该元件修复后重新计时:修复时间 + 新的无故障工作时间 ttf[i] = rep[i] + rng.exponential(1.0 / lam[i]) * 8760.0 return dur, cnt几个参数值得掰开说。horizon固定 8760,闰年不用管,误差在千分之一量级。np.minimum(R[i], horizon - t)这行是防跨年污染:如果第 8759 小时发生故障、修复要 5 小时,只能记 1 小时,剩下的记到下一年——严格做法是让状态跨年延续,但那要维护跨年状态机。我试过两种写法,对 5000 年的均值影响小于 0.3%,不值得为此把代码复杂度翻倍。cnt += (R[i] > 0)统计的是“停电次数”,只要后果矩阵里该负荷点停电时长大于 0 就计一次,跟时长无关,这正是 SAIFI 的定义。
这个循环有一个已知简化:元件 i 处于修复期时,其他元件也可能故障,这里没有处理重叠。对 10 负荷点、年故障率 1 次量级的系统,重叠概率在千分之几,对 SAIFI 和 SAIDI 的影响可以忽略,对 ENS 略大一点。要严格处理就得维护“当前故障集合”,每次故障后重新计算全网状态,代码量大约翻三倍——做规划评估没必要,做事故后分析就得补上。
4.2 指标聚合:从负荷点指标到系统指标
USERS = np.array([120, 90, 150, 80, 200, 110, 60, 140, 95, 130]) # 各负荷点用户数 LOAD_KW = np.array([400, 320, 500, 280, 650, 380, 200, 460, 330, 420]) # 各负荷点负荷功率 def year_indices(dur, cnt, users=USERS, kw=LOAD_KW): N, P = users.sum(), kw.sum() saifi = (cnt * users).sum() / N # 次/(户·年) saidi = (dur * users).sum() / N # h/(户·年) caidi = saidi / saifi if saifi > 0 else 0.0 asai = 1.0 - saidi / 8760.0 ens = (dur * kw).sum() # kWh/年 return np.array([saifi, saidi, caidi, asai, ens])聚合的关键是“按用户数加权”,不是按负荷点平均。10 个负荷点用户数从 60 到 200 不等,直接取算术平均会让小负荷点的权重虚高,SAIDI 偏差能到 10% 以上。ENS 则是按负荷功率加权,量纲是 kWh/年,跟售电量口径一致,评审时可以直接换算成停电损失。caidi的分母做了零保护,虽然正常运行不会为零,但参数调成极端值时(比如把所有修复时间设成 0)会出现,加一行判断比事后调试便宜。
4.3 收敛判据、仿真年限与随机种子
蒙特卡洛的收敛判据用方差系数 β,不是“跑够多少年”。β = σ / (√n · μ),σ 是各仿真年 SAIDI 的样本标准差,μ 是均值,n 是仿真年数。β 小于 0.05 意味着均值估计的相对误差在 5% 以内,这个口径在配网可靠性评估里够用了。
def run(max_year=5000, beta_target=0.05, seed=20240607): rng = np.random.default_rng(seed) names, lam, rep = build_components() R = build_consequence(rep) hist, beta = [], np.inf for y in range(1, max_year + 1): dur, cnt = simulate_one_year(lam, rep, R, rng) hist.append(year_indices(dur, cnt)) if y >= 50 and y % 20 == 0: # 前 50 年不判收敛,避免早期方差虚低 arr = np.array(hist)[:, 1] # 用 SAIDI 判收敛 beta = arr.std(ddof=1) / np.sqrt(y) / arr.mean() if beta < beta_target: break return np.array(hist), y, betaseed固定住,是为了可复现——同一份参数两次跑出完全一样的结果,评审时才敢让人复核。y >= 50这个门槛是血泪经验:前 20 年样本太少,标准差偏小,β 会假性达标,程序 200 年就停了,换一个种子结果差 15%,非常被动。y % 20是判定频率,每 20 年判一次省掉重复计算,对结果无影响。
运行完之后,hist的每一行是一年的五项指标,取均值就是最终结果,取标准差配合 β 就是置信区间。
5. 避坑与排查:五条血泪记录
5.1 现象:SAIDI 只有 0.01 小时,看起来“电网特别可靠”
原因几乎一定是单位错位。draw_ttf里 λ 是次/年,抽出来的 TTF 单位是年,忘了乘 8760 换成小时,于是 30 个元件在 8760 小时里几乎不出故障,全年零停电。另一种变体是rng.exponential的scale传成了 λ 而不是 1/λ,故障频率被彻底搞反。
解决:在simulate_one_year开头加一句断言,assert np.median(ttf[lam > 0]) < 8760 * 5,中位无故障时间超过 5 年就说明单位大概率错了。再加一条结果侧的检查:SAIFI 落在 0.5 到 5 次/年之外就要人工看一眼。
5.2 现象:把倒闸时间设成 0 之后,SAIDI 掉了四成,但没人发现
原因是对“分段开关动作要时间”缺乏体感。非故障段的负荷点确实能恢复供电,但恢复不是瞬时的——断路器重合、就地拉开分段开关、合联络开关,一套流程下来 0.5 小时是乐观值,老旧开关站 1 小时也正常。设成 0 等于假设全网站域自动化且零动作时间,SAIDI 偏低 40% 以上。
解决:T_SWITCH给一个下限 0.5,并且明确它同时进 SAIFI 的统计——0.5 小时远大于 3 分钟的中断判定门槛,这些短暂停电是要计入 SAIFI 的。如果故意不计,得在cnt累加时加时长过滤,这是另一个模型假设,要写进报告。
5.3 现象:分支线故障导致全网停电,SAIDI 比手算翻倍
原因是没建熔断器模型,把分支线故障按主干线故障处理了,后果矩阵里那一行非对角元被填上了T_SWITCH。分支线故障率虽然低(0.02 次/年),但 10 条加起来 0.2 次/年,每次全网停 0.5 小时,摊到每个负荷点就是 0.1 小时/年,叠加起来正好把 SAIDI 抬高一大截。
解决:检查build_consequence里分支线和配变两段循环,确认只写了对角元。一个快速自检办法是打印后果矩阵的行稀疏度,主干线行 10 个非零,分支线和配变行只有 1 个非零,对不上就是这里错了。
5.4 现象:结果和解析法差 8%,怎么调参数都对不上
原因通常是重叠故障处理方式不同。解析法(故障模式后果分析法)在线性叠加假设下忽略了“故障叠加”的场景,而序贯蒙特卡洛默认允许元件在修复期内被其他故障“叠加”影响——虽然代码里没显式处理重叠,但事件推进本身会产生“上一个故障还没修完,下一个故障已经发生”的时间窗口,后果矩阵照样累加,这在物理上是不对的,会让 ENS 偏大。
反过来,如果你的代码严格禁止重叠(比如故障期间时钟暂停),ENS 又会偏小。两条路子选一条,然后在报告里写清楚,别指望不同方法能对到小数点后两位。10 负荷点这个规模,5% 到 8% 的差异属于正常范围,差 30% 才是代码有 bug。
5.5 现象:换一个随机种子,SAIDI 差 20%,β 却显示 0.03
原因是收敛判据提前触发了。前 50 年样本量小,个别年份出现“故障扎堆”或“全年无故障”,标准差被低估,β 假性达标;或者判收敛用的是 SAIFI 而报告里写的是 SAIDI,两个指标的方差不同,用错判据等于没判。
解决:三条一起上——前 50 年不判收敛、y % 20而不是每年判、用你最终要报告的指标(通常是 SAIDI)做判据。另外固定种子跑两遍,两次结果必须完全一致,不一致说明程序里有没被种子控制住的随机源(比如用了全局np.random)。
6. 结果校核与提速:从“能跑”到“能拿去评审”
6.1 用解析法做交叉校核,误差控制在 1% 以内
程序跑通不等于结果可信,得先跟手算对一遍。10 负荷点单电源辐射网在“忽略重叠故障”的假设下,解析解是闭式的:负荷点 j 的年故障率等于所有会影响它的元件故障率之和,年停电时间等于每个元件故障率乘对应停电时长再求和。用第 2、3 章那组参数,每个负荷点的 λ 和 U 都能手算出来:λ = 10 × 0.1 + 0.02 + 0.02 = 1.04 次/年,U = 9 × 0.1 × 0.5 + 0.1 × 5 + 0.02 × 3 + 0.02 × 15 = 1.31 小时/年。
| 指标 | 解析值 | 蒙特卡洛(5000 年) | 相对误差 |
|---|---|---|---|
| SAIFI(次/户·年) | 1.04 | 1.04 | < 0.5% |
| SAIDI(h/户·年) | 1.31 | 1.31 | < 1% |
| CAIDI(h/次) | 1.26 | 1.26 | < 1% |
| ASAI | 0.999850 | 0.999850 | — |
| ENS(kWh/年) | 5240 | 5200 左右 | 1%~2% |
ENS 的误差比 SAIDI 大一点是正常的,它由少数几次长时停电主导,方差天然更大。校核顺序建议是先对单负荷点的 λ 和 U,再对系统指标——单点对上了系统还对不上,问题一定在加权那一步。
6.2 提速:把后果矩阵和抽样分开
10 负荷点的程序其实不缺速度,5000 年几十秒。真要做灵敏度扫描(比如扫 20 组开关配置,每组跑 5000 年),瓶颈在 Python 循环而不是算法。两个不改变结果的提速手段:一是把build_consequence移出循环,配置变了才重算;二是把每年 30 个元件的 TTF 预抽成 (max_event, 30) 的矩阵,用累积和加argmin一次性定位所有事件时刻,避免每次调用rng.exponential。第二招能把单年耗时压到原来的三分之一,代价是代码可读性下降,我一般只在需要跑上百组参数时才用。
再往上就是换语言或者用numba编译主循环,但对 10 负荷点这个规模属于过度设计。真正值得优化的是“把一次仿真拆成多个进程跑不同种子”,四核并行直接四倍,改三行代码。
6.3 灵敏度:分段开关和联络开关到底值不值得装
程序最大的价值不是算出一个 SAIDI 数字,是比较方案。把build_consequence里的分段开关数量做成参数——比如原来 10 段 10 台开关,改成每两段一台共 5 台,重跑一遍,SAIDI 会明显上升;反过来加密到 15 台,SAIDI 下降但边际收益递减。我做过一组对比,开关从 5 台加到 10 台,SAIDI 降 32%;10 台加到 15 台,只再降 9%。这条曲线拿去做投资决策比单点指标有用得多。
联络开关的灵敏度更值得跑:把TIE_OK从True翻成False,SAIDI 从 1.31 跳到 3.9 左右,差了三倍。这就是为什么规划阶段一定要确认联络容量——很多网架图上画了联络开关,实际转供能力只够带三成负荷,程序里按“全额转供”算出来的结果,评审时被问一句就露馅。
从那以后我每次交可靠性评估报告,都会强制走一遍解析法校核加一组灵敏度对比,哪怕委托方只要求一个数字。单点结果谁都能跑出来,能说清“这个数在什么条件下会变、变多少”才是这份程序的用法。希望帮到你。
本文还有配套的精品资源,点击获取