最近在复现一个与充换电站有关的优化项目,模型全名很直白——考虑用户充电负荷与最优分时电价互动的光储充换电站优化模型。第一次看到这个标题,估计不少人和我一样,第一反应是:这到底是电网调度问题,还是充电站运营问题?答案都是。它本质上是一个站级能量管理系统的优化决策模型,只不过把“用户充电负荷”和“分时电价”这对因果变量同时纳入了优化闭环。简单说,就是让充电站既当“售电商”又当“调度员”,通过动态制定分时电价引导用户削峰填谷,再配合光伏和储能实现低成本供电,最终让整个站的经济收益最大化。如果你接触过微电网、充电桩运营或者电力市场,这个模型很适合拿来练手,既能加深对双层优化的理解,又能训练Matlab建模能力。这篇文章就把我从建模到代码实现的整个思路,以及复现时踩过的坑完整梳理一遍。
1. 先拆清楚项目在做什么:光储充换电站、用户负荷与分时电价的互动逻辑
1.1 光储充换电站为什么值得专门建模型
普通充电站基本就是“电网买电、充电桩卖电”,运营模型很薄,只需要盯住买卖价差。光储充换电站完全不同,它把光伏发电、储能系统、直流快充桩和换电柜放在同一个站址内,本质上是一个小型微电网。运营方可控的除了各充电桩的功率,还有储能的充放电策略,甚至换电站里电池组的周转计划。可控资源变多后,简单“成本加价”的运营方式就不够用了。最经济的方案一定是在光伏出力大、电价低的时段多充电、给储能蓄电,等到电价高或光伏出力弱的时候,靠储能放电和低价时段储备能量去满足用户需求。
但因为用户充电行为存在时间弹性,仅靠储能“硬扛”还不够。如果能通过电价信号,把用户的一部分充电负荷从高价时段挪到低价时段,站内的设备利用率会更高,购电成本也能进一步下降。这个模型的价值,就是把这些可调元素放进同一个优化框架,同时算出一组最优分时电价和一套储能调度方案。模型输出的不是拍脑袋定的峰谷电价,而是跟站内光伏、储能容量、用户响应特点深度耦合后的结果。
1.2 用户充电负荷和分时电价如何“互相影响”
很多人习惯性地把分时电价当成外部给定参数,这是理解这个项目时最大的误区。这个项目里的分时电价是运营方的决策变量,但它又不是“想定多少就定多少”——用户看到电价后会改变充电行为。你电价定高了,用户可能少充、错峰,甚至去别的站;电价定低了,负荷会挤在一起,光伏和储能未必能覆盖,站内变压器也可能过载。所以模型里必须有一条“用户响应回路”:
电价变化 → 用户调整充电安排 → 充电负荷曲线变化 → 反过来影响运营方收益。
这条回路在这个项目里是通过需求弹性模型或用户效用函数刻画的。理解这一点非常重要,因为后面看代码时,如果看到“电价更新”和“负荷计算”在交替执行,就知道这不是随便写的迭代逻辑,而是整个双层优化的核心。我最初复现时就是因为没想明白这条回路,把上层收益和用户负荷当成两件独立的事来处理,结果折腾了很久。
1.3 弄懂“双层优化”才能真正看懂代码
这个模型在数学结构上是典型的双层优化。上层是充电站运营商,决策变量是分时电价和储能充放电计划,目标函数是站内总收益最大;下层是用户,面对给定的分时电价,决定什么时间充电、充多少电量,目标是充电成本最小,或者充电带来的效用最大。下层用户的优化结果会作为上层模型的输入,两层存在耦合,不能简单分开求解。
复现时通常有两种实现路线。一种是把下层用户的KKT最优性条件作为约束加入上层,将双层问题转化为单层数学规划,再由求解器一次性求解。另一种是迭代求解,先给一组电价,计算用户负荷,再优化上层调度,然后根据结果调整电价,循环往复直到收敛。第二种方法代码写起来直观很多,也便于理解,但收敛性需要仔细打磨,在文章后面我会详细讲。搞懂了这个分层结构,读源码时就会很清楚每一步在做什么,而不是对着变量名猜含义。
2. 模型骨架:决策变量、目标函数和关键约束
2.1 决策变量、目标函数与收益构成
先来明确到底在优化什么。设调度时段为t=1,2,...,T,常见取24个时段(每小时一个点)或96个时段(每15分钟一个点),时间间隔记为Δt。决策变量可以分为三类。
第一类是分时电价π_t,这是上层决策变量。如果每个时段都单独设价格,模型自由度太大,且不符合实际计费习惯,通常会把一天切成几个价格档位,比如峰、平、谷三段,同一档内的所有时段共享同一价格水平,实际决策变量只有几个档位价格和分档的时间边界。
第二类是储能充放电功率P_ch_t、P_dis_t,以及从电网购电的功率P_buy_t。如果允许余电上网,还包括卖出功率P_sell_t。第三类是换电站相关的电池管理变量,比如当前时段送入充电仓的电池数量、完成换电的电池组数量。
目标函数用收益表达如下:
max Σ_t [ π_t·L_t + r_swap·N_t - c_spot_t·P_buy_t - c_om·P_pv_t - c_bat·(P_ch_t + P_dis_t) ]
其中π_t是站内零售电价,L_t是用户充电负荷,r_swap是单次换电服务费,N_t是换电次数,c_spot_t是电网购电电价,c_om是光伏运维成本系数,c_bat是储能退化成本系数。这个式子把收入和成本拆得很清楚,后面写代码时目标函数就是按这个结构一行行加出来的。注意储能退化成本经常被忽略,但如果模型要滚动运行一年,这部分必须带上,否则储能会被频繁充放,结果明显失真。
2.2 用户负荷响应建模的两种主流方式
用户响应模型是这类研究的灵魂,主流做法有两种。
第一种是需求价格弹性矩阵。设定基础充电负荷L_base_t,优化后的负荷L_t与各时段电价变化量之间存在线性关系:
L_t = L_base_t × [1 + Σ_s E_{t,s} × (π_s - π_base_s) / π_base_s]
其中E_{t,t}是自弹性系数,一般为负值,表示本时段涨价会让本时段负荷减少;E_{t,s}是交叉弹性系数,一般为正值,表示其他时段涨价会让部分负荷转移到本时段。这种矩阵形式很直观,但参数标定比较麻烦,实际复现时往往需要从论文图表里反推。
第二种是用户效用函数法。假设用户的充电效用为U(L_t)=a·ln(1+L_t),a是效用敏感系数,用户在下层优化中最大化U(L_t)-π_t·L_t。对L_t求导并令其为零,可以得到显式的负荷响应函数:
L_t = a/π_t - 1
这种形式非常简洁,能保证负荷与电价反向变化,而且求解时可以直接代入,不用引入一堆0-1变量。我复现时更喜欢用这种方式,因为数学性质更干净,出问题也好排查。看到源码时要先搞清楚用的是哪一种,因为后续对目标函数线性化处理的方式完全不同。
2.3 光储和换电站运行的硬约束
光储部分的核心约束是储能SOC递推方程:
SOC_{t+1} = SOC_t + (η_ch·P_ch_t - P_dis_t/η_dis)·Δt / E_rated
其中E_rated是储能额定容量,η_ch和η_dis分别是充放电效率。SOC要限制在合理区间,比如0.2到0.9之间;充放电功率分别不能超过各自上限;同一时段不允许同时充放电,这个互斥约束一般通过引入0-1变量实现,也可以利用功率上下限和SOC递推关系来规避。
换电站的约束比充电站复杂。站内有一批标准电池组,用户换电会取走满电电池,同时留下亏电电池;亏电电池进入充电仓位,充满后再成为可用电池。因此需要有电池数量守恒约束:
N_avail_t = N_avail_{t-1} + N_finish_t - N_swap_t
其中N_finish_t是当前时段完成的电池数量,N_swap_t是当前时段换电消耗的数量。还需要保证任何时段站内可用电池数量不低于最小值,否则会出现“用户排队等电池”的违约问题。这些约束看起来简单,但加上充电仓功率限制后,变量和约束数量会迅速膨胀,是后面求解性能优化的重点。
2.4 功率平衡与电网交互边界
任意时刻,站内功率必须平衡:
P_pv_t + P_dis_t + P_buy_t = L_t + P_ch_t + P_swap_ch_t + P_aux_t
P_pv_t是光伏发电功率,P_swap_ch_t是换电充电仓的总充电功率,P_aux_t是站内辅助用电。等号左边是电源,右边是负荷,两边必须时时相等。如果允许光伏余电上网,等号右侧还要加入P_sell_t,同时需要设置与电网交互功率的上限,防止变压器过载,也避免出现大量倒送电。
这个约束看起来只是“能量守恒”,但实际是复现中出错率最高的地方。问题通常出在单位上:光伏和储能用MW,负荷和购电用MWh,或者时间间隔是0.25小时却按1小时参与计算,都会让功率平衡失衡。我的习惯是先把所有功率统一成kW、能量统一成kWh,再把Δt按小时值统一写入SOC递推,最后才去写约束,可以有效减少低级错误。
3. Matlab实现:工具选型、参数设置与核心代码结构
3.1 建模工具与求解器选择思路
如果论文给的是线性规划或混合整数线性规划模型,最省事的组合是Matlab加YALMIP再加商用求解器。YALMIP的优势在于建模语法和数学表达式几乎一一对应,比如定义一个优化变量、写目标函数、加约束,都是几行代码的事,不需要手动组装矩阵,调试起来非常直观。
求解器方面,混合整数线性规划问题直接交给商用MIP求解器即可;如果模型是连续凸优化,可以用内点法求解器;如果只是小规模线性规划,Matlab自带的线性规划函数也够用。不过有一点要提醒,如果模型中含有“电价×负荷”这类乘积项,问题会变成非凸,求解器很容易陷入局部最优,或者报出“模型不是凸”的警告。所以建模时就要想办法避免非凸项,不能全指望求解器。
3.2 参数准备和典型场景设定
复现时不要一上来就追求完整场景,建议先用一个典型日、24时段跑通全流程。光伏出力可以选某地夏季典型日的光照曲线,先归一化再乘以光伏装机容量,得到一个0到峰值之间的时序数组。用户基础负荷设成双峰曲线,早高峰在8点到10点,晚高峰在18点到21点,这样能比较好地体现价格引导效果。
分时电价的基础值先设为0.8元/度,电价上下限分别设为1.5和0.4元/度,这个范围参考一般工商业电价来定,既不会让用户流失,也不会让电价比成本还低。储能额定容量取500 kWh,额定功率200 kW,初始SOC取0.5,充放电效率取95%。需求弹性系数先取自弹性-0.3、交叉弹性0.05。如果你用的参数和论文不一致,最优电价曲线可能有很大差异,不要看到数值不同就以为代码错了,先检查参数口径。
3.3 模块化代码结构与迭代求解伪码
代码不建议写成一个巨大的一体化脚本,最好按功能拆分。我的目录结构一般长这样:
- main.m:主程序,设置参数、调用模型、输出结果
- data_init.m:把所有常量放进一个结构体,便于后续统一修改
- build_model.m:用YALMIP定义变量、目标函数、约束
- user_response.m:输入电价,输出用户充电负荷
- plot_results.m:输出电价、负荷、SOC曲线
如果复现的是迭代型双层模型,主循环的伪代码可以写成:
price = base_price; for k = 1:30 load = user_response(price, params); [storage_plan, status] = solve_upper(load, price, params); price_new = update_price(price, load, storage_plan, params); if max(abs(price_new - price)) < tol break; end price = price_new; end这段代码的难点在update_price函数。它决定了电价如何朝着收益最优的方向调整,如果只是简单加减,很容易振荡。这里建议把电价更新理解成“带反馈的控制器”,增益不能太大,位置不能跳变,必要时对相邻时段电价差也加约束。
3.4 从非线性到线性化的处理技巧
在2.2节提到,用户负荷如果采用效用函数法,有L_t = a/π_t - 1,那么售电收入项π_t·L_t = a - π_t,竟然直接变成了线性的,这是效用函数法一个非常讨巧的性质。如果采用弹性矩阵法,L_t是电价的线性函数,那么π_t·L_t就是二次项。当目标函数对这个二次项求最大化时,凹性无法保证,需要做分段线性近似。
一个最稳妥也最容易理解的回避方式,是用“迭代固定-更新”方案:在一轮优化时,假设用户负荷固定,先求解储能和功率分配;然后在电价更新的环节,再用解析的用户响应函数重新计算负荷。这样就把非凸乘积项拆开了,每轮都是线性规划或混合整数线性规划,全局收敛性有保障。很多论文里的迭代算法就是这种思路。如果你的模型要求严格的单层最优解,那还是要走KKT条件转换的路线,但代码复杂度和调试难度都会上一个台阶。
4. 排障实录:收敛性、整数变量和SOC初值那些坑
4.1 双层迭代不收敛的排障经验
最容易翻车的点是双层迭代不收敛,具体表现是电价在峰谷两个极端值之间来回震荡。我第一次跑的时候,初始电价设得比较高,第一轮算出来用户负荷骤降,运营方觉得电量卖少了,第二轮就把电价调到很低,结果用户负荷又爆满,然后再把电价调高,来回跳就是不收敛。
后来排查发现,问题基本出在电价更新步长太大,模型缺少“阻尼”。解决办法有几个:一是对电价更新加阻尼系数,让新电价是旧电价和候选电价加权平均,系数取0.2到0.5;二是给电价设置单轮变化幅度上限,比如每次调整不超过0.1元/度;三是在上层目标中增加电价波动惩罚项,从模型层面抑制振荡。实测下来,加阻尼的效果最直接,而且不改变模型物理含义。
4.2 整数变量太多导致求解慢
换电约束和储能互斥约束都会引入0-1变量,96时段下,光储能互斥就要96个变量,换电再引入一批,模型变量会很快过千,商用求解器也未必能在可接受时间内跑出结果。我的做法是先压缩到24时段,把模型逻辑验证清楚,再逐步扩展到15分钟粒度。
此外,储能互斥约束不一定非要用0-1变量。如果把SOC递推关系和功率上限写清楚,模型通常不会主动同时充电又放电,因为那明显不经济。只有在模型有特殊目标促使来回切换时,才需要强制加互斥。即使要加,也建议把Big-M系数取得尽可能小,并配合功率上下限使用,这样能显著降低求解难度。
4.3 SOC初值敏感与电量单位坑
储能SOC初值对最终结果的影响非常大。我遇到过一种情况:初始SOC设成0.5,晚高峰净负荷很大,储能容量不够用,模型被迫把晚高峰电价提得很高来压低负荷,导致最终电价曲线出现一个异常尖峰,看起来根本不像分时电价。后来我在正式优化前,先跑一遍不带储能、电价固定的纯购电模型,观察净负荷曲线,再根据净负荷峰值和储能容量反算一个合理的SOC初值,结果马上正常了。
单位坑则更隐蔽。比如储能额定容量是500 kWh,如果代码里写成500,但光伏和负荷的单位是kW,时间间隔是1小时,那储能只能支撑1小时,SOC很容易越界。还有个常见错误是时间间隔Δt单位忘记换算,明明用15分钟数据,SOC递推里却按1小时计算,结果能量翻4倍。我的习惯是把所有物理量统一写成带单位注释的变量,并在SOC递推前后用assert检查数值范围。
4.4 常见报错与对应解决速查
这一节把复现中容易踩的坑汇总成一张速查表,方便大家对照:
| 现象 | 可能原因 | 解决思路 | | 迭代电价一直震荡 | 电价更新步长过大 | 降低学习率或加阻尼系数 | | 用户负荷出现负值 | 弹性模型缺下限约束 | 增加负荷下限或改用对数效用函数 | | SOC递推越界 | 单位或时间间隔不一致 | 统一功率/能量单位,确认Δt取值 | | 求解器报告不可行 | 功率平衡或SOC约束太紧 | 放宽储能初始SOC,检查功率平衡方程 | | 求解时间过长 | 二进制变量过多 | 压缩时段数,简化换电约束 | | 结果与论文差异很大 | 基础负荷、弹性参数或其他参数不同 | 根据论文图表反推参数口径 |
这张表基本覆盖了我复现时遇到的绝大部分问题。另外还有一个小建议:每次改完模型,先跑一个只有电价、没有储能和光伏的退化版本,验证目标函数和负荷响应是否合理,再逐步加回储能和换电约束,这样做能大幅缩短定位问题的时间。
5. 结果分析与可扩展方向
5.1 输出结果怎么读
模型跑完以后,至少要画四张图:最优分时电价曲线、用户充电负荷对比曲线、储能SOC曲线、光伏出力与负荷匹配图。
最优分时电价曲线能直接看出峰谷差是否合理。正常结果应该是低谷时段电价明显压低、高峰时段电价拉高,与优化前的负荷曲线呈反向趋势,说明价格信号确实在引导用户错峰。用户充电负荷对比曲线则显示,低谷时段负荷抬升,高峰时段负荷下降,这个“削峰填谷”的现象越明显,说明用户响应参数设得越有效。SOC曲线能反映储能运行策略:电价低时充电,电价高时放电,一般呈现从早到晚先充电后放电,再充电再放电的锯齿形。光伏出力与负荷匹配图最直观,可以看光伏出力是否被站内负荷直接消纳,判断储能起到的作用。
5.2 经济性指标怎么算
只看收益绝对值不够,要算几类综合指标才能说明模型有效。总收益提升比例是最基础的,直接反映优化价值。用户平均充电成本也要看,如果最优电价虽然提升了运营收益,但把用户充电费用推得很高,长期看用户会流失,模型结果并不稳健。光伏消纳率等于光伏直接供给站内负荷的电量除以光伏总发电量,这个指标能反映光储配合水平,消纳率越高说明光伏自发自用越充分。峰谷差降低率等于原始负荷峰谷差减优化后峰谷差,再除以原始峰谷差,直接衡量削峰填谷效果。建议在代码里一次性输出这些指标,写报告和做对比实验都用得上。
5.3 工程落地和后续研究扩展
这个模型在论文场景里很完整,但离真正工程落地还有一段距离。分时电价一般不会每天重新优化,而是一个月或一个季度更新一次,所以最优电价要考虑统计意义上的基础负荷分布,而不是某一日的特定曲线。用户响应也不是即时的,看到电价后未必马上改变充电习惯,存在时滞和惯性。光伏出力和充电负荷都带随机性,确定性模型给出的是“理想最优”,实际运行中需要引入滚动优化来应对预测误差。
后续有几个值得做的扩展方向:把确定性优化改成鲁棒优化或随机规划,让模型能应对最坏情况和概率场景;给电价优化加滚动更新机制,每15分钟根据实际运行状态重新计算下一时段的调度策略;引入电动车V2G双向充放电,让车载电池也参与站内能量调剂。这些扩展在不推翻现有框架的前提下,只需要改目标函数和部分约束,非常适合在现有代码基础上继续做。
最后聊点个人体会。我复现这个模型最大的收获,不是把双层优化代码跑通,而是真正理解了“价格-负荷互动”是一个闭环,不是两段独立计算。做优化的人容易只盯着运营方的收益函数,却忘了用户是最会用脚投票的。还有一个小技巧:用YALMIP建模以后,可以先执行模型导出命令,把生成的文件打开看看约束矩阵的规模。如果约束数量比预期多很多,多半是变量维数写错了,这个习惯在调试大模型时特别能省时间。下次再遇到类似的站级能量管理模型,我建议先从常数电价开始,把储能和光伏逻辑跑顺,再放开电价优化,一步步观察负荷曲线变化规律。很多想不通的问题,在这一步一步的对照实验里,自然就变得清楚了。