做这个项目复现的起因是对着一篇学位论文里的算例折腾了很久。那篇论文做的是并网/离网两种模式下的风光互补制氢合成氨系统容量-调度优化,算例结果很漂亮,风电光伏配比、电解槽容量、储氢罐大小、氨产量曲线都有,但正文里没有给完整参数,只有目标和约束的大致描述。读的时候觉得“也就那样,我上我也行”,真正动手用Python复现的时候才发现,从模型到数据,每个环节都藏着细节。
这类系统的核心逻辑其实不复杂:风电和光伏发电,一部分电给电解槽制氢,氢气连同氮气进合成氨装置产出氨,多余的电和氢可以储存起来,并网模式下还能和电网交易。复杂的地方在于两个问题绑在一起——系统里每一台设备容量该建多大,是规划问题;在给定容量下,每个小时的电怎么分配、储氢罐怎么充放,是运行问题。容量变了,最优调度跟着变;调度策略变了,最优容量也变。这就是标题里“容量-调度优化”六个字的分量。
这篇文章按我自己做项目复现的实际顺序来讲:系统物理构成、数学模型怎么搭、Python代码怎么组织、容量和调度怎么联合求解,最后是复现过程中踩过的坑。适合正在做新能源制氢、综合能源系统优化方向的学生和工程师参考,尤其是打算复现论文代码但不知道从哪下手的读者。
1. 风光互补制氢合成氨系统:先搞清楚我们在优化什么
1.1 系统的物理构成与能量流
风光互补制氢合成氨,本质上是把不稳定的可再生能源转换成稳定可储存的化学产品。系统由这些部分组成:风力发电机组、光伏阵列、电解水制氢装置、氢气储罐、合成氨单元,以及配套的电力变换和控制系统。制氢装置目前工程上常用碱性电解槽,也有用PEM电解槽的,两者在效率和最低负荷率上有差别,我后文会细说。
能量流可以这样理解:风电和光伏发出的电汇入母线,母线电量优先供给电解槽制氢,多余部分在并网模式下可以上网,在离网模式下只能依赖储能或者限制出力。电解槽产出的氢气先进储氢罐缓冲,再按合成氨单元的用氢需求供给,合成氨单元将氢气和氮气在高温高压催化条件下合成为氨,最终形成可运输的化工产品。
这条能量链有一个天然的时间解耦作用:风光出力波动是小时级的,而合成氨单元一旦运行就不希望频繁启停。储氢罐在这里起到类似“蓄水池”的作用,把波动的产氢和相对平稳的用氢衔接起来。这是调度模型里非常重要的一环,很多复现代码没有把储氢动态建模清楚,结果就是氨产量跟着风光波动乱跳,一看就不合理。
1.2 容量优化和调度优化为什么必须联合建模
我最初也想过简单处理:先凭经验固定一个容量配置,跑一次调度优化,然后手动调容量。跑了几轮发现这种思路行不通——风光资源、电价结构、设备成本参数不同,最优容量配置差别非常大,手调根本找不到最优解,而且容量和调度之间是双向耦合的。
举个具体例子:电解槽容量建大了,光照充足的时段可以多产氢,但投资成本也高;建小了,风光大发时氢气产量受限,白白弃风弃光。而最优电解槽容量取决于它在全年调度中实际能利用多少小时、能替代多少外购电,这个信息只有做过调度模拟才拿得到。反过来,储氢罐容量越大,调度时越能平滑电解槽出力,但这又会影响电解槽的最优容量。所以容量和调度本质上是一个联合优化问题,分开解会丢失关键的耦合信息。
行业里常见的解法是双层优化框架:外层搜索容量配置,内层在固定容量下求解逐时调度问题,把内层最优运行成本返回给外层作为适应度评价。逻辑上很清晰,工程上也很好落地,我的复现代码最终就走这个路线。
1.3 并网与离网:两种模式下优化逻辑的根本差异
并网模式下,系统的功率平衡约束多了一个与电网的交互项,电价高的时候可以卖电获利,风光不足时可以购电维持生产。这相当于电网提供了一个近似无限容量的“储能池”,系统的自平衡压力小很多,优化结果往往倾向于适度缩小储氢规模,因为电网兜底了。
离网模式完全不同。系统没有电网兜底,任何时刻都必须靠自身的光伏、风电和储能来满足需求。这里会出现“可靠性”约束的问题——某时段风光出力不够,储氢也放完了,系统就不得不削减氢产量。这种失负荷(氢产量缺口)成本必须放进目标函数,否则模型会给出一个容量很小但完全不可靠的方案。
我在代码里用了一个比较实用的处理方式:给氢产量不足加惩罚项,惩罚系数取合成氨产品的单位缺货成本。这样模型会在“多建设备减少缺货”和“少建设备承受惩罚”之间自动权衡,更贴近实际投资决策的逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学模型是怎么一步步搭起来的
2.1 目标函数的设计:成本往哪里算,收益从哪里来
我采用最小化年化总成本作为目标函数,包含四大部分:设备投资年化成本、运行维护成本、并网交互成本和缺氢惩罚。目标函数写成这样:
code复制min C_total = C_inv + C_om + C_grid + C_penalty
如果做的是经济性分析,也可以改成最大化年净利润,在目标函数里加上售电收益和售氨收益,原理一样,只是项数更多。这里有一个必须注意的点:投资成本要年化,否则时间维度不对。年化因子的公式是:
code复制CRF = r * (1 + r)^n / ((1 + r)^n - 1)
其中 r 是折现率,n 是设备寿命。风电、光伏、电解槽、储氢罐、合成氨单元的寿命各不相同,要分别算年化系数,不能偷懒统一用一个系数。就这一个细节,我在复现时搞错过一次,结果风电和光伏的容量配比和原论文差了将近20%。
并网交互成本这一项,在并网模式下是购电费用减去售电收益,电价按时段变化;在离网模式下这一项直接置零,同时增加缺氢惩罚项。所以同一个目标函数框架可以覆盖两种模式,只是开关不同。
2.2 风光与电解槽建模:时序出力与动态特性
风电出力工程上最常用的方法是风速-功率曲线。以某型风机为例,切入风速3m/s,额定风速12m/s,切出风速25m/s,在切入和额定之间,功率按风速的三次方关系上升。
光伏出力相对简单:P_pv(t) = eta_pv * G(t) * A_pv,其中 eta_pv 是光伏组件效率,G(t) 是太阳辐照度,A_pv 是光伏阵列面积。但这里要小心效率不是常数,温度升高时组件效率会下降,如果原论文考虑了温度修正,你也要加上温度修正项,否则夏季出力会偏高。
电解槽是系统里最需要精细建模的设备。它有两个关键特征:一是效率随负荷率变化,二是运行范围有下限。碱性电解槽通常在20%~40%负荷率以下不能稳定运行,这意味着调度模型里必须加入最小出力约束,否则会出现频繁启停、低负荷运行等现实里根本不允许的方案。
我还加了一个启停成本项去抑制电解槽的频繁启停。如果不加,模型会在电价谷段频繁开关电解槽来省那一点点运维成本,实际工程里这是非常伤设备的操作。
2.3 合成氨单元与储氢环节的约束处理
合成氨单元建模的关键是氢氮比3:1,以及氨合成的转化率限制。严格来说,合成氨塔内的反应动力学非常复杂,但在容量-调度优化这个尺度下,没有必要建那么细。简化模型可以假设合成氨单元的用氢速率在一定范围内可调,且设定最小连续运行时间约束,避免氨产量曲线剧烈波动。
储氢罐的动态约束是调度模型的核心之一:
code复制SOC(t+1) = SOC(t) + (m_H2_in(t) - m_H2_out(t)) / C_H2 * dt
其中 SOC 是当前储氢水平,C_H2 是储罐容量,m_H2_in 和 m_H2_out 分别是充放氢速率。这里单位一定要统一,氢量要么都用kg,要么都用Nm3,千万别混。我曾经因为氢气密度换算问题,储氢罐容量和实际差了6倍,排查了半天才发现是单位问题。
储氢罐还需要设定初始SOC,一般取50%,并加上周期约束让末时段SOC回到初始值,否则模型会通过“把储氢罐放空”来降低目标函数,得出完全不合理的解。
电网交互约束在并网模式下很简单,就是一个功率上限:
code复制0 <= P_buy(t) <= P_grid_max
0 <= P_sell(t) <= P_grid_max
离网模式下这两个变量直接置零,功率平衡方程里也不再有电网项。
3. Python代码实现:从数据预处理到求解器调用
3.1 整体架构:按模块拆解,别写成一坨
写这类优化代码最忌讳把所有内容堆在一个.py文件里。我按功能把项目拆成了这几个模块:
code复制project/
├── data/ # 气象数据、电价数据、设备参数
├── models/ # 目标函数与约束条件
├── solver/ # MILP求解封装
├── outer_algo/ # 外层容量优化算法(PSO)
├── utils/ # 数据处理、场景缩减等工具
├── main.py # 程序入口
└── config.py # 全局参数配置
config.py 里集中放所有设备参数和优化参数,这样换一组数据或者调参数只用改一个文件,不用满项目找。models 目录下的约束函数是纯函数,输入是决策变量和参数,输出是约束表达式列表,这样便于单独测试每个约束。
3.2 风光时序数据的获取与场景缩减
风光出力数据是调度的基础。气象数据我用的NASA POWER数据库,免费、接口稳定、覆盖全球,直接按经纬度和时间范围拉取。但要注意两点:一是NASA POWER的辐照度单位是kWh/m²/day,要换算成W/m²才能用于小时级计算;二是它给的是地表风速,要换算到轮毂高度,公式是:
code复制v = v_ref * (h / h_ref)^0.14
其中 h_ref 是测量高度,h 是轮毂高度。这个指数0.14是经验值,不同地表粗糙度会略有差异。
如果直接跑全年8760小时,模型规模会很大。工程上常用典型日或场景缩减来压缩问题规模。我用了K-means聚类,把全年日数据聚成12个典型日(春夏秋冬各取3类),每类附带一个权重代表该类场景在全年中出现的天数比例。这样调度模型的计算量降到原来的十分之一,结果仍然能反映季节性规律。
3.3 线性化处理与求解器选型
这个模型是混合整数线性规划(MILP),因为二进制变量(设备启停)和分段线性函数的存在。求解器我用的Gurobi,学术免费,MILP求解速度在同类求解器里属于第一梯队。如果手头没有Gurobi,用开源的CBC或SCIP也能跑,只是模型规模大时求解时间会明显变长。
风电功率曲线和电解槽效率曲线都是非线性函数,但都可以用分段线性近似。Gurobi支持FAC_PLUS或FAC_PWL参数来建立分段线性约束,Pyomo的Piecewise组件也封装了这套逻辑。分段数我取了6段,精度足够了,分段太多会引入大量额外变量,求解速度明显下降。
电解槽启停用二进制变量y_on表示,如果y_on=0则出力为0,如果y_on=1则出力在最小值和最大值之间:
code复制P_el_min * y_on(t) <= P_el(t) <= P_el_max * y_on(t)
这种经典的big-M约束在MILP里非常常见,但要小心大M值不要取得过大,否则会破坏求解器的数值稳定性。我一般取设备额定容量的1.2倍作为M值。
4. 联合优化框架:容量层与调度层怎么联动
4.1 双层迭代思路:外层PSO,内层MILP
我在第1节说过,容量和调度必须联合求解。落地的方案就是双层迭代:外层用粒子群算法搜索容量配置,内层用Gurobi求解该容量下的最优调度,返回最小运行成本。
外层粒子群的每个粒子代表一组容量配置,比如:
code复制[风电装机MW, 光伏装机MW, 电解槽MW, 储氢罐容量kg, 合成氨单元容量kg/h]
每个粒子的适应度 = 该容量配置下的投资年化成本 + 内层调度返回的最优运行成本。PSO迭代30~50次后收敛到最优容量配置。这里有个工程技巧:内层MILP求解结果要缓存。因为PSO种群中很多粒子离得很近,缓存在一定迭代次数内直接复用,能省大概20%的求解时间。
4.2 一体化MILP建模的可行性
可能有人会问:为什么不把容量变量和调度变量放进同一个MILP模型里直接解?理论上完全可以,容量变量就是第一阶段决策变量,调度变量是第二阶段决策变量,构建一个大MILP让Gurobi一次解出来。
但实际跑下来发现,一体化模型的规模太大了。以12个典型日、每典型日24小时、每小时约40个变量为参考,加上容量相关的二进制变量,总变量数好几万个,其中带整数性质的变量数千个。Gurobi能解,但求解时间常常超过几个小时,而且对内存要求高。在论文里一体化模型是“可行”的,在工程复现场景下,我更推荐双层迭代方案,一个多小时能出一组完整的容量-调度结果,调试效率完全不同。
4.3 收敛性处理与PSO参数整定
PSO算法本身容易早熟收敛,尤其当变量维度高、搜索空间大时。我做了两件事来改善:第一,种群规模取40~60,惯性权重从0.9线性递减到0.4,加速常数c1=c2=2;第二,PSO收敛后,用局部搜索在最优解附近再做一次细粒度搜索,比如把每个容量变量以5%步长上下微调,重新评估适应度,往往能找到比PSO直接结果更优的配置。
另外要特别注意容量变量的取值范围。风电装机如果设[0, 500MW],粒子群会在超大范围里乱飞,收敛慢且容易陷入局部最优。我根据负荷水平和场址资源条件,把每个变量的搜索上下界设在一个合理区间内,比如风电取[50, 200MW],光伏取[30, 150MW],这样搜索效率高得多。上下界的依据是:氨产量目标乘以单位氨耗氢量、单位氢耗电量,大致能推出制氢需求总电量,再结合典型日风光出力的小时数,就能估算出装机量级。
5. 复现过程中踩过的坑与结果校验
5.1 数据问题:单位、口径与源头追溯
这可能是复现论文代码最容易翻车的地方。我遇到的头号问题是辐照度单位。NASA POWER下载的太阳辐照度默认单位是kWh/m²/day,这是日均辐照量的累计值,不是瞬时功率。如果直接当成W/m²来算光伏出力,光伏装机算出来会大一个数量级,结果自然是错的。换算方法很简单:日均辐照量除以24小时,再乘以1000转成W/m²。
第二个坑是风速数据。NASA POWER的地表风速通常是在10米高度测的,而风机轮毂高度可能是80米甚至100米。必须用幂指数公式换算到轮毂高度层,否则低风速时段占的比例不对,风电年利用小时数会明显偏低。
第三个坑是原论文没有公布完整的设备成本参数。这类参数在论文里往往只给一个“单位投资成本”表格,甚至只给汇总结果。我的做法是参考相近领域论文和工程报告补全参数,然后在config.py里集中标注参数来源。如果发现复现结果和原论文对不上,先检查是不是参数口径不一致,而不是怀疑模型写错了。
5.2 求解性能问题:整数变量太多、求解时间爆炸
我第一次跑全年8760小时的并网模式MILP时,Gurobi跑了将近3个小时还没收敛,gap停在2%左右。后来做了两处优化,求解时间降到5分钟以内。
第一处是用典型日场景替代全年小时级数据。12个典型日聚类下来,模型规模缩到原来的十分之一。这个处理精度损失不大,因为容量配置呈现的是季节性规律,典型日能够保留季节特征。第二处是审查整数变量。我发现电解槽启停变量其实可以不要,改为最小出力约束加上一个“最小运行时间”约束,就能表达实际运行约束,同时省掉大量二进制变量。合成氨单元也类似,只留启停变量,中间环节不再额外设二进制变量。
还有一个技巧是给Gurobi设置求解参数:MIPGap控制在1e-3,TimeLimit设为600秒,超过时限就接受当前最优可行解。在优化研究里,严格最优证明虽然是必要的,但工程复现时,一个gap在1%以内的可行解已经足够指导决策了。
5.3 结果合理性校验:对照工程常识和原论文指标
模型跑通之后,不要急着把结果写进报告,先做合理性校验。我会检查以下几项:
第一,功率平衡是否逐时满足。我会把风电出力、光伏出力、购电、售电、电解槽用电逐时加总,应该严格等于0(考虑损耗)。如果有偏差,优先检查平衡方程里的正负号。
第二,设备利用率是否在合理范围。风电年利用小时数如果在2000~3500小时之间,光伏在1000~1800小时之间,说明容量配置匹配了当地资源条件。如果算出来风电年利用小时数才800小时,说明要么数据换算错了,要么风电机组装机明显过大。
第三,对照原论文的关键指标。通常论文会给出最优容量配置和年运行成本,如果复现值和论文值相差超过15%,就要回头逐步检查参数。我这边最终并网模式复现的容量配置与原论文误差在5%以内,离网模式误差稍大,约8%,原因是离网模式下缺氢惩罚系数的取值有主观性,不同取值对容量配置影响显著。
第四,看调度曲线的形态。电解槽出力曲线应该比较平滑,不会频繁跳变;储氢罐SOC应该在5%~95%之间上下波动,不会一直贴着边界跑。这些曲线形态异常往往是约束漏掉的信号。
最后说一点我做这类项目复现的个人体会:数学模型的功夫在模型之外,对物理系统运行方式的理解、对工程参数的量级感觉、对求解器数值特性的把握,这三样缺一不可。纯照抄论文公式是复现不出好结果的,你得在Python代码里把这些约束用工程语言重新表达一遍,中间自然会遇到一堆论文没写的细节问题。能把这些问题一个个解决掉,这个复现项目才算真正做透了。
