光储充换电站这个概念最近两年有多热,看看知网就明白了——尤其是“分时电价”和“用户负荷”两个词,几乎成了这类论文标题的流量密码。但真正动手复现过的人都知道,大多数公开代码只给你一张固定电价表去调度,用户完全是被动接收指令;而这个标题里的“互动”两个字,才是整套模型的真正分水岭。
这个模型的核心逻辑不复杂:站点先给出一版分时电价,用户看到电价后调整自己的充电时间和充电量,形成新的负荷曲线,站点再基于这条新曲线重新优化电价和储能策略,跑几轮之后收敛到一个双方都能接受的平衡点。说白了,这是一个“运营商定价”与“用户用脚投票”相互咬合的闭环优化问题。
它能干的实事儿不少:输出一套可执行的分时电价建议、给出储能和换电电池的充放电计划、评估用户负荷转移后的削峰填谷效果、算清楚运营商的日收益构成。对于电力方向刚入门的研究生,这是一个练手价值很高的Matlab复现项目;对于做充电运营平台或者光储站规划的人,这套模型也能直接拿来当仿真推演工具,提前判断定价策略和储能配置是否合理。
我在Matlab里完整复现过一版,跑通之后最大的感受是:模型本身的数学难度不算高,真正的坑全在“用户响应怎么建”和“双层迭代怎么收敛”这两件事上。下面按建模思路、代码实现、踩坑排查、扩展方向这个顺序,把整个复现过程一步一步拆开讲。
1. 模型在设计什么:客流、电价、电池的三方博弈
1.1 光储充换电站和普通充电站的本质区别
普通充电站就是电网取电,最多加个储能做峰谷套利,结构简单。光储充换电站是“四合一”结构:屋顶光伏发电、站内储能调峰、直流快充桩给车充电、换电通道直接更换整包电池。
这四个部分通过一条直流母线或交流母线耦合成一个微型电力系统,而用户的充电和换电行为决定了这条母线上的负荷曲线。光伏出力、储能SOC、换电电池库存、排队用户数量,全都是优化模型的输入或约束条件。
有一个容易被忽略的设计点:普通充电站只有“电”这一种能量载体,而换电站手里握着一批可更换的电池组。这批电池不但在车上是动力源,在站内就是天然的储能资源——低谷时段可以把电池充满,高峰时段既能供车换电,多余电量还能反哺母线。这就是光储充换电站在调度灵活性上比单纯光储充站点多出来的那部分优化空间,代码里如果不单独建模,模型价值会大打折扣。
1.2 分时电价在这里是决策变量,不是历史数据
很多人一听“分时电价”就以为是电网公司公布的峰谷平固定表,拿来当输入参数。但这个模型里,分时电价是优化模型的决策变量。
运营站在电网允许的价格上下限内,自己设计一天24个小时的电价曲线,目标很现实:引导用户在光伏出力最大的午间时段充电,把站内光伏和储能资源吃干榨净;同时避开晚高峰买电,减少购电成本。
“互动”这个词有两层含义。第一层是电价发生变化后,用户会调整充电行为——原来打算晚上七点充一小时的用户,看到下午两点电价便宜,就可能改到两点来充。第二层是用户调整后的负荷曲线,会反过来影响下一轮电价优化的结果。这是一个闭环,不是一个单向的调度指令。
用生活里的场景类比:充电站就像一个给不同时段不同折扣的餐厅,用户就是看折扣安排吃饭时间的食客。餐厅发现工作日中午人少,就把午餐时段折扣加大,客人来了,客单价和翻台率也上去了。这个模型做的就是“动态折扣设计”这件事,只不过折扣变成了每小时的实时电价,客流变成了充电负荷。
1.3 这个模型解决的三个真实痛点
我梳理了三个直接用钱衡量的痛点,这也是模型输出结果最值钱的地方。
一是弃光问题。光伏中午出力猛,但那个时段大家都在上班,充电需求低,多余光伏要么白白限掉、要么上网卖个低电价。通过电价引导,让用户把充电需求挪到午间,弃光率能明显降下来。
二是晚高峰变压器过载。下班后扎堆充电,负荷尖峰把变压器顶得很高,扩容成本动辄几十上百万。通过分时电价的杠杆把一部分负荷平移到下午和深夜,尖峰负荷直接削平,站内设备利用率反而更高。
三是换电备用电池的库存决策。电池备多了压资金,备少了高峰期用户排队骂街。模型里把换电电池组也纳入调度,根据预测负荷和电价信号决定“低谷多充几组、高峰留多少组”,相当于把库存问题从拍脑袋变成了优化求解。
2. 数学骨架:目标函数、响应机制与约束边界
2.1 目标函数的两条路线:单层收益与双层互动
目标函数的建法分两类。如果站在运营商角度,模型是收益最大化:
max R = 充电售电收入 + 换电服务收入 + 光伏上网补贴 − 电网购电成本 − 储能循环损耗 − 换电折旧费用
如果站在用户角度,目标是充电成本最小化。但标题里强调的是“互动”,所以更合理的框架是双层模型——上层是运营商制定分时电价追求收益最大,下层是用户接收电价后调整充电行为追求自身成本最小。
复现时双层模型有三条技术路线。第一条是用KKT条件把下层用户问题转化为上层的约束条件,写成单层混合整数规划求解;第二条是外层用粒子群或遗传算法搜索电价,内层用线性规划或二次规划求解用户响应和储能调度;第三条是用主从分裂的迭代思路,两层交替求解直到结果收敛。
我的建议是:初学复现优先走第二条路线,也就是“启发式算法外层 + 线性规划内层”。这条路线逻辑清楚,每一层都能单独验证对错,出了问题方便排查。KKT转化那条路线虽然求解效率高,但对数学功底要求高,代码一旦报错很难定位是约束错还是对偶变量错。
2.2 用户负荷对电价的响应如何表达
用户充电负荷的响应建模是整个模型里最见功力的部分。最常用的是价格弹性模型,用一个系数反映需求量对价格变化的敏感度:
P_response(t) = P_base(t) × (1 + ε × (price(t) − price_ref) / price_ref)
其中ε是需求价格弹性系数,通常取−0.1到−0.5。ε取−0.3的意思是,电价相对参考价上浮10%,该时段充电量下降3%。
但这个简单模型有一个明显的缺陷:它只调整了单个时段的负荷大小,没有体现“负荷转移”这个真实行为。用户今天不充这200公里电,不代表他不充了,而是他会换个便宜时段来充。所以更精细的做法是引入可转移负荷矩阵,把某个时段的负荷按比例转移到相邻几个时段。
我在实际建模时还会把用户分成三类:刚性用户,必须当下充电,对电价无感;弹性用户,对价格敏感,愿意把充电时间挪到低价段;换电用户,关心的是电池能不能及时换上,对等待时间敏感,对电价相对钝感。三类用户的比例不同,整个站点的价格弹性就不同。没有实测数据时,可以用正态分布随机生成用户的到达时间和充电需求,再按比例合成基础负荷曲线,最后匹配一个聚合弹性系数。
还有一个很多复现代码都会犯的错误:响应模型计算出的总充电量发生了漂移。真实场景里,用户一天的总充电量基本由出行需求决定,电价改变的是“什么时候充”,不是“充不充”。所以我在响应函数最后加了一步总量守恒修正,把响应后的总电量按比例归一化回响应前的水平,这一步直接决定了后面功率平衡约束能不能闭合。
2.3 储能与换电电池的特殊约束
储能约束是电力系统的老面孔,但对于复现新手,SOC更新式子里效率和单位的坑很大。正确的离散格式是:
SOC(t+1) = SOC(t) + (η_ch × P_ch(t) − P_dis(t) / η_dis) × Δt / E_bat
其中P_ch是充电功率,P_dis是放电功率,η_ch和η_dis分别是充放电效率,Δt是时间步长,E_bat是储能容量。这个式子要求所有功率单位统一成kW,容量单位统一成kWh,时间步长统一成小时,否则SOC会跑出[0,1]区间,后面一切结果都是废的。
换电约束是这个模型最有特色的地方。站内电池组数量有限,每组电池有充放电状态、SOC状态、是否被装上车运走三个状态。每次换电行为消耗一组满电电池,同时释放一组空电池进入充电队列。这个“让哪些电池在什么时段充电”的小决策,本质上是一个资源调度子问题——把电池组看成n个虚拟储能单元,每组的SOC和可用状态分开建模,模型就立体了。
峰值约束也不难,关键是别漏:充电桩数量对应的同时充电功率上限、换电工位数量对应的服务能力上限、电价必须在电网公司允许的价格区间内、光伏出力不能超过额定容量。约束不是越多越好,但功率平衡等式和SOC上下限不等式是底线,漏了模型就失真。
2.4 输入数据从哪里来
复现这样的项目需要四类输入数据。一是光伏出力曲线,可以用典型日的辐照度数据配合电站装机容量折算,也可以从PVsyst导出全年数据截取典型日。二是基础充电负荷曲线,可以用充电运营平台脱敏后的历史数据,没有就用蒙特卡洛抽样生成典型工作日曲线。三是电网侧的购电价格和光伏上网电价。四是站内设备参数,储能容量、充电桩功率、换电工位数、电池组容量这些。
没有实际数据不是卡住的理由。用Matlab生成典型场景、跑多场景取期望值,是这类复现论文项目的标准做法。关键是要在代码里留一个data_input函数,统一管理所有数据来源,后面换真实数据时只改这一个文件,其他模型代码不动。
3. Matlab代码复现:框架、参数与逐段实现
3.1 文件结构与执行顺序
复现这种多层迭代模型,最忌讳把所有代码堆在一个脚本里。我建议按执行顺序用数字前缀组织文件:
main.m % 主入口,调用各模块 1_data_input.m % 参数和数据加载 2_pricing_layer.m % 外层电价优化主循环 3_user_response.m % 用户负荷响应函数 4_storage_dispatch.m % 储能和换电平抑策略 5_objective_evaluate.m % 站点收益目标函数 6_result_plot.m % 结果可视化用数字前缀的好处是执行顺序一目了然,调试时能清楚知道谁拿了谁的输出。双层迭代模型里内层函数会被外层调用几十次,如果函数封装不好,后面改一个参数要连带改五个地方,非常折磨。文件拆开之后,单元测试也好做,把每层函数单独喂一组已知数据,验证输出是否符合预期,再拼在一起联调。
3.2 关键参数设置与数据口径
我复现时用的一组典型参数如下:
| 参数 | 取值 | 说明 |
|---|---|---|
| 光伏额定容量 | 500 kW | 站顶光伏和车棚光伏合计 |
| 储能容量 | 1000 kWh | 磷酸铁锂电池组 |
| 快充桩 | 20 台 × 60 kW | 直流快充 |
| 换电电池组 | 50 组 × 60 kWh | 按单车型适配 |
| 电池循环寿命 | 3000 次 | 折算循环折旧成本 |
| SOC上下限 | 0.1 / 0.9 | 保护电池 |
| 需求价格弹性系数 | −0.3 | 用户聚合弹性 |
| 时间步长 | 1 小时 | 24 个决策时段 |
开始写代码之前,先把单位统一这件事办妥。论文里功率可能是MW,时间可能是15分钟一个点,代码里如果混着用,SOC更新、购电成本计算全都会偏。我的习惯是先在代码开头加一段单位转换注释,把所有数据统一到“kW、kWh、小时、元/kWh”,后面所有公式都不用再纠结单位。
3.3 用户响应函数:弹性模型加总量修正
用户响应是整个模型里最值得花时间打磨的函数。我实际用的实现是这样:
function P_res = user_response(price, P_base, epsilon, ref_price) % 价格弹性需求响应模型 % price: 24x1 分时电价序列 % P_base: 24x1 基础充电负荷 % epsilon: 需求价格弹性系数 % ref_price: 参考电价,用于归一化 P_res = P_base .* (1 + epsilon .* (price - ref_price) ./ ref_price); % 防止负荷被削成负值 P_res = max(P_res, 0); % 总量守恒修正:电价只改变充电时段,不改变总充电需求 E_before = sum(P_base); E_after = sum(P_res); if E_after > 0 P_res = P_res * (E_before / E_after); end end这个函数逻辑不复杂,但两个修正步骤是关键。第一步max截断,防止高电价时段把负荷算成负数。第二步总量归一化,保证响应前后的充电总电量一致。如果不做这两步,后面的功率平衡和收益计算全会在边缘工况下出问题,收益被高估或者被低估,结果完全不可信。
如果你想再精细一点,可以在函数里加一个负荷转移耗时参数,比如允许用户把充电需求向后转移最多2小时,用循环把转移后的负荷叠加回去。但这会让计算量变大,初版代码不建议搞这么复杂,先跑通简单弹性模型再说。
3.4 外层电价优化:粒子群与目标函数
外层负责搜索24个时段的电价序列。我用Matlab自带的particleswarm函数,简单够用。由于24个变量如果完全自由搜索,电价曲线会像一个锯齿,现实中根本执行不了,所以我在搜索空间设计上做了限制,让每时段电价在参考基准价附近小范围浮动:
nvars = 24; % 24小时电价 lb = 0.3 * ones(nvars, 1); % 谷时电价下限 ub = 1.2 * ones(nvars, 1); % 峰时电价上限 options = optimoptions('particleswarm', ... 'SwarmSize', 50, ... 'MaxIterations', 100, ... 'Display', 'iter'); [price_opt, fval] = particleswarm(@(x) obj_station(x, pv, P_base, params), ... nvars, lb, ub, options);外层目标函数是站点收益的负值,因为粒子群默认做最小化。目标函数内部串起整个链路:
function obj = obj_station(price, pv, P_base, params) % 1. 用户响应产生新的负荷曲线 P_load = user_response(price, P_base, params.epsilon, params.ref_price); % 2. 内层调度储能和换电电池,计算电网交换功率 [P_grid, P_bat] = storage_dispatch(price, pv, P_load, params); % 3. 计算收益项 revenue_sell = sum(P_load .* price); % 充电售电收入 revenue_swap = params.fee_swap * params.n_swap; % 换电服务费 % 4. 计算成本项 cost_buy = sum(max(P_grid, 0) .* params.grid_price); % 购电成本 cost_bat = params.battery_cost_per_kWh * sum(abs(P_bat)); % 电池循环损耗 % 5. 净利润取负,作为粒子群目标 net_profit = revenue_sell + revenue_swap - cost_buy - cost_bat; obj = -net_profit; end这里有两个容易被忽略的细节。第一个是购电成本用max(P_grid,0)计算,因为P_grid可能为负,负值代表光伏多余电量反送回电网,那是收入不是成本。第二个是电池损耗的简化处理,我按每充放一度电折算成本计入目标函数,这样优化器不会不计代价地滥用储能,结果更贴近工程实际。
3.5 内层储能调度与功率平衡
给定净负荷和电价后,内层要决定储能什么时候充、什么时候放。最规范的做法是建一个小型线性规划,变量是24个时段的储能充放电功率,约束是功率平衡和SOC连续性。
如果不想一上来就碰linprog,我提供了一个规则版的过渡实现,适合先验证逻辑:
function [P_grid, P_bat, soc_series] = storage_dispatch(price, pv, P_load, params) T = length(P_load); P_bat = zeros(T, 1); soc = params.soc_0; soc_series = zeros(T, 1); for t = 1:T P_net = P_load(t) - pv(t); % 净负荷,正为缺电,负为光伏富余 if P_net < 0 && soc < params.soc_max % 光伏富余且电池未满,优先充电消纳 P_bat(t) = max(P_net, -params.p_bat_max); elseif P_net > 0 && soc > params.soc_min % 缺电且电池有余量,优先放电 P_bat(t) = min(P_net, params.p_bat_max); end % SOC更新,注意充放电效率分开放置 soc = soc + (params.eta_ch * max(P_bat(t), 0) ... - max(-P_bat(t), 0) / params.eta_dis) / params.E_bat; % SOC饱和处理 soc = min(max(soc, params.soc_min), params.soc_max); soc_series(t) = soc; end % 电网交换功率 = 净负荷 - 储能出力 P_grid = P_net_total(P_load, pv, P_bat); end这个规则版逻辑简单但有一个问题:它没有全局视角,不会为了避开高购电电价而提前在低谷蓄电。所以最终版本强烈建议换成linprog规范求解,将SOC连续性约束写成矩阵不等式,把“低谷充、高峰放”的套利空间真正优化出来。这也是复现质量和入门代码之间最大的分水岭。
3.6 结果可视化四件套
代码跑通之后,出图是展示成果的关键一步。我常规输出四张图:优化前后电价与负荷对比图、光伏出力与储能SOC和电网购电功率的多曲线图、各时段收益构成柱状图、电价负荷互动迭代收敛曲线。
电价和负荷对比图用双轴表示:
figure; yyaxis left; stairs(1:24, price_opt, 'LineWidth', 1.5); ylabel('电价(元/kWh)'); yyaxis right; plot(1:24, P_base, '--', 'LineWidth', 1.2); hold on; plot(1:24, P_res, 'r-', 'LineWidth', 1.2); ylabel('充电负荷(kW)'); legend('最优分时电价', '响应前负荷', '响应后负荷', 'Location', 'best'); xlabel('时刻(h)'); grid on;这张图是模型效果最直观的证明:电价低的地方负荷鼓起来,电价高的地方负荷瘪下去,一眼就能看出削峰填谷的效果。迭代收敛曲线也很关键,我一般是记录每一轮外层的收益值,画出来看是否从锯齿状逐渐趋于平稳,这是判断双层互动是否收敛的最快方式。
3.7 双层迭代的终止条件
主从迭代怎么停,是个实操问题。我用三条经验准则:电价变化量连续3轮小于0.01元;目标函数变化小于0.1%;达到最大迭代次数50轮防止死循环。
只盯着目标函数变化是不够的,因为电价的微小波动可能引起收益变化并不大,但电价曲线还在振荡,这种情况直接停掉,结果看似稳定实则没收敛。把电价变化量加进去作为第二重判据,能显著减少“假收敛”的情况。
4. 复现踩坑与排查实录
4.1 负荷为负、SOC发散、维度报错
这三个是复现初期最常撞的墙,我逐个说。
负荷为负:用户响应函数里弹性系数取得太猛,某个高电价时段的基础负荷被削成负数。明明该时段没人充电了,模型却出现负的充电功率,功率平衡立刻崩掉。解决办法就是加max截断和总量归一化,两个修正一起上。
SOC发散:SOC更新时充放电效率乘反了,或者时间步长单位没统一,导致SOC在循环里越跑越离谱,甚至出现1.3这种超过上限的值。注意规范写法是“充电效率乘在充电项,放电项除以放电效率”,并且每步更新后强制做饱和处理,把SOC卡在上下限之间。这个方法虽然粗暴,但能防止数值意外带崩下一轮计算。
linprog维度报错:Matlab的linprog对变量维度极其敏感,Aeq矩阵的列数必须和变量数量严格一致。我最开始图省事用全零矩阵慢慢填约束,跑一次报一次错。后来把所有变量统一到一个State列向量里,按顺序固定排列,再用稀疏矩阵逐行生成约束,一次通过。强烈建议从一开始就用这种“变量字典”的方式组织约束矩阵,每一行约束都写注释说清楚“这是功率平衡等式”还是“这是SOC不等式”,排查速度快十倍。
4.2 双层迭代不收敛或锯齿振荡
这是互动类模型复现最容易翻车的地方。我用粒子群加内层线性规划的方案,通常要迭代20到40轮才能稳定,收益曲线初期会有比较明显的抖动。
如果收益曲线从头到尾都像锯齿一样上下乱跳,大概率是电价搜索步长太大,价格跨度过宽。解决办法有两个:一是把粒子群的SwarmSize和MaxIterations加大到50和100以上;二是对粒子群每次生成的price序列做一次平滑滤波,限制相邻时段电价变化幅度在0.05元以内。第二招在工程上很常见,因为现实中不可能出现相邻小时电价差出天际的定价策略,加这个限制不仅收敛快,结果也更可落地。
还有一种情况是外层和内层目标函数的数值量级差太多,收益目标是几十万元量级,用户响应约束是几千kW量级,粒子群在搜索时会偏向量级大的那一项。建议在目标函数里对各项做归一化,或者给收益和成本分别乘权重系数,平衡量级。
4.3 复现问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 负荷响应后出现负值 | 弹性系数过大或电价跨越太大 | 截断至0,再做总量守恒归一化 |
| SOC超过1或低于0 | 充放电效率方向错、单位不统一 | 修正效率公式,加饱和处理 |
| linprog维度报错 | 变量排列与约束矩阵不一致 | 用变量字典+稀疏矩阵构建约束 |
| 双层迭代锯齿振荡 | 价格步长过大,搜索范围过宽 | 限制单步价格变化,加平滑滤波 |
| 结果与论文数值对不上 | 数据口径不同:功率、价格单位混用 | 统一kW/kWh/小时制,仔细核对 |
| 粒子群跑得非常慢 | 内层调用了太多次线性规划 | 适当减少粒子数量,或者内层改用规则近似 |
| 优化结果购电成本反而升高 | 目标函数里P_grid符号处理错误 | 检查max(P_grid,0)和min(P_grid,0)的使用 |
这个表是我多次复现同类项目后整理出来的高频问题清单。基本都是数据口径和约束处理问题,很少是算法本身的锅。
5. 进阶方向:从复现到能落地
5.1 从确定性到随机场景
目前模型用的是确定性的光伏和负荷曲线。真实场景里,光伏受天气影响波动很大,用户到达也有随机性。想往深做,可以把输入改为场景生成加场景削减:用蒙特卡洛抽样生成几百个天气和负荷场景,再用K-means聚类选出有代表性的七八个场景,每个场景算一次优化,最后按场景概率加权得到期望收益。这样电价策略不再只对一个理想日有效,而是对可能出现的多种情况都有鲁棒性。
这个改动对代码结构来说不算伤筋动骨,主要是在数据输入层加一个场景维度,模型主体不变。但对于论文审稿或者实际决策,可信度完全不是一个量级。
5.2 从单站到集群协同
单个站点做电价弹性的空间始终有限,用户再怎么说也就那些。多站场景下,不同站点的区域用户构成不同、光伏条件不同、储能配置也不同,可以引入站间差异化电价,引导用户在几个站点之间分流。比如A站光伏足、电便宜,就引导用户去A站充;B站变压器余量少,就提高价格自动劝退一部分非刚需用户。
这个方向是目前光储充换电领域比较热的集群协同调度,代码上要在外层增加多站耦合约束,比如总负荷不能超过区域变压器容量上限,难度上了一个台阶,但价值也明显更高。
5.3 从离线优化到滚动时域控制
论文里的优化常常是日内一次性求解完毕,但工程落地更适合模型预测控制(MPC)的思路:每15分钟滚动一次,更新最新的光伏预测和需求预测,只执行下一时刻的决策,然后时间窗整体前移,重新计算。这样做的好处是对光伏突变和用户临时变动反应快,不会一条优化结果用到黑。
Matlab里实现MPC可以直接用自带工具箱,也可以用YALMIP配合求解器自己写滚动窗口。我实测下来,对于光伏突变明显的阴雨天,滚动窗口方案比单次离线优化方案的总收益能高出5%到8%,这个差距是实打实的。
我在复现这个模型时最大的体会是:别急着调算法,先把用户响应那层做对。这个模型的灵魂在于“互动”,而这个互动最朴素的实现就是用户对电价的弹性响应。很多复现代码卡壳,都是因为这个环节处理得不够真实——负荷凭空增减、忽略总量守恒,导致后面所有优化结果全部失真。
如果你的目标是把这套模型跑通并真正理解,我的建议是:第一个版本宁可全部用简单规则和线性规划,把逻辑链路完整跑通,再逐步升级成双层优化、随机场景、滚动控制。等你能画出一张“电价拉动负荷、负荷影响收益、收益反哺电价调整”的迭代收敛曲线时,这个模型就算真正吃透了。到那个时候,想换算法、换场景、换数据,都会顺畅很多。