不知道你有没有做过这样的微电网调度项目:风机光伏的预测出力看着挺乐观,可一到傍晚负荷走高、光照消失的时候,电网购电费瞬间就上去了;储能系统装了,但充放电时机把握不好,要么过早放空、要么在低价时段根本没充满。我最近完整跑通了一套“基于风光储和需求响应的微电网日前经济调度”模型,用Matlab实现,优化周期覆盖24小时,目标就是在满足负荷要求的前提下,让购电成本、柴发费用、储能损耗和需求响应补偿的总和最小。项目的最终效果挺直观:储能基本跟着分时电价走,谷时充、峰时放;高电价时段用需求响应削掉一部分尖峰负荷;风电出力充足时就压低柴发出力。整套思路不算复杂,但如果建模细节没想清楚,优化结果很容易出现“电池一边充一边放”“SOC越界”这类匪夷所思的问题。
这篇文章就把我从搭模型到写代码、调参、排查问题的全过程整理出来。适合正在做微电网能量管理、储能调度、需求响应研究的同学参考,也适合刚接触日前调度、想找一个可以落地的Matlab框架的工程师。我会把目标函数、约束条件、Yalmip建模代码、常见坑位都拆开讲,尽量让拿到文章的人能直接复现出一版能跑的结果。
1. 微电网日前经济调度:先把问题拆明白
1.1 调度场景与核心目标
微电网的“日前经济调度”其实是在做一件很朴素的事:提前24小时,把各台设备的出力计划排好,让微电网在明天全天以最低的总运行成本满足负荷需求。这个场景里的“设备”通常包括风电、光伏、储能、柴油发电机、与主网的交换功率,以及可以被调度的柔性负荷。由于风电和光伏的预测出力在日前就被当作已知量,所以问题简化成了“已知明天24小时的风光出力和负荷曲线,如何分配各设备的出力”。
总成本不是只有购电费。一个完整的调度目标函数通常包含四块:
- 向主网购电的费用,也就是分时电价下的电费;
- 柴油发电机的燃料成本,通常用二次函数近似,或者分段线性化;
- 储能的充放电损耗和寿命折损成本,做法上会给充放电功率乘一个小系数;
- 需求响应补偿费用,也就是让用户削负荷或转移负荷时需要支付的价格。
对我这个项目来说,核心目标公式可以写成:
min Σ [ 购电电价(t) × P_grid(t) + 柴发成本(P_diesel(t)) + 储能损耗系数 × (P_ch(t) + P_dis(t)) + DR补偿单价 × P_dr(t) ]
这里的求和范围是 t = 1 到 24。调度结果出来之后,能直接看到每个时段风电、光伏、储能、柴发、购电、需求响应各自承担了多少负荷,以及全天总成本是多少。
1.2 为什么选“日前调度”而不是实时调度
刚接触微电网优化的人常问:既然风电光伏出力会波动,实时调度不是更贴近实际吗?我的看法是,日前调度和实时调度解决的是不同层面的问题。日前调度解决的是“明天大致怎么运行”,它要提前确定储能的充放电策略、需求响应的调用计划、柴发的启停安排,这些安排往往有提前通知或准备时间,不能等到实时运行那一刻再拍板。而实时调度解决的是“预测误差来了怎么修正”,它是滚动修正层面的问题。
所以这个项目用日前模型打底是合理的。风电和光伏的出力直接采用预测数据,可以理解为“不管预测准不准,先在日前做一版最优计划”。后续如果要做更精细的控制,完全可以在日前计划的基础上叠加滚动修正、模型预测控制等模块。我在这篇文章里不展开实时部分,但会在最后聊一下扩展思路。
1.3 整体求解流程
整个Matlab实现流程可以概括为四步:
- 准备输入数据:24小时的风电出力、光伏出力、负荷曲线、分时电价、柴发参数、储能参数、需求响应容量。
- 搭建优化模型:用Yalmip定义决策变量、目标函数和约束。
- 调用求解器求解:问题本质上是混合整数线性规划MILP,因为储能“不能同时充放电”需要0-1变量。
- 结果提取与可视化:画功率平衡图、SOC曲线、成本构成图,分析调度策略是否合理。
工具链方面,Matlab负责数据预处理、建模和可视化,求解器用的是Cplex或Gurobi这种商用MILP求解器,通过Yalmip统一调用。没有商业求解器的环境,也可以用Matlab自带的intlinprog替代,但建模起来会麻烦一些,后面我会细说差异。
2. 风光储与需求响应的建模要点
2.1 风电与光伏出力模型
风电和光伏在这个问题里不作为优化变量,而是“已知的不可控电源”,所以建模重点不在机理,而在数据曲线。典型做法是用一个典型日或若干个典型日的预测数据,给出24个时点的出力标幺值。
光伏出力比较容易理解。它和光照辐照度基本成正比,还要考虑温度影响。我用一个简化公式:
P_pv(t) = P_pv_rated × [ G(t) / 1000 ] × [ 1 - γ_pv × ( T_cell(t) - 25 ) ]
其中G(t)是水平面辐照度,单位W/m²,T_cell(t)是电池板温度,γ_pv是温度系数。这个公式的优势是,输入数据只要有辐照度曲线和温度曲线就能得到光伏出力,不需要太复杂的物理模型。当然,如果你手里直接有光伏预测功率曲线,那更省事,直接读取就行。
风电出力模型比较多样。工程上常见的是给出一条风速曲线,然后用风机厂家提供的功率曲线插值。如果没有厂家曲线,也可以用分段函数近似。但说实话,在日前经济调度这个层面,直接用预测功率曲线更高效。因为任何风速转功率的模型都包含大量假设,而这些假设对最终调度策略的影响,远小于预测数据本身的误差影响。所以我的建议是:风电和光伏统一用“预测功率曲线”输入,省时且聚焦在调度优化本身。
2.2 储能系统约束:SOC与充放电逻辑
储能是微电网经济调度的核心灵活性资源,它的建模有几个绕不开的变量。首先定义t时段末的荷电状态SOC(t),用E_ess表示储能额定容量。充放电功率分别记为P_ch(t)和P_dis(t),对应充放电效率分别是η_ch和η_dis。那么SOC的递推关系是:
SOC(t) = SOC(t-1) + η_ch × P_ch(t) × Δt / E_ess - P_dis(t) × Δt / ( η_dis × E_ess )
这个式子看起来简单,但有三个特别容易踩坑的地方。
第一,Δt的量纲。如果数据和调度周期按小时计,Δt就是1;如果你的数据步长是15分钟,Δt就是0.25。很多人直接把小时模型套到15分钟数据上,SOC变化幅度异常,其实就是这里出了问题。
第二,充放电功率的上下限。通常有额定功率约束P_ess_max,同时还有SOC边界:
0 ≤ P_ch(t) ≤ P_ess_max × u_ch(t) 0 ≤ P_dis(t) ≤ P_ess_max × u_dis(t) SOC_min ≤ SOC(t) ≤ SOC_max
这里的u_ch(t)、u_dis(t)是0-1变量,用来表达“要么充、要么放、要么都不动”。这一对互斥约束是整个建模里唯一的整数变量来源,也是MILP问题的由来。
第三,初始SOC的设定。常见做法是让SOC(0)等于SOC(24),也就是一天结束后电池电量回到初始状态。这样做的好处是让储能调度具有“日循环”的周期性,结果不会出现一天结束时电池被彻底放空或者是把前一天电量用光的情况。如果你不设这个约束,优化器很可能把初始SOC当成免费电量可劲消耗,计划结果在实际运行中根本没意义。
2.3 需求响应的两种建模思路
需求响应在微电网里一般有两种实现路径。一种是价格型需求响应,就是通过分时电价引导用户转移用电时段,模型上用负荷的价格弹性矩阵来表达。另一种是激励型需求响应,就是调度中心直接指定某个时段削减多少负荷,并支付补偿费用。在日前经济调度里,激励型需求响应更好量化、更容易纳入优化模型,因此大多数代码实现都采用这个方式。
激励型需求响应的核心约束是:每个时段的削减量不能超过用户可削减上限,全天削减总量还有上限。公式就是:
0 ≤ P_dr(t) ≤ P_dr_max(t) Σ P_dr(t) ≤ P_dr_total_max
在目标函数里给它一个补偿单价,优化器就会自动判断“什么时候削减负荷划算”。比如分时电价尖峰时段电价1.2元/kWh,而需求响应补偿单价只要0.6元/kWh,那优化器就会倾向于用需求响应去平抑尖峰。这个机制理解起来很直观,写进模型也简单,但真正实施时还需要考虑用户的响应意愿和提前通知时间,在建模中体现为P_dr_max(t)的取值。
我注意到很多学习者在做需求响应时喜欢把P_dr当“免费电量”来用,结果模型疯狂削减负荷,成本下降了但实际根本不现实。这里的关键是补偿单价不能设得太低,至少要能反映“用户凭什么配合你”的成本。我一般会给一个略高于平均购电价格但低于峰值购电价格的补偿单价,这样模型才有合理的经济信号。
2.4 功率平衡与柴发约束汇总
微电网运行的硬性约束是瞬时功率平衡。必须满足:
P_wind(t) + P_pv(t) + P_dis(t) + P_diesel(t) + P_grid(t) + P_dr(t) = P_load(t) + P_ch(t)
注意,储能充电是负荷,所以放在等式右侧;放电是电源,放在左侧。需求响应削减掉的负荷等效为电源,放在左侧。这个等式是所有调度模型的“宪法”,任何方案都必须满足它。
柴油发电机的约束包括出力上下限和爬坡约束:
P_diesel_min ≤ P_diesel(t) ≤ P_diesel_max
- Ramp_down ≤ P_diesel(t) - P_diesel(t-1) ≤ Ramp_up
柴发成本函数我采用二次函数形式:
C_diesel(t) = a × P_diesel(t)² + b × P_diesel(t) + c
在MILP里不能直接处理二次项,现实中常用分段线性化处理。但如果你用Yalmip配合支持MIQP(混合整数二次规划)的求解器,比如Gurobi,也可以直接写二次目标。我项目里用的是分段线性化,这样对求解器更友好,不容易出现数值问题。
电网交互这块,支持双向功率交换时,P_grid可以正可以负,负值表示向主网售电。这需要引入两个0-1变量表示购售电互斥,同时购电价格和售电价格也不相同。如果只考虑“从主网购电,不上网卖电”,那P_grid直接限制为不小于0就可以。我在这套实现里先用单向购电模型,因为售电上网涉及的政策因素较多,经济参数也不好定。
到这里,整个优化模型已经成型:目标函数加功率平衡、储能约束、柴发约束、需求响应约束、电网购电约束。下面就是怎么把这些数学式子变成Matlab代码。
3. 基于Matlab的实现过程与核心代码
3.1 环境准备与工具链选型
这个项目我建议使用Matlab配合Yalmip工具箱,再外接一个商业求解器。Yalmip是一个建模层,它让你用接近于数学公式的语法描述优化问题,不需要手动拼矩阵。对做研究、快速迭代模型的人来说,这是效率最高的一套组合。
Matlab自带的intlinprog也能解MILP,但需要把目标函数、约束系数矩阵手动写成标准形式,对于24小时、几十个约束的问题来说工作量非常大,而且改一个参数就得动一堆矩阵代码。Yalmip把这层封装掉了,yalmip('clear')之后,你就像写代数式一样定义变量和约束,非常符合直觉。
安装步骤不复杂:
- 下载Yalmip并添加到Matlab路径;
- 安装Gurobi或Cplex。如果暂时没有商业求解器,Yalmip也可以调用intlinprog;
- 运行yalmiptest检查求解器是否能被识别。
实际项目中,我用的求解器是Gurobi。它在MILP上的求解速度很快,24小时调度模型的求解时间基本在几秒以内,完全满足“日前调度”的时间要求。
3.2 参数初始化与数据输入
数据部分我用典型日数据做示例。先定义24小时的风电出力、光伏出力、负荷曲线和分时电价。代码框架如下:
%% 基础参数 H = 24; % 调度周期长度(小时) dt = 1; % 时间步长(小时) % 负荷曲线(单位:kW) P_load = [120; 110; 105; 100; 95; 98; 110; 150; 190; 210; 220; 225; 230; 210; 195; 185; 180; 175; 190; 220; 240; 235; 200; 160]; % 风电预测出力(单位:kW) P_wind = [35; 32; 30; 28; 25; 22; 20; 18; 16; 15; 18; 22; 25; 28; 30; 32; 35; 38; 40; 42; 40; 38; 36; 33]; % 光伏预测出力(单位:kW) P_pv = [0; 0; 0; 0; 0; 8; 25; 45; 70; 90; 110; 120; 125; 115; 100; 80; 55; 30; 5; 0; 0; 0; 0; 0]; % 分时电价(单位:元/kWh) price_buy = [0.38; 0.38; 0.38; 0.38; 0.38; 0.38; 0.58; 0.58; 1.05; 1.05; 1.05; 0.58; 0.58; 0.58; 0.58; 0.58; 1.05; 1.05; 1.05; 1.05; 0.58; 0.58; 0.58; 0.38];储能和柴发参数单独列一个参数块:
%% 储能参数 E_ess = 100; % 储能额定容量(kWh) P_ess_max = 40; % 储能最大充放电功率(kW) eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 SOC_min = 0.2; % SOC下限 SOC_max = 0.9; % SOC上限 SOC_0 = 0.5; % 初始SOC cost_ess = 0.02; % 充放电损耗成本系数(元/kWh) %% 柴发参数 P_diesel_min = 10; % 柴发最小出力(kW) P_diesel_max = 80; % 柴发最大出力(kW) Ramp_up = 30; % 爬坡上限(kW/h) Ramp_down = 30; % 爬坡下限(kW/h) a = 0.002; % 柴发成本二次项系数 b = 0.6; % 柴发成本一次项系数 c = 5; % 柴发成本常数项 %% 需求响应参数 P_dr_max = 25; % 单时段最大可削减负荷(kW) P_dr_total_max = 80; % 全天最大削减总量(kWh) price_dr = 0.65; % 需求响应补偿单价(元/kWh)这些参数看起来不起眼,但它们直接决定了优化结果的形态。比如P_ess_max是40kW,而晚高峰负荷削减需求可能是30kW以上,储能能不能扛住尖峰就看它和柴发、购电的配合。另外,风光的比例决定了主网购电压力,我这份数据里风光占比不算小,所以储能在午间基本处于充电状态,为晚高峰做准备。
3.3 用Yalmip构建优化模型
这是整套代码最核心的部分。我用Yalmip定义变量,然后按数学公式逐块加上约束。
变量定义:
%% 决策变量定义 P_grid_buy = sdpvar(H, 1); % 从主网购电功率(kW) P_diesel = sdpvar(H, 1); % 柴发出力(kW) P_ess_ch = sdpvar(H, 1); % 储能充电功率(kW) P_ess_dis = sdpvar(H, 1); % 储能放电功率(kW) SOC = sdpvar(H+1, 1); % SOC状态变量(t=0到t=24) u_ch = binvar(H, 1); % 充电状态0-1变量 u_dis = binvar(H, 1); % 放电状态0-1变量 P_dr = sdpvar(H, 1); % 需求响应削减量(kW)柴发成本需要分段线性化。我用一个简单的二段线性近似,把出力区间分成两段,分别用斜率表示。也有人用更细的分段,但两段足以展示方法。你可以先跑通这个版本,再按精度需求增加分段。
目标函数这样写:
%% 目标函数 objective = 0; for t = 1:H % 购电成本 objective = objective + price_buy(t) * P_grid_buy(t); % 柴发成本(分段线性近似) if P_diesel(t) <= 45 c_diesel = 0.75 * P_diesel(t) + 8; else c_diesel = 0.65 * P_diesel(t) + 12.5; end objective = objective + c_diesel; % 储能损耗成本 objective = objective + cost_ess * (P_ess_ch(t) + P_ess_dis(t)); % 需求响应补偿 objective = objective + price_dr * P_dr(t); end严格来说,if分支不能直接用于sdpvar,因为sdpvar是符号变量,Matlab的if无法对符号表达式做逻辑判断。正确的做法是使用辅助0-1变量描述分段线性,或者用Yalmip内置的piecewise功能。我这里为了描述逻辑方便写成if的样子,实际代码里要换成分段线性化的标准写法。我会在下面的完整代码框架里给出可复用版本。
储能约束块很重要,我把每一类约束都单独写出来:
%% 约束条件 Constraints = []; % 1. 功率平衡约束 for t = 1:H Constraints = [Constraints, P_wind(t) + P_pv(t) + P_ess_dis(t) + ... P_diesel(t) + P_grid_buy(t) + P_dr(t) == ... P_load(t) + P_ess_ch(t)]; end % 2. 储能充放电功率边界与互斥 for t = 1:H Constraints = [Constraints, 0 <= P_ess_ch(t) <= P_ess_max * u_ch(t)]; Constraints = [Constraints, 0 <= P_ess_dis(t) <= P_ess_max * u_dis(t)]; Constraints = [Constraints, u_ch(t) + u_dis(t) <= 1]; end % 3. SOC递推与边界 Constraints = [Constraints, SOC(1) == SOC_0]; Constraints = [Constraints, SOC(24+1) == SOC_0]; % 日循环约束 for t = 1:H Constraints = [Constraints, SOC(t+1) == SOC(t) + ... (eta_ch * P_ess_ch(t) - P_ess_dis(t)/eta_dis) * dt / E_ess]; Constraints = [Constraints, SOC_min <= SOC(t+1) <= SOC_max]; end % 4. 柴发约束 for t = 1:H Constraints = [Constraints, P_diesel_min <= P_diesel(t) <= P_diesel_max]; end for t = 2:H Constraints = [Constraints, -Ramp_down <= P_diesel(t) - P_diesel(t-1) <= Ramp_up]; end % 5. 需求响应约束 for t = 1:H Constraints = [Constraints, 0 <= P_dr(t) <= P_dr_max]; end Constraints = [Constraints, sum(P_dr) <= P_dr_total_max]; % 6. 购电约束(单向购电,不上网) for t = 1:H Constraints = [Constraints, 0 <= P_grid_buy(t) <= 150]; end注意到我加了SOC日循环约束SOC(25) == SOC_0,这是很多论文的标准做法。它把储能当成一个“日平衡”装置:一天结束回到相同电量,不会出现偷取初始电量的情况。
柴发分段成本如果要严谨实现,可以用以下几行:
% 引入柴发分段辅助变量 P_diesel_1 = sdpvar(H, 1); P_diesel_2 = sdpvar(H, 1); lambda_diesel = binvar(H, 1); for t = 1:H Constraints = [Constraints, P_diesel(t) == P_diesel_1(t) + P_diesel_2(t)]; Constraints = [Constraints, 0 <= P_diesel_1(t) <= 45 * lambda_diesel(t)]; Constraints = [Constraints, 0 <= P_diesel_2(t) <= (P_diesel_max - 45) * (1 - lambda_diesel(t))]; end objective = objective + sum(0.75 * P_diesel_1 + 0.65 * P_diesel_2 + 8 - 4.5);这里用lambda_diesel选择柴发工作在哪一段。常数项处理是分段线性化的细节,需要根据两段函数的截距差来调整。我建议先跑通单段模型,再加分段逻辑,这样定位问题容易得多。
求解和结果提取:
%% 求解 optimize(Constraints, objective, sdpsettings('solver', 'gurobi', 'verbose', 2)); %% 提取结果 P_grid_buy_opt = value(P_grid_buy); P_diesel_opt = value(P_diesel); P_ess_ch_opt = value(P_ess_ch); P_ess_dis_opt = value(P_ess_dis); SOC_opt = value(SOC); P_dr_opt = value(P_dr); total_cost = value(objective);运行完之后,检查一下总成本、SOC曲线是否在边界内、充放电互斥约束是否满足,这些基础检查能帮你快速排除模型错误。
3.4 结果可视化与策略分析
拿到优化结果后,我习惯画三张图:功率平衡堆叠图、SOC与充放电功率图、成本构成图。
功率平衡堆叠图很有意思,它把风电、光伏、柴发、放电、购电、需求响应按时间轴堆起来,看它们如何共同满足负荷曲线。从结果里能清楚观察到几个典型行为:
- 白天光伏出力大的时段,储能充电,购电减少;
- 傍晚光伏归零、负荷走高,储能开始放电,柴发和购电作为补充;
- 分时电价尖峰时段,需求响应会承担一部分负荷削减。
成本构成图能直接看出哪项费用占比最大。我跑出来的结果里,购电成本通常占大头,柴发成本其次,储能损耗和需求响应补偿只占很小的比例。这说明调度策略的优化空间主要在“什么时候买电、买多少”和“什么时候开柴发、开到多大”。
有一件事需要提醒:优化结果里的“最优”,只是在你给定的参数条件下的最优。比如需求响应补偿单价如果比峰值电价低很多,优化器可能把需求响应容量全部用满;如果补偿单价太低,可能一个时段都不用。所以要结合经济性设定参数,不能只看“优化器怎么算”。
4. 常见问题与排查技巧实录
4.1 无可行解:先看约束是否冲突
MILP模型最让人抓狂的问题就是求解器返回“无可行解”。我遇到的无可行解案例,九成都是约束写重或写冲突了。最常见的几个来源:SOC日循环约束与初始SOC设置冲突。比如SOC_min是0.2,SOC_0是0.9,而日循环要求一天后回到0.9,但储能在某个时段被迫大量放电导致SOC跌破了0.2,那就无解。
排查方法也很机械:先把边界约束适当放宽,比如把SOC日循环约束去掉,看能不能解出来;如果能解,说明是循环约束和SOC边界太紧导致的问题。然后再逐个加回约束,看是哪个约束把问题搞成无解的。一个更优雅的做法是在目标函数里对功率平衡加一个“松弛变量”,并给很大的惩罚系数。这样即使无解,也能从松弛变量在哪段时间非零来定位是哪个时段的平衡有问题。
4.2 SOC曲线异常:递推公式的方向别搞反
我见过很多人的SOC曲线在一天内剧烈波动,甚至“充电时SOC下降”。这多半是递推公式符号反了。正确逻辑是:充电让SOC上升,放电让SOC下降。所以你看到的应该是正比于充电功率上升、反比于放电功率下降。还有一个容易忽略的点是效率位置。充电效率作用在充入SOC的分量上,所以SOC增量要乘以eta_ch;放电效率作用在放出的能量上,所以SOC减量要除以eta_dis。别把效率位置放反,否则SOC要么涨太快要么掉太快。
还有一种情况是步长不匹配。如果把Δt当成1但实际数据间隔是15分钟,SOC变化幅度会放大4倍,经常触发SOC上限或下限。所以写递推公式前,先确认自己的数据步长是多少,再统一Δt。
4.3 需求响应“疯狂削减”或者“完全不削减”
需求响应结果要么全用满、要么一点不用,这类现象通常是经济参数设置不合理。如果补偿单价高于很多时段的购电电价,优化器会倾向多削减负荷来“套利”,造成实际运行中用户被频繁调用。反之,如果补偿单价远低于电费节省空间,优化器又可能完全不用需求响应。所以需求响应的补偿单价最好设在“略高于平均发电成本、低于尖峰购电价格”的区间。同时,P_dr_total_max这个全天总量约束也决定了需求响应不可能无限度参与,合理设置容量也是保证结果不极端的关键。
另外,功率平衡方程中P_dr(t)的位置要放对。它是“负荷削减量”,等效于一个电源,放在等式左侧。如果放在右侧作为负负荷也等效,但符号相反,一旦写反,功率平衡会错乱,结果毫无意义。
4.4 求解时间偏长或求解器报数值警告
24小时微电网调度问题规模不算大,一般求解时间都在几秒以内。如果求解时间突然变得很长,通常是整数变量过多导致。比如充放电互斥用了两个0-1变量,如果你又把柴发启停、购售电互斥全加上,整数变量就有几十上百个,难度会显著上升。但24小时模型的规模一般还不至于“难到解不动”。
如果Gurobi或Cplex报数值警告,优先检查参数的量级是否统一。比如功率用kW,电价用元/kWh,容量用kWh,这种单位体系下数值都在1到1000以内,比较合适。如果你某些变量用的是MW、MWh、元/MWh混在一起,矩阵条件数会变大,求解器容易出现数值问题。经验教训是:单位统一这件事,在建模前就要想清楚,等求解器报警告再回头改,工作量翻倍。
5. 从确定性日前调度走向更复杂的扩展
5.1 多场景与鲁棒优化
基础版日前调度用的是“预测值等于真实值”的确定性假设。实际工程中,风电光伏预测误差是常态,尤其风电,短期预测误差可能超过20%。如果对这个误差敏感,可以考虑两种扩展:多场景随机优化和鲁棒优化。
多场景随机优化的思路是生成若干组风光出力场景,给每个场景一个概率权重,目标函数变成所有场景下成本的期望值。约束仍然需要满足,但可以在不同场景下调整可控设备出力。当然,储能SOC、柴发爬坡这类带记忆的约束要注意场景之间的独立性,不能让一个场景的SOC状态影响到另一个场景,否则物理上说不通。
鲁棒优化则更保守,它假设风光出力落在一个不确定区间内,优化目标是“最恶劣场景下的成本最小”。这类模型的求解规模更大,参数也更多,但结果对预测误差的容忍度更好。我自己的体会是:先跑通确定性模型,再逐步加随机性或鲁棒性,是最稳的学习路径,不要一上来就上鲁棒优化,否则很难判断模型哪里出了问题。
5.2 与滚动修正和实时控制的衔接
日前调度出来的是24小时计划,但实际运行中风速、辐照、负荷每几分钟都在变化,所以更好用的微电网能量管理系统都是“日前计划+日内滚动修正”。一种常见做法是,把日前模型的优化结果作为参考计划,然后在日内以15分钟为步长重优化一次短时窗,比如未来4小时,并给“偏离日前计划”的成本惩罚。这样既保留日前全局优化的经济性,又有日内的局部修正能力。
在代码实现上,只需要把H从24改成预测时域长度,然后刷新预测数据、重新求解即可。这个扩展不难,但要注意滚动窗口之间的初始状态衔接,比如SOC要取上一次优化结果对应时段的SOC值,不能每次从固定初值开始。我当时做扩展时,曾经因为忘记更新SOC初值,结果日内计划的储能策略完全失真,排查了半天才发现是这个低级问题。
5.3 其它可玩的方向
如果你做完这个基础项目还想再延伸,可以试这几个方向:
- 把柴油发电机换成燃气轮机、燃料电池等其它分布式电源;
- 把需求响应从“可削减负荷”扩展为“可转移负荷”,引入可转移时间窗口;
- 加入网络拓扑约束,做微电网的潮流约束优化;
- 把成本模型改为包含碳排放成本的多目标优化。
每个方向都能让模型更贴近实际,但也会显著增加建模和求解复杂度。建议循序渐进,先保证基础模型能稳定复现最优解,再逐步叠加复杂度。
最后再分享一个小技巧:写这类优化代码时,每次只加一类约束,然后立刻求解并检查结果。我的习惯顺序是:先不加储能,看纯购电加柴发能否平衡;然后加储能,观察SOC是否合理;最后再加需求响应。每加一层,都检查目标函数值的变化是否符合你对该设备经济性的直觉判断。如果某一层结果突然不合理,八成是那一层的约束或参数有问题。这套“分层验证”的方法帮我躲过了无数个调bug的夜晚,希望你也能用上。