简介:基于双层优化的微电网系统规划设计方法MATLAB程序包,面向电力系统、新能源与微电网方向的研究生、工程师及科研人员,重点解决微电网中分布式电源、储能等设备的容量配置与运行调度协同优化问题。包体共6个文件,含5个m脚本和1个xlsx数据表,整体仅38KB,结构紧凑。m脚本分别承担主程序入口、双层优化上下层模型求解、参数初始化与数据处理等功能,xlsx文件提供四个典型日的负荷与新能源出力数据,便于直接运行和测试算例。目前已有165人学习下载,适合需要快速搭建微电网规划仿真实验的读者。通过该资源可系统理解双层优化的模型构建、上下层迭代求解流程与MATLAB实现技巧,并能根据自身场景修改参数与数据,复现微电网规划方案,为论文或工程应用提供基础。 做微电网规划时,我经常看到两种极端:一种只算容量配置,不考虑日内的启停和爬坡,方案落地后运行费用对不上;另一种把8760小时逐时调度全部塞进优化模型,结果刚迭代两步就发现内存溢出。双层优化恰好在这中间找到了一个平衡点——上层决定光伏、储能、柴油机的安装容量,下层在给定容量下跑经济调度,再把运行成本反馈回上层。这套MATLAB程序正是按这个思路拆分的:shuang_main.m负责主循环,UP_1.m、UP_2.m处理上层规划,DOWN_.m解决下层调度,配套canshu.m参数文件和四个典型日数据.xlsx。适合做园区微电网可研、储能容量配置,或者用典型日代替全年数据做工程估算的读者。
2. 双层优化模型拆解:UP_1、UP_2、DOWN_ 与 shuang_main 的分工
2.1 上层规划层:UP_1.m 与 UP_2.m 到底在算谁
双层优化的标准形式是:上层最小化总费用,下层在给定上层决策后最小化运行费用。用数学语言描述是:
min C_inv(X) + C_ope(X) s.t. g(X) <= 0其中C_ope(X)的值来自下层问题:
min C_ope = sum_t ( C_fuel + C_grid + C_om ) s.t. 功率平衡、DG出力、储能SOC约束在微电网容量规划里,X是光伏安装功率、储能容量、柴油机台数这类“装机后不再改变”的决策变量;下层的是每个时段的柴油机出力、储能充放电、购售电功率。如果把这些全部压进单层模型,会遇到混合整数非线性规划,求解器极容易陷入局部最优甚至无解。所以工程上把问题拆成两层,上层只负责搜索配置方案,下层专注求解最短时间尺度的经济调度。
2.1.1 上层决策变量与目标函数
上层决策变量通常可以写成向量X = [N_pv, N_wt, N_bat, P_pv_r, P_wt_r, E_bat_r],含义分别是光伏台数、风电台数、储能组数、光伏额定功率、风电额定功率、储能额定容量。目标函数一般使用等年值费用:
C_total = C_inv(X) + C_ope(X) % 单位:元/年C_inv把一次性投资按项目寿命和折现率折算成等年值,包括设备更换成本;C_ope是下层返回的年运行成本。UP_1.m 我习惯理解为“上层目标函数计算入口”:它接收一个候选容量 X,调用下层调度函数得到运行费用,再叠加投资年值。这里特别注意,C_ope不能直接用拟合公式近似,否则双层优化就退化成普通的单层容量搜索,失去调度约束的意义。
2.1.2 UP_2.m 在迭代里的角色
UP_2.m 在不同作者的程序里作用略有差异。有的版本把 UP_2.m 写成种群更新函数,负责粒子群的位置和速度更新;有的写成上层约束罚函数,检查可靠性、自给率、SOC初值等约束。我一般会这样设计 UP_2.m:
function [new_pop, new_vel] = UP_2(pop, vel, pbest, gbest, param) % 输入:当前种群、速度、个体最优、全局最优 % 输出:更新后的种群和速度 [n, dim] = size(pop); w = param.w; c1 = param.c1; c2 = param.c2; new_vel = w * vel + c1 * rand(n,dim) .* (pbest - pop) ... + c2 * rand(n,dim) .* (gbest - pop); new_pop = pop + new_vel; % 对越界容量做边界吸收或随机重置 new_pop(new_pop < param.lb) = param.lb(new_pop < param.lb); new_pop(new_pop > param.ub) = param.ub(new_pop > param.ub); end这个文件的核心是“生成新的候选配置”,方便在主循环里循环调用 UP_1.m 计算适应度。把种群操作单独放在 UP_2.m 里,能避免主程序变得臃肿,也方便把粒子群换成遗传算法或差分进化。
2.2 下层运行层:DOWN_.m 的经济调度逻辑
DOWN_.m 解决的是“给定这套设备,未来24小时怎么开机组、怎么充放电最便宜”。典型的目标函数是:
min sum_t [ c_fuel * P_dg(t) + c_buy * P_buy(t) - c_sell * P_sell(t) ]约束条件包括功率平衡、柴油机出力上下限、爬坡率、储能SOC递推式和容量约束。如果柴油机的开停机状态也要优化,就得引入二进制变量,用intlinprog;如果只做连续经济调度,直接用linprog就够了。我一般会把 DOWN_.m 写成独立函数:
function [cost, result] = DOWN_(param, X, load_data, pv_data) % param : 系统参数结构体 % X : 上层传下来的容量配置 % load_data : 典型日负荷曲线,24x1 % pv_data : 典型日光伏出力曲线,24x1 % cost : 该典型日运行成本 % result : 各设备出力、SOC、弃光量等,用于校验约束这个函数的输出很有讲究:第一个返回值cost是为了给上层用,其余出力序列留给后处理画图。不要把成本计算和调度约束写死在主程序里,否则换一组典型日数据就得改代码。我在实际项目中还会让 DOWN_.m 额外输出一个result.is_feasible标志,当下层无解时,这个标志比任何罚函数都直观。
2.3 shuang_main.m 的主循环与数据流
shuang_main.m是唯一需要用户直接运行的脚本。它先执行canshu.m加载全部参数,再读取四个典型日数据,然后进入上层迭代。数据流方向是:
canshu.m -> 典型日数据.xlsx -> UP_2.m 生成候选容量 -> UP_1.m 调用 DOWN_.m 算调度 -> 将成本返回上层在实际代码里,UP_1.m 内部会读取DOWN_.m返回的result,把弃光率、失负荷率、储能SOC越限等硬约束转换成惩罚项,叠加到适应度里:
function fitness = UP_1(X, param, data) ... year_cost = 0; for s = 1:4 % 四个典型日 [cost, result] = DOWN_(param, X, data(s).load, data(s).pv); year_cost = year_cost + data(s).days * cost; end C_inv = sum(X .* param.cost_vec) * param.crf; fitness = C_inv + year_cost + param.penalty * (violation > 0); end这段代码里,data(s).days代表典型日对应的实际天数;param.crf是资金回收系数。violation是下层返回的约束越限量,如果不做这个合法性检查,上层粒子群很容易朝着“根本不满足运行约束”的方向收敛。
3. 参数体系与典型日数据:canshu.m 和“四个典型日数据.xlsx”怎么配合
3.1 canshu.m 的参数分类
没有一套好用的参数管理,双层优化程序会变成一团乱麻。canshu.m里的参数我习惯分成以下四类:
| 类别 | 变量名示例 | 说明 |
|---|---|---|
| 负荷参数 | P_load_peak,load_scale | 典型日负荷峰值与缩放系数 |
| 发电设备参数 | pv_eff,dg_min,dg_max,dg_ramp | 光伏效率、柴油机出力上下限与爬坡率 |
| 储能参数 | bat_energy,soc_min,soc_max,eta_ch,eta_dis | 储能容量、SOC允许范围、充放电效率 |
| 经济参数 | pv_cost,bat_cost,dg_fuel_cost,grid_buy_price | 单位造价、燃料费、购电价 |
典型的一段参数设置长这样:
% canshu.m 片段 param.pv_cost = 4500; % 光伏单位装机成本,元/kW param.bat_cost = 1500; % 储能单位容量成本,元/kWh param.dg_fuel_cost = 1.2; % 柴油发电燃料成本,元/kWh param.grid_buy_price = 0.8; % 购电价,元/kWh param.discount_rate = 0.06; % 折现率 param.project_life = 20; % 项目寿命,年 param.crf = param.discount_rate * (1+param.discount_rate)^param.project_life / ... ((1+param.discount_rate)^param.project_life - 1);最后一行param.crf是资金回收系数,上层C_inv的年值化全靠它。把经济参数集中到canshu.m里的好处是,之后做灵敏度分析只需要改一个文件,不用在十几个脚本里来回找魔法数字。
3.2 四个典型日怎么生成年运行成本
四个典型日文件通常代表春、夏、秋、冬四季,每个典型日有24点负荷曲线、光伏出力曲线,有的还包含风速曲线。读取方式用readtable最省事:
T = readtable('四个典型日数据.xlsx', 'Sheet', '春季'); P_load = T.Load; % 24x1 P_pv = T.PV; % 24x1读取之后,把四个典型日的调度成本加权相加,就是年运行成本。权重是每个季节的天数,例如春季90天、夏季92天、秋季91天、冬季92天:
season_days = [90 92 91 92]; year_cost = sum(season_days .* daily_cost_vector);典型日方法本质上把8760小时压成了96小时,计算量下降一个数量级。代价是缺失了极端天气和连续多日储能耗尽的风险,所以我们会在下层调度结果里额外检查储能SOC是否长期贴在边界上,必要时把典型日增加到8个或12个。
3.3 我一般会用哪些参数做灵敏度分析
拿到这套程序后,我通常先做两个参数的灵敏度扫描:一个是grid_buy_price,一个是discount_rate。购电价直接决定储能套利收益,折现率影响储能这种高初始投资、低运行成本方案的年值化计算结果。改参数后记得重新运行shuang_main.m,不要只跑DOWN_.m单独验算。有条件的可以把param.pv_cost分档扫描,画出容量配置随PV成本变化的曲线,这份结果比任何一段文字都更适合写进智能微电网PPT或可研报告。
4. 求解流程与计算效率:从粒子群到内点法的典型搭配
4.1 下层用内置 linprog 还是第三方求解器
下层经济调度本质上是线性规划,MATLAB优化工具箱自带的linprog就能解。如果引入了储能充放电状态或柴油机启停二进制变量,则用intlinprog。我做项目时倾向于先用连续模型跑通,再逐步加入整数变量,原因很简单:连续模型能提供一个平滑的下界,方便判断整数模型的结果是否合理。
| 求解方式 | 擅长问题 | 缺点 |
|---|---|---|
linprog | 连续经济调度 | 无法处理启停整数变量 |
intlinprog | 含储能的日前调度 | 分支定界较慢 |
| Yalmip + Cplex/Gurobi | 大规模混合整数规划 | 需要单独安装第三方求解器 |
当前两层循环的规模是“一次上层迭代要调四个典型日调度”,也就是下层被调用数千次。如果每个典型日都用intlinprog求解,总耗时很难接受。一个常见的优化技巧是:上层迭代初期只启用连续调度,临近收敛的最后几代再切换成整数调度,这样既保证精度,又把总运行时间压到小时级。
4.2 上层用粒子群还是遗传算法
MATLAB里实现上层优化,我默认选粒子群而不是遗传算法。粒子群收敛快,参数只有三个:惯性权重w、个体学习因子c1、群体学习因子c2;不像遗传算法要处理编码、交叉、变异三个环节。针对容量规划的连续变量,粒子群的边界处理也更直接:
% UP_2.m 内部的边界处理 new_pop = pop + new_vel; new_pop = max(new_pop, repmat(param.lb, n, 1)); new_pop = min(new_pop, repmat(param.ub, n, 1));如果模型中还有“光伏板数量必须是整数”的约束,可以在粒子群更新之后加一句round。但注意,先round再进下层调度可能导致SOC约束轻微波动,我一般让整数变量只出现在UP_1生成X之后,不在UP_2更新前取整。
4.3 两层怎么迭代、停机条件怎么设
4.3.1 主循环代码骨架
下面给出一个适合改到shuang_main.m里的简化版主循环:
% shuang_main.m 核心循环 last_best = inf; for iter = 1:max_iter [pop, vel] = UP_2(pop, vel, pbest, gbest, param); for i = 1:pop_size fitness(i) = UP_1(pop(i,:), param, data); end [updated_fitness, idx] = min(fitness); if updated_fitness < pbest_fitness(idx) pbest(idx,:) = pop(idx,:); end [gbest, gbest_fitness] = min(pbest_fitness); if abs(gbest_fitness - last_best) / max(1, abs(last_best)) < 1e-4 fprintf('迭代%d代收敛\n', iter); break; end last_best = gbest_fitness; end这个循环每次先用《UP_2》更新种群,再遍历种群调用《UP_1》,UP_1内部会按季节循环调用《DOWN_》。收敛判据用的是相对变化量1e-4,而不是“迭代次数是否跑满”。这样可以在前期加速搜索,后期稳定收敛。
4.3.2 传递罚函数避免不可行调度
双层优化最常见的卡壳点是下层不可行。以我的经验,纯粹返回一个超大罚值并不好,因为粒子群会在不可行域和可行域之间反复震荡。更好的做法是让DOWN_.m返回“违反约束的最小调整量”,在UP_1里按违反量乘一个动态系数:
penalty = param.penalty_base * iter; % 罚系数随迭代增大 fitness = C_inv + year_cost + penalty * violation;violation可以是失负荷电量、储能SOC越限量、备用不足缺额的总和。前期罚系数小,粒子群可以大范围探索;后期罚系数变大,迫使粒子回到可行域。这个思路比固定罚值更鲁棒,尤其是面对四个典型日负荷差异很大的情况。
5. 边界条件、收敛判断和双层优化的三个坑
5.1 坑一:下层最优解并不能提供梯度信息
有些理论派做法要求上层利用下层对偶变量构造拉格朗日函数,但MATLAB数值环境里,DOWN_.m返回的最优值是分段线性的,直接求导不现实。工程上的解法是:上层只用粒子群这类无梯度优化算法,所以下层只需要传回最优目标函数值,不需要传回KKT乘子。如果你确实需要更深层的耦合信息,那就要把下层用KKT条件替换成单层约束,变成一个数学规划问题,这已经不是“微电网规划设计”的程序能做到的了。
5.2 坑二:典型日代表性不足导致储能寿命被高估
四个典型日把全年压缩成96小时,储能充放电循环次数会被严重低估。验证方法是引入一个简单的寿命折算模型:调度结束时累加储能放电深度,按100%DOD折算等效循环次数,看看年循环次数是否在厂家承诺的6000次以内。我一般会加一个result.cycle_count字段,在DOWN_.m里用如下方式统计:
delta_soc = abs(diff(soc)); cycle_count = sum(delta_soc(abs(delta_soc) > 0.1)); % 简单累加如果计算出的年循环次数超过预期,说明典型日没捕捉到连续调节的储能行为,需要增加数据点或引入日间SOC回退约束。
5.3 坑三:SOC初值设置影响结果
四个典型日之间本身没有时间连续性,SOC初值设成0.5和设成1.0会产生完全不同的运行成本。常见做法是第一个典型日SOC初值取0.5,后面每个典型日继承前一天的SOC末值。不想让问题变得太复杂的话,可以给每个典型日加SOC首末等量约束,即在每个典型日结束时将SOC调回初始值。修改完成后,用dbstop if error跑一次全流程,观察输出目录里生成的曲线,对比不同初值下的配置方案是否一致。如果差别大于5%,就应该缩短调度周期或改成连续8760小时滚动优化。
本文还有配套的精品资源,点击获取