恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
虚拟电池模型:EV与HVAC需求侧灵活性聚合及Python日前调度
首页
资讯中心
/
虚拟电池模型:EV与HVAC需求侧灵活性聚合及Python日前调度
虚拟电池模型:EV与HVAC需求侧灵活性聚合及Python日前调度
发布时间:2026/9/18 0:50:42
简介面向能源工程与智能电网领域的研究者和技术开发者这份文档围绕电动汽车与温控负载的需求侧灵活性刻画及日前优化调度展开提供基于虚拟电池模型的Python复现材料兼顾理论推导与代码解释。压缩包共1个docx文件约24KB内容涵盖市场电价、备用容量价格及设备参数设置单辆EV与集群EV的虚拟电池建模HVAC温控负载的舒适区间与功率约束表达以及调用pulp库构建96时段线性规划日前调度模型的完整代码与逐段说明文档以代码片段配合公式解释展示从单辆EV功率、电能上下限到集群聚合的转换过程。已有72人学习。读者可借此理解如何将分散的EV与HVAC聚合为等效虚拟电池嵌入日前计划以降低运行成本、平抑负荷曲线并预留辅助服务容量同时掌握pulp建模、约束构造与结果可视化思路便于对照论文复现或开展二次开发。1. 从负荷曲线到虚拟电池需求侧灵活性为什么必须显式建模做过配网侧调度的工程师大概都遇到过一个尴尬局面日前调度程序里把 200 辆电动汽车和 200 台空调简单打成一个可调负荷包按额定功率的一个固定比例参与优化结果要么过于保守、白白浪费了调节空间要么给出的调度指令在日内根本执行不了——车辆早就开走了空调温度已经撞到舒适区边界。问题的根源在于EV 和 HVAC 都是带时间耦合的设备它们的可调能力不是当前时刻功率上限这么一个标量而是一条与并网时段、离网期望电量、室内热惯性同时挂钩的可行域。虚拟电池Virtual Battery, VB模型解决的正是这件事把一个物理过程复杂的设备等效成一块有功率边界、有能量边界的电池对外只暴露 P_t^max、P_t^min、E_t^max、E_t^min 四条曲线。调度层看到的是标准储能设备层的约束被折叠进边界里模型解耦得非常干净。这篇复现面向的是做需求响应、日前市场投标、或者正在啃这篇论文的工程师和研究生Python 版本没法直接照搬论文里的 Matlab 代码聚合边界和求解器写法都得重新推一遍。2. 虚拟电池的数学骨架与 96 时段参数体系搭建虚拟电池不是新造一个物理模型而是把原设备的约束做投影。理解这一点后面写代码才不容易在边界公式上走偏。2.1 功率-电能双边界是怎么投影出来的以 EV 为例一辆车在 t 时刻的可达能量区间的上界取决于它从并网时刻 T_o 起就以最大功率充电能充到多少E_t^max min( E_{T_o} P^max · (t − T_o) · Δt , E_max )下界则要同时考虑两个方向。正向看如果一路按最小功率放电会掉到哪反向看为了在离网时刻 T_d 达到车主期望电量 E_ex当前时刻至少得留多少能量E_t^min max( max(E_{T_o} P^min · (t − T_o) · Δt, E_min), max(E_ex P^min · (T_d − t) · Δt, E_min) )功率边界再由能量边界差分得到这里的关键是必须存在一条可行轨迹想在第 t 时段用某个功率得保证从当前的能量下界出发、走到下一时刻的能量上界是走得通的所以P_t^max min( (E_{t1}^max − E_t^min) / Δt , P^max )P_t^min max( (E_{t1}^min − E_t^max) / Δt , P^min )这两行是整个 VB 模型里最容易写错的地方。很多人会直觉地写成 E_t^max 减 E_t^max那得到的是一直贴着上界走的功率完全没有容纳向下的调节空间聚合之后备用容量会被严重高估。2.2 参数体系与单位口径96 个时段、15 分钟步长是一天调度的标准切法。所有跟时间步长有关的量都必须显式带上 Δt这是最容易出隐性错误的地方——功率是 kW电量是 kWh中间的换算全靠它。import numpy as np T 96 dt 15 / 60 # 0.25 h功率(kW)与电量(kWh)之间唯一的桥梁 # 市场价格实际项目里应从日前市场出清结果导入这里用分段占位 day_ahead_price np.array([0.42]*24 [0.68]*24 [0.55]*24 [0.31]*24) # 元/kWh reserve_price np.array([0.20]*48 [0.12]*48) # 元/kW·h # EV 集群参数 num_evs 200 ev_cap 64.0 # kWh单车电池容量 ev_p_lim 6.5 # kW充放功率对称上限 ev_soc [0.1, 1.0] # 允许 SOC 区间 # HVAC 集群参数 num_hvac 200 hvac_cop 2.5 hvac_pmax 3.0 # kW电功率上限 hvac_band [-0.5, 0.5] # ℃设定温度两侧死区 hvac_comfort [19.0, 25.0] # ℃硬约束舒适区间逻辑说明dt一旦确定就不要在函数内部重算作为参数层层传递避免有人图省事在子函数里写死 0.25。day_ahead_price用重复块填充只是让代码能跑通真实场景应该按 96 点导入否则优化结果的时段分布没有参考价值。EV 和 HVAC 的数量直接决定聚合边界的量级也决定了后面 LP 的规模——EV 和 HVAC 都是聚合后进模型单机不进决策变量所以问题规模只跟 T 有关。参数含义单位典型取值对模型的影响T / dt时段数 / 步长个 / h96 / 0.25决定变量数与时间分辨率day_ahead_price日前电价元/kWh分段曲线决定充放电时移方向ev_cap单车容量kWh64决定 E_max 天花板ev_p_lim充放功率上限kW6.5决定 P 边界与爬坡速度ev_socSOC 允许区间—[0.1, 1.0]收紧 E_min / E_maxhvac_cop能效比—2.5电功率折算制冷量hvac_pmax电功率上限kW3.0单机 P 上界hvac_band温度死区℃[−0.5, 0.5]直接决定可累积电量hvac_comfort舒适区间℃[19, 25]温度硬约束上下限提示死区宽度是可调灵活性最敏感的旋钮。把 ±0.5℃ 放宽到 ±1℃集群 HVAC 的能量边界往往能扩大近一倍但用户舒适度投诉随之上升这是一组需要和运营方谈的参数。3. 单台到集群EV 虚拟电池的 Python 实现单机模型是地基集群模型是把它加总。加总听着简单但并网/离网造成的能量基线跳变如果不处理聚合边界会凭空多出一块并不存在的调节能力。3.1 单辆 EV 的边界递推先写一个干净的、可复用的单机函数。这里对原始版本做了几处必要修正功率边界的差分循环只跑到 T_d − 1避免 t 1 越界下界的两个来源合并成一次max功率变量初始化为零防止未并网时段被误用。def single_ev_vb(E_To, T_o, T_d, P_max, P_min, E_max, E_min, E_ex, dt, T96): E_To: 并网时车载电量; T_o/T_d: 并网/离网时段索引; E_ex: 离网期望电量 P_up np.zeros(T); P_dn np.zeros(T) E_up np.zeros(T); E_dn np.zeros(T) if T_o T_d: return P_up, P_dn, E_up, E_dn for t in range(T_o, T_d): # 能量上界从并网起一路满充能到多高 E_up[t] min(E_To P_max * (t - T_o) * dt, E_max) # 能量下界 正向放空 与 反向倒推期望电量两者取更紧的 fwd max(E_To P_min * (t - T_o) * dt, E_min) bwd max(E_ex P_min * (T_d - t) * dt, E_min) E_dn[t] max(fwd, bwd) for t in range(T_o, T_d - 1): # 功率边界由相邻时刻能量边界差分保证轨迹可行 P_up[t] min((E_up[t 1] - E_dn[t]) / dt, P_max) P_dn[t] max((E_dn[t 1] - E_up[t]) / dt, P_min) return P_up, P_dn, E_up, E_dn参数含义上P_min传负值放电方向E_ex传soc_expected * ev_cap。注意bwd这一项随着 t 逼近 T_d(T_d − t)变小反向约束会逐步收紧这正是必须按时充满这个硬需求在数学上的体现。如果发现某些车辆的下界在整个窗口内都被bwd主导说明期望电量定得过高实际可调空间几乎为零这类车在集群里应该单独筛出来。3.2 集群聚合与并离网基线的修正集群的功率边界可以直接相加因为各车的功率约束彼此独立能量边界也可以相加但前提是所有车在同一时刻都处于并网状态。现实中车辆的并网时段参差不齐一辆车在 t 时刻离网之后它车上那部分电量就不再属于可调度资源必须从聚合基线里扣掉。def cluster_ev_vb(num_evs, E_To_l, T_o_l, T_d_l, P_max, P_min, E_max, E_min, E_ex_l, dt, T96): P_up np.zeros(T); P_dn np.zeros(T) E_up np.zeros(T); E_dn np.zeros(T) E_base np.zeros(T) # 聚合车载电量基线 Exch np.zeros(T 1) # 并离网造成的基线跳变 for i in range(num_evs): p_u, p_d, e_u, e_d single_ev_vb(E_To_l[i], T_o_l[i], T_d_l[i], P_max, P_min, E_max, E_min, E_ex_l[i], dt, T) P_up p_u; P_dn p_d E_up e_u; E_dn e_d # 并网指示量 W: 1 表示该车此刻在线 W np.array([1.0 if T_o_l[i] t T_d_l[i] else 0.0 for t in range(T)]) E_base W * E_To_l[i] # W^2 与 W 等价差分的物理意义就是接入加、退出减 Exch[:T] E_To_l[i] * (np.roll(W, -1) - W) return P_up, P_dn, E_up E_base, E_dn E_base, Exch这里的Exch用 W² 差分和用 W 差分是同一个东西因为指示量只有 0 和 1平方不改变取值。原始代码里写成W ** 2容易让人以为还有非线性含义实际直接做一阶差分更清楚。E_base必须叠加到能量边界上否则聚合模型会认为车辆并网时是零电量起步E_dn会被系统性低估。注意如果 T_o 和 T_d 直接从最后一次出行结束时间和下一次出行开始时间生成务必保证每辆车在每个调度日至少完成一次完整的并离网循环否则状态机在跨日时会错位。4. 温控负荷 HVAC 的虚拟电池建模与边界求解HVAC 的复杂度和 EV 不在一个量级。EV 的能量约束是线性的HVAC 的约束藏在温度的一阶微分方程里得先把它转换到电量这个统一口径上才能和 EV 放进同一个优化框架。4.1 一阶热力学模型到电量边界房间温度的离散演化通常写成θ_{t1} θ_t a · (θ_0 − θ_t) − b · P_t · Δt其中 a Δt / (R·C) 反映热惯性b COP · Δt / C 反映制冷效率θ_0 是室外温度P_t 是空调电功率。所谓虚拟电量是把实际功率相对基准功率的偏差按时间累加E_t (1 − a) · E_{t−1} (P_t − P_base) · Δt基准功率 P_base 对应让房间稳定维持在设定温度 θ_c 所需的功率由稳态条件 a·(θ_0 − θ_c) b·P_base·Δt 反解得到。原始代码里把 P_base 简化成(θ_0 − θ_c)/(COP·1)隐含假设 R 1实际项目里应该用 a、b 反推否则边界会整体偏移一个常数量。def single_hvac_vb(a, b, P_max, theta_0, theta_c, eps, dt, T96, modemax): modemax: 温度始终压在死区下沿耗电最多; modemin: 温度飘在上沿耗电最少 theta theta_c P np.zeros(T); E np.zeros(T) # 基准功率稳态下热增益与制冷量平衡 P_base a * (theta_0 - theta_c) / (b * dt) if b * dt 0 else 0.0 for t in range(T): if mode max: P[t] P_max theta theta a * (theta_0 - theta) - b * P_max * dt if theta theta_c - eps / 2: theta theta_c - eps / 2 # 撞到下沿就保持住 else: P[t] 0.0 theta theta a * (theta_0 - theta) if theta theta_c eps / 2: theta theta_c eps / 2 # 撞到上沿就不再升温 E[t] (E[t - 1] if t 0 else 0.0) (P[t] - P_base) * dt return E, Pmode的两条轨迹就是单机能量边界的上下界一条是尽可能多耗电、把屋子拉到最冷一条是完全不耗电、让温度爬到最热。这两条轨迹之间的每一条都可能被执行所以它们的包络就是可行域。eps越小上界下界越贴近可调空间越窄这也解释了为什么死区宽度是灵活性最直接的来源。参数物理含义影响方向a热惯性系数 Δt/(RC)a 越小惯性越大温度变化越迟钝可调窗口越宽b制冷效率系数 COP·Δt/Cb 越大同样电功率降温越快边界扩张P_base稳态基准电功率偏移量只影响 E 的零点不影响宽度eps死区宽度线性影响 E 的上下界间距4.2 集群边界的正确聚合方式原始代码里用scipy.optimize.minimize对集群做整体优化然后用result.x同时给 P_max 和 P_min 赋值这样两个边界会完全相等备用能力直接归零属于典型的伪代码陷阱。正确做法要朴素得多集群的能量上界是所有单机在最耗电轨迹下的电量之和下界是所有单机在最省电轨迹下的电量之和功率上界是各自 P_max 的求和。def cluster_hvac_vb(num_hvac, a_l, b_l, Pmax_l, th0_l, thc_l, eps_l, dt, T96): P_up np.zeros(T); P_dn np.zeros(T) E_up np.zeros(T); E_dn np.zeros(T) for z in range(num_hvac): e_hi, p_hi single_hvac_vb(a_l[z], b_l[z], Pmax_l[z], th0_l[z], thc_l[z], eps_l[z], dt, T, modemax) e_lo, p_lo single_hvac_vb(a_l[z], b_l[z], Pmax_l[z], th0_l[z], thc_l[z], eps_l[z], dt, T, modemin) E_up e_hi; E_dn e_lo P_up p_hi; P_dn p_lo return P_up, P_dn, E_up, E_dn这样得到的上下界是所有机组同步动作的极端情形作为外近似是安全的——用它做优化得到的结果一定可行只是略微保守。想收紧边界可以在每台机组的温度轨迹上做滚动修正把已经撞到死区边界的机组从可调集合里暂时剔除这部分在最后一章展开。注意室外温度 θ_0 在一天内是变化的用常数会把早晚的灵活性差异抹平。建议按 96 点传入 θ_0 数组函数内部按 t 取值。5. pulp 线性规划日前调度模型建模边界准备齐了最后一步是把它们塞进一个线性规划。这一步最容易出错的地方不是算法而是符号和目标函数方向。5.1 目标函数与决策变量目标是净购电成本减去备用容量收益是最小化问题。原始代码用LpMaximize装了一个费用表达式求解方向反了结果会倾向于多买电。正确写法是import pulp def day_ahead_opt(price, r_price, P_ev_up, P_ev_dn, E_ev_up, E_ev_dn, P_hv_up, P_hv_dn, E_hv_up, E_hv_dn, P_load, P_pv, P_limit, dt, R_cap200.0): T len(price) prob pulp.LpProblem(DA_VB, pulp.LpMinimize) P_ev pulp.LpVariable.dicts(P_ev, range(T)) P_hv pulp.LpVariable.dicts(P_hv, range(T)) E_ev pulp.LpVariable.dicts(E_ev, range(T)) R pulp.LpVariable.dicts(R, range(T), lowBound0) for t in range(T): # 功率边界来自 VB 模型 prob P_ev[t] P_ev_dn[t] prob P_ev[t] P_ev_up[t] prob P_hv[t] P_hv_dn[t] prob P_hv[t] P_hv_up[t] # 能量边界这条最容易被漏掉只写功率约束会算出一辆永远放不完电的车 prob E_ev[t] E_ev_dn[t] prob E_ev[t] E_ev_up[t] # 电量递推 if t 0: prob E_ev[t] P_ev[t] * dt else: prob E_ev[t] E_ev[t - 1] P_ev[t] * dt # 线路容量约束 prob P_ev[t] P_hv[t] P_load[t] - P_pv[t] P_limit # 备用容量不能超过资源当下的上调余量 prob R[t] (P_ev_up[t] - P_ev[t]) (P_hv_up[t] - P_hv[t]) prob R[t] R_cap prob pulp.lpSum( (P_ev[t] P_hv[t] P_load[t] - P_pv[t]) * price[t] * dt - R[t] * r_price[t] * dt for t in range(T) ) status prob.solve(pulp.PULP_CBC_CMD(msg0, timeLimit120)) return status, [P_ev[t].value() for t in range(T)], [R[t].value() for t in range(T)]5.2 约束清单与常见失灵点约束数学表达缺失后果功率边界P_dn ≤ P_t ≤ P_up调度指令设备执行不了能量边界E_dn ≤ E_t ≤ E_up出现永远放不完电的假储电电量递推E_t E_{t−1} P_t·Δt功率与电量脱钩边界形同虚设功率平衡P_ev P_hv P_load − P_pv ≤ P_limit变压器过载备用容量R_t ≤ 上调余量R_t ≤ R_cap中标后无法实际响应几个实操层面的坑值得单独说。第一lpSum里面每一项都必须乘dt否则量纲变成 kW 而电价是元/kWh总成本会莫名其妙放大 4 倍。第二pulp 默认 CBC 求解器对 T 96、变量数几百的问题几乎是秒解如果加了爬坡约束后变慢换PULP_CBC_CMD(timeLimit...)或 HiGHS 后端即可。第三prob.solve()之后一定要检查status 1LpStatus[status]会告诉你到底是不最优、不可行还是无界。常见的不最优原因就是能量边界过紧导致某个时段没有任何可行解。提示如果只写能量上界不写E_ev[t] E_ev_dn[t]优化器会把 EV 当成一块只在需要时才存在、不需要时凭空消失的电池成本会降到不真实的水平这是审模型时最该盯的一处。6. 调度结果反代校核与边界收紧技巧LP 求解成功不代表结果真的能执行。最有效的验证方式是拿调度出来的功率曲线逐辆车跑一遍单机 VB 模型看是否真的存在一条可行充放电轨迹。def check_ev_feasible(p_sched, T_o, T_d, E_To, E_ex, P_max, P_min, E_max, E_min, dt, T96): 把聚合调度结果按比例分摊回单车再跑一次递推校验 E E_To for t in range(T_o, T_d): p p_sched[t] if p P_max 1e-6 or p P_min - 1e-6: return False, ft{t} 功率越界 {p:.3f} E min(max(E p * dt, E_min), E_max) # 离网时必须到达期望电量 return (abs(E - E_ex) 1.0, f离网电量 {E:.2f} kWh, 期望 {E_ex:.2f})这个函数的价值在于能快速定位是哪一类设备出了问题。如果大量车辆在 t 接近 T_d 时报离网电量不足说明E_ex普遍定得偏高或者 VB 的反向约束在集群聚合时被平均掉了——聚合边界的加总会让单车约束变松这是聚合建模的固有代价。收紧边界有两个实用手法。一是按并网窗口分组聚合把 T_o 和 T_d 相近的车辆分成若干子群分别建 VB 再求和比一次性全加总紧得多代价是变量数线性增长。二是滚动剔除已饱和的 HVAC每求解完一个时段检查哪些机组温度已经贴到死区边界下一个时段把它们从可调集合里摘出来用更新后的集群边界重解剩余时段本质是一个启发式的滚动窗口实测能把备用容量高估压下去 10% 到 20%。数值精度上还有一处很隐蔽E的量级是几百 kWh 甚至更高而dt 0.25如果两者都存在同一个约束里CBC 的默认数值容差可能导致E_t E_{t−1} P_t·dt出现 1e-6 级别的漂移累加 96 步后变成可观测的偏差。一个习惯做法是把电量统一换算到与功率同量级或者直接在求解后用上面那个校验函数兜底比调求解器容差省事得多。本文还有配套的精品资源点击获取