ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YALMIP+CPLEX的微电网优化调度建模与求解全攻略

基于YALMIP+CPLEX的微电网优化调度建模与求解全攻略 做微电网优化调度的人应该对“基于yalmipcplex的微网优化调度”这句话不陌生。说白了就是用MATLAB里的YALMIP工具箱写优化模型然后交给CPLEX求解器去算。我最早接触这套组合是做日前经济调度当时要在普通笔记本上求解24时段、带柴油机启停、储能充放电、并网购售电的混合整数线性规划问题手写求解器不现实只用MATLAB自带函数又不够灵活最后老老实实落到yalmipcplex上——建模快、求解快、调试起来也直观。这套程序解决的核心问题一句话说清楚在已知次日负荷、光伏出力、风电出力的前提下决定柴油机怎么启停、储能怎么充放、联络线什么时段买电什么时段卖电让一天的总运行成本最低。它适合正在做微电网、综合能源、储能优化调度方向的研究生和工程师也适合拿来做毕业设计、课程作业或科研论文的算例底座。即使是刚接触优化的新手只要看明白这个程序的建模框架后面迁移到鲁棒优化、多微网协同、综合能源系统这些方向都会顺利很多。下面我把这套程序从环境搭建、核心建模到求解排坑完整拆开讲尽量把我踩过的坑和验证过有效的做法都写进去。1. 微网优化调度到底在解什么问题1.1 调度问题的本质目标、约束与决策变量理解这套程序之前得先理解优化调度本身。微网优化调度说白了就是一个数学规划问题目标函数通常是“总运行成本最小”约束条件包括功率平衡、设备出力上下限、储能荷电状态范围、爬坡速率、联络线功率限制等决策变量则是各时段各设备的出力计划。拿生活里的例子类比你每天做饭的“调度问题”是在预算钱和备菜时间时间限制下决定买什么菜、几点做饭、几道菜用什么锅让在家里吃最划算、最省事。微网调度也一样只不过决策对象从买菜换成了柴油机出力、储能充放电功率、联络线购售电功率。这类问题在数学上有几个难点。第一是时间耦合储能当前时段的充放电状态会影响到后续时段的荷电状态所以不能只看单时段要把整条时间轴上的变量一起优化。第二是整数变量柴油机的启停状态是0/1变量储能充放电互斥也需要0/1变量这就把问题带入了混合整数线性规划MILP的范畴。第三是数据规模普通微网算例24时段、几十个变量不算大但一旦扩展到8760小时年度运行模拟或者数百节点系统手写求解流程就很痛苦了。yalmipcplex这套组合正好卡在这个痛点上yalmip让你用数学表达式的语言建模把繁琐的矩阵组装和求解器API包装掉CPLEX负责在底层用拉格朗日松弛、分支定界、启发式算法把MILP解出来。用户只需要关心“模型对不对”不需要关心“怎么把模型喂给求解器”。1.2 为什么偏偏是yalmipcplex先说CPLEX。它能成为电力系统调度领域的事实标准靠的不是花哨是稳定和快。对于微网调度这种中等规模的MILPCPLEX的预求解阶段就能消掉一大半冗余约束再加多线程并行和启发式初始解求解时间通常只要几秒到几十秒。相比之下MATLAB自带的intlinprog虽然也能解MILP但面对复杂的调度问题经常慢一个数量级数值稳定性也差一些。再说YALMIP。第一次用YALMIP建模的人会有种“原来优化建模可以这样写”的感觉。比如你要表示功率平衡约束直接在MATLAB里写Constraints [Constraints, sum(P_dg) P_pv P_dis P_load P_ch];完全和数学公式一一对应。调试的时候可以打印约束列表可以随时切换求解器可以把非线性约束交给内置的线性化工具处理。这种表达自由度是手写求解器接口无法比的。不过要提醒一句yalmip不是求解器它只是一个建模层。真正干活的是CPLEX、Gurobi、CBC这些底层求解器。所以“yalmipcplex”的完整链路是MATLAB里用YALMIP语法写模型YALMIP解析模型后转换成CPLEX能读的格式CPLEX求解完再通过YALMIP把结果映射回MATLAB变量。链路看起来绕但稳定性很高而且切换求解器比如把CPLEX换成Gurobi只需要改一行设置这是这套组合很实用的优势。1.3 这套程序能干什么、适合谁从功能上看这套程序可以做以下事情日前经济调度以24小时为周期滚动优化第二天各设备出力计划这是最基础的用法。机组组合与启停优化考虑柴油机开机的固定成本和启停次数限制避免频繁启停造成损耗。储能优化调度在峰谷电价下自动选择充电和放电时段实现低买高卖套利同时维持SOC在合理区间。并网与孤岛运行模式切换通过联络线功率约束模拟并网运行去掉联络线就是孤岛运行。风光储联合优化在光伏和风电出力不确定的情况下安排柴油机和储能作为灵活性资源填补缺口。它适合的研究对象很广可以是单一微网也可以扩到多微网、工业园综合能源系统。适合的人包括研究微电网调度方向的硕士博士用算例验证算法的工程师以及刚开始学优化建模、想把课本上线性规划内容落到MATLAB代码上的同学。2. 环境搭建MATLAB、YALMIP、CPLEX的安装与配置2.1 版本匹配是第一个坑很多人拿到程序第一步栽在环境上而且栽得很冤程序逻辑完全没问题就是求解器找不到或者模型读不进去。根本原因通常是MATLAB版本和CPLEX版本不兼容。CPLEX装完后对外提供的最关键东西之一是MATLAB接口的mex文件而这个mex文件是和具体MATLAB版本强相关的。官方说明里会写支持哪些MATLAB版本比如CPLEX 12.9支持R2017a到R2019b而CPLEX 12.10支持到R2021a。如果你用的MATLAB太新或者CPLEX版本太老mex文件加载失败YALMIP调CPLEX时就只能报“could not find solver”或者“Unable to load mex file”。我自己遇到过最典型的情况MATLAB R2022b配CPLEX 12.9结果CPLEX的mex文件完全加载不了最后只能换一个支持新版的CPLEX版本才解决。所以我的建议是装环境前先上官方兼容性表确认匹配版本不同版本能试出来就试出来这比后面排查一两个小时有意义得多。提示如果只做线性规划和混合整数规划CPLEX Community Edition社区版免费版通常够用但它会限制问题规模比如变量数或约束数上限。论文算例规模一般不大社区版能撑住如果做大系统或真实电网数据就要考虑完整授权版或学校采购的学术版。2.2 YALMIP安装与CPLEX路径配置YALMIP的安装很简单它本质上是纯MATLAB代码不需要mex编译。从官方GitHub仓库或者官网下载压缩包解压后把文件夹加入MATLAB路径在MATLAB主界面选“设置路径”点“添加并包含子文件夹”把YALMIP主目录加进去保存即可。CPLEX的安装稍微麻烦一点。安装好之后在CPLEX安装目录下有一个文件夹专门用于MATLAB接口路径通常是...\cplex\matlab\。要把这个路径也加入MATLAB路径YALMIP才能通过它去调用CPLEX。有些版本还要求在MATLAB里运行cplex.setup(install)或手动添加mex文件的路径视版本而定。我习惯在启动脚本里统一配置比如在startup.m里写addpath(genpath(D:/Programs/yalmip)); addpath(C:/Program Files/IBM/ILOG/CPLEX_Studio_Community2210/cplex/matlab);genpath会把子目录一起加入省得漏掉。配置完记得重启MATLAB或手动执行一次startup。2.3 求解器连通性验证环境配好之后最关键的一步是验证YALMIP能不能正确识别CPLEX。在MATLAB命令窗口输入yalmiptest这个命令会检查当前系统里所有可用的求解器、测试各求解器是否连通。看到CPLEX的状态是found并且测试问题能正常解出来就说明链路通了。如果yalmiptest显示CPLEX不可用按这个顺序排查MATLAB路径里有没有加CPLEX的matlab文件夹CPLEX版本与MATLAB版本是否匹配运行check命令看YALMIP认为当前默认求解器是谁必要时运行yalmip(clear)清空缓存再重试。另外可以用一个极小的测试问题快速验证x sdpvar(1,1); optimize([x 0, x 1], -x, sdpsettings(solver,cplex)); value(x)如果输出接近1说明CPLEX已经可以正常求解了。这步验证很值因为很多临时出现的“程序跑不动”其实是环境问题不是算法问题。2.4 常用求解器参数模板当你能确认CPLEX连通后建议把求解参数封装成统一模板。大多数情况下微网调度用默认参数已经很快但有几个参数值得经常调options sdpsettings(solver,cplex, ... verbose,1, ... cplex.mip.tolerances.mipgap,1e-4, ... cplex.mip.tolerances.integrality,1e-5, ... cplex.threads,4, ... cplex.output.writelevel,1);这里面mipgap控制最优性间隙设置太紧比如1e-9会让求解时间暴涨而实际收益微不足道设置太松比如1e-1则可能给不出足够精确的结果。做调度分析建议取1e-4到1e-6之间。threads控制并行核数个人电脑设4到8核都能提升速度Licence限制单线程的情况另说。要注意YALMIP里给CPLEX参数的前缀是cplex.mip.tolerances.mipgap这种结构不是直接写MIPGap。写错参数名YALMIP不会立刻报错而是忽略它等于白设。这一点我踩过坑后面做参数敏感性分析时才发现某些参数根本没生效。3. 主程序建模目标函数、决策变量与约束的完整设计与实现3.1 算例设定与数据准备一个标准的微网算例通常包含以下设备光伏PV出力与光照有关调度中作为不可控电源直接给定曲线。风电WT同光伏给定预测出力曲线。柴油机DE可控电源有最小出力、最大出力、爬坡率限制和启停成本。储能ESS可充可放有容量上限、功率上限、充放电效率和SOC边界。负荷Load用户侧负荷曲线。联络线Grid能买电能卖电购电电价和售电电价通常不同。数据准备阶段最简单的做法是用Excel或MATLAB脚本生成所有曲线统一单位。比如负荷功率用kW储能容量用kWh电价用元/kWh。我建议所有功率相关量都折算到同一个量纲最好用kW和kWh时间步长用1小时。一个24时段算例的数据结构可以参考%% 基础数据 N 24; % 时段数 P_load [30 28 26 25 25 27 35 45 55 60 62 60 55 50 52 55 58 65 68 70 65 55 42 35]; % 负荷 P_pv [0 0 0 0 0 1 5 12 18 22 24 25 23 20 16 10 3 0 0 0 0 0 0 0]; % 光伏 P_wt [12 14 15 13 10 9 8 8 9 10 12 13 14 15 14 12 10 8 7 6 8 10 12 12]; % 风电 price_buy [0.5 0.5 0.5 0.5 0.5 0.6 0.8 1.0 1.2 1.2 1.2 1.0 0.9 0.8 0.8 0.8 0.9 1.1 1.2 1.2 1.0 0.8 0.6 0.5]; price_sell price_buy * 0.7;对于走读研究的人可能更习惯从Excel读取但无论哪种方式重要的是数据内部口径一致尤其是单位否则会把问题引入数值病态。3.2 决策变量定义与规模化写法在YALMIP里定义决策变量非常直观。柴油机出力是连续变量启停状态是二进制变量储能充放电功率可以定义成两个非负连续变量。P_dg sdpvar(N,1); % 柴油机出力 On binvar(N,1); % 柴油机启停状态1表示开启 P_ch sdpvar(N,1); % 储能充电功率 P_dis sdpvar(N,1); % 储能放电功率 SOC sdpvar(N,1); % 储能荷电状态 P_buy sdpvar(N,1); % 购电功率 P_sellsdpvar(N,1); % 售电功率这里有几个细节值得说。第一P_dis和P_ch分开定义比用一个带符号的变量舒服因为目标函数里购售电成本和充放电收益的边际价值不同分开后在约束和成本项里写起来都更清晰。第二SOC变量的范围需要按标幺值或者百分比定义。比如用0~1表示初始SOC是0.2上限0.9下限0.1这样目标函数和约束里写起来都很直接。第三随着模型变大建议不要用单个标量变量循环写24次而是直接用向量sdpvar(N,1)。YALMIP会把这个向量看成一个整体约束和求解时效率更高代码也干净。还有一个容易忽略的细节如果你需要储能“既不能同时充也不能同时放”那最好引入一个二进制变量u_ch用于互斥但如果目标函数本身不会出现“一边充电一边放电还能套利”的情况靠电价差异其实就能自然避免加互斥变量主要是为了严谨。后面约朇部分我会详细写怎么处理。3.3 目标函数的构建逻辑微网调度的目标函数一般写成“总运行成本最小”包括下面几块柴油机燃料成本通常是二次函数形式比如a*P_dg^2 b*P_dg c但在MILP里二次项不好处理工程上常用分段线性化代替。简单算例可以直接用线性成本系数k * P_dg把燃料成本近似成与出力成正比。柴油机启停成本每次开机有启动成本每次关机有停机成本这是整数变量驱动的固定成本。假设启动成本是C_startup停机成本是C_shutdown那么可以用非负数变量表示但更常见的做法是直接在目标函数里写C_startup * On(t)因为只要开机就会触发。购电成本与售电收益分别用购电功率乘购电价、售电功率乘售电价注意购电是支出售电是收益符号不同。储能运维成本通常按充放电电量乘以一个极小系数比如0.01元/kWh用来给储能动作添加一点经济惩罚防止优化结果出现频繁无意义的充放电操作。目标函数用YALMIP写出来大概是Objective sum(k_dg .* P_dg) C_startup * sum(On) ... sum(price_buy .* P_buy) - sum(price_sell .* P_sell) ... c_ess * sum(P_ch P_dis);如果柴油机成本是非线性的YALMIP本身支持二次画面但混合整数二次规划MIQP的求解难度比MILP高很多。我的建议是除非必要把二次项通过分段线性化处理成线性约束。下面第5节会专门讲到这个。3.4 约束条件的核心细节与线性化处理约束是调度的灵魂模型是否可靠全看约束写得全不全。下面逐个讲。功率平衡约束功率平衡指的是任意时刻发电与负荷相等但储能和联络线两侧都要考虑清楚。表达式为Constraints [Constraints, P_dg P_pv P_wt P_dis P_buy P_load P_ch P_sell];这是整个模型的基础。这里要注意如果P_ch和P_dis被定义成两个非负变量但没有任何约束限制它们互斥那么理论上一台所谓“永动机”储能可以一边充电一边放电两边功率相同功率平衡照样满足但对物理无意义。所以需要额外加互斥约束。储能约束储能约束包括SOC递推、SOC上下限、充放电功率上限和互斥条件。SOC递推公式写成Constraints [Constraints, SOC(1) SOC_0 (eta_ch * P_ch(1) - P_dis(1)/eta_dis) / Cap_ess]; for t 2:N Constraints [Constraints, SOC(t) SOC(t-1) (eta_ch * P_ch(t) - P_dis(t)/eta_dis) / Cap_ess]; end这里eta_ch是充电效率eta_dis是放电效率Cap_ess是储能容量。注意充电和放电效率不要都写成同一个值真实场景里充放电损耗并不对称。SOC上下限和充放电功率上下限Constraints [Constraints, SOC_min SOC SOC_max]; Constraints [Constraints, 0 P_ch P_ch_max]; Constraints [Constraints, 0 P_dis P_dis_max];如果不加互斥储能能同时充放为了杜绝这种情况引入二进制变量并加约束u_ess binvar(N,1); % 1表示充电 Constraints [Constraints, P_ch P_ch_max * u_ess]; Constraints [Constraints, P_dis P_dis_max * (1 - u_ess)];这两条约束合起来的效果是当u_ess1时充电功率可以非零放电功率被强制为0当u_ess0时放电功率可以非零充电功率被强制为0。这属于典型的“大M法”简化形式不需要一个很大的M因为功率上限本身就是天然的M。柴油机约束柴油机有最小/最大出力约束、爬坡约束、启停逻辑约束。最小/最大出力Constraints [Constraints, P_dg_min * On P_dg P_dg_max * On];当On1时出力被限制在上下限范围内当On0时出力强制为0。这个写法非常经典也是一种隐式大M约束。爬坡约束需要注意方向既要限制同一时段内功率上升也要限制功率下降Constraints [Constraints, -R_down P_dg(2:end) - P_dg(1:end-1) R_up];这里R_up和R_down是单位时段内的爬坡速率。如果时间步长是1小时那么R_up的值就是“1小时内最多增加多少出力”要和柴油机技术参数的量纲匹配。启动与停机逻辑有时还会加“最小运行时间”约束防止柴油机频繁启停。比如某台柴油机一旦启动必须至少运行2小时可以写成for t 2:N-1 Constraints [Constraints, On(t) - On(t-1) On(t1)]; end这条约束表达了“上一时段停机、本时段开机则下一时段必须开机”概念上符合最小运行时间为2小时的要求。这种逻辑约束在论文里很常见实际工程中也能显著减少启停次数优先加上。联络线约束联络线购售电功率的上下限Constraints [Constraints, 0 P_buy P_line_max]; Constraints [Constraints, 0 P_sell P_line_max];同样理论上购电和售电不能同时进行否则就有了一个“零成本挪移功率”的漏洞。除了加二进制互斥变量另一种做法是让售电价低于购电价这样优化器自动不会“低卖高买”。但为了模型严谨建议也加互斥u_grid binvar(N,1); Constraints [Constraints, P_buy P_line_max * u_grid]; Constraints [Constraints, P_sell P_line_max * (1 - u_grid)];如果联网电价结构复杂也可以不加互斥而是用“购电价恒大于售电价”的设计来天然避免这样还能省下若干个整数变量。两种做法我都试过前者模型更精确但求解稍慢后者模型更轻量但需要靠数据保证合理性。3.5 求解与结果提取模型建好后求解几乎是固定动作options sdpsettings(solver,cplex,verbose,1); result optimize(Constraints, Objective, options);求解之后一定要检查result.problem它相当于求解状态码。0表示成功解出1表示求解器认为问题不可行2表示数值问题3表示问题无界4表示求解类型不受支持。不要直接跳过状态检查就拿变量值去画图我遇到过一种情况YALMIP返回problem0但部分变量在最优解里出现微小偏离物理边界的值这时候要用abs(abs(value(x)) - 边界) 1e-6这类容差来过滤。解出来后所有变量的值都通过value()获取P_dg_opt value(P_dg); SOC_opt value(SOC); P_ch_opt value(P_ch); P_dis_opt value(P_dis); P_buy_opt value(P_buy); P_sell_opt value(P_sell);值得提醒的是如果模型里用了二进制变量value返回的0/1值在数值上可能是0.0001或0.9999这种浮点结果画图前最好先四舍五入一下否则图例上会显示出难看的0.7-0.9毛刺。3.6 典型结果与验证方法求解完成后最怕的是“结果看起来能画图但物理上一塌糊涂”。所以我的习惯是先做三个验证再画图。第一是平衡校验把每个时段的值分别代入功率平衡公式确认左右两边误差在10^-6量级。如果误差很大说明要么约束写错了要么求解精度设置不当。第二是SOC连续性校验从初始SOC开始用最优的充放电功率递推SOC和value(SOC)对比确认递推公式和结果一致。这个验证常常能查出约束里Cap_ess单位写错的问题。第三是目标函数重算把最优解代入目标函数手工核对每一项成本和YALMIP报告的result.problem 0且目标值不矛盾。如果重算的目标值差很多检查是不是抽了错误的变量或者单位统一出了问题。画图方面我通常画两张图。第一张是24小时功率曲线堆叠图横轴是时段纵轴是功率负荷在上光伏、风电、柴油机、储能放电、购电一层层叠起来能直观看出“谁在何时顶负荷”。第二张是SOC曲线配合理想的低买高卖时段能直观看出储能是否起到了削峰填谷的作用。画图用plot加bar或者area都可以关键是让每条曲线的颜色清晰可辨。4. 完整代码骨架从数据到出图的模块化实现4.1 程序结构划分真正能反复使用的调度程序不建议一个大脚本从数据到绘图全写完而是按功能拆成几个模块。下面是一个我常用的目录结构microgrid_dispatch/ ├── data/ │ └── case_data.xlsx ├── function/ │ ├── load_data.m │ ├── build_variables.m │ ├── build_constraints.m │ └── plot_results.m └── main_dispatch.mmain_dispatch.m负责串联整个流程读取数据、定义变量、写目标函数和约束、调用求解器、提取结果并绘图。function文件夹里的函数各自只做一件事方便后续替换数据或约束。对初学者来说这种模块化可能显得“多此一举”但如果后面要改储能参数、加碳约束、换预测数据你就会感受到收益改一个函数就够了不用翻上千行脚本找变量名。4.2 关键模块代码解读main_dispatch.m的核心骨架大概是%% 主程序入口 clear; clc; yalmip(clear); % 1. 读取数据 [P_load, P_pv, P_wt, price_buy, price_sell, params] load_data(data/case_data.xlsx); % 2. 创建变量 [P_dg, On, P_ch, P_dis, SOC, P_buy, P_sell] build_variables(params.N); % 3. 模型约束 Constraints build_constraints(P_dg, On, P_ch, P_dis, SOC, P_buy, P_sell, P_load, P_pv, P_wt, params); % 4. 目标函数 Objective sum(params.k_dg * P_dg) params.C_startup * sum(On) ... sum(price_buy .* P_buy) - sum(price_sell .* P_sell) ... params.c_ess * sum(P_ch P_dis); % 5. 求解与结果输出 options sdpsettings(solver,cplex,verbose,1,cplex.mip.tolerances.mipgap,1e-4); result optimize(Constraints, Objective, options); if result.problem 0 plot_results(P_dg, On, P_ch, P_dis, SOC, P_buy, P_sell, P_load, P_pv, P_wt); else disp([求解失败, 状态码: , num2str(result.problem)]); end这里有个容易被忽略的点yalmip(clear)在每次运行前清空YALMIP缓存防止上次运行留下的变量声明和这次冲突。尤其在调试过程中改了变量维度后不执行它经常会报“Variable already exists”之类的莫名错误。build_constraints函数里按约束类型分段加入约束方便阅读function Constraints build_constraints(...) Constraints []; % 功率平衡 Constraints [Constraints, P_dg P_pv P_wt P_dis P_buy P_load P_ch P_sell]; % 储能约束 Constraints [Constraints, SOC_min SOC SOC_max]; ... end这是YALMIP的惯用写法每一次约束拼接都重新赋值给Constraints相当于动态构造一个约束集合。用[]拼接同样适用于多个约束。4.3 结果导出与可视化结果出图后我一般还会把关键结果导出成表格方便写论文或者做对比。最简单的做法是writetableT table((1:24), P_load, P_pv, P_wt, P_dg_opt, P_ch_opt, P_dis_opt, P_buy_opt, P_sell_opt, SOC_opt); writetable(T, results/dispatch_results.xlsx);如果是做多场景对比我会把不同算例的目标函数值、储能最终SOC、购电成本、售电收益等指标汇总成一行记录最后合成一个总表。这样写论文时做“结果对比”章节非常方便。画图注意一点堆叠功率曲线和SOC曲线用不同子图别挤在一个图里。否则24个时段的功率堆叠图本身信息量就很大再加一条SOC曲线图会显得乱。我的经验是subplot(2,1,1)画功率subplot(2,1,2)画SOC。5. 实战踩坑与排查技巧实录5.1 不可行问题infeasible怎么查这是用这套程序最高频的故障。模型不可行说明“你写的约束里至少有两条在打架”但YALMIP和CPLEX通常不会直接告诉你哪两条打架只会返回problem1。我的排查手段按顺序是逐段注释约束。把一个约束块注释掉后如果模型变得可行那问题大概率出在这一块。虽然粗暴但很有效。对储能约束做“可行性校验”。很多时候不可行都是SOC约束太紧比如初始SOC太低但又要求SOC最低值很高或者充放电效率同时给得很苛刻导致总能量不够。把SOC下限放宽到0再看结果就清楚了。对功率平衡做异常的定量检查。比如把所有可再生出力加起来小于负荷同时购电上限又卡得很低那当然不可行。可以先放宽联络线功率上限试跑一次确认问题出在边界条件还是约束逻辑。检查爬坡约束的时序边界。爬坡约束第一段P_dg(2:end) - P_dg(1:end-1)没问题但如果配上启停逻辑可能发生“停机状态到开机状态瞬间变化超出爬坡限制”的冲突。这时候需要额外保证只有On从0变1时才允许功率阶跃不然就加入专门的启动状态约束。可以给关键等式约束加一个很小的松弛变量比如1e-4如果加上松弛变可行说明某个等式约束写死了再去看是哪条。提示不要一上来就怀疑求解器。CPLEX返回不可行时YALMIP可以调用model export(Constraints, Objective, options)导出模型再用CPLEX的冲突识别工具找出最小不可行约束集。这是进阶操作但对复杂模型非常有用学一次能省很多调试时间。5.2 求解慢、内存爆怎么破微网调度模型不算大但如果加入“8760小时年度仿真”或者“多微网并网”整数变量会迅速膨胀求解时间从几秒变成几小时也不是稀奇事。碰到这种情况先从以下几个方向下手。第一减少整数变量数量。储能互斥和购售电互斥各引入一个二进制变量如果算例里电价结构合理完全可以只保留储能互斥购售电互斥靠“售电价低于购电价”来自动避免。柴油机启动状态如果允许简化也可以考虑直接用分段线性化代替完整的启停逻辑。第二设置合理的MIP容忍度。默认的MIPGap可能是1e-4对于年度仿真想快速得到次优解可以放宽到1e-2或5e-3最终成本差异往往只有百分之零点几但求解时间能降一个数量级。第三给CPLEX一个初始解。YALMIP里通过assign给变量赋初始值然后设置sdpsettings(cplex.mip.strategy.mipstart,1)求解器会从你给的解开始搜索大幅缩短上升阶段。特别适合做“连续多个场景滚动调度”时用上一时段的最优解作为下一时段的初始解。第四如果模型是线性但变量很多可以检查是否存在“冗余约束”。比如大量时段间同性的约束CPLEX的预求解阶段会删掉一部分但如果预求解还是不够快可以考虑把单个模型拆成多个子问题再串起来求解不过这属于高级优化了不推荐新手一开始就尝试。5.3 非线性项的线性化处理与数值稳定性微网调度里最常见的非线性是柴油机成本和出力呈二次关系。YALMIP可以直接写二次项然后交给CPLEX求解MIQP但这么做的求解速度和数值稳定性都不如MILP。我强烈建议对成本曲线做分段线性化。分段线性化的思路是把出力范围分成若干段每段内成本率恒定再引入分段权重变量。YALMIP自带了piecewise或pw函数但个人实践下来手写分段约束更可控。假设分三段每段上限P_seg_max成本系数k_seg那么成本C_fuel可以写成P_seg sdpvar(3,N); % 各段出力 s_seg binvar(3,N); % 各段是否启用 Constraints [Constraints, sum(P_seg) P_dg]; Constraints [Constraints, 0 P_seg(1,:) P_seg_max(1)]; for k 2:3 Constraints [Constraints, P_seg_max(k-1)*s_seg(k,:) P_seg(k,:) P_seg_max(k)*s_seg(k,:)]; end C_fuel sum(k_seg(:) .* P_seg, 1);这里用二进制变量s_seg(k,:)强制后段只有在更便宜的段满了后才能启用避免优化器“跳跃式”启用高价段。这种写法非常常用也适合扩展到碳排放分段惩罚等场景。数值稳定性方面一个反复出现的坑是单位导致的数量级失衡。比如功率用W、容量用MWh那么储能递推约束里会出现10^6量级的系数CPLEX的求解容差是相对量遇到系数差异太大时会给出脱离物理意义的解。我的做法是全部统一为kW和kWh或者用标幺值比如所有功率除以基准功率100kW保证系数在10^-3到10^3之间。5.4 常见报错速查表把我在实际使用中遇到的报错和解决办法整理成一张表方便排查报错现象常见原因解决方法Could not find solverCPLEX路径没添加或版本不匹配检查MATLAB路径查看yalmiptest换匹配版本Unable to load mex fileCPLEX与MATLAB版本不兼容确认兼容性表换对应CPLEX版本problem1不可行约束互相矛盾分段注释约束放宽SOC下限检查爬坡与启停关系problem2数值问题系数数量级差异大统一单位检查是否存在极小数或极大数变量重复定义上次运行未清理开头加yalmip(clear)求解很慢整数变量多MIPGap太紧减少整数变量放宽MIPGap提供初始解结果SOC不连续递推公式或单位错误用初始SOC手动递推验证检查Cap_ess单位CPLEX参数未生效参数名拼写或前缀格式不对用cplex.xxx.xxx格式运行时观察参数是否打印这张表不敢说覆盖所有情况但列出的都是我真实踩过并验证过解法的坑给初入这个方向的人应该能省不少时间。6. 扩展方向与一些个人体会6.1 从确定性到不确定性鲁棒/随机调度如果程序已经能稳定运行下一步值得考虑的扩展方向是处理风光出力不确定性。确定性调度把光伏和风电当成已知曲线实际上预测误差不可避免。常见扩展有两种一是鲁棒优化把风光出力放在一个不确定集里求最坏情况下的最省钱方案二是随机规划用大量场景表示可能的出力目标是期望成本最小。yalmip对这两类都支持得很好。鲁棒优化里可以用uncertain等工具构造不确定集随机规划里直接把每个场景当成一组约束和目标再加一台求解器选出来的“非预期约束”做关联。很多论文的模型核心就是在这个确定性程序上做扩展所以先把基础模型完全跑通非常重要。我自己的经验是先做确定性调度并彻底理解每一条约束再去动不确定性否则很容易把模型改得“运行得起来但分析不清”。磨刀不误砍柴工。6.2 多能互补、碳约束、需求响应扩展基础微网程序的框架能容纳的扩展方向相当多。比如把单一电能扩展到“电-气-热”多能流增加燃气锅炉、热电联产、电转气设备或者增加碳排放约束让目标函数里除了运行成本还有碳交易成本再或者加入需求响应资源允许负荷在峰时削减或平移这会涉及新的0-1变量和时间耦合约束。做这些扩展时建议保持“目标函数只加成本项、约束只加物理限制”的思路。模型变复杂是必然的但调试方法不变逐段验证、先用小规模算例、确认单个新增模块不会把模型搞到不可行再叠加。6.3 一点点实战体会最后说点我自己的体会。用yalmipcplex做微网调度真正难的地方其实不在语法也不在求解器配置而在“怎么把物理世界的问题表述成数学约束”。很多新手卡在“模型能跑但结果和现象对不上”几乎都是因为约束少了某一条物理逻辑比如没考虑弃风弃光、没考虑储能SOC末时刻约束、没考虑最小运行时间。我给自己的项目定的一个小规矩是每加一个新约束就单独跑一个最小算例验证它的效果。比如想验证储能互斥约束就只保留一台储能和一个负荷看它是否真的不会同时充放。这种“最小验证”的习惯比等到整套模型跑完再Debug高效得多。也希望看这篇博客的你能从这套程序里不仅拿走代码还能带走这种调试思路。
返回列表