
1. 模型背后的问题多微网为什么要做电能互补和需求响应先说个我自己的直观感受。近两年做微网优化方向的课题和项目越来越多但很多人上来就套算法、调参数结果模型跑出来数字漂亮放到实际场景里根本不成立。原因很简单你优化的对象没想清楚。这个“考虑多微网电能互补与需求响应的微网双层优化模型”标题看起来长其实拆开就是三件事——多微网之间怎么互相借电、用户负荷怎么配合调度、上下层决策怎么协调。这三个问题不解决单微网优化的天花板非常明显。单微网运行的最大痛点在于“看天吃饭”。光伏大发时用不完弃光率居高不下晚高峰负荷拉满储能和燃气机组拼了老命也顶不上去。如果一个区域里同时存在好几个微网有的光伏富余、有的负荷紧张为什么不能让它们直接互济这就是多微网电能互补的核心动机把几个微网的灵活性资源打包成一个集群在内部先消化不平衡功率实在消化不了再跟配电网交互。我做过的算例里带互补机制的多微网集群整体弃光率能下降10到15个百分点这个数据在不同场景下有波动但趋势是稳定的。需求响应则是另一只抓手。单纯靠供给侧调节储能充放、机组启停都有物理极限而且成本高。需求响应的思路是让负荷侧“动起来”价格型需求响应通过分时电价引导用户把部分可转移负荷挪到光伏大发时段激励型需求响应则是在尖峰时刻直接削减一部分可中断负荷。这样既降低微网的峰值购电压力又给新能源消纳腾出空间。一个有需求响应参与的微网系统削峰填谷效果通常比纯供给侧调度好得多尤其是在夏季空调负荷和冬季采暖负荷集中的时段。那双层优化为什么绕不开因为多微网系统里天然存在两类决策者上层是配电网运行方或微网聚合商关心的是整体运行经济性和对电网的友好度下层是各微网的能量管理系统关心的是自己内部的购售电成本最小化。上下层目标不一致决策变量互相嵌套单层优化根本无法准确描述这种博弈关系。所以双层模型不是炫技是这类分层决策场景的必然选择。本文要分享的就是这套模型的数学建模思路、Matlab代码实现框架以及我在实际调试中踩过的坑。这篇文章适合谁看正在做微网优化调度方向研究的硕士博士做园区级综合能源系统设计的工程师以及准备用Yalmip工具箱搭优化模型的Matlab使用者。读完你会得到一套可以改参数直接跑的双层优化代码骨架更重要的是你能理解每一行约束在真实系统中对应什么物理意义。2. 双层优化的数学建模目标、变量与约束的层层拆解2.1 上层模型协调者的视角把上层决策者想成“园区微网群的总调度”。ta手里握着两类核心变量一是微网群与配电网之间的交换功率二是给各微网下达的内部交易电价信号。为什么要控制内部交易电价因为电价是激励下层微网调整自身行为的杠杆。比如某一时段光伏富余上层就把内部购电价压低让缺电微网更愿意从光伏富余微网购电而不是从配网购高价电。上层目标函数一般是系统总运行成本最小化表达式大致是% 上层目标总成本 向配网购电成本 - 向配网售电收益 机组运行成本 储能折旧成本 F_upper sum(price_buy .* P_grid_buy) - sum(price_sell .* P_grid_sell) ... sum(C_MT .* P_MT) sum(C_ES .* (abs(P_ch) abs(P_dis)));约束条件首先要满足功率平衡这是所有电力量化模型的基本盘。然后是交换功率上下限不能超过联络线容量。还有一个容易被忽略的约束——多微网内部交易电价的上下限设定。内部电价太低微网亏本卖电不干太高缺电微网宁可找配电网买。这个区间的合理设置直接决定双层模型能不能收敛到有意义的解我通常把内部购电价设在配网购电价的0.7到1.2倍之间效果比较理想。2.2 下层模型各微网的独立优化每个微网是一个独立的利益主体目标是自身运行成本最小化。在下层模型里上一层的内部交易电价是已知参数微网根据这个信号优化自己的行为决定向配网购电多少、跟邻居微网买多少、储能充放策略、燃气机组出力、需求响应负荷调整量。下层目标函数可以写成各微网独立的优化问题% 每个微网独立求解 F_sub sum(price_internal .* P_trade) sum(price_buy .* P_grid_buy) - sum(price_sell .* P_grid_sell) ... sum(C_gas .* P_gas) sum(C_es .* (P_ch P_dis)) ... sum(C_dr_incentive .* P_dr);注意这里的price_internal是上层传下来的参数不是本层决策变量。这是双层模型和单层模型最本质的区别下层变量在上层问题中不是直接控制而是通过下层优化问题的解间接影响上层目标。下层约束条件需要覆盖微网内部的功率平衡、储能SOC递推方程与充放电上下限、燃气机组爬坡约束与出力上下限、需求响应可转移负荷量和可中断负荷量的上下限、以及跟配网和邻居微网的交换功率上下限。每个约束都对应实际的物理设备能力少了哪一条求解结果都可能在现实中无法执行。2.3 双层耦合为什么不能逐层独立求解新接触双层模型的人最容易犯的错误先优化上层把结果代入下层再优化下层然后把下层结果返回上层——来回迭代几次就以为收敛了。这个做法在数学上不严谨。真正的原因是上下层之间存在一个“领导者-跟随者”的Stackelberg博弈关系下层的反应函数不是简单的显式表达式而是另一个优化问题的解。上层做决策时必须预判下层会怎么响应。处理这种耦合关系工程上最常用的方法是KKT条件转换把下层问题用其最优性条件替代然后作为约束加入到上层问题中。这样双层问题就转化成了一个带互补约束的单层数学规划问题MPEC再用大M法把非线性互补约束线性化最终变成MILP或MIQP交给商业求解器处理。线性化之后会出现大M参数选择的问题。大M取太大数值稳定性差求解慢取太小可能把可行域切掉得到错误解。我调试时的一般做法根据约束中变量的实际量级估算M值上限再放宽1.5倍留出余量。比如功率交换上限是1MW大M取2到5之间就够了没必要取10的6次方。3. Matlab实现的关键技术选型Yalmip与求解器的搭配3.1 为什么用Yalmip而不是手写求解器双层优化模型线性化之后是一个规模不小的混合整数线性规划。手写单纯形法或者内点法学术研究阶段完全不现实。Yalmip作为Matlab下的建模工具箱最大优势在于可以用接近数学公式的语言描述优化问题然后在后端无缝切换Cplex、Gurobi、GLPK等求解器。它跟手写约束的差别就像写高级语言和写汇编语言的差别调试效率完全不在一个量级。安装方面其实没什么好说的下载Yalmip源码包把matlab路径加进去运行yalmiptest看到输出OK就行。但有几个坑值得提醒老版本Yalmip在新版Matlab上偶尔会出现求解器识别不到的情况我遇到过两次都是因为Cplex的mex文件和新版Matlab不兼容解决方法是重新编译mex文件或者用Gurobi替换。3.2 场景数据与参数初始化代码跑之前要先准备好输入数据这是整个模型的地基。典型数据包括24小时的预测光伏出力曲线标幺值乘以装机容量即可、24小时的负荷预测曲线、分时电价、燃气机组如微型燃气轮机的技术参数、储能电池的容量与充放电效率、联络线功率上限、需求响应资源潜力等。场景生成我有几个建议。光伏和负荷数据尽量用真实历史数据打底再叠加白噪声扰动生成多个场景如果没有实测数据用典型日的公开数据也能凑合但要记得在代码注释里标注数据来源和假设条件不然模型跑出来的结果没法复现。需求响应参数更需要手动标定比如可转移负荷比例我一般取总负荷的5%到15%太高了在工程上不现实。3.3 程序结构设计模块化是调试的前提我见过太多人把整个优化模型写在一个巨型脚本里变量、约束、求解代码全混在一起出了问题根本不知道从哪查起。我的建议是拆成几个职责清晰的模块project/ ├── main.m % 主程序流程控制 ├── data_define.m % 所有参数和场景数据的初始化 ├── upper_model.m % 上层变量定义与约束构建 ├── lower_model.m % 下层变量定义与约束构建含KKT ├── linearization.m % 互补约束的大M线性化 ├── solve_model.m % 组装模型并调用求解器 └── plot_results.m % 结果可视化和输出模块化最大的收益是想改需求响应参数只用动data_define.m想换求解器只影响solve_model.m想debug模型在upper_model和lower_model之间来回查递推关系就能定位问题。我在二三十个子项目里验证过这个结构是目前稳定性最高的组织方式。4. 完整实现流程从参数设置到多场景求解4.1 基础参数定义与场景生成一起把代码骨架过一遍这是整个工程最费工夫的部分。下面是一段数据的示例格式用来定义微网数量、设备参数和负荷曲线%% 基础参数定义 num_mg 3; % 微网数量 T 24; % 调度时段数小时 dt 1; % 时间步长小时 % 各微网光伏装机容量kW pv_capacity [800, 600, 1000]; % 各微网负荷峰值kW load_peak [900, 700, 1100]; % 储能参数容量kWh最大充放电功率kW初始SOC es_capacity [400, 300, 500]; es_pmax [100, 80, 120]; es_soc_init [0.5, 0.5, 0.5]; es_eff 0.95; % 充放电效率 % 机组参数 gas_pmax [200, 150, 250]; gas_eta [0.35, 0.35, 0.35]; % 发电效率 % 分时电价元/kWh24个时段 price_grid_buy [0.48*ones(1,8), 0.95*ones(1,4), 1.2*ones(1,4), ... 0.95*ones(1,4), 1.2*ones(1,2), 0.48*ones(1,2)]; price_grid_sell 0.35*ones(1,T); % 上网电价 % 需求响应参数 dr_ratio 0.10; % 可转移负荷比例 dr_incentive 0.15; % 激励补偿单价元/kWh这些参数看起来很平淡但它们决定了模型的行为边界。比如储能效率0.95看起来只是一个小数实际影响很大每次充放电损耗5%的能量一天两充两放累积下来就是20%的循环损耗这个损耗在优化中会跟峰谷价差博弈价差不到储能损耗成本时求解器不会让储能动作这是很合理的决策。场景生成的思路是在光伏和负荷预测值上叠加扰动生成多组场景我常用拉丁超立方采样生成20个场景并进行等概率分配num_scenario 20; pv_scen zeros(num_mg, T, num_scenario); for k 1:num_scenario for i 1:num_mg pv_base pv_capacity(i) * load_pv_profile(i,:); % 基准曲线 pv_scen(i,:,k) max(0, pv_base .* (1 0.1*randn(1,T))); end end场景越多模型对不确定性的刻画越准但求解规模成倍增长。20个场景是我测试下来精度与速度的折中选择。每个场景对应一组互补约束线性化变量Solver需要处理的变量数量是单场景乘场景数所以你没限制场景数量之前先要把求解器的性能瓶颈搞清楚。4.2 上层与下层变量的Yalmip定义Yalmip定义变量的核心思路是函数内部不能直接访问工作区已有结构体所以所有传入参数必须通过函数参数列表传入。我一般把数据打包进一个struct用变量名d引用。function [Constraints, Objective, P_grid_buy, P_grid_sell] upper_model(d) % 上层决策变量 P_grid_buy sdpvar(d.num_mg, d.T, full); % 各微网向配网购电功率 P_grid_sell sdpvar(d.num_mg, d.T, full); % 各微网向配网售电功率 P_internal sdpvar(d.num_mg, d.T, full); % 内部交易电价信号送给下层 Constraints []; % 交换功率上下限 Constraints [Constraints, 0 P_grid_buy repmat(d.Pgrid_max, 1, d.T)]; Constraints [Constraints, 0 P_grid_sell repmat(d.Pgrid_max, 1, d.T)]; % 同一时段不能同时买售电 Constraints [Constraints, P_grid_buy P_grid_sell repmat(d.Pgrid_max, 1, d.T)]; % 内部电价限幅 Constraints [Constraints, 0.6*repmat(d.price_grid_buy, d.num_mg, 1) P_internal ... 1.2*repmat(d.price_grid_buy, d.num_mg, 1)]; % 上层目标总购售电成本 运行成本 Objective sum(sum(d.price_grid_buy .* P_grid_buy)) ... - sum(sum(d.price_grid_sell .* P_grid_sell)) ... sum(sum(d.C_gas .* sdpvar(d.num_mg, d.T, full))); % 简化示意实际需传入机组出力变量 end这里要注意sdpvar定义维度。多微网多时段的决策变量用矩阵定义最方便行对应微网编号列对应时段后续所有约束都基于这个矩阵维度展开出问题排查也快。下层模型的结构类似但目标函数里的电价是传入参数而非本层变量约束里需要把储能SOC的状态递推方程表达清楚function [Constraints, Objective, P_gas, P_ch, P_dis, P_dr] lower_model(d, price_internal) % 下层决策变量某个微网内部 P_gas sdpvar(1, d.T, full); P_ch sdpvar(1, d.T, full); P_dis sdpvar(1, d.T, full); P_dr sdpvar(1, d.T, full); % 需求响应功率调整量 SOC sdpvar(1, d.T, full); Constraints []; % 储能SOC递推 Constraints [Constraints, SOC(1) d.es_soc_init (P_ch(1)*d.es_eff - P_dis(1)/d.es_eff)*dt/d.es_capacity]; for t 2:d.T Constraints [Constraints, SOC(t) SOC(t-1) (P_ch(t)*d.es_eff - P_dis(t)/d.es_eff)*dt/d.es_capacity]; end Constraints [Constraints, 0.2 SOC 0.9]; Constraints [Constraints, 0 P_ch d.es_pmax, 0 P_dis d.es_pmax]; % 需求响应变量上下限 Constraints [Constraints, -d.dr_ratio*d.load_profile(1) P_dr d.dr_ratio*d.load_profile(1)]; end4.3 KKT条件转化与大M线性化下层问题是一个线性规划可以用KKT条件完全等价替代。核心要处理的是互补松弛条件原始约束的对偶变量乘以约束的松弛量等于0这一项是非线性的需要用大M法切开。单条不等式约束的KKT条件线性化可以写成以下模板原始约束Ax b对偶变量λ 0互补条件λ * (b - Ax) 0引入二进制变量z拆成两条不等式% λ M * z % b - Ax M * (1 - z)在Matlab代码里这个过程大概是lambda sdpvar(size(A,1), 1); z binvar(size(A,1), 1); M 1e3; % 大M需根据实际变量量级调整 Constraints [Constraints, lambda 0, A*x b]; Constraints [Constraints, lambda M*z]; Constraints [Constraints, b - A*x M*(1-z)];这里的大M取值是最容易翻车的地方。M取太大会让线性规划松弛问题病态Cplex求解时迭代步数暴涨M取太小会砍掉可行域导致下层问题找不到最优解。一个实用经验先不加互补条件求解一次原始LP记录下约束最大松弛量和对偶变量最大值把M设成这个量级的5到10倍。4.4 求解器调用与结果输出组装完成后统一求解核心逻辑是ops sdpsettings(solver, cplex, verbose, 2, ... showprogress, 1, ... cplex.mip.tolerances.mipgap, 0.001); sol optimize(Constraints, Objective, ops); if sol.problem 0 disp(求解成功); elseif sol.problem 1 disp(求解器返回不可行请检查约束); else disp([求解失败: , sol.info]); endMIP gap设为0.001已经足够工程精度。需要提醒的是很多微网模型问题是可以用严格混合整数线性规划精确求解的不需要启发式。如果碰到大规模场景Gurobi通常比Cplex更快尤其是互补线性化变量多的模型Gurobi的presolve步骤做得更激进对冗余变量剪得干净。结果输出阶段我习惯把功率曲线、SOC曲线、电价曲线画在一起一眼就能看出调度逻辑是否合理。如果光伏大发时段储能没有充电、电价高峰时段储能在放电那大概率是SOC初始值设错或者效率参数不合理。5. 实际调试中的常见问题与排查经验5.1 模型不可行一看二拆三缩模型报不可行是最常见的。我的排查顺序是先看求解器输出的冲突约束列表定位是哪组约束在作怪再拆开检查把上层和下层模型分别单独求解看哪一边先出现问题最后缩小范围把时段从24小时缩到4小时比如只看下午4点到8点变量规模小了之后问题会更容易暴露。很多时候不可行的原因极度朴素比如联络线功率上限是500kW而某微网负荷峰值是900kW在没有本地储能的时段功率平衡永远满足不了。这种问题根本不需要修改算法把联络线容量调大或给微网配储能装机就够了。5.2 求解时间过长怎么调双层模型线性化之后变量数量膨胀得很快尤其是场景数多的时候。如果模型跑了几分钟还不出结果我一般按顺序做三件事第一检查大M值过大是求解慢的头号元凶第二关闭不需要的输出日志verbose改成1第三给求解器设置合理的MIP gap上限和迭代时间上限让它在达到工程精度时及时收住。更激进的做法是采用场景缩减技术。20个场景里有些场景概率很低或最坏情况几乎相同可以用K-means聚类选出代表性的5到8个场景模型规模大幅缩小精度损失往往在可接受范围内。5.3 双层迭代策略的实际替代方案严格求解KKT转化后的MPEC问题数学上干净但处理上层存在整数变量时计算负担很大。于是很多人退而求其次用迭代法先给内部电价一个初值求解下层问题拿到微网响应再根据响应更新电价循环往复直到收敛。这种做法我实测过对收敛条件极其敏感。电价更新步长取大了振荡取小了爬行缓慢。我的实测体会是如果你不是做算法理论研究的优先使用KKT严格转换方案如果模型规模实在太大再用启发式迭代并且至少加上阻尼因子平滑迭代轨迹或者用连续几次目标函数变化小于阈值的收敛判据别只看两次循环之间的差异。5.4 结果不合理时的检查清单我还总结了一份结果合理性的排查清单每次跑完模型都会过一遍储能SOC曲线是否有跳变、是否始终在安全区间内SOC剧烈跳变说明充放电功率约束或效率递推写错了。各微网的购电价格高的时段购电量是否真的更低如果电价敏感度不明显说明需求响应参数设置不恰当。P_grid_buy和P_grid_sell是否同时为正值如果存在同时购售电的情况说明约束条件漏掉了互补性限制。多微网之间是否出现了无意义的环流功率这种异常的功率流动通常代表内部电价信号与购电成本逻辑不匹配。每次跑完模型打印出各时段各微网的功率流向表用表格横向对比购售电量和设备出力问题很容易就在表格里露馅。5.5 从单微网到多微网的扩展心得最后分享一个扩展上的体会。很多人的基础模型是单微网优化在此基础上扩展到多微网时最容易忽略的是微网之间的功率交互约束。每个微网同时扮演买方和卖方的角色P2P交易功率在购电微网是正收益在售电微网是负成本变量符号统一不出错非常关键。我习惯统一流量方向定义从微网A流向微网B的功率定义为正值约束中A的售电变量等于B的购电变量这样功率守恒约束就只是简单的等式连接。不要在A和B之间用两个方向相反的独立变量否则求解器很容易在两者之间来回震荡导致收敛困难。这是我从几次失败经历里总结出来的最实用的一条经验。