ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微电网日前经济调度Matlab建模与求解实战指南

微电网日前经济调度Matlab建模与求解实战指南

微电网经济调度这个话题,这几年关注的人越来越多。一方面是多能互补和分布式能源并网的项目在增加,另一方面是"风光储+需求响应"这个组合提出来之后,很多做研究或者做工程的朋友都想知道,日前调度计划到底怎么排、成本能降多少、Matlab里该怎么落地。这篇博文我会把整个思路捋清楚:先讲清楚微电网日前经济调度的核心矛盾,再把模型里的变量、目标函数、约束条件一项项拆开,最后落到Matlab代码的架构和调试技巧上。无论你是刚接触调度的研究生,还是在做微电网示范项目的工程师,这篇文章都值得收藏慢慢看。

1. 微电网日前经济调度到底在解决什么问题

1.1 调度的本质:在不确定性里做一天的计划

日前经济调度的"日前"两个字,指的是提前一天、通常以1小时为时间间隔,滚动制定未来24小时各台设备的出力计划。这个计划要回答的问题很朴素:明天每个小时,光伏发多少、风机发多少、蓄电池充多少放多少、联络线从主网购多少电或者卖多少电、需求响应负荷削减多少,才能让一整天的总运行成本最低,同时满足供电可靠性。

这听起来就是一个优化问题,但实际上难点在"不确定性"。光伏出力受云层影响,风速变化更是毫无规律可言,负荷本身也有较强的随机波动。你前一天做的计划,到了第二天实际运行时可能完全对不上。所以在模型层面,最常见的处理是先做"确定性日前调度",也就是基于光照、风速、负荷的预测曲线,把明天当成一个"已知"的24小时序列来优化;后续再通过滚动修正、鲁棒优化或者场景随机规划来兜底不确定性。这不是偷懒,而是行业里的通用做法——先解决"计划怎么排"的问题,再去解决"计划怎么跟上实际"的问题。

1.2 各个角色谁花成本、谁省钱

微电网里的"成员"各有各的成本特性,这也是为什么调度不是简单地把所有电源加在一起打个包就行。

光伏和风电的边际成本几乎为零,所以从成本最优的角度看,调度策略一定会"优先消纳"所有可用的风光出力。但风光出力不能随意控制,除非你有弃风弃光的条件,否则它应该作为基荷被全额吸收。这也是为什么经济调度结果里,风光出力曲线一般会贴着预测上限走。

储能是微电网中最灵活的"缓冲带"。它既能当电源(放电),又能当负荷(充电),关键约束在于SOC不能越界、充放电功率有上限、一天之内还要尽量回到初始荷电状态,否则第二天就没法继续用了。储能本质上不是在"发电",而是在"搬电"——把低谷时段便宜的电搬到高峰时段用,赚的是峰谷价差。因此储能参与调度时,会有一个循环寿命损耗成本,一般折算成每次充放电的折旧费,否则模型会倾向于无节制地频繁充放,现实里电池根本扛不住。

需求响应是把"负荷侧"从被动变主动的机制。分时电价下,用户可以在峰时减少用电、把可转移负荷挪到谷时,但这需要给用户补偿,所以需求响应在目标函数里不是免费资源,而是一项有成本的调节手段。它和储能有些功能重叠,但时间尺度不同:储能能应对分钟级到小时级的波动,需求响应更适合小时级以上的负荷转移。

联络线是微电网和主网之间的"边界"。它可以买电也可以卖电,买电价格按分时电价,卖电价格通常设定为固定上网电价或者较低的反向价格,以避免微电网故意在低谷卖高价电套利。很多模型里还会加一个联络线功率上限,防止微电网把电网当成一个无限容量的垃圾桶,想送多少就送多少。

1.3 目标函数和决策变量的一次性梳理

从数学角度归纳,日前经济调度是一个典型的带约束混合整数规划问题。目标函数包括五个部分:向主网购电成本、机组运行维护成本、储能充放电的折旧损耗成本、需求响应补偿成本,再减去售电收益。约束条件包括功率平衡约束、各设备出力上下限、储能SOC连续性约束、需求响应容量上下限、联络线功率约束等连续约束,以及储能充放电互斥、机组启停状态等整数约束。

决策变量分成两类:连续变量(每小时光伏实际出力、风电实际出力、储能充放电功率、联络线交换功率、可转移负荷量等)和整数变量(储能充放电状态0-1变量、机组启停0-1变量)。因为有了整数变量,这个模型是混合整数线性规划,只能靠分支定界法这类算法求解,规模不大时Matlab的intlinprog或者Yalmip配置Gurobi/Cplex都能吃掉。

2. 模型搭建:变量、目标、约束怎么一步步写清楚

2.1 风光出力的简化处理

在实际工程中,光伏和风机不是独立的"免费电源",它们有各自的出力特性约束。光伏出力上限由光照强度和环境温度决定,风速出力则受切入风速、额定风速和切出风速三阶段约束。但在日前调度模型里,通常不会把风速到功率的完整映射公式写进去,因为那是微观选址和功率预测层的事;调度层只需要拿到"明天24小时的风光预测出力曲线",然后把它作为上限约束:实际出力不能超过预测值,如果允许弃风弃光,则实际出力可以小于等于预测值;如果要求全额消纳,则直接令实际出力等于预测值。

在Matlab里这只是一条约束:P_pv(t) <= P_pv_forecast(t)。但我想提醒一个细节:如果你在代码里直接写等式约束P_pv(t) == P_pv_forecast(t),模型其实就退化成纯功率平衡调度了,储能和需求响应的调节空间会变窄。更常见且更实用的写法是允许弃光弃风,给实际出力设一个上限,让优化器自己去判断"要不要全额消纳"。因为从经济角度,风光边际成本为零,优化器大概率会选择全额消纳;但当风光过剩、储能已满、联络线已到上限时,弃风弃光反而是全局最优选择。

2.2 储能的SOC递推关系和充放电互斥

储能建模是整个微电网模型中最容易出错的地方,也是Matlab代码实现时bug的高发区。SOC(荷电状态)的递推公式是这样的:

SOC(t+1) = SOC(t) + (eta_c * P_ch(t) - P_dis(t) / eta_d) * dt / E_max

其中eta_c是充电效率,eta_d是放电效率,E_max是储能额定容量,dt是调度时间间隔(1小时)。这个公式的含义很直观:电池里存的电量,充电时并不是1度电进去就存了1度电,会有充电损耗;放电时为了放出1度电,电池内部实际要消耗比1度电更多的化学能,所以放到SOC变化上要除以放电效率。

充放电互斥约束在代码里通常这样写:

P_ch(t) <= M * u_ch(t); P_dis(t) <= M * u_dis(t); u_ch(t) + u_dis(t) <= 1;

M是一个大数,一般取储能最大充放电功率的几倍即可,不需要特别大,减少大M数值对求解器的影响。u_ch和u_dis就是0-1整数变量,保证同一时刻不能既充电又放电。这里有一个我自己踩过的坑:如果把充放电功率上限都设为同一个值,但充电和放电的最大功率允许不同,那么两条约束的M和上限都要分别设置,别取一个统一大M了事,否则模型会失真。

SOC的边界约束是SOC_min <= SOC(t) <= SOC_max,一般取0.1到0.9之间。很多刚入门的朋友会把SOC_min设成0,在模型里看似可行,但实际电池管理系统的最低荷电状态通常不会低于10%到20%,因为深度放电会严重损伤电池寿命。我个人的习惯是:SOC范围先按0.1到0.9设置,留出安全余量,后期如果想测试储能容量大小对调度成本的影响,再把这个区间放宽重跑几组对比。

还有一个容易被忽略的约束:调度周期末SOC要回到初始值。也就是SOC(24) == SOC(0)。如果你不加这个约束,优化器会把储能里的电全部放光,因为放电能降低购电成本,反正模型只管一天内的成本最小化。这会导致你算出来的成本被低估,第二天根本没法按这个SOC继续跑。不少论文里会加这个约束,他们的说法是"储能日循环约束",保证每天的调度计划具有可持续性。实际工程里如果储能确实能跨天调度,那么末态SOC可以由第二天的预测来决定,但在单日调度模型里,加上"末态等于初态"其实是一种更稳妥的做法。

2.3 需求响应:可转移负荷和可削减负荷怎么建模

需求响应听起来高深,但建模的方式其实很直接。常用的两种机制是可转移负荷和可削减负荷。

可转移负荷是指那些用电时间可以灵活调整、但总用电量固定的负荷。比如洗衣机的洗涤程序、电动汽车充电、工业上的批次生产设备。这类负荷在模型中的处理方式是:每个时刻的可转移负荷功率不应超过它的调度功率上限,同时一天内可转移负荷的总用电量必须等于原始电量。后一条约束是关键,它保证了需求响应只是"搬移用电时间",而不是凭空减少用电需求。

可削减负荷则是指那些可以直接切除一部分用电量的负荷。比如商业楼宇的空调在尖峰时段削减20%的功率,用户会短时间牺牲一点舒适度,微电网给用户相应的补偿。这类负荷在模型里允许一天内总用电量减少,减少量乘以补偿单价就是需求响应成本。和可转移负荷相比,可削减负荷更灵活也更好建模,因为不需要"总量守恒"的约束,只需要设置每个时刻可削减量的上下限。

在Matlab代码实现里,两类需求响应变量都用连续变量表示即可,不一定要设置整数变量。原因是用户的负荷响应通常是大规模聚合后的连续调节量,粒度已经足够细,强行用整数变量反而会让模型求解时间成倍增加。

2.4 功率平衡和联络线的完整约束体系

功率平衡是电力系统最基本的物理定律,在微电网模型里就是:任意时刻,所有发电功率加上购电功率,等于所有负荷用电功率加上充电功率和售电功率。

在Matlab里用一行约束代码即可表达:

P_pv(t) + P_wt(t) + P_dis(t) + P_buy(t) == P_load(t) + P_load_shift(t) + P_ch(t) + P_sell(t) + P_cut(t);

注意这里P_load_shift(t)表示可转移负荷在当前时刻的额外用电量(正值表示在原来基础上多用电),P_cut(t)表示当前时刻削减的负荷功率。这样写的好处是功率平衡中每个变量都带上了正负方向,检查起来一目了然。

联络线约束包含三层含义:一是交换功率的上下限,二是买电和卖电不能同时发生,三是购电电价和售电电价通常是不同值。买电卖电互斥在代码里和储能充放电互斥一样,也是用0-1变量加大M实现。有几个工程上的细节值得注意:分时电价表通常是这样的结构——峰时电价高、谷时电价低、平时段居中;售电价格通常比购电价格低,否则微电网可以靠"低买高卖"套利,这虽然也是最优解,但已经偏离了经济调度的本意,在模型里需要明确限制。

3. 求解路线与Matlab代码架构设计

3.1 为什么首选混合整数线性规划

微电网日前经济调度这个模型,目标函数是线性的,约束也基本都是线性的,只有两类非线性因素:储能充放电效率导致的乘积项,以及机组启停、充放电互斥带来的逻辑判断。前者可以通过分段线性化处理,后者则引入整数变量,整体就是一个混合整数线性规划。

我见过不少初学者一上来就用粒子群算法或者遗传算法去求解,理由是"微电网问题太复杂,需要用智能优化算法找到全局最优"——这是误区。对于MILP模型,粒子群这类启发式算法不仅求解时间不稳定,而且每次结果可能有差异,完全无法保证得到全局最优解。相比之下,商业求解器(Gurobi、Cplex)和开源求解器(SCIP)都内置了分支定界和割平面技术,能稳定给出最优解和最优性间隙。

对于这个规模的问题——24个时段、几十个连续变量、十几个整数变量,Gurobi通常只需要几秒到几十秒就能证明最优性。intlinprog作为Matlab自带的MILP求解器,也可以求解中等规模的问题,但性能和稳定性比Gurobi差不少。我的建议是:优先用Yalmip工具箱统一建模,底层求解器可以灵活切换,先跑intlinprog验证正确性,再切到Gurobi跑大算例。Yalmip最大的优势在于你不需要为了换求解器而改动模型代码,只需要ops = sdpsettings('solver','gurobi')一行就能切换。

3.2 Yalmip工具箱的安装与配置

Yalmip不是求解器,它只是一个建模层,负责把Matlab里的优化表达式翻译成求解器能理解的格式。安装Yalmip很简单:去官网下载压缩包,解压后把整个文件夹加入Matlab路径,然后在命令行输入yalmiptest,它会检测你装了哪些求解器。如果你只装了intlinprog,它检测到的就是内置的Matlab求解器;如果你装了Gurobi,它也会显示Gurobi已就绪。

Gurobi的安装比Yalmip麻烦一些,因为需要申请学术许可或者商业许可。申请学术许可时填学校邮箱和工作单位信息就可以,下载安装包后,把gurobi.m这个文件名对应版本号改好,然后确保Yalmip能找到它。一个常被忽略的问题是:Matlab是64位还是32位,Gurobi也要装对应版本,这个匹配不对的话Yalmip会报"求解器初始化失败"。

如果你不想折腾Gurobi,直接用Matlab自带的intlinprog也是可以的。只不过要把建模语言从Yalmip的optimize改成intlinprog需要的矩阵形式。手动转换成向量和矩阵虽然可行,但非常繁琐,一旦链条长就会有变量对应关系上的错误。所以我更推荐Yalmip,这也是目前主流论文中Matlab实现优化问题最常见的选择。

3.3 代码的整体模块划分

写微电网调度代码,不要把所有内容堆在一个脚本里。我习惯的模块划分是这样的:

data/ # 存储负荷、风光预测、分时电价、储能参数等数据 model/ # 建立目标函数和约束的Yalmip代码 solver/ # 求解设置和求解器调用 result/ # 结果存储和图表输出 main.m # 主程序入口

主程序main.m的流程大致是:加载数据 → 定义决策变量 → 添加目标函数 → 添加约束 → 求解 → 提取结果 → 画图。这样写的最大好处是,你想更换数据文件或者修改某个约束条件时,不需要动主程序逻辑,只改对应模块即可。而且不同约束条件写在独立的函数里,比如constraints_storage.m、constraints_dr.m、constraints_balance.m,每一块的检查边界都能做到很清楚。

实际上我在带新手做项目的时候发现,很多人喜欢把模型分得很碎,结果画图时找不到结果存储在哪个变量里。我的建议是:变量命名尽量采用统一前缀,比如P_pv、P_wt、P_ch、SOC,导出结果时用一个result结构体把所有变量存起来,画图脚本直接从result里面取数据。这样后期做灵敏度分析或者多方案对比时,可以省去大量的调试时间。

3.4 从确定性模型到不确定性扩展的思路

基础的日前调度模型是确定性的,但在实际工程中,光靠确定性模型远远不够,因为预测误差在真实运行中会实实在在地影响系统。比如你预测明天中午光伏出力是100kW,但实际上只有80kW,储能和联络线如果没能及时顶上就可能会切负荷。所以很多项目在确定性模型的基础上,会做两层升级:

第一层是滚动修正。日前调度给出下一小时的出力计划,然后在运行当天,每隔15分钟或1小时根据最新预测重新优化未来几个小时的计划。这叫"日前+日内"双层调度。这种思路实现起来不难,只需要把确定性模型的时段缩短,然后在一个滚动循环里反复调用求解器。

第二层是鲁棒优化或者随机规划。鲁棒优化处理的是"最坏情况",你设定一个不确定性预算,让调度方案在所有可能的风光出力范围内都可行;随机规划则是给出一组可能的风光场景,每个场景带一个概率,用一个两阶段模型同时优化"日前计划"和"实时调整"。这两类方法复杂度比确定性模型高不少,但从科研角度和工程前瞻性角度都很值得做。

4. 关键代码段的逐行解读

4.1 数据初始化的正确姿势

数据初始化看起来简单,但直接影响后续约束建模的正确性。我是这样组织的:

%% 基础参数 T = 24; % 调度总时段数,24小时 dt = 1; % 时间步长,小时 P_load = [...]'; % 24小时负荷预测,kW P_pv_forecast = [...]'; % 24小时光伏预测,kW P_wt_forecast = [...]'; % 24小时风电预测,kW price_buy = [...]'; % 分时购电电价,元/kWh price_sell = [...]; % 售电电价,通常是固定值 %% 储能参数 E_max = 600; % 储能额定容量,kWh SOC_init = 0.5; % 初始荷电状态,0.5表示50% SOC_min = 0.1; SOC_max = 0.9; P_ch_max = 100; % 最大充电功率,kW P_dis_max = 100; % 最大放电功率,kW eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 cost_battery_per_kwh = 0.02; % 储能单位循环损耗成本,元/kWh %% 需求响应参数 P_dr_shift_max = 30; % 每时段可转移负荷最大功率,kW P_dr_cut_max = 20; % 每时段可削减负荷最大功率,kW price_dr_shift = 0.3; % 可转移负荷补偿价格,元/kWh price_dr_cut = 0.5; % 可削减负荷补偿价格,元/kWh

这里的单位统一非常重要。有些模型里用kW、kWh、元,另一些用MW、MWh、万元,如果你混着用,求解器不会报错,但结果的量级会非常奇怪。我见过有人把功率单位写成kW,容量写成MWh,最后优化结果储能SOC看起来正常,但实际上功率平衡约束根本对不上。全文保持统一的量纲体系,这是写这段代码时最高优先级的纪律。

4.2 用Yalmip定义决策变量和目标函数

定义决策变量时,我习惯把连续变量和整数变量分开写,这样每一次调试时定位更快:

P_pv = sdpvar(T, 1); % 光伏实际出力,kW P_wt = sdpvar(T, 1); % 风电实际出力,kW P_ch = sdpvar(T, 1); % 储能充电功率,kW P_dis = sdpvar(T, 1); % 储能放电功率,kW SOC = sdpvar(T+1, 1); % 荷电状态,维度T+1因为包含初始状态 P_buy = sdpvar(T, 1); % 购电功率,kW P_sell = sdpvar(T, 1); % 售电功率,kW P_dr_shift = sdpvar(T, 1); % 可转移负荷的功率变化量,kW P_dr_cut = sdpvar(T, 1); % 可削减负荷量,kW %% 整数变量:储能充放电状态、购售电状态 u_ch = binvar(T, 1); u_dis = binvar(T, 1); u_buy = binvar(T, 1); u_sell = binvar(T, 1);

目标函数写成这样:

objective = 0; for t = 1:T objective = objective + price_buy(t) * P_buy(t) * dt; % 购电成本 objective = objective - price_sell(t) * P_sell(t) * dt; % 售电收益 objective = objective + cost_battery_per_kwh * (P_ch(t) + P_dis(t)) * dt; % 储能折旧成本 objective = objective + price_dr_shift * abs(P_dr_shift(t)) * dt; % 可转移负荷调度成本 objective = objective + price_dr_cut * P_dr_cut(t) * dt; % 可削减负荷补偿成本 end

这里有两个容易踩的坑。第一个是abs(P_dr_shift(t)),因为可转移负荷可以增加用电(把负荷搬过来),也可以减少用电(把负荷搬走),所以功率变化量有正有负。如果你直接把它写进目标函数里,且单位价格是正的,那么优化器会倾向于把可转移负荷变成"减少用电"的白嫖行为——因为减少用电相当于降低了负荷功率,本身就能降低购电成本,同时它还获得补偿。但实际上可转移负荷是双向等概率的事件,你不能只奖励"减少"而不惩罚"增加"。用绝对值函数把它处理成双向调度成本,虽然不完美,但比直接用线性项合理得多。

第二个坑是储能折旧成本项。如果你给它设置得太小,优化器会让储能频繁充放,赚取峰谷价差;但如果设置得太大,储能就基本不参与调度了,系统的灵活性无从体现。我的经验是:先跑一版不带储能的模型,得出基准成本;再跑带储能模型时,把折旧成本从小往大调,观察充放次数和系统总成本的变化。这样可以找到一个合理区间。

4.3 约束条件逐段添加的实操细节

约束添加顺序不直接影响求解结果,但影响你排查问题的效率。我习惯按"设备约束 → 交互约束 → 平衡约束"的顺序添加:

%% 设备约束:储能SOC递推 Constraints = []; for t = 1:T Constraints = [Constraints, SOC(t+1) == SOC(t) + (eta_ch * P_ch(t) - P_dis(t)/eta_dis) * dt / E_max]; end Constraints = [Constraints, SOC(1) == SOC_init]; Constraints = [Constraints, SOC(T+1) == SOC_init]; % 日循环约束 Constraints = [Constraints, SOC_min <= SOC <= SOC_max]; %% 储能充放电功率上限和互斥 for t = 1:T Constraints = [Constraints, 0 <= P_ch(t) <= P_ch_max * u_ch(t)]; Constraints = [Constraints, 0 <= P_dis(t) <= P_dis_max * u_dis(t)]; Constraints = [Constraints, u_ch(t) + u_dis(t) <= 1]; end

这里有个性能优化技巧:如果把0 <= P_ch(t) <= P_ch_max * u_ch(t)改成P_ch(t) <= P_ch_max * u_ch(t),因为你没有给P_ch增加非负约束,理论上优化器可以让P_ch变成负值,从而绕过约束。所以非负约束是不能少的。但这个写法在Yalmip里是允许的,求解器也能正确识别,但如果你切换到某些底层求解器,可能会遇到约束雅可比矩阵奇异的问题。稳妥的写法是拆成两条:

Constraints = [Constraints, P_ch(t) >= 0]; Constraints = [Constraints, P_ch(t) <= P_ch_max * u_ch(t)];

这样写虽然多了几行代码,但兼容性更好,也更方便你在调试时单独注释查看某个约束的效果。

需求响应约束的写法如下:

%% 可转移负荷:全天净转移量之和为零(只搬移,不增减) Constraints = [Constraints, sum(P_dr_shift) == 0]; %% 可转移负荷:每时段调节量上下限 Constraints = [Constraints, -P_dr_shift_max <= P_dr_shift <= P_dr_shift_max]; %% 可削减负荷:每时段削减量上下限 Constraints = [Constraints, 0 <= P_dr_cut <= P_dr_cut_max];

还需要加一条:可转移负荷的实际效果是改变负荷曲线,所以在功率平衡里,负荷侧要加上P_dr_shift和减去P_dr_cut。具体写法是:

for t = 1:T Constraints = [Constraints, P_pv(t) + P_wt(t) + P_dis(t) + P_buy(t) == ... P_load(t) + P_dr_shift(t) - P_dr_cut(t) + P_ch(t) + P_sell(t)]; end

这里如果你把P_dr_cut写成加号,就相当于削减负荷变成了新增负荷,方向就反了,优化结果会非常离谱。我调试时经常干这件事,所以后来习惯在所有变量名里,方向性很强的变量统一带上前缀或者后缀,比如P_load_shift表示负荷偏移量,P_load_reduce表示负荷削减量,避免靠"正负号记忆"来维持正确性。

4.4 功率平衡约束里的小细节

功率平衡约束是模型的核心,但也最容易出现"模型没收敛但也不知道哪里错"的情况。第一个常见错误是SOC维度对不上。SOC变量定义成了T+1长度,功率平衡里使用的是P_ch(t)而不是SOC(t+1)-SOC(t),两者在时间尺度上等价,但一旦写错索引,求解出来SOC曲线会莫名其妙地跳变。

第二个常见错误是忽略P_buy和P_sell的互斥。如果你只在目标函数里区分购电和售电价格,但没有加互斥约束,优化器可能会同时给出正值的P_buy和正值的P_sell,两个源都卖电、同时又买电,这在实际电网中不可能发生。用一个u_buy和u_sell把两个功率互斥掉,或者更简单的方法是把净交换功率定义成一个变量P_exchange,当它为正时表示购电,为负时表示售电,加上分段线性化的价格约束。后者更经济,但代码更复杂。我通常先加互斥约束验证正确性,后续优化时再考虑合并变量。

4.5 求解和结果输出

求解部分的代码不难,但有几个设置值得调优:

ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'showprogress', 1); ops.gurobi.MIPGap = 0.001; % 设置MIP间隙为0.1% ops.gurobi.TimeLimit = 300; % 设置求解时间上限为300秒 sol = optimize(Constraints, objective, ops);

MIPGap这个参数很关键。默认情况下Gurobi会一直算到证明最优性,但对于大规模模型,证明最优性本身可能就需要很长时间。你如果在做参数灵敏度分析,可以放宽MIPGap到1%甚至5%,求解速度能快一个数量级,结果差异通常在工程可接受范围内。时间限制参数能防止个别算例卡死,尤其是当你把整数变量增加之后,分支定界树可能非常大。

求解完成后,提取结果时要格外检查求解状态:

if sol.problem == 0 P_pv_opt = value(P_pv); P_ch_opt = value(P_ch); SOC_opt = value(SOC); else disp('求解失败,请检查约束和参数') return end

sol.problem等于0表示求解成功,1表示不可行,2表示无界,3表示求解器报错。这里我强烈建议你养成"先检查状态再处理结果"的习惯,否则如果模型不可行,你提取出来的value(P_pv)是一堆NaN,画出来的图会让你误以为是数据问题而不是模型问题。

5. 调试与避坑:从不可行解到求解缓慢

5.1 模型不可行时,如何十分钟内定位到问题

模型不可行是调度代码里最常见的故障。我处理这个问题的标准流程是:

先检查基本参数有没有明显矛盾:SOC范围是否合理、储能容量是否小到连一次充电都放不下、负荷预测和风光预测的量级是否匹配等。

再看功率平衡约束。如果可转移负荷的P_dr_shift_max设置得比实际负荷还大,那么"可转移后负荷为负"这件事就可能发生,破坏功率平衡约束的可行性。解决方法是把P_dr_shift_max限制在负荷的一定比例内,比如不超过该时段负荷的20%。

接着检查储能SOC递推的方向。很多人会在P_dis/eta_dis这一项上弄反效率和分母,导致SOC一边充电一边往下掉,模型当然不可行。自检方法很简单:写一个小脚本,固定P_ch=50,P_dis=0,连续推24小时,看SOC是否单调上升;再反过来推,看是否单调下降。如果方向都不对,模型模型必挂。

最后一个狠招是把所有约束都注释掉,只保留目标函数和变量边界,跑一次求初解。然后逐步加约束,每次加一组就求解一次,直到找到哪组约束让模型不可行。这样虽然繁琐,但通常十分钟内就能定位到问题。用过Yalmip的同学可以额外利用sol.info和diagnostics字段打印的更详细报错信息,配合relax模式把整数约束松弛掉,能更直观找到不满足的连续约束。

5.2 求解时间爆炸:整数变量才是最贵的

同一个模型,把所有变量设为连续变量后求解只要0.5秒,加上整数变量后可能要10分钟,这是非常常见的现象。原因在于分支定界法在最坏情况下需要遍历所有整数组合,虽然Gurobi会通过割平面和启发式剪枝大幅减少节点数,但如果模型规模大或者整数变量设置不当,求解时间会指数增长。

优化思路有几个方向。一是减少不必要的整数变量。比如储能充放电互斥,如果你的储能是"全程要么充电要么放电"这种策略性很强的场景,就可以用固定时序代替互斥变量,把24个互斥约束变成12个或者更少;二是添加更多有效约束来收紧可行域。Gurobi一般来说很擅长处理MILP,但如果你的模型里有大量大M约束,求解器的预求解时间会显著增加,尽量把大M设小一点,或者改成指标约束。

三是考虑把MILP改为线性规划来求解一个近似版本。比如储能充放电互斥,如果你允许储能同时充放且净功率为两者之差,其实在物理上也不完全错,因为实际变流器可以做功率抵消的。只是效率上会有差异。如果只是做方案比较和趋势分析,线性松弛版本完全够用。

5.3 Yalmip/Gurobi的版本兼容性问题

版本兼容性这个问题,是很多人在装完工具箱之后第一个碰到的坑。Yalmip对Gurobi版本的支持有滞后性,往往新版本的Gurobi出来后,旧版Yalmip无法识别。如果你用gerbo求解器去解优化问题时报错"Unable to initialize Gurobi"或者"gurobi: command not found",优先检查版本匹配题。一个简单的规避方案是装LTS版本的Gurobi,而不是最新版本,因为Yalmip对LTS版本的支持通常更好。

如果实在不想碰Gurobi,推荐试试开源方案。Matlab自带的intlinprog足够验证小型模型,更大规模可以用Cbc或SCIP这些求解器,Yalmip也都能对接。我在一个模拟项目X中对比过,同样模型规模下Gurobi比intlinprog快大约3到5倍,但intlinprog最终也能在几分钟内收敛,对于教学和验证完全够用。

5.4 单位、量纲和数值尺度问题

数值尺度问题很少被初学者重视,但它在求解器中非常关键。一般优化求解器内部都有预处理过程,但如果你的目标函数里有一项是10的6次方量级,另一项是10的负2次方量级,数值上会给求解器带来极大麻烦。比如购电成本可能是几千元量级,储能折旧可能是几块钱量级,需求响应补偿可能是几十块钱量级。这些量级差异很大的项出现在同一优化问题里,求解器在计算对偶间隙和收敛判据时容易出现误判。

解决办法有三种:统一使用合适单位,比如功率用MW而不是kW,费用用万元而不是元,这样数值能控制在1到1000的范围内;或者对目标函数每一项做一个归一化权重,但这种归一化会改变优化问题的经济意义,不推荐;再或者调整求解器容差参数,比如把Gurobi的FeasibilityTol从默认值改成1e-6,也能缓解一部分数值问题。

我在实际调试时发现,调整量纲后效果是最明显的。原来某个算例求解时间要5分钟,把kW换成MW后,求解时间降到约30秒,而且最优性间隙收敛得更平滑。这背后没有复杂的理论,就是纯数值问题。

6. 从基础模型到进阶扩展的路径

6.1 考虑风光不确定性的两阶段随机规划

前面的基础模型是确定性模型,适合做入门和基准测试。但如果你要做科研或者工程应用,风光不确定性是绕不开的坎。两阶段随机规划是比较自然的扩展:第一阶段是在知道预测值但不知道真实值的情况下,确定日前调度计划(比如机组启停、储能充放电计划、可转移负荷的时间安排),第二阶段是在给定真实风光出力场景后,以最小的调整成本去修正计划(比如调整储能功率、购电功率、削减负荷量)。

在Matlab里实现两阶段模型时,我习惯用cell数组存储每个场景的决策变量:

P_dis_scene = cell(N_scene, 1); for s = 1:N_scene P_dis_scene{s} = sdpvar(T, 1); end

第一阶段变量(整数变量和可转移负荷的日前计划)对所有场景共享,第二阶段变量则随场景变化。目标函数是第一阶段的成本加上每个场景概率加权的第二阶段成本。两阶段模型比单阶段模型大N倍,但对N取几十个场景时,Gurobi依然能在可接受时间内求解。

6.2 鲁棒优化的工程意义

鲁棒优化更适合对"最坏情况"有严格要求的场景。假设光伏出力在一定区间内波动,目标是最小化最坏情况下的调度成本,同时保证每个场景下系统约束都成立。它不会给你一个期望成本最低的方案,而是给你一个"无论明天实际天气如何,系统都能安全运行"的方案。

实现上有一种比较流行的"盒式+预算"不确定性集合:风光出力的每个时段波动范围给定,整个调度周期内的总波动量受限。这种集合的好处是参数少、直观、不容易过于保守。鲁棒优化在Matlab里很难手写,但Yalmip内置了鲁棒优化的部分支持,你可以直接把不确定性变量声明为uncertain类型,然后定义它们的范围,其余约束语法不变。遇到不方便直接支持的场景,可以采用对抗性场景枚举法,用一组最恶劣场景代替连续不确定性集合来逼近鲁棒解,实现简单且效果不错。

6.3 多微电网互联和碳排放约束的扩展思路

单微电网模型写通之后,往多微电网方向扩展的核心是把功率平衡约束改成带耦合项的公式:每个微电网内部的平衡依然成立,但微电网之间增加交换功率变量,并在全网层面增加交换功率互斥和容量约束。在Matlab里,用三维索引的sdpvar变量来存储多微电网的交换功率是比较直观的做法。

碳排放约束近年越来越受关注。标准做法是在目标函数中加入碳成本项,给火电和购电分别赋予碳排放因子,碳排放总量不能超过配额,如果超额就需要购买碳排放权。这样模型目标函数就从纯经济成本扩展成"经济+环境"的双目标,但求解方式没有本质变化。如果想认真研究碳约束的影响,我建议先跑一遍无碳约束基准模型,然后把碳配额从大往小扫一遍,记录总成本和碳排放量的变化关系,画成曲线比单点结论直观得多。

6.4 代码复用的工程经验

很多人写完一套调度代码,下一套模型又从头开始写。这是个常见问题。我建议从一开始就按照"参数结构体 + 约束函数 + 求解封装"的方式组织代码,把参数统一放进结构体:

params.T = 24; params.E_max = 600; params.P_ch_max = 100; params.price_buy = [...];

约束函数只接受params和决策变量,返回Constraints。这样一个函数跑到另一个项目时,只需要改params,不用改模型主体。我自己的代码库里,storage_constraints.m、balance_constraints.m、dr_constraints.m这三个函数被多个项目反复复用,稳定性和可维护性都远高于每次重新写一遍的方式。

7. 调度结果的分析与可视化

7.1 结果表格应该展示哪些关键指标

调度跑完之后,不能只看一张曲线图就完事,要形成系统性的指标表。我习惯输出这样一张表:

指标数值说明
日运行总成本元目标函数值,最终判定调度的好坏
购电成本元从主网购电的总支出
售电收益元向主网售电的总收入
储能折旧成本元储能损耗折算出的总成本
需求响应补偿成本元两个DR项的总补偿费用
峰时段最大购电功率kW考核联络线容量是否够用
弃风弃光总量kWh风光削减的总电量
SOC最低值-储能运行最极限的点

这张表有两个地方值得关注:一是购电成本和售电收益的差,代表了微电网和主网打交道时的净支出;二是弃风弃光总量,如果这个数值大,意味着你的储能容量或者联络线容量可能不够,或者储能的功率上限限制了充入的速度。如果你优化出来的SOC在全时段几乎都是贴着0.1在跑,说明你的储能折旧成本可能设置得太高了,它只想用最低电量,根本不想充电。这时候把折旧成本降一个数量级,再对比一次结果,你会看到SOC明显变得更加活跃。

7.2 图的布局:功率平衡图和SOC曲线

可视化方面,我推荐最少画两张图:一张是24小时的功率平衡堆叠图,一张是SOC和购售电功率的双轴曲线图。

功率平衡堆叠图用bar或者area展示光伏出力、风电出力、储能放电、购电功率的堆叠,叠起来的高度正好等于负荷加储能充电。如果面积图上有明显的"凹陷"或"溢出",说明功率平衡约束被破坏,这通常会暴露你在约束里写反了符号的问题。

SOC图用stairs画阶梯状曲线更符合时段离散的物理意义,别用连续平滑的plot,因为SOC是按小时递推的,楼散会让读者误以为SOC连续变化。配合右侧Y轴画购电功率曲线,可以很直观地看到储能"谷充峰放"的行为:购电功率低谷时段SOC上升,购电功率高峰时段SOC下降。如果SOC的谷值和峰值出现在错误的时间段,你就要回过头去检查分时电价表的时段划分有没有设置反。

7.3 对比实验:怎么证明需求响应真正降低了成本

写论文、做项目汇报,"需求响应有没有用"是最常被问到的问题。最好的回答方式是给出对比实验数据:用同一套系统和同一份负荷、风光数据,分别跑三组配置——无储能无需求响应、仅有储能、储能加需求响应,然后对比总成本。

我按这个方法做过一次模拟项目X,结果非常典型:无储能无DR时总成本最高,纯靠固定联络线功率追随负荷,峰时不得不高价购电;加入储能后,借助峰谷价差降低了一部分成本;再加入需求响应后,成本进一步下降,原因不仅是负荷被搬移到谷时,还因为需求响应让储能能够在更合适的时段运行。这里我建议大家做分析时,不仅关注总成本数值,也关注需求响应的"净收益"——DR的总补偿成本是多少,DR帮助系统省了多少钱,差额才是DR的真实贡献。有些时候需求响应看似降低了总成本,但补偿成本本身很高,真实收益为负。这在工程上也是很重要的发现。

8. 进阶调参和灵敏度分析的实战经验

8.1 储能容量和成本的关系怎么评估

做微电网规划时,储能容量往往是一个有待决策的量。你可以在调度模型外面套一层容量扫描:从100 kWh到1000 kWh,每次增加50 kWh,分别运行一遍调度模型,记录总成本和总弃风弃光量。把结果画成曲线后,你会看到一个典型的边际收益递减过程:储能从100加到300 kWh时成本下降非常明显,从500加到800 kWh时曲线趋于平坦。

这种曲线对投资决策很有参考价值——选择"拐点"附近的容量往往性价比最高。我见过很多项目直接按"最大负荷的20%"来配储能,这个粗略公式在部分场景下并不合理,还是应该根据当地峰谷价差和负荷特性做灵敏度扫描再拍板。

8.2 分时电价对调度行为的影响

分时电价是微电网外部最重要的经济信号。我做过一组测试:改变峰谷价差大小,从2倍到5倍,观察储能利用率的变化。价差小时,储能几乎没有充放行为,因为折旧成本比套利收益还高;价差拉大到3倍以后,储能开始"谷充峰放";到了5倍,SOC曲线几乎天天走满整个运行区间。

这个测试还有一个重要的实用角度:如果当地实际峰谷价差不大,那么投资储能的经济性就存疑,用需求响应或者直接调峰可能是更划算的选项。调度模型在这里不只是"排计划",它还可以帮决策者回答"配置多少储能、配什么类型的响应资源"这类规划问题。

8.3 联络线功率上限的影响

联络线功率上限直接决定了微电网在高峰时段能"依赖主网到什么程度"。上限设得很小时,微电网基本要自给自足,建模结果里会看到更多的储能放电和需求响应调用;上限设得很大时,微电网倾向于直接从主网购电,因为主网电价在谷时段非常便宜。

我在调试时发现,如果把联络线功率上限设置得太低,模型有时候会得到"弃风"这样看起来很不合理的解——原因很简单:电力送不出去,储能容量不够,负荷也消纳不了那么多,只能弃掉。这种结果其实不是模型错了,而是系统物理能力的真实体现。在汇报时可以把这条曲线加进灵敏度分析,直观说明微电网对外部电网的依赖程度。

8.4 需求响应补偿价格的边界怎么找

需求响应补偿价格直接影响DR参与度。如果把补偿单价设得非常高,模型会把所有可转移负荷都搬到谷时段,下达所有可削减负荷都削减掉。这样虽然总成本表格好看,但用户舒适度大打折扣,实际上未必可行。

好的做法是设置一个"DR预算上限",比如全天的DR补偿总额不能超过某个值,或者给DR设置参与率上限。也可以用多目标分析来看成本-舒适度之间的权衡:从补偿单价0.1元到1.0元之间扫描,你会看到DR调用量的S形增长曲线。价格低时用户不愿响应,价格高时响应饱和,中间过渡带的斜率就是最佳补偿定价区间。这条经验在实际的微电网负荷聚合商谈判中非常有用——你给的补偿单价不用最高,在S形曲线拐点附近就已经能获得大部分DR收益。

9. 最后的几点实操心得

我在多个微电网项目里用过这套模型——包括模拟项目X,仿真数据和实测数据都验证过。个人最大的体会是:模型本身的数学形式并不复杂,真正难的是把你的"物理直觉"转写成可求解的约束和合理的目标函数。很多初学者拒绝了需求响应,是因为搞不定abs函数、搞不定"SOC日循环约束";很多工程师放弃了可转移负荷,是因为不知道它的"总量守恒"到底约束在哪一行。这些细节一旦打通,整个调度模型就活了。

上面这些技巧和踩坑记录,是我在一次次的"模型不可行"和"求解不收敛"中折腾出来的。如果你们在自己的代码里遇到一些更特殊的问题——比如储能限功率充电、需求响应带有最小连续运行时间约束、联络线功率有爬坡限制等等,这些扩展在Yalmip里其实也都有成熟写法,主要思路就是加新的约束和时间耦合条件。微电网经济调度的核心逻辑一通,后面做多微电网、多能互补、碳交易约束的扩展都只是工作量问题,不是思维问题。

另外建议你动手敲代码时别贪多,把最基础的一天24小时模型完整跑通、调试到结果合理,再考虑加场景、加不确定性、加碳成本。这个领域没有捷径,但每一步走得稳,后边的路就能跑得快。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进