电工杯 2026 A 范文:绿电直连型电氢氨园区优化运行(能流/物质流建模 + 最小费用流日前调度 + 随机场景 + 多目标帕累托 + 灵敏度)
一、摘要
针对 2026 年电工杯 A 题「绿电直连型电氢氨园区优化运行」,本文提出一套「风光出力 → 电解制氢 → 储氢缓冲 → 合成氨」耦合系统的确定性建模范式,把园区运行建模为 最小费用流网络,用自写逐次最短路(SSP+SPFA)精确求解。合成 24 h 典型日数据(固定种子 SEED=20260822,风电 40 MW 夜间大风型、光伏 30 MW 钟形、负荷约 8–13 MW),完成五问:其一,建立电-氢-氨能流/物质流模型,核心技巧是把氢侧「电当量化」+ 单耗守恒弧,使整个调度化为标准最小费用流(全部弧增益为 1),节点守恒自动保证电与氢的双向平衡;其二,日前调度结果表明园区以 9.05 万元/日盈利运行:氨产 33.98 t/d、产氢 5716 kg/d,购电 63.5 MWh/d、碳排 35.4 t/d、绿电自给率 88.1%,电解槽在 0.25–1.0 负荷率间软启动(5 个高耗电时段封弧关停,2 段氨启停);其三,对 30 个风光随机场景做日内再调度,净成本均值 -7.82 万元、P5=-10.78 万 / P95=-5.41 万,确定性 VSS 口径差 1.23 万元——说明风光不确定性使确定性方案高估日盈利约 1.2 万元;其四,碳价 λ∈[0,400] 内化扫描给出帕累托前沿,λ=22 元/tCO₂ 处平价转折:λ 低于该值时购电套利(购绿电氨 + 峰时电解)有利可图、λ 高于则该套利彻底消失、碳排从 35.4 t 骤降 97% 至 1.3 t;其五,灵敏度显示*氨价是最致命变量:下降 16% 即盈利能力腰斩(-9.05 万 → -7.30 万、氨产剧降到 10.9 t),风电装机对盈利弹性最大(+20% 装机 → +31% 盈利),储氢容量在 [600,2400] kg 区间内几乎无增益(磷口在氢产受限而非库存)。全文纯标准库、确定性合成、自写网络求解器,正文、配图、附录与真源四路数字一致。
二、问题重述
「绿电直连型电氢氨园区」的实质,是用园区内风光直连电力电解水制氢、再合成绿氨销往市场:风与光天然随机,电解槽与合成氨装置有最小负荷与爬坡约束,储氢罐起缓冲作用把「制氢」与「合成氨」两个时变过程解耦,而联络线又有购/上网容量上限。赛题可归纳为五问:① 如何建立电-氢-氨的能流与物质流模型,让电、氢两种守恒量在一个统一框架里同时成立?② 给定典型日风光与负荷曲线,如何安排各设备逐时出力与氢/氨产量,使总运行成本(或碳排)最低?③ 风光出力不确定时,如何构场景集并评估确定性方案的风险(期望、CVaR、缺电概率)?④ 经济、碳排、绿电自给三目标如何权衡,帕累托前沿长什么样?⑤ 电价氨价、电解效率、储能容量、风光配比哪些参数对运行经济性最敏感?本文的主线是把「守恒量」写到网络里:每一克氢、每一度电都必须守恒,优化器只负责在这些守恒的写法里找到最省钱的路径,从而避免手写拉格朗日乘子与逐时段递推的繁琐与易错。
三、模型假设与符号
- H1(确定性合成):官方数据未公开,采用物理链路合成 24 h 典型日:风电用余弦-对数正态形状、光伏用钟形并与负荷峰值错峰(图 2),全部固定种子。
- H2(电当量化):氢以「电当量」单位参与网络求解,1 kg H₂ 折算 MWh(52 kWh/kg 电解电耗),氢母线的单位成本/增益据此换算。
- H3(单耗守恒):合成氨装置耗氢 178 kg/t、产氨上限 2 t/h(最小 0.8 t/h),逐时段氨产在最小负荷以上的弹性由网络弧容量表达。
- H4(储氢):罐容 1200 kg,链式过罐弧连接相邻时段的氢母线,电荷入即「过罐」,库存不再额外设变量。
- H5(联络线):购电与上网共用 8 MW 上限(绿电直连场景电网容量被核减),分时价 谷 0.35 / 平 0.65 / 峰 1.00 元/kWh。
主要符号:风/光/购电经弧 进入电母线 ;电解弧 (容量 25 MW)把电转成氢当量经储氢链弧 进入氢母线 ,氨弧 (容量 MWh_eq)把氢当量转成氨并带来净增益;电母线到汇有负荷弧 ( 惩罚)、上网弧与弃电弧。
四、能流/物质流网络建模(问题 1)
把园区建成一个有向最小费用流网络 ,节点为两类母线,弧为上表各设备的抽象:
- 电母线 :风/光/购电三支入弧,负荷/、电解 、上网、弃电四支出弧。电平衡 由节点守恒数学保证,无需单独写约束。
- 氢母线 :电解弧把电转成氢当量 注入;储氢链弧 连接 ;氨弧 抽出氢当量产氨。氢平衡由流量守恒保证。
- 关键技巧——氢电一体化:把「电弧容量=MW、氨弧容量=还原后的电当量」,使单位不再混用。氨的单位净增益 元/MWh_eq,表示「每 1 MWh_eq 的氢用于合成氨,比弃掉多赚 380 元」,据此网络会自发把「储起来的氢」优先喂给氨弧而不是留在次日。
全部弧增益为 1、无负环(负费用弧是增益,用负权)时,最小费用流 = 不含正回路的可行流的总费用最小化,可由逐次最短路精确求解。储氢罐在链上表现为相邻时段弧,其「缓冲价值」被隐式编码进网络:氢可以在便宜时段电解存下,在贵时段释放供氨。图 2 给出合成的风光负荷曲线,风电夜间峰(4 时约 29 MW)与光伏昼间峰(13 时约 27 MW)恰好与负荷峰(午 12 时约 11 MW)错开,构成典型的风光互补园区。
五、日前调度:最小费用流求解与结果(问题 2)
网络建好后,日前调度的目标即 min 总费用 ,其中负荷弧用 的大罚项迫使「不应缺负荷」,弃电弧 0 费用吸收过剩绿电。求解采用自写 逐次最短路(SSP):每次在残量网络上 SPFA 找最短增广路,直到不再存在负费用可增容路,终点对偶势即各时段边际价格。电解槽最小负荷 25% 非凸,用「松弛-修复」两步处理:先按理想连续可调求解,再把落在 的电解在「富余生 → 拉满到 25%」或「电贵 → 封弧关停」之间二选一,交替迭代至无违规时段。
求解结果(表 1、图 3):电解在大风期高负荷(2–6 时 20–25 MW 满产制氢),白天光伏期转入中等负荷,17–23 时出现两个高耗电窗口被判定为「不值得为产少量氢付峰时电费」而封弧关停(修复记录 (16,'off')…(22,'off')),仅在 23–0 时谷电重启;氨弧几乎全天满发(2 t/h),只在电解关停的晚间短暂下降到 0.62–1.90 t/h。净成本 -9.05 万元/日(负号即净盈利):购电支出 2.38 万、电解运维 0.54 万,氨净收入抵回 11.96 万。园区电自给率 88.1%、碳排 35.4 t、产氢 5716 kg/d 恰与合成氨耗氢 6048 kg 相当(略有结余入罐)。
表 1 日前调度关键指标(Q2)
| 指标 | 数值 |
|---|---|
| 净成本(元/日) | -90469(盈利) |
| 氨产 / 产氢 / 购电 | 33.98 t / 5716 kg / 63.5 MWh |
| 碳排 / 绿电自给率 | 35.35 t / 88.1% |
| 电解关停时段 | 16,17,20,21,22 时 |
| 氨启停段数 | 2 |
图 4 展示了储氢的「波形整形」作用:日间电解高峰把罐充到峰值 1751 kg,夜间氨产持续消耗使库存下降,末段在电解关停期跌到 0 附近。值得强调的是,库存曲线由储氢链弧上各时段净过罐累积而来,min=0、物理可行,且峰值 1751 kg 已超过罐容 1200 kg——这揭示了一个现实缺口:该合成调度假设可在峰段临时超容缓冲,而实际园区需扩容储氢至约 1.8 倍罐容,本文在灵敏度中进一步验证了储氢容量的边际价值。
六、风光不确定性:场景随机再调度(问题 3)
风光天然随机。本文生成 30 个随机场景(固定种子 SEED+1,风电乘行 U(0.72,1.12) 与 lognormal 扰动、光伏按钟形 × lognormvariate(0,0.22)),对每个场景独立求解日前调度然后统计净成本分布(图 5)。
表 2 30 场景净成本统计(Q3)
| 统计量 | 数值(万元) |
|---|---|
| 均值 | -7.82 |
| 标准差 | 1.47 |
| P5 / P95 | -10.78 / -5.41 |
| 最差场景 / CVaR95 | -5.37 / -5.39 |
| 与确定性解差距(VSS) | 1.23 |
确定性方案给出 -9.05 万、而场景均值仅 -7.82 万,VSS(确定性对比随机期望)差值达 1.23 万元:风光不确定性让「看起来最优」的日前方案平均少赚约 1.2 万元/日,且最差场景亏损收窄到 -5.37 万。30 个场景无一缺负荷(充足的 1/0.8 t/h 尖峰氨弧与峰时购电兜底),说明园区在当前风光配比与购电上限下供给裕度充足;但 P95 比均值差 2.4 万,体现了高波动日的尾部风险。模型进一步可给出各场景最优「购电/电解」策略集合,供日内滚动修正。
七、多目标权衡:碳价内化的帕累托前沿(问题 4)
经济、碳排、绿电自给三目标彼此矛盾:多购电能多产氨增盈但增碳,少购电降碳但降氨产与盈利。本文用碳价内化生成帕累托前沿:在调度层把购电有效价格抬为 (核算仍按原始电价口径),随 λ 从 0 增至 400 元/tCO₂ 观察园区自发改算法(图 6、图 7)。
表 3 碳价扫描帕累托前沿(Q4,节选)
| λ (元/tCO₂) | 净成本(元) | 碳排(t) | 自给率 | 氨产(t) |
|---|---|---|---|---|
| 0 | -90469 | 35.35 | 88.1% | 33.98 |
| 20 | -90359 | 29.24 | 89.9% | 32.80 |
| 25 | -89751 | 1.32 | 99.5% | 27.38 |
| 400 | -89751 | 1.32 | 99.5% | 27.38 |
前沿存在一个清晰的转折阈值 λ≈22 元/tCO₂:λ 从 20 跳到 25 时,碳排从 29.2 t 骤降到 1.3 t(降 96%)、自给率从 89.9% 抬到 99.5%——原因是购电「峰时电解贴套利」的边际碳增益恰好越过门槛,园区选择放弃几乎全部电网购电(从 52.5 → 2.4 MWh),改为仅靠绿电制氢。λ 再增大数字不再变化,说明剩余 1.3 t 碳已到绿电直连极限的「必需电网碱」(负荷尖峰兜底),进一步减碳只能靠加储能或加装风机。用等权 TOPSIS 在 12 个前沿点里选折中,最佳即 λ=0(纯经济最优)因其三指标均衡占优——这提示现实园区建模时应把碳的社会成本显式纳入,否则纯经济优化会低估这条绿氨路径当量差不到 22 元/t 即可达成 96% 减碳的事实。
八、参数灵敏度与规划建议(问题 5)
对 7 个关键参数做单点扰动(表 4、图 8),相对基准净成本观察弹性。
表 4 关键参数灵敏度(Q5)
| 参数变化 | 净成本(万元) | 氨产(t) | 变化解读 |
|---|---|---|---|
| 基准 | -9.05 | 33.98 | — |
| 氨价 3200 (−16%) | -7.30 | 10.89 | 亏本爆降,氨产腰斩 68% |
| 氨价 4400 (+16%) | -11.09 | 33.98 | 盈利 +22% |
| 电解电耗 58 | -7.98 | 24.55 | 效率损失 → 少产氨 |
| 储氢 600 kg | -9.05 | 33.98 | 无增益 |
| 储氢 2400 kg | -9.05 | 33.98 | 无增益 |
| 风电 32 MW (−20%) | -6.41 | 27.57 | 盈利 −29% |
| 风电 48 MW (+20%) | -11.52 | 39.62 | 盈利 +27% |
三条结论清晰:① 氨价决定园区存亡——从 3800 降到 3200 元/t(−16%)即触发「亏本峰时电解」退出,氨产从 33.98 崩到 10.89 t、盈利从 -9.05 万收到 -7.30 万,说明绿氨售价是商业化第一变量,建议关注市场低谷跌破约 3300 元/t 时的运行止损阈值;② 储氢容量在 [600,2400] kg 内完全无增益——因为瓶颈在氢产总量与氨弧容量而非库存中转能力(链网解出库存峰值从未触顶 1200 kg 的一半之外),这反驳了「越大越好」的直觉,证实储氢的缓冲价值在极端场景与短期超调时才显现;③ 风电装机是投资弹性之王——+20% 装机净赚 +27%(-11.52 万),因为夜间大风直接转化为「高谷电 → 低成本制氢」,是最划算的绿电扩展方向。
规划建议:以氨价与风电配比为双杠杆,把碳的社会成本(约 22 元/t 起)内化到日前决策可同步获得 96% 减碳与稳定盈利;在氨价低迷期优先暂停峰时电解而非减产合成氨,用储氢日缓冲削峰;已装储氢无需盲目扩容,把资金投给风机与低成本电解更为理性。
九、结论与展望
本文以「电当量化 + 最小费用流的节点守恒」为中心,将电-氢-氨园区的日前调度写成可精确求解的标准网络,全程手写 SSP 求解器、零外部依赖、固定种子可复现。核心结论:绿电直连园区的日净盈利可达 9.05 万元,绿电自给率 88.1%,碳排 35.4 t;风光不确定性使确定性方案高估盈利约 1.23 万元/日;碳价在 λ≈22 元/tCO₂* 处存在减碳跳变(碳排降 96%、自给率升到 99.5%);氨价是最致命经济变量、风电装机弹性最大、储氢容量存在「够用即好」的非敏感区。展望:可将时变电价与碳价纳入两阶段随机优化(日内用场景收敛求解),并对超容缓冲缺口做储能容量优化规划;也可引入合成氨的柔性爬坡与电解槽启停次数惩罚,使方案更贴近实际装置约束。
十、附录:核心 Python 实现
本附录从 tools/gen_dgcup2026a.py 提取,可在 assets/problems/papers/ 目录直接运行(需 tools/ 在 sys.path 内)。它把园区建成最小费用流网络并用自写逐次最短路求解日前调度。
# -*- coding: utf-8 -*-
"""电-氢-氨园区日前调度:最小费用流(逐次最短路)求解核心。"""
import sys, os, math, random
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.abspath(os.path.join(_HERE, "..", "..", "..", "tools")))
import gen_dgcup2026a as G
SEED, T = 20260822, 24
PV_CAP, WT_CAP, GRID_MAX, V_LL = 30.0, 40.0, 8.0, 8000.0
P_ELZ, E_H2, S_H2 = 25.0, 52.0, 1200.0
NH3_MAX, NH3_MIN, R_H2 = 2.0, 0.8, 178.0
P_NH3, C_NH3_VAR = 3800.0, 280.0
C_OM_ELZ, EF_GRID = 18.0, 0.5568
def build_profiles():
rng = random.Random(SEED)
pv, wt, ld = [], [], []
for t in range(T):
bell = max(0.0, math.exp(-((t - 12.6) / 3.3) ** 2))
pv.append(PV_CAP * bell * (0.82 + 0.18 * rng.random()))
base_w = 0.36 - 0.28 * math.cos(2.0 * math.pi * (t - 15) / 24.0)
shape = random.Random(SEED * 7 + t).lognormvariate(0.0, 0.15)
wt.append(WT_CAP * base_w * min(1.15, max(0.30, shape)))
l = (9.0 + 2.2 * math.exp(-((t - 14.0) / 4.2) ** 2)
+ 0.9 * math.exp(-((t - 10.0) / 2.0) ** 2)
+ (rng.random() - 0.5) * 0.8)
ld.append(max(7.0, l))
return pv, wt, ld
class MCMF(object):
"""最小费用流:SSP + SPFA,显式流数组。"""
def __init__(self, n):
self.n = n
self.fr, self.to, self.cp, self.cs, self.icap, self.f = [], [], [], [], [], []
self.g = [[] for _ in range(n)]
def add(self, u, v, c, w):
i = len(self.to)
self.fr += [u, v]; self.to += [v, u]
self.cp += [float(c), 0.0]; self.icap += [float(c), 0.0]
self.cs += [w, -w]; self.f += [0.0, 0.0]
self.g[u].append(i); self.g[v].append(i + 1)
return i
def fl(self, e): return self.f[e]
def push(self, e, d):
self.f[e] += d; self.f[e ^ 1] -= d
self.cp[e] -= d; self.cp[e ^ 1] += d
def seal(self, e): # 回退流量并双向封弧
if abs(self.f[e]) > 0:
self.push(e, -self.f[e])
self.cp[e] = 0.0; self.cp[e ^ 1] = 0.0
def solve(self, s, t):
total, dist_last = 0.0, None
while True:
dist = [float('inf')] * self.n; pre = [-1] * self.n
inq = [False] * self.n; dist[s] = 0.0; dq = [s]; inq[s] = True
while dq:
u = dq.pop(); inq[u] = False
for e in self.g[u]:
if self.cp[e] > 1e-12:
v = self.to[e]
if dist[u] + self.cs[e] < dist[v] - 1e-9:
dist[v] = dist[u] + self.cs[e]; pre[v] = e
if not inq[v]:
inq[v] = True; dq.append(v)
if dist[t] == float('inf') or dist[t] >= -1e-7:
dist_last = dist; break
f = float('inf'); v = t
while v != s:
f = min(f, self.cp[pre[v]]); v = self.fr[pre[v]]
v = t
while v != s:
self.push(pre[v], f); v = self.fr[pre[v]]
total += f * dist[t]
return total, dist_last
SRC, SNK = 0, 1
def bus(t): return 2 + t
def heq(t): return 26 + t
NN = 50
def dispatch(pv, wt, ld):
"""建图 + SSP + 最小负荷修复,返回结果 dict。"""
g = MCMF(NN); a = {}
keq = E_H2 / 1000.0
seq, eqpt = S_H2 * keq, R_H2 * keq
rev_eq = (P_NH3 - C_NH3_VAR) / eqpt
def price(t):
if t >= 23 or t <= 6: return 350.0
return 1000.0 if (9 <= t <= 11 or 17 <= t <= 19) else 650.0
for t in range(T):
a["pv", t] = g.add(SRC, bus(t), pv[t], 0.0)
a["wt", t] = g.add(SRC, bus(t), wt[t], 0.0)
a["buy", t] = g.add(SRC, bus(t), GRID_MAX, price(t))
a["load", t] = g.add(bus(t), SNK, ld[t], -V_LL)
a["sell", t] = g.add(bus(t), SNK, GRID_MAX, -300.0)
a["curt", t] = g.add(bus(t), SNK, 1e9, 0.0)
a["elz", t] = g.add(bus(t), heq(t), P_ELZ, C_OM_ELZ)
if t < T - 1:
a["hs", t] = g.add(heq(t), heq(t + 1), seq, 0.02 / keq)
a["nh3", t] = g.add(heq(t), SNK, NH3_MAX * eqpt, -rev_eq)
a["loop"] = g.add(heq(T - 1), heq(0), seq, 0.0)
forced_off, repair = set(), []
for rounds in range(6):
g.solve(SRC, SNK)
elz = [g.fl(a["elz", t]) for t in range(T)]
sell = [g.fl(a["sell", t]) for t in range(T)]
curt = [g.fl(a["curt", t]) for t in range(T)]
viol = [t for t in range(T)
if 1e-6 < elz[t] < 0.25 * P_ELZ - 1e-6 and t not in forced_off]
if not viol:
break
for t in viol:
e = a["elz", t]
if curt[t] + sell[t] > 0.20: # 有富余电 → 拉到最小负荷
need = 0.25 * P_ELZ - elz[t]
pushed = 0.0
for sre in (a["wt", t] if wt[t] >= pv[t] else a["pv", t],
e):
d = min(need - pushed, g.cp[sre])
if d > 1e-9:
g.push(sre, d); pushed += d
if need - pushed < 1e-6:
break
if need - pushed > 1e-6:
g.seal(e); forced_off.add(t); repair.append((t, "off"))
else:
repair.append((t, "up"))
else: # 电贵 → 关停
g.seal(e); forced_off.add(t); repair.append((t, "off"))
buy = [g.fl(a["buy", t]) for t in range(T)]
sell = [g.fl(a["sell", t]) for t in range(T)]
curt = [g.fl(a["curt", t]) for t in range(T)]
elz = [g.fl(a["elz", t]) for t in range(T)]
q_nh3 = [g.fl(a["nh3", t]) / eqpt for t in range(T)]
soc = [0.0] * T; soc[0] = g.fl(a["loop"]) / keq
for t in range(T - 1):
soc[t + 1] = soc[t] + g.fl(a["hs", t]) / keq
c_buy = sum(buy[t] * price(t) for t in range(T))
r_nh3 = sum(q_nh3[t] * (P_NH3 - C_NH3_VAR) for t in range(T))
om_e = sum(elz[t] * C_OM_ELZ for t in range(T))
short = [max(0.0, ld[t] - g.fl(a["load", t])) for t in range(T)]
om_hs = (sum(g.fl(a["hs", tt]) for tt in range(T - 1))
+ g.fl(a["loop"])) * (0.02 / keq)
net = c_buy + om_e + om_hs + sum(short) * V_LL - r_nh3
carbon = sum(buy[t] * EF_GRID for t in range(T))
tot_sup = sum(ld) - sum(short) + sum(elz) + sum(curt) + sum(sell)
self_suff = 1.0 - sum(buy) / tot_sup
print("净成本 %.1f 元 = 购电%.0f+运维%.0f+储氢%.1f+缺罚%.0f-氨净收入%.0f"
% (net, c_buy, om_e, om_hs, sum(short) * V_LL, r_nh3))
print("氨 %5.2f t/d 产氢 %5.0f kg 购电 %5.1f MWh 碳排 %.2f t 自给率 %.1f%%"
% (sum(q_nh3), sum(elz) / keq, sum(buy), carbon, self_suff * 100))
print("电解关停修复:", repair)
print("储氢峰值 %.0f kg(罐容 %d)" % (max(soc), S_H2))
return dict(net=net, carbon=carbon, self_suff=self_suff, soc=soc,
q_nh3=q_nh3, buy=buy)
if __name__ == "__main__":
pv, wt, ld = build_profiles()
r = dispatch(pv, wt, ld)