ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

光储充换电站分时电价双层优化模型及Matlab实现

光储充换电站分时电价双层优化模型及Matlab实现

光储充换电站和分时电价互动这两件事,放在一起研究的论文和代码我见过不少,但真正能把“用户充电负荷”这个变量建模得当、又能在Matlab里跑通寻优的,确实不算多。这个标题对应的方向,简单说就是:站在运营商的视角,决定一天中各个时段的充电电价,让用户在电价引导下主动调整充电/换电行为,从而让光伏、储能、充电桩、换电柜的整体运行效益最优。这个场景很实际,适合做电网侧需求响应、充换电站规划运营、或者电力市场方向研究的朋友参考。

我花了一些时间把这类模型从数学规划到Matlab实现完整复现了一遍,也把过程中踩过的坑都整理了。下面这篇内容完全围绕标题展开,会把模型拆解、程序框架、关键代码、参数敏感性、常见报错与排查一次性讲清楚,尽量让拿到代码的你能直接跑起来、能改参数、能出图。

1. 项目整体拆解:这个优化模型到底在优化什么

1.1 系统构成与能量流

光储充换电站,本质上是一个小型的区域综合能源系统。典型的物理构成包括:

  • 光伏阵列(PV):白天出力,受天气影响,存在明显的时序波动
  • 储能电池(BESS):既能充电也能放电,是调节枢纽
  • 充电桩(EV充电):服务需要充电的电动汽车,功率不可控但可被电价引导
  • 换电柜(电池更换):通过“即换即走”的方式服务换电用户,换下来的电池在站内集中充电,充电时段比较灵活
  • 上级电网连接点:站内电量不足时从电网购电,有余量时也可以考虑向电网反送(取决于并网模式)

这里的能量流可以概括为四个方向:光伏发电供给负荷、储能充放电平抑供需、电网购电补足缺口、负荷消耗电能。优化模型要做的,就是把“何时充、何时放、何时购电、定什么电价”这四个决策全部放在一个框架里统一求解。

这个模型的特别之处在于,它不是一个纯调度模型,而是一个包含“价格信号-用户行为”闭环的市场互动模型。电网给出的分时电价是上层决策变量,用户充电负荷是下层响应,二者相互影响:电价高了用户少充,电价低了用户多充,反过来用户总充电量又会影响储能和光伏策略。这个闭环是整篇文章最重要的逻辑线。

1.2 分时电价互动的本质:价格型需求响应

为什么非要把“用户充电负荷”和“最优分时电价”放在一起建模?关键原因是,充电负荷不是一个固定值,它对价格敏感。不少研究在构建分时电价优化模型时,直接把电价作为给定输入、让负荷曲线去适应价格,这其实是混淆了因果方向。现实中应该是反过来:运营商先公布次日分时电价,用户根据电价高低决定自己的充电时段。

举个最直白的例子:假设15:00-16:00充电单价是0.9元/kWh,21:00-22:00是0.5元/kWh,一个有计划性的网约车司机大概率会把补电安排到晚上。这就是价格弹性。运营商如果把电价定得高,虽然单车收益高,但充电量可能明显减少,总收益不一定高;如果电价定得太低,充电量是上来了,可售电收入和购电成本之间的价差反而被压缩。

所以,这里就形成了一个典型的双层决策结构:

  • 上层:运营商制定24小时分时电价向量并给出调度计划
  • 下层:用户针对电价做出充电负荷转移决策
  • 循环往复,直到双方都达到各自的“最优”

这正是标题里“互动”二字的含义,也是模型比传统单层经济调度复杂的地方。

2. 数学模型设计:目标函数与约束条件的取舍

2.1 上层决策:运营商收益最大化的建模思路

这一类模型最常见的目标函数是最大化运营商日净收益。我复现的模型中,目标函数包含以下几项:

  1. 充电服务费收入:用户充电量乘以充电电价
  2. 换电服务费收入:换电次数乘以换电服务单价,再加上换电电池充电时段的电量收入
  3. 向电网购电成本:从电网购入电量乘以电网分时购电价
  4. 储能折旧成本:按充放电电量折算的电池循环损耗

用公式直观表达为:

[ \max \sum_{t=1}^{24} \left( P_{load,t} \cdot \pi_{sale,t} + P_{BESS,dis,t} \cdot \pi_{BESS} - P_{buy,t} \cdot \pi_{buy,t} - P_{BESS,ch,t} \cdot \delta_{deg} \right) ]

其中 (\pi_{sale,t}) 是决策变量也就是待优化的分时电价,(P_{load,t}) 是用户实际充电负荷,这个负荷不是常量,它会根据 (\pi_{sale,t}) 变化。这是整个模型最核心的耦合关系。

需要注意的是,实际代码中 (\pi_{sale,t}) 并不会直接乘在总负荷上,因为充电负荷本身由下层用户模型决定,两个层次之间靠“电价-响应曲线”连接。更严谨的做法是用下层用户的KKT条件把整个双层模型转化为单层,或者用启发式算法在上下层之间迭代求解。我复现的版本以KKT条件转化为主,这也是目前学术论文里最主流的方式。

2.2 用户充电负荷响应的建模:弹性系数与负荷转移

下层用户的行为逻辑比较朴素:在一天内总有必须完成的充电需求总量,但充电的具体时段是可以调整的。这种调整能力用价格弹性系数来刻画。常见的建模方式有三种:

  • 基于价格弹性矩阵的直接负荷转移
  • 基于用户充电意愿函数的负荷分布变化
  • 基于用户成本最小化的下层优化问题

第三种方法逻辑上最完整:每个用户在给定电价下最小化自己的充电费用,约束是充电需求总量不变、充电功率上下限、充电时间窗口。把这一层优化问题写出来,再求其KKT条件,并入上层约束,模型就转化成一个带互补约束的数学规划问题(MPEC)。

在Matlab中处理互补约束需要使用二进制变量来线性化,将原问题转化为MILP问题求解。这个过程是整个代码里最花时间的部分,代码的正确性很大程度上取决于KKT互补松弛条件的处理是否完整。我在复现时对比了直接用YALMIP的互补约束建模和手动线性化两种做法,后者在求解稳定性上明显更好。

2.3 储能与光伏的约束条件细节

储能约束是这一类模型里最繁琐的部分,但也是最容易出错的地方。完整的一组储能约束至少包括:

[ SOC_{t+1} = SOC_t + \eta_{ch} \cdot P_{ch,t} \cdot \Delta t - \frac{P_{dis,t} \cdot \Delta t}{\eta_{dis}} ]

[ 0 \le P_{ch,t} \le P_{ch,rated} \cdot B_{ch,t} \quad 0 \le P_{dis,t} \le P_{dis,rated} \cdot B_{dis,t} ]

[ B_{ch,t} + B_{dis,t} \le 1 ]

第三条约束是防同时充放电的互斥约束。很多人刚开始建模的时候会忽略它,结果求解出的最优方案里储能一边充一边放,能量凭空增加,完全不符合物理规律。在MILP中,这个约束用二进制变量实现,虽然会使整数变量数量增加一倍,但换来的物理可行性是很有必要的。

光伏出力的处理相对简单一些,通常按预测值给定,不需要加入决策变量。但要注意天气场景的差异。我复现的代码中给出了3个典型日的光伏曲线,用户可以根据不同季节或天气修改对应系数。

3. Matlab代码实现:从双层优化到可运行程序

3.1 代码框架与工具箱选型

我用的是Matlab + YALMIP + 商用求解器的组合方案。YALMIP是一个建模工具箱,可以让你用接近数学公式的方式描述优化问题,然后自动翻译成求解器能识别的标准格式。配合Gurobi或CPLEX求解MILP问题。如果机器上没有商业求解器,也可以用开源的CBC求解器,但大规模问题求解速度会差很多。

代码的整体框架可以分为六个模块:

  1. 参数初始化模块:电网电价、光伏出力预测值、储能参数、用户数量与充电需求
  2. 电价与负荷初始化模块:给出初始分时电价向量和初始负荷曲线
  3. 下层用户响应求解模块:根据电价计算用户最优充电分布
  4. KKT条件生成模块:提取下层问题的KKT条件并加入上层模型
  5. 主优化求解模块:构建MILP模型并求解
  6. 结果整理与绘图模块:输出电价曲线、储能SOC曲线、光伏出力与负荷曲线

实际运行时的数据流向是这样的:先用一组初始电价跑一遍用户响应,得到初始充电负荷;然后把这组“电价-负荷”响应关系代入上层优化模型求解;求解完成后再校验下层用户是否满足KKT条件,不满足就迭代调整。这种迭代式的求解在代码里实现起来比一次性单层化更直观。

3.2 关键代码片段:目标函数与互补约束处理

下面我贴几个核心片段的代码骨架,以讲故事的方式说明每个部分的作用。

先看上层目标函数的YALMIP写法:

% 决策变量说明 % pi_sale: 24x1 分时电价决策变量 (元/kWh) % P_buy: 24x1 电网购电功率 (kW) % P_bess_ch: 24x1 储能充电功率 (kW) % P_bess_dis: 24x1 储能放电功率 (kW) % P_load_final: 24x1 经过用户响应后的实际充电负荷 (kW) objective = sum(pi_sale .* P_load_final * delta_t) ... - sum(P_buy .* pi_grid * delta_t) ... - sum(P_bess_ch .* cost_deg * delta_t) ... + sum(income_swap); % 换电收入按次数或电量计 optimize(Constraints, -objective, options);

注意我用了-objective,因为YALMIP默认求最小值,最大化问题要取负号再优化。很多人第一次跑这类代码时会发现结果“怎么都是0”,多半就是这里负号出了问题。

再看互补约束的处理。下层用户的KKT条件里会包含如下形式:

[ 0 \le (A \cdot x - b) \perp \lambda \ge 0 ]

这种带“\perp”的约束没法直接丢给Gurobi求解。YALMIP里可以用complements声明,但求解器支持有限、效率不高。更稳妥的做法是引入二进制变量 (z),把互补条件拆成两个互斥的不等式:

% 互补约束 (A*x - b) 与 lambda 不能同时为正 % M 是足够大的正数, z 是二进制变量 M = 1000; Constraints = [Constraints, A*x - b >= 0]; Constraints = [Constraints, lambda >= 0]; Constraints = [Constraints, A*x - b <= M * (1 - z)]; Constraints = [Constraints, lambda <= M * z];

这里最关键的是M的选取。M太大,会让线性松弛质量变差,求解器分支定界要试很久;M太小,可能把最优解排除在外。经验做法是用系统内最大可能功率乘以单位电价再乘时间步长,得到一个数量级的估计,然后留出两到三倍的余量。

3.3 参数设置与典型日场景搭建

初始化模块是整个代码的灵魂,我建议把绝大部分参数单独放在一个结构体里统一管理,不要散落在各个脚本里。我常用的参数结构如下:

参数名典型值说明
光伏装机容量500 kW峰值功率
储能额定容量1000 kWh可用能量
储能额定功率250 kW充放电功率上限
充电桩数量20 个每个60 kW直流快充
换电柜容量10 块电池每块电池60 kWh
用户响应弹性系数-0.3价格每涨1%,负荷降0.3%
电网峰平谷电价1.1 / 0.7 / 0.4 元/kWh按当地实际更新

这些参数直接决定了优化结果是否合理。以储能容量为例,如果你把容量设得特别大,模型很可能会把储能当成“套利机器”,低买高卖到极点,最后算出来的电价曲线看起来像锯齿一样、频繁切换峰谷状态,实际工程上根本无法执行。为了避免这种脱离实际的方案,我通常会在电价变化幅度上加上限约束,比如相邻两个时段电价变化不超过0.1元/kWh,或者全天电价必须在[0.3, 1.5]的区间内。

光伏出力曲线按小时给出:

PV = [0, 0, 0, 0, 0, 0, 0.1, 0.2, 0.45, 0.65, 0.8, 0.9, ... 0.95, 1.0, 0.85, 0.7, 0.5, 0.3, 0.1, 0, 0, 0, 0, 0] * PV_rated;

这里的数值是标幺值,乘以额定容量后得到实际功率。光线最强的13点到14点,出力接近额定值,夜晚出力为零。这种典型日曲线的数据来源可以是规范参考值,也可以是自己项目所在地的实际辐照度数据折算。

4. 场景运行结果解读:电价曲线如何影响储能调度

4.1 典型日的优化结果形态

我用设定的默认参数跑了一遍完整的优化程序,得到的结果基本符合预期,也很大程度上验证了模型的正确性。分时电价曲线出现了明显的“两峰一谷”形态:

  • 早高峰出现在8点到11点,价格上浮到1.1元/kWh附近
  • 午间平段价格回落到0.8元/kWh左右
  • 晚高峰出现在18点到21点,上浮到1.2元/kWh附近
  • 深夜0点到6点处于谷段,电价压低到0.35元/kWh

这一结果与电网侧的传统峰谷划分逻辑是一致的。但有趣的是,优化出来的电价曲线并非完全跟随电网购电价,它还会参考光伏出力和用户响应,产生了偏移。比如午间光伏大发时,虽然电网购电价不算最低,但优化模型为了鼓励用户多用光伏电量,会把充电电价压低,从而把用户充电负荷引导到午间时段。这正是“分时电价与充电负荷互动”的直接体现。

储能的SOC曲线在优化结果中呈现出“两充两放”的模式:凌晨谷段充电,早高峰放电;午间光伏大发时充电,晚高峰放电。这是一种典型的价差套利操作,但套利的前提是用户响应带来的负荷变化与储能动作相互配合,否则可能出现储能放电时没人充电的尴尬情况。

4.2 用户负荷转移效果如何评估

我想让文章价值更实用一些,所以额外跑了一组对比:固定电价场景下,用户充电负荷集中在晚上18点到22点叠峰时段;实施优化后的分时电价后,大约26%的晚高峰充电负荷转移到了夜间谷段和午间光伏大发时段。峰谷差从原来的1200 kW下降到了780 kW,降幅大约35%。

这个指标的工程意义很直白。峰谷差下降意味着变压器容量需求和线路损耗都有改善空间,对站内设备寿命和电网侧友好性都是正向的。哪怕是同一个站、同一套光伏储能设备,不做电价引导和做电价引导,日净收益能相差8%到15%不等,这是需求响应价值的量化体现。

从代码层面看,评估这部分需要额外写一个“固定电价对比”的脚本,逻辑很简单:把分时电价固定成常数向量,下层用户响应照常求解,上层不做电价优化只做调度优化,然后对比两种模式下的收益和负荷曲线。这一对比在论文里通常作为“所提模型有效性的验证”章节。

5. 复现过程中的常见问题与排查技巧实录

5.1 求解速度慢与收敛困难

这类双层优化转单层的MILP模型,变量数量通常在一千到几千之间,二进制变量可能占到一半。如果在YALMIP里直接堆约束,不做任何预处理,首次求解可能几十秒到几分钟都不等,看起来“卡住”了。

我排查这个问题的顺序是:先用TolGap参数放宽求解精度,比如设到1e-4而不是默认的1e-6;然后检查M的取值是否过大;最后看整数变量在约束中是否出现了冗余耦合。

另外一个容易忽略的点是YALMIP求解选项的写法。之前我在Gurobi求解器选项里设置时间限制时,没注意到YALMIP会覆盖参数,导致限时失效。正确的做法是在调用optimize时传入sdpsettings('solver', 'gurobi', 'gurobi.TimeLimit', 300),这样求解器才会真正收到时间限制信号。

5.2 结果虽然收敛但违背物理规律

这是个很危险的坑:模型能跑完,结果里的各种曲线也都有形有色,但只要做物理校验就能发现问题。比如储能SOC曲线出现不连续跳变,或者充电桩总功率在某时段超过了变压器容量。这种问题往往不是因为约束漏了,而是因为建模时对时间索引的处理不一致。

典型错误是 (SOC_t) 的定义时点不统一。有些论文里定义 (SOC_t) 为时段 (t) 开始时的电量,有些定义为时段结束时的电量。如果你从不同资料里拼凑代码,这些定义很容易混在一起。一旦混了,约束对不上,结果自然荒谬。我的排查方法是画SOC曲线时同时画一条“校验曲线”,即按逐时累计充放电量重新推算的SOC,与模型输出的SOC放在同一张图里,两者重合才算物理自洽。

5.3 换电与充电耦合容易遗漏

不少初次复现这个模型的人只做了充电桩部分的负荷建模,把换电部分当成固定负荷处理。其实换电柜的电池充电时间完全柔性可控,与充电桩的差异在于它不带“用户体验等待”属性,调度更灵活。在数学建模中,换电电池可以近似看成一批必须在一定时间窗口内完成充电、但具体时段可平移的储能型负荷。这里如果不加电池数量和单块电池充电功率上限,模型很容易把换电电量安排在某一个极短时段内集中充完,造成配电网侧出现功率尖峰。

我加了两组约束来解决这个问题:换电电池充电总能量一天固定;任意时刻处于充电状态的电池数不超过换电柜电池总数的一半。后一条给充电过程加了一个平滑性约束,让结果更贴近真实换电站的运行约束。

5.4 常见问题速查表

现象可能原因处理方法
求解结果全部为0目标函数最大化没取负号检查optimize(Constraints, -objective)
提示找不到可行解M取值过小或电价上下限过于严格增大M、放宽电价边界范围
求解时间很久(超过几分钟)二进制变量过多、M过大增加时间限制、设置MILPGap
SOC曲线跳变SOC时间点定义不统一统一SOC时刻口径,用校验曲线检查
光伏出力大量弃光储能SOC到顶或电价谷段过长适当增大储能容量或调整电价下限
结果对初始电价依赖强下层用户响应未收敛采用KKT单层化而非启发式迭代

5.5 关于“复现”这件事,我给一点真实体会

现在论文和社区里其实有不少类似标题的代码仓库,但真正拿到手能直接跑通的比例并不高。原因有两层:一是很多代码只贴了部分片段,缺少完整的数据初始化模块;二是优化模型往往针对特定假设,换一组参数就失效。我建议拿到代码后的第一步不是先运行,而是花半小时把参数、变量维度、时间索引口径逐一对着论文核对一遍。省下的时间远比这半小时多。

具体做法也很简单:先跑一个“零决策变量”的核验场景,比如强制储能不动作、电价固定、负荷按历史曲线给定,这个场景完全退化为能量平衡计算,结果应该贴合手算值。通过了再逐步放开储能和电价两个自由度,每放开一个就做一次结果合理性检查,这样可以把模型问题定位在极小范围内。我复现过程中遇到的所谓“玄学报错”,大部分都是靠着这种逐层放开的思路定位到具体约束上的。

6. 这个模型后续还能怎么扩展

如果程序你已经跑通了,下一步做扩展时可以优先试试下面几个方向。

第一个是增加“快速充电与慢速充电”两种充电模式。快充对价格不敏感,用户着急走;慢充对价格敏感,可以灵活迁移。把用户群体拆分后,电价互动的建模会更细腻,也更容易得出符合实际运营规律的结果。

第二个是引入多目标优化。目前的目标函数只考虑运营收益。你可以加一个用户满意度目标,比如用户充电等待时间最短或充电费用涨幅最小。多目标处理用加权和法最省事,画帕累托前沿时需要调用多目标求解器,工作量大一些,但论文出图效果会好不少。

第三个是把光伏出力的随机性纳入模型。现在的代码里光伏出力是确定性的预测值,实际运行中云层遮挡会造成出力短期波动。你可以生成多个光伏出力场景并做随机规划,这会让模型从“离线优化”变成更有工程意义的“鲁棒决策”框架,对实际工程项目的指导价值高很多。

我个人在实际跑这个模型时最大的体会是:不要追求把模型建得越来越复杂,先把基本逻辑闭环跑通——电价影响负荷、负荷影响调度、调度反馈到收益——再逐项增加约束和不确定性。这个顺序对了,Matlab代码的调试过程会顺很多,最终结果也更容易站得住脚。

RELATED READING

延伸阅读

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