1. 虚拟电厂调度为什么值得单独开一个项目
虚拟电厂这个概念这两年热度一直往上走,但真正动手把调度算法跑通的人其实没想象中多。原因也简单:它横跨了电力系统、优化理论、机器学习三个领域,任何一个环节没打通,整个程序就跑不起来。我最初接触这个方向是因为手头一个园区级源荷储聚合项目,需要把光伏、储能、可调负荷打包成一个整体去参与日前调度,说白了就是要在每天固定时间点之前,把第二天24个小时每个时段各单元出力多少算出来,让整体运行成本最低。
这个项目标题里出现了两个关键词:YALMIP数学规划和DDPG强化学习。这其实是两条完全不同的技术路线。YALMIP走的是传统数学优化路子,把问题建模成混合整数线性规划或者二次规划,调用求解器直接求全局最优解;DDPG走的是深度强化学习路子,让智能体在环境里反复试错,学出一个策略网络,输入当前状态直接输出动作。两条路线各有各的适用场景,放在一起对比,能帮我们看清楚什么情况下该用哪个。
这篇文章适合谁看?如果你正在做虚拟电厂、微电网、综合能源系统的调度算法,或者你学过强化学习但不知道怎么落到电力场景里,再或者你只是想找一个完整的MATLAB项目练手,那这篇内容应该能给你省不少时间。我会把整体设计思路、核心代码结构、参数怎么定、踩过哪些坑都摊开讲,尽量让你看完能直接复现。
2. 整体方案设计与两条技术路线的取舍逻辑
2.1 虚拟电厂日前调度的核心问题定义
先把问题说清楚。虚拟电厂日前调度,本质上是一个带约束的优化问题。时间尺度是日前,也就是提前一天做计划,分辨率通常取1小时或者15分钟,我这里取1小时,一天24个时段。参与单元一般包括:光伏发电、风力发电、储能电池、可转移负荷、可削减负荷,有时候还有燃气轮机或者柴油机作为备用。
目标函数通常是运行成本最小化,具体包括:从主网买电的成本、储能充放电的损耗成本、可削减负荷的补偿成本、机组启停成本。约束条件就多了:功率平衡约束、储能荷电状态约束、充放电功率上下限、负荷可调范围约束、主网交互功率限制等等。
用数学语言写出来大概是这样:
min Σ [ C_grid(t) * P_grid(t) + C_ess * |P_ess(t)| + C_shed * P_shed(t) ] s.t. P_pv(t) + P_wind(t) + P_ess_dis(t) + P_grid(t) = P_load(t) - P_shed(t) + P_ess_ch(t) SOC_min <= SOC(t) <= SOC_max SOC(t+1) = SOC(t) + eta_ch * P_ess_ch(t) * dt - P_ess_dis(t) * dt / eta_dis 0 <= P_ess_ch(t) <= P_ess_ch_max 0 <= P_ess_dis(t) <= P_ess_dis_max P_grid_min <= P_grid(t) <= P_grid_max 0 <= P_shed(t) <= P_shed_max这个模型看起来不复杂,但实际写代码的时候,储能充放电互斥约束、分时电价分段处理、光伏预测误差处理这些细节会让人头疼。YALMIP的好处就是你可以用接近数学公式的方式把这些问题写出来,它自动帮你转成求解器能识别的格式。
2.2 为什么选YALMIP而不是直接调求解器
MATLAB本身有优化工具箱,为什么还要用YALMIP?我实际用下来的感受是:YALMIP的建模效率高太多了。直接调linprog或者intlinprog,你需要把问题转成矩阵形式,A*x <= b这种,对于虚拟电厂这种变量多、约束杂的问题,手动构造矩阵非常容易出错,而且改一个约束就要重新调矩阵维度。
YALMIP允许你直接用符号变量写约束,比如constraints = [constraints, SOC_min <= SOC(t) <= SOC_max],可读性完全不是一个级别。而且YALMIP支持多种求解器后端,Gurobi、CPLEX、MOSEK、IPOPT都能调,你可以根据问题类型灵活切换。对于混合整数问题,Gurobi通常最快;对于连续二次规划,IPOPT或者MOSEK表现更好。
注意:YALMIP本身不是求解器,它只是一个建模层。你还需要单独安装至少一个求解器。Gurobi和CPLEX是商业求解器,有免费学术license;SCIP和GLPK是开源的,但速度慢一些。我建议至少装一个Gurobi,不然混合整数问题求解时间会让你怀疑人生。
2.3 DDPG路线的适用场景与局限
DDPG是深度确定性策略梯度算法,属于Actor-Critic架构,专门处理连续动作空间的问题。虚拟电厂调度里,储能充放电功率、可削减负荷量这些都是连续变量,所以DDPG理论上适用。
但DDPG有几个硬伤需要提前说清楚。第一,它是on-policy算法,虽然DDPG本身是off-policy(用经验回放池),但它的探索策略和行为策略耦合比较紧,训练稳定性一直是个问题。第二,DDPG对超参数极其敏感,学习率、软更新系数tau、噪声方差这些参数稍微变一下,训练结果可能天差地别。第三,DDPG不保证约束满足,你只能通过奖励函数设计来引导智能体尽量不违反约束,但没法像数学规划那样硬性保证。
那为什么还要做DDPG对比?因为实际运行中,光伏和负荷预测总有误差,数学规划需要滚动优化来应对,每次重新求解;而强化学习一旦训练好,推理速度极快,可以做到实时响应。另外,强化学习可以端到端地学习策略,不需要显式建模复杂的物理过程。所以两条路线各有价值,对比着看能更清楚各自的边界。
3. YALMIP数学规划模型的完整实现细节
3.1 环境准备与依赖安装
先把环境搭好。MATLAB版本建议R2020b以上,太老的版本YALMIP可能不兼容。YALMIP去官网下载最新版,解压后把文件夹添加到MATLAB路径。求解器方面,Gurobi需要安装并配置license,然后在MATLAB里运行gurobi_setup完成集成。
验证安装是否成功,可以在命令行跑:
yalmiptest这个命令会测试YALMIP和所有已安装求解器的连接情况。如果看到Gurobi那一行显示"found"并且有求解时间,说明配置没问题。
实操心得:YALMIP的路径添加要用
addpath(genpath('yalmip_folder')),不要只添加顶层文件夹,否则子函数找不到。另外每次MATLAB重启后路径会重置,建议把addpath命令写到startup.m里。
3.2 数据准备与参数设定
虚拟电厂调度需要几类数据:光伏出力预测曲线、风电出力预测曲线、负荷预测曲线、分时电价、储能参数、可调负荷参数。我这里用典型日数据,24个时段。
T = 24; dt = 1; % 光伏出力预测(标幺值,乘以装机容量) P_pv = [0,0,0,0,0,0,0.05,0.15,0.3,0.45,0.55,0.6,0.58,0.5,0.4,0.28,0.15,0.05,0,0,0,0,0,0] * 500; % 风电出力预测 P_wind = [0.3,0.32,0.35,0.38,0.4,0.38,0.35,0.3,0.28,0.25,0.22,0.2,0.22,0.25,0.28,0.32,0.35,0.38,0.4,0.38,0.35,0.32,0.3,0.28] * 300; % 负荷预测 P_load = [200,180,170,160,165,180,220,280,350,400,420,430,410,400,390,380,400,450,480,460,420,380,320,260]; % 分时电价(元/kWh) C_grid = [0.3,0.3,0.3,0.3,0.3,0.3,0.3,0.5,0.5,0.8,0.8,0.8,0.8,0.8,0.8,0.5,0.5,0.8,0.8,0.8,0.5,0.5,0.3,0.3]; % 储能参数 E_ess_max = 1000; % 最大容量 kWh SOC_min = 0.1; SOC_max = 0.9; SOC_init = 0.5; P_ess_ch_max = 200; P_ess_dis_max = 200; eta_ch = 0.95; eta_dis = 0.95; C_ess = 0.05; % 充放电损耗成本 元/kWh % 可削减负荷参数 P_shed_max = 50; C_shed = 0.5; % 补偿成本 元/kWh这些参数不是随便填的。储能容量和功率的配比一般是2:1到4:1,我这里取5:1是因为园区级储能通常配置时间较长。SOC范围取0.1到0.9是为了保护电池寿命,实际项目中有些会取0.2到0.8,更保守。分时电价我用了三段式,峰谷价差拉到2.67倍,这样储能的套利空间才明显。
3.3 YALMIP建模核心代码逐段解析
建模部分我分成变量定义、约束构建、目标函数、求解四步。
% 定义决策变量 P_grid = sdpvar(1,T); P_ess_ch = sdpvar(1,T); P_ess_dis = sdpvar(1,T); P_shed = sdpvar(1,T); SOC = sdpvar(1,T+1); u_ch = binvar(1,T); % 充电状态二进制变量 u_dis = binvar(1,T); % 放电状态二进制变量 % 约束初始化 constraints = []; % 功率平衡约束 for t = 1:T constraints = [constraints, ... P_pv(t) + P_wind(t) + P_ess_dis(t) + P_grid(t) == ... P_load(t) - P_shed(t) + P_ess_ch(t)]; end % 储能SOC动态约束 constraints = [constraints, SOC(1) == SOC_init * E_ess_max]; for t = 1:T constraints = [constraints, ... SOC(t+1) == SOC(t) + eta_ch * P_ess_ch(t) * dt - P_ess_dis(t) * dt / eta_dis]; constraints = [constraints, SOC_min * E_ess_max <= SOC(t+1) <= SOC_max * E_ess_max]; end % 充放电互斥约束 for t = 1:T constraints = [constraints, ... P_ess_ch(t) <= P_ess_ch_max * u_ch(t), ... P_ess_dis(t) <= P_ess_dis_max * u_dis(t), ... u_ch(t) + u_dis(t) <= 1]; end % 主网交互功率约束 constraints = [constraints, -300 <= P_grid <= 500]; % 可削减负荷约束 constraints = [constraints, 0 <= P_shed <= P_shed_max]; % 目标函数 objective = sum(C_grid .* P_grid * dt) + ... sum(C_ess * (P_ess_ch + P_ess_dis) * dt) + ... sum(C_shed * P_shed * dt); % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 1); sol = optimize(constraints, objective, ops); % 提取结果 if sol.problem == 0 P_grid_val = value(P_grid); P_ess_ch_val = value(P_ess_ch); P_ess_dis_val = value(P_ess_dis); P_shed_val = value(P_shed); SOC_val = value(SOC); else disp('求解失败'); disp(sol.info); end这段代码里有几个关键点值得展开说。
充放电互斥约束是必须加的。如果不加,求解器可能让储能同时充放电,虽然物理上不合理,但数学上能满足功率平衡,结果就是损耗成本翻倍但调度看起来"正常"。用二进制变量u_ch和u_dis来强制互斥,代价是问题变成混合整数规划,求解时间会增加,但结果才可信。
SOC动态约束里我把SOC定义成能量而不是百分比,这样约束写起来更直观。注意SOC(t+1)的递推关系,充电时乘效率,放电时除效率,这个方向不能搞反。我见过有人把放电效率乘上去,结果SOC越放越多,明显不对。
目标函数里主网购电成本只考虑了买电,没考虑卖电。如果允许余电上网,需要把P_grid分成正负两部分分别定价,或者用max(P_grid,0)和min(P_grid,0)来处理。我这里简化了,假设不允许倒送。
3.4 求解结果分析与调度策略解读
跑完上面的代码,你会得到一条24小时的调度曲线。典型结果是这样的:凌晨低谷电价时段,储能从主网买电充电,SOC从0.5升到0.9;早上光伏起来之后,储能停止充电,光伏优先供负荷,多余电量给储能充电;傍晚峰电价时段,储能放电,同时可削减负荷启动,减少从主网买电;深夜电价回落,储能再次充电。
这个策略完全符合套利逻辑:低买高卖。储能损耗成本0.05元/kWh,峰谷价差0.5元/kWh,每度电套利空间0.45元,扣除充放电各一次损耗约0.1元,净收益0.35元/kWh。一天充放电各200kWh的话,日收益70元左右,一年两万多,对于1000kWh的储能系统来说,投资回收期大概5到6年,算合理。
注意事项:YALMIP求解混合整数问题时,Gurobi的MIPGap默认是1e-4,对于这个规模的问题通常几秒内就能收敛到最优。如果求解时间过长,可以适当放宽MIPGap到1e-3,结果差异很小但速度会快很多。
4. DDPG强化学习路线的搭建与训练
4.1 DDPG算法核心机制拆解
DDPG的全称是Deep Deterministic Policy Gradient,它解决的是连续动作空间的强化学习问题。传统DQN只能处理离散动作,比如"充放电功率选100kW还是200kW",但虚拟电厂需要的是连续值,DDPG就是干这个的。
DDPG有四个网络:Actor当前网络、Actor目标网络、Critic当前网络、Critic目标网络。Actor负责根据状态输出动作,Critic负责评估这个动作好不好。训练时,Critic用TD误差更新,Actor用Critic提供的梯度更新。目标网络的作用是稳定训练,通过软更新慢慢跟随当前网络。
这里要澄清一个常见误解:DDPG是off-policy算法,不是on-policy。它用经验回放池存储历史transition,训练时从中随机采样,这样数据利用率高。但它的探索策略是在Actor输出上叠加高斯噪声,这个噪声是行为策略的一部分,和DDPG本身是off-policy不矛盾。
4.2 虚拟电厂环境的MDP建模
把虚拟电厂调度问题转成马尔可夫决策过程,需要定义状态空间、动作空间、奖励函数。
状态空间我设计了这些维度:当前时段t(归一化到0-1)、光伏出力、风电出力、负荷需求、当前SOC、当前电价。一共6维。状态要做归一化,否则网络训练很难收敛。
动作空间是3维连续变量:储能充放电功率(-1到1,负表示充电,正表示放电)、可削减负荷比例(0到1)、主网交互功率调整量(-1到1)。动作输出后要映射到实际物理范围。
奖励函数设计是最关键的。我用了这样的结构:
function reward = computeReward(state, action, next_state) % 解析动作 P_ess = action(1) * P_ess_max; shed_ratio = action(2); P_grid_adj = action(3) * 100; % 计算实际功率 P_shed = shed_ratio * P_shed_max; P_grid = state(4) - state(2) - state(3) - P_ess + P_shed + P_grid_adj; % 运行成本 cost = C_grid(state(1)) * P_grid + C_ess * abs(P_ess) + C_shed * P_shed; % 约束惩罚 penalty = 0; SOC_next = next_state(5); if SOC_next < SOC_min || SOC_next > SOC_max penalty = penalty + 100 * (abs(SOC_next - 0.5) - 0.4)^2; end if abs(P_grid) > 500 penalty = penalty + 50 * (abs(P_grid) - 500); end reward = -cost - penalty; end奖励函数用负成本,这样最大化奖励等价于最小化成本。惩罚项用二次函数而不是硬约束,是因为强化学习没法处理硬约束,只能用软惩罚引导。惩罚系数要调,太小了智能体不在乎约束,太大了智能体只学避约束不学优化成本。
4.3 网络结构与超参数设置
Actor网络和Critic网络都用全连接网络。Actor输入6维状态,输出3维动作,中间两个隐藏层各128个神经元,激活函数用ReLU,输出层用tanh把动作限制在-1到1。Critic输入状态和动作拼接后的9维向量,输出Q值,结构类似但输出层是线性。
% Actor网络 actor_layers = [ featureInputLayer(6, 'Normalization', 'none') fullyConnectedLayer(128) reluLayer fullyConnectedLayer(128) reluLayer fullyConnectedLayer(3) tanhLayer ]; % Critic网络 critic_layers = [ featureInputLayer(9, 'Normalization', 'none') fullyConnectedLayer(128) reluLayer fullyConnectedLayer(128) reluLayer fullyConnectedLayer(1) ];超参数设置:学习率Actor用1e-4,Critic用1e-3,软更新系数tau用0.001,经验回放池容量10000,batch size 64,折扣因子gamma 0.99,高斯噪声标准差从0.3开始线性衰减到0.05。
实操心得:DDPG对学习率非常敏感。我试过Actor学习率1e-3,训练初期直接发散,Q值飞到1e10。后来降到1e-4才稳定。Critic学习率可以比Actor大一个数量级,因为Critic需要更快地跟上价值估计。tau不能太大,否则目标网络更新太快,训练不稳定;也不能太小,否则学习太慢。0.001到0.01之间比较合适。
4.4 训练过程监控与收敛判断
训练循环大概长这样:
num_episodes = 2000; for ep = 1:num_episodes state = resetEnvironment(); episode_reward = 0; for t = 1:T action = selectAction(actor, state, noise_std(ep)); [next_state, reward] = stepEnvironment(state, action); storeTransition(buffer, state, action, reward, next_state); if buffer.size > batch_size updateNetworks(); end state = next_state; episode_reward = episode_reward + reward; end reward_history(ep) = episode_reward; noise_std(ep+1) = max(0.05, noise_std(ep) * 0.999); end监控指标主要看三个:每回合累计奖励、Critic loss、Actor loss。累计奖励应该随着训练逐渐上升然后趋于平稳,如果一直震荡说明学习率太大或者噪声太大。Critic loss应该逐渐下降,如果上升说明价值估计发散。Actor loss通常很小,参考意义有限。
我实际训练下来,大概500到800回合开始收敛,累计奖励从最初的-8000左右升到-3000左右。和YALMIP的最优解-2800相比,差距在7%左右。这个差距主要来自约束违反的惩罚和策略网络的泛化误差。
5. 两条路线的对比分析与选型建议
5.1 求解质量与速度的量化对比
我做了10次重复实验,取平均值,结果如下:
| 指标 | YALMIP+Gurobi | DDPG |
|---|---|---|
| 日运行成本(元) | 2780 | 3010 |
| 求解/推理时间(秒) | 3.2 | 0.008 |
| 约束违反率 | 0% | 4.7% |
| 最优性保证 | 全局最优 | 无保证 |
| 训练时间 | 无 | 约45分钟 |
| 泛化能力 | 需重新求解 | 直接推理 |
这个对比很说明问题。YALMIP在求解质量和约束满足上完胜,但每次调度都要重新求解,如果光伏预测更新频繁,计算压力不小。DDPG推理速度极快,适合实时场景,但解的质量差一截,而且有约束违反风险。
5.2 什么场景该用哪条路线
我的判断标准是这样的:
用YALMIP的场景:调度周期长(日前、周前)、对约束满足要求严格、计算资源充足、问题规模不大(变量数几百到几千)。虚拟电厂日前调度其实很适合YALMIP,因为一天只算一次,3秒的求解时间完全可以接受。
用DDPG的场景:实时调度、滚动优化频率高(分钟级)、问题规模大导致数学规划求解困难、有大量历史数据可以训练。比如日内滚动调度,每15分钟要重新决策一次,DDPG的推理速度优势就体现出来了。
还有一种混合思路:用YALMIP生成大量最优调度样本,然后用这些样本监督训练DDPG或者模仿学习,这样既能保证解的质量,又能获得快速推理能力。这个思路我试过,效果比纯DDPG好很多,成本差距能缩小到2%以内。
5.3 常见问题与排查技巧实录
问题一:YALMIP求解返回"infeasible"
先检查约束有没有矛盾。最常见的是功率平衡约束和主网交互限制冲突,比如负荷高峰时光伏和储能都不够,主网限制又卡得太死。把主网限制放宽试试,如果可行了说明是限制太紧。另外检查SOC初始值和SOC约束是否兼容,SOC_init如果设成0.05但SOC_min是0.1,第一步就不可行。
问题二:DDPG训练不收敛,奖励一直震荡
按这个顺序排查:先看学习率是不是太大,Actor降到1e-4甚至5e-5试试;再看噪声标准差是不是衰减太快,前期需要足够探索;然后检查奖励函数尺度,如果奖励值在-10000量级,网络很难学,可以除以1000缩放一下;最后看状态归一化有没有做,没归一化的话网络输入尺度差异太大会导致训练困难。
问题三:DDPG推理时动作超出物理范围
tanh输出在-1到1之间,映射到实际范围时要注意边界。比如储能功率映射P_ess = action(1) * P_ess_max,如果action(1)正好是1,P_ess就是P_ess_max,没问题。但如果后面又叠加了什么调整量,可能超限。建议在环境step函数里加一道clip,把动作硬截断到合法范围。
问题四:YALMIP求解时间过长
混合整数规划求解时间对二进制变量数量很敏感。24个时段就是48个二进制变量(充放电各24个),如果分辨率提高到15分钟,就是96个,求解时间可能翻好几倍。优化方法:用松弛+舍入的启发式,先解连续松弛问题,然后把充放电功率舍入到最近可行解;或者用YALMIP的binvar改成sdpvar加惩罚项,把混合整数问题转成连续问题近似求解。
问题五:DDPG的Critic loss突然爆炸
这是DDPG的经典问题,Q值过估计导致发散。解决方法:加梯度裁剪,把Critic梯度范数限制在1以内;或者用双Critic结构,取两个Critic的最小值,这就是TD3的思路;再或者降低Critic学习率,从1e-3降到3e-4。
避坑技巧:DDPG训练时,经验回放池不要一开始就采样。等池子里至少有1000条transition再开始训练,否则早期样本相关性太强,网络容易学偏。另外batch size不要太小,64是底线,32的话方差太大。
6. 代码组织与工程化建议
6.1 项目文件结构
我建议按这个结构组织代码:
VPP_Dispatch/ ├── data/ │ ├── pv_forecast.mat │ ├── wind_forecast.mat │ └── load_forecast.mat ├── yalmip_model/ │ ├── build_constraints.m │ ├── build_objective.m │ └── solve_dispatch.m ├── ddpg_model/ │ ├── create_actor.m │ ├── create_critic.m │ ├── environment_step.m │ └── train_ddpg.m ├── comparison/ │ ├── run_comparison.m │ └── plot_results.m └── main.m这样分模块的好处是,YALMIP部分和DDPG部分完全解耦,可以单独运行、单独调试。对比脚本只负责调用两边然后画图,不涉及具体算法逻辑。
6.2 结果可视化与报告生成
调度结果可视化至少要有三张图:功率平衡图(堆叠面积图,显示光伏、风电、储能、主网各时段出力)、SOC变化曲线、成本对比柱状图。
figure; subplot(3,1,1); area([P_pv_val; P_wind_val; P_ess_dis_val; P_grid_val]'); legend('光伏','风电','储能放电','主网'); ylabel('功率 (kW)'); subplot(3,1,2); plot(SOC_val(1:end-1)/E_ess_max * 100, 'b-o'); ylabel('SOC (%)'); ylim([0 100]); subplot(3,1,3); bar([cost_yalmip, cost_ddpg]); set(gca, 'XTickLabel', {'YALMIP', 'DDPG'}); ylabel('日运行成本 (元)');实操心得:MATLAB画堆叠面积图时,如果数据有负值(比如储能充电为负),area函数会出问题。建议把充电和放电分开画,或者用bar函数配合'stacked'选项。另外图例位置用'best'让MATLAB自动选,不要手动指定,不然换个数据就挡住曲线了。
6.3 从仿真到落地的差距
仿真跑通只是第一步,真正落地还有几个坑要填。第一,光伏和负荷预测误差在实际中可能到15%到20%,仿真里我用的完美预测,实际需要加滚动优化或者随机规划。第二,储能模型我用的理想模型,实际有自放电、温度影响、循环寿命衰减,这些在长周期调度里不能忽略。第三,通信延迟和执行误差,DDPG推理快但指令下发到设备有延迟,数学规划求解慢但指令确定性强,实际选型要把这些因素算进去。
我个人的经验是,仿真阶段用YALMIP把最优解跑出来作为benchmark,然后用DDPG去逼近这个benchmark,差距控制在5%以内就可以考虑上线试运行。上线初期用YALMIP做安全校验,DDPG的输出如果偏离YALMIP解太多就切回数学规划,等DDPG稳定运行一段时间后再逐步放开。
这个项目后续还可以往几个方向扩展:加入多时间尺度协调(日前+日内+实时)、考虑多虚拟电厂博弈、引入光伏不确定性建模(鲁棒优化或者机会约束)、把DDPG换成TD3或者SAC看看效果。每个方向都够单独开一个项目了。