
做这个项目的起因挺实在的领导甩过来一个题目说“风光制氢合成氨并离网考虑容量和调度一起优化用Cplex跑Matlab实现复现一下”。几个词分开看都熟悉合起来就是一堆连环问题——电解槽的制氢曲线怎么跟风电光伏的出力匹配合成氨工段对氢气流量和压力有什么要求离网时蓄电池和储氢容量怎么填缺口并网时又要跟电网怎么交互最关键是容量配置建多大风场、铺多大光伏、上几台电解槽和运行调度每个小时怎么分配电、怎么产氢、怎么产氨本来是不同时间尺度的决策现在要把它们放进同一个优化模型还得用Cplex求解这本身就是个小工程。这篇文章就按我实际复现的路径来写不绕概念直接讲清楚系统拓扑、数学模型、Cplex建模技巧以及我在Matlab里踩过的坑。适合这几类人看做综合能源系统优化的研究生、刚接触Cplex但不知道怎么把工程问题翻译成数学模型的工程师、以及所有想用有限时间“复现一篇论文”但不想被一堆晦涩公式劝退的同行。1. 先用大白话拆解场景风光制氢合成氨到底在优化什么1.1 系统组成与能量/物质流这个系统本质上是一个“可再生能源→电能→氢能→氨能”的流动链条。风机和光伏发出来的直流/交流电一部分直接供给电解槽制氢一部分给工厂辅助设备用电剩下的要么进入蓄电池存储要么在并网模式下卖给电网离网模式下只能放弃或靠储能消化。电解槽产出的氢气一部分进入储氢罐缓冲一部分直接送到合成氨装置与氮气反应。合成氨装置需要连续稳定运行对氢气供应量的波动非常敏感所以储氢罐是衔接制氢和用氢之间的“蓄水池”。这里有个容易忽略的点合成氨工段不是简单的一进一出。虽然很多论文用氢气的化学计量比折算氨产量但实际工程里合成氨回路需要维持操作压力和催化剂温度进气流量一旦频繁波动就会影响合成塔效率甚至安全。所以在调度模型里我会把合成氨单元的用氢量当作一个相对平滑的决策变量而不是追着电解槽的波动跑。这也是为什么储氢容量往往需要覆盖数小时甚至跨日的缓冲量。风光互补的物理基础是资源特性互补风电通常夜间和冬春季出力大光伏是白天强、夏天强。一条典型日曲线上两者叠加后总出力曲线比单一电源更平坦能明显减少制氢系统启停次数和蓄电池吞吐深度。优化模型不需要“解释”这些物理特征但它会在容量优化时自动让你看到如果只配风机蓄电池容量会大得离谱只配光伏电解槽夜间几乎得停摆。互补配置的目标就是让各设备都在相对舒服的工况下运行。1.2 并网和离网的本质差异并网和离网不是模型里一个0/1参数那么简单它决定了系统设计哲学。并网模式下联络线是“安全垫”W型缺口可以买电补上富余电力可以卖钱所以优化器往往会把系统容量压得比较紧把电网当成兜底。离网模式下所有缺口都得靠蓄电池、储氢罐和后备电源自己扛، 所以容量配置必然更冗余且“可靠性”需要被显式建模——比如允许切负荷/切风光的上限保证全年99%以上时间能自平衡。在我的模型里并网模式会加入联络线功率约束通常包括最大受电/送电功率、购售电分时电价甚至可能加入功率方向切换次数限制以保护变压器。离网模式则完全去掉联络线变量但需要额外引入“失负荷率”惩罚或备用容量约束否则优化器可能为了省钱把系统做到几乎绝对自给自足造成浪费。还有一种“计划孤岛”模式即正常情况下并网极端条件下强制离网这时需要同时考虑联络线和离网切换条件我这次复现的是常规并/离网分开建模再对比的方式。2. 容量—调度联合优化为什么两件事必须放一起算2.1 分步优化的陷阱很多入门做法的第一步是先假设一个容量配置做全年小时级调度优化得到运行成本第二步再改容量重复调度画曲线找最优。这种“嵌套循环”思路简单但有两个致命问题一是计算量太大如果容量变量有6个每个候选20档就要跑20的6次方次调度受不住二是它会系统性地劣化方案因为容量决策本身就会改变最优调度策略分步优化无法捕捉这种耦合。比如增加电解槽容量后储氢罐调度逻辑可能完全变化但分步法里调度层不会提前“知道”容量层在想什么。一体化建模则把容量变量和调度变量都放进同一个优化问题。目标函数中同时包含容量投资年度化成本和运行成本约束同时覆盖容量上限和小时级平衡。Cplex对这种问题非常擅长——只要它能转换成混合整数线性规划(MILP)。一体化求解的结果是一套“容量该建多大”和“每个时段怎么运行”同时满足最优的方案这才算真正的全局最优。2.2 一体化问题的数学骨架把问题压缩成一个标准MILP可以写成min C_invest * X_capacity C_oper * X_schedules.t. A_capacity * X_capacity A_schedule * X_schedule ≤ b_balance B_schedule * X_schedule ≤ b_equipment X_capacity 中的整数/连续变量 X_schedule 为连续或0-1变量设备启停这里X_capacity代表风机台数、光伏容量、电解槽台数、储氢罐容量、蓄电池容量等“维度”变量X_schedule代表全年8760小时各设备的出力、储能充放电功率、购售电功率等。如果直接用8760个小时模型规模会非常庞大变量数可能到几十万Cplex虽然能处理但求解时间可能以小时计。所以复现论文时通常采用“典型日聚类”或用几个月代表性场景替代全年这是工程精度与计算资源之间的折中。这种骨架的价值在于它把两类完全不同性质的决策统一在了一个优化框架里避免了手工调参的玄学。Cplex内置的分支切割算法会自动搜索容量与调度的组合空间并给出一份带有最优性间隙(gap)的报告这一点是普通启发式算法做不到的。3. 用Cplex能直接吃下的模型变量、约束与线性化细节3.1 目标函数怎么定我这次复现的目标函数采用“年化总成本最小化”因为这是工程决策最常用的经济指标。年化总成本包含三块设备投资等额年值风机、光伏、电解槽、储氢罐、蓄电池、合成氨设备的投资乘以等额分付系数。等额分付系数等于i*(1i)^n / ((1i)^n - 1)i是折现率n是设备寿命。风机寿命20年电解槽寿命10年蓄电池可能只有5年这个系数要分别算。年运行维护费用通常按设备初投资的百分比估算电解槽的运维费率偏高因为电解液更换、电极保养都要钱。运行成本/收入并网模式下的购电费用减去售电收入加上氢气原料水费用、合成氨压缩机能耗费用。离网模式没有购售电项但我会加一个“失负荷惩罚”项用很小的成本系数允许系统在极端时段切负荷否则模型很容易无解。具体表达式不展开成几十行的数学公式但在Matlab代码里它是通过矩阵实现的目标函数系数向量f包含容量变量的年度化成本系数和调度变量的单位运行成本系数Cplex中通过“最小化f*x”来定义。3.2 关键约束群及线性化处理这个系统里拧出来几个最关键的约束群每个都对应实际物理规则第一个是电功率平衡约束。每个时段风机出力 光伏出力 蓄电池放电功率 购电功率 电解槽耗电 辅助设备耗电 蓄电池充电功率 售电功率。所有变量单位统一为MW。如果风机、光伏的单个机组容量已知那么总出力就是“台数乘以单机标准出力曲线”或“容量乘以标幺出力曲线”。这里有个线性化陷阱如果把光伏容量设为连续变量标准出力曲线是已知序列那么“容量变量 × 标准出力”还是线性的因为标准出力是参数不是变量但如果把风电出力也设成连续变量并与风机台数相乘就产生非线性。解决办法是把风机台数设为整数变量通过整数线性表达光伏容量可以设为连续但需要限制为离散档位或用分段线性化我尽量把涉及乘积的地方转换成“整数变量×固定参数”或者“连续变量×常数系数”这样Cplex才不会说“模型不是MILP”。第二个是电解槽运行约束。电解槽有最小运行负荷一般是额定功率的20%-40%太低会导致产氢纯度下降甚至氧中氢超标。所以在模型中每个时段电解槽耗电功率P_el有上下限可以是0也可以是P_el_min到P_el_max。如果只允许整数变量表示“开/关”0和最小负荷之间的跳变逻辑可以写成一个0-1变量z_el乘以对应容量变成线性不等式组。第三个是储氢罐动态约束。储氢罐的储氢量S_h_t1 S_h_t η_el * P_el_t / 电解槽电耗制氢系数 - M_H2_t - 放空量其中M_H2_t是合成氨用氢流量。这个递推约束需要把初始储量设定为模型变量或者在循环场景中要求周期一致性——比如一年末储量等于年初储量用一个大等式约束。还要给储氢罐容量设上限且储量上下限比例一般设为10%-90%。第四个是合成氨单元约束。氨产量Q_NH3与用氢量M_H2之间按化学计量比例约束为常数关系同时合成氨装置有最低运行负荷和最高产能限制且启动频率受限制。这个部分我会用线性关系Q_NH3 eta_NH3 * M_H2然后对Q_NH3设上下限。第五个是并网联络线约束。购电功率和售电功率不能同时为正需要一对0-1变量互相排斥还要限制联络线最大功率和最小功率防止来回倒买倒卖。离网模式下直接把购售电变量置为0模型结构保持一致很好写代码。线性化是这里最花时间的部分。比如蓄电池充放电如果充放电效率不同并且充放电功率可以同时为正那就要用0-1变量约束二者之和小于等于1如果认为电池同一时段只能充或只能放更简单直接加“充电功率≤Mz_ch放电功率≤M(1-z_ch)”。Cplex对0-1变量的处理已经很快但太多冗余约束会影响求解速度所以要养成“约束尽量紧”的习惯。4. Matlab调用Cplex的完整链路从安装到出结果4.1 环境准备和Cplex配置我用的环境是Matlab R2023b配合IBM ILOG Cplex 20.1.0。Cplex 20.1.0的安装包可以直接在IBM官网申请社区版支持的变量数和约束数有限制但对于教学复现足够了。安装完Cplex之后Matlab里要做的第一件事是把Cplex的动态库加进路径。在Windows下通常是C:\Program Files\IBM\ILOG\CPLEX_Studio201\cplex\matlab\x64_win64然后在Matlab里执行addpath(genpath(那个目录))再运行cplexSetup。这一步很多人会漏导致输入命令cplexlp或cplexmilp时提示“未定义函数”。社区版虽然限制规模但跑我这种几十个容量变量、几百个时段的小模型绰绰有余。如果你的场景大到几千个0-1变量且需要商业授权建议走学校或公司的学术版授权否则求解到中途弹出“Problem size exceeds community edition limit”就很难受。4.2 建模两种路径直接写cplexmilp还是用YALMIP一开始我直接用cplexmilp这个低级接口发现自己要把所有变量堆到一个大向量里然后写A矩阵、b向量、Aeq、beq、lb、ub、ctype等复查一个下标错误能花一下午。后面改用YALMIP来建模型代码量少了至少一半。YALMIP支持用运算符如、直接描述约束内部会帮你翻译成Cplex需要的标准形式。这个选择不是偷懒而是可维护性碾压你可以在Matlab脚本里清晰看到每一套约束的数学表达式改一个参数也很快定位。当然直接用YALMIP也有坑需要先安装YALMIP再在代码里调用YALMIP的sdpvar和binvar定义变量然后用optimize(Constraints, Objective, sdpsettings(solver,cplex))求解。如果Cplex路径没配好YALMIP会报准找不到求解器所以要把Cplex的matlab模块路径和YALMIP的路径都加进去。下面是一段示意核心代码% 定义变量容量变量 n_wind intvar(1,1); % 风机台数整数 cap_solar sdpvar(1,1); % 光伏容量 cap_elec intvar(1,1); % 电解槽台数 cap_h2 sdpvar(1,1); % 储氢容量 cap_bat sdpvar(1,1); % 蓄电池容量 % 定义调度变量以T个时段为例 P_wind sdpvar(1,T); % 风电出力 P_pv sdpvar(1,T); P_el sdpvar(1,T); P_bat_d sdpvar(1,T); % 蓄电池放电 P_bat_c sdpvar(1,T); S_h2 sdpvar(1,T1); % 储氢量 M_h2 sdpvar(1,T); % 制氨用氢 z_el binvar(1,T); % 电解槽启停 % 然后写约束...这段代码看起来简单但在实际编写时容量变量和调度变量的下标维度必须仔细对齐。我在初版时把T设为8760结果YALMIP构建约束数组时内存直接爆了后面改成典型日聚类用12个典型日×24小时共288个时段问题规模立刻小了三个数量级Cplex几秒钟就能给到gap0.1%的解。4.3 数据组织和结果落盘一个容易让复现者头疼的点是输入数据的维度怎么组织。我的习惯是把所有曲线数据做成结构体或表格包括风速标幺值、辐照度标幺值、环境温度影响电解槽效率、分时电价、负荷曲线等。每列是一个时段行是不同的典型日。然后写一个函数把这些数据转换成模型参数数组。这样以后替换数据只需要改Excel或CSV不需要动主脚本。求解结束后要做的不是看一眼目标函数值就完事而是把结果导出到Excel或MAT文件画运行曲线图。我习惯保存最优容量向量、全年各时段各变量值、储能充放电状态、购售电量和总成本。这些结果后来可以用于灵敏度分析——比如把光伏单位造价降20%看容量怎么变。这个后处理逻辑也建议写成独立脚本因为它和优化模型本身是解耦的。5. 复现时的参数取值参考与典型结果解读5.1 一组可用的示例参数论文复现最怕没有参数来源。我这次先用一套“演示级”参数来源基本靠公开报告和常规工程经验值具体见下表参数数值单位说明风机单机容量2.5MW可设整数台数变量光伏单位容量投资3500元/kW包含组件、逆变器、安装风机单位容量投资6000元/kW按单机容量折算电解槽单位容量投资4500元/kW碱性电解槽储氢罐单位容量投资3000元/kg按储氢量计算蓄电池单位容量投资1200元/kWh锂离子电池电解槽制氢效率4.8kWh/Nm³标准状态下制1方氢耗电合成氨氢耗0.167kg H2 / kg NH3化学计量比购电价0.6元/kWh适当做分时处理售电价0.4元/kWh上网电价表里电解槽效率一定要与目标函数、能量平衡约束的单位保持一致。如果效率写的是kWh/Nm³而调度模型里的电能单位是MWh、氢气单位是kg那就得先做单位换算。1 Nm³氢气质量约为0.0899kg那么生产1kg氢气耗电约53.4kWh。这个换算错误我曾犯过导致结果中电解槽容量小了一个数量级检查了很久才发现是单位问题。5.2 结果中真正值得看的几个指标模型跑完后不要只盯着总成本。我会重点看三个指标第一个是容量配置比例。比如风机台数为10、光伏容量为55MW电解槽台数为8储氢量为3t。这个比例反映的是当地的资源互补情况——如果光伏多白天制氨多、晚上几乎停摆储氢罐会很大如果风电多曲线平滑但投资高。对比并网与离网两种情况通常离网系统容量会大20%-40%原因就在于它必须自己解决问题。第二个是设备利用率。电解槽全年平均利用率如果低于60%说明系统里制氢冗余偏大可以把电解槽台数降下来或者增加储氢罐让电解槽在风光高发时满负荷运行、低发时停机。储氢罐的吞吐次数也很关键频繁吞吐说明制氢和制氨之间缓冲不足。第三个是弃风弃光率。在离网模式下如果弃电率超过5%说明风光配套储能容量偏小或者电解槽没有足够弹性去吞噬瞬时尖峰。并网模式下弃电率可能接近0因为多出来可以直接卖但售电价低时优化器也可能选择主动限功率这需要在运行结果里对P_wind和P_pv的实际输出与理论出力做对比。典型结果通常还会告诉你一个反常识的结论并网模式下最优点不一定是“自发自用最大化”而是让电网帮你在低电价时提供基础电力、高电价时把多余风电卖掉。比如在夜间低电价段购买便宜电制氢白天高电价段却把电解槽降功率甚至停机相当于把风光主力留在白天自用和售电。这种策略在纯离网场景是绝对看不到的也是并离网对比分析最有意思的地方。6. 踩坑记录求解器报错、无界解和线性化翻车6.1 Infeasible别急着调参先查等式一致性我第一次运行完整模型时Cplex直接就报“Infeasible Problem”。这种时候千万不要拍脑袋去调成本系数最有效的方法是“约束松弛”。先关掉离网约束、关掉联络线限制保留最松的功率平衡约束看能不能出解能出解就逐步加约束直到定位到具体哪一组约束导致无解。最终发现是我在储氢递推约束里把初始储氢量S_H2(1)写成了常数0但合成氨装置却要求每个时段用氢量M_h2至少要满足最小负荷而电解槽从0开始储能需要时间导致第一时段氢气无法供应。解决办法是把S_H2(1)设为自由变量并加入年首末储量一致的等式约束相当于让系统先“预热”到一个合理水平。这个问题很隐蔽纯看约束方程很难发现但用“最小化无解约束数量”就是一个很好的调试技巧。6.2 双线性项处理不当导致的数值异常另一个大坑是储氢罐动态约束中的充电效率与放氢效率。如果将储氢量的变化表示为η_ch * P_el - M_h2 / η_disch而充电效率η_ch和P_el都是变量时看起来没错但如果你把η_ch也设置成变量为了模拟电解槽效率随负荷变化就和P_el产生了双线性项Cplex会拒绝求解或给出非凸警告。我的做法是忽略效率随负荷的微小时变把平均效率固定为0.75这样整个约束保持线性。如果你确实想考虑变效率曲线就得进行分段线性化把功率区间切成几段每段对应一个效率系数并用0-1变量激活对应段这个工作量会明显变大。对于初步复现固定平均效率是完全合理的精度损失通常小于2%。6.3 求解时间爆炸的应对思路用全年8760个时段又加了32台风机整数变量和8760个电解槽启停0-1变量Cplex求解时间会非常夸张。我的经验是第一版先用12个典型日×24小时每个典型日权重为它代表的原始天数。这样目标函数中“年化成本”能通过权重折算而容量变量是全年共享的调度变量只对288个代表性时段建模。这样求解时间从“数小时”降到“数十秒”最优性gap也能控制在0.05%以内。如果还要更精细可以加密到20个典型日用k-means聚类选取但要注意避免聚类后极端风速光辐被平滑掉否则系统容量会被低估。另外Cplex的MIP容差(MIP Gap)参数也值得调。如果只是比较方案默认gap0.01可能就够但如果你发现求解到几十分钟gap还下不来可以把gap放松到0.05同时设置时间上限避免无意义等待。在YALMIP里通过sdpsettings(cplex.mip.tolerances.mipgap,0.001)就能改。关于求解结果我建议始终保留一份“人工可验证的小规模算例”用来调试代码。比如把时段数缩到只有24小时设备容量手动设成已知最优解看模型能否复现预期调度策略。这样能帮你确认约束没有写反、目标函数系数没有错位。很多时候Cplex输出结果看起来“假”不是因为求解器有问题而是某个约束里的指数下标从1开始还是从0开始导致错位了一个时段。最后说一个很实用的小技巧做并/离网对比时不要在同一个脚本里反复清空工作区建议写两个独立入口函数run_grid.m和run_offgrid.m共用同一个模型构建函数仅用一个布尔参数控制是否调用联络线约束。这样能极大减少复制粘贴带来的不一致。我在实际复现中吃过“并网结果比离网还贵”的亏排查半天发现是复制脚本时把一个约束里的购电上下限从[-500,500]写成了[0,500]导致购电功率永远非负模型自然扭曲。这种低级错误最好是靠代码审查而不是看图发现。整个项目做完我对Cplex在容量—调度联合优化上的能力评价是对于线性或混合整数线性问题它确实是最可靠的商用求解器之一配合Matlab的矩阵运算和绘图生态非常适合做这类工程优化复现。但工具再强模型的物理意义和约束逻辑才是地基只有把能量流、物料流和单位换算彻底理清楚Cplex给出的“最优解”才有真正的工程价值。后续如果想把模型扩展到多目标经济性与碳排放双目标、考虑电解槽变负荷效率曲线、加入合成氨装置动态响应约束这套MILP骨架依然能继续复用只需要在约束线性化上多花功夫。