
电采暖这件事一到冬天就成了热点话题。特别是非居民用户——办公楼、学校、商场、工业厂房这些地方冬季采暖负荷大分时电价下峰段电费高得让人肉疼。单纯用电锅炉直供热负荷跟着电价走完全没有调节余地。加蓄热罐之后情况好了一些但仍然存在一个现实问题电采暖本身是刚性需求如果没有额外的储能手段削峰填谷的空间始终有限。这篇博文想聊的就是“考虑非居民自建共享储能的含蓄热式电采暖用户冬季日前优化调度”这个课题重点落在Matlab代码实现上。我做这个项目花了不少时间中间踩过很多坑从建模思路、约束写错导致无解到求解器配置、结果怎么解读都折腾过一遍。这篇文章就把整个过程梳理出来适合正在做综合能源、电力系统优化调度方向的研究生或者工程上做园区能源管理的朋友参考。1. 项目背景与问题拆解1.1 非居民电采暖用户的核心痛点先捋清楚这个问题的来龙去脉。非居民用户和居民用户最大的区别在于用能时间高度集中。办公楼是白天9点到18点用热学校是白天教学时段用热商场是营业时段用热。这种“白天高峰、夜间低谷”的用热特征恰好和分时电价的峰谷规律撞在一起。更要命的是电采暖不像空调那样可以随时关停。热负荷是刚性的室内温度掉下去供采暖不达标租户投诉、办公环境恶劣这些都是实际运营中无法接受的。所以很多用户选择“含蓄热式电采暖”——白天用蓄热罐放热夜间谷电时段把蓄热罐烧热这样既保证了供热质量又把电费成本压下来。但蓄热式电采暖有一个天然限制它只能“平移”热负荷不能“平移”电负荷。也就是说蓄热罐的调节能力决定了电锅炉的运行计划但用户自身的其他电负荷照明、电梯、办公设备依然无法参与调节。如果能把储能加进来情况就不一样了。1.2 为什么是“自建共享储能”储能这个东西单个用户自建成本高、利用率低。一栋办公楼的用电负荷曲线白天高、晚上低储能如果只在白天放电、晚上充电一天的循环次数有限投资回收周期很长。但多个用户共享一套储能负荷曲线互补之后储能的充放电机会更多利用率上去了单位成本自然降下来。这里“自建”两个字值得注意。它不是电网侧集中建设的独立储能电站也不是第三方运营商投资的共享储能而是由非居民用户群体例如一个园区内的多家企业共同出资建设、共同调度使用的储能系统。这种模式下储能的所有权和调度权都在用户侧运营灵活收益也直接归属参与用户。从数学建模的角度看共享储能引入了“容量分配”或“服务费分摊”的问题。是每个用户独立决策还是联合优化不同模式对应不同的模型结构后面会详细展开。1.3 日前调度的定位与时间尺度“日前”意味着决策是提前一天做的在当天运行前一天依据次日的气象预测、负荷预测和电价信息制定第二天24小时或96个时段的设备运行计划。为什么选日前调度而不是实时调度原因很简单电价信息是提前公布的分时电价政策明确热负荷虽然有一定波动但非居民用户的热负荷规律性强预测误差可控更重要的是储能设备的充放电切换需要一定的响应时间提前规划能避免频繁启停带来设备磨损。实际工程中日前调度给出的计划到当天运行时再用实时调度或模型预测控制MPC微调形成“日前-日内”两层协同。这是目前园区级能源管理的标准做法。2. 数学模型构建目标函数与约束条件2.1 目标函数设计这个问题的目标很明确最小化系统日运行总成本。但成本构成需要仔细拆不同方案下成本项不一样。我按“用户群联合优化 共享储能参与调节”的模式建模目标函数包括以下几项购电成本各时段从电网购电的电费等于购电功率乘以分时电价再累加。这是成本的大头也是调度优化的主要对象。共享储能分摊成本储能投资成本折算到日的容量费用或按各用户实际使用电量分摊的服务费。这一步直接把“共享”特性引入模型。弃热惩罚成本如果蓄热罐容量有限或者调度策略不合理导致无法满足热负荷需求会产生惩罚项。在实际模型中我更倾向于直接用“热负荷需求必须满足”作为硬约束而不用惩罚项这样模型更干净求解也更快。后面会解释为什么。数学上写成标准形式min Σ_t ( C_buy(t) * P_buy(t) ) C_share其中第一项是购电成本C_share是储能分摊成本常数项联合优化时可省略不影响调度策略t是时段编号。2.2 决策变量与状态变量建模之前先把变量列全这是初做优化调度最容易乱的地方。变量分两类决策变量优化求解要输出的量变量含义单位P_buy(t)t时段从电网购电功率kWP_es_ch(t) / P_es_dis(t)t时段储能充/放电功率kWP_eb(t)t时段电锅炉输入电功率kWQ_hs_ch(t) / Q_hs_dis(t)t时段蓄热罐蓄/放热功率kW热Q_heat(t)t时段电锅炉直接供热功率kW热状态变量反映系统状态的量本质上也参与优化SOC_es(t)储能在t时段结束时的荷电状态0~1SOC_hs(t)蓄热罐在t时段结束时的储热状态0~1注意区分决策变量是各时段功率状态变量是各时段末的能量存量。两者通过动态方程关联这是调度模型的核心逻辑。2.3 关键约束条件约束条件决定了模型是否合理、是否可解。我按约束的物理含义分层来写。第一层功率平衡约束电功率平衡是整个模型的地基P_buy(t) P_load(t) P_eb(t) P_es_ch(t) - P_es_dis(t)这里的P_load(t)是用户的基础电负荷非采暖部分。这个约束的含义是从电网买的电一部分给用户直接用一部分给电锅炉一部分充进储能储能放电则反过来抵消一部分购电。注意充放电不能同时进行这在连续线性模型里靠约束保障或者直接用变量上下限 二元变量处理。热功率平衡η_eb * P_eb(t) Q_hs_ch(t) Q_heat(t)电锅炉产生的热量一部分蓄进蓄热罐一部分直接供热。这里η_eb是电锅炉的热效率一般取0.9~0.98。第二层储能动态方程储能SOC的递推关系SOC_es(t) SOC_es(t-1) (η_es_ch * P_es_ch(t) - P_es_dis(t) / η_es_dis) / E_es * Δtη_es_ch和η_es_dis分别是充放电效率E_es是储能容量Δt是时段长度1小时。这个方程说明储能容量小、调度周期长的场景下SOC变化幅度大必须引入约束防止过充过放。第三层蓄热罐动态方程SOC_hs(t) SOC_hs(t-1) (η_hs_ch * Q_hs_ch(t) - Q_hs_dis(t) / η_hs_dis) / E_hs * Δt蓄热罐的状态方程和电池储能几乎一样区别在于能量载体是热能而不是电能损耗体现在蓄放热效率上而不是自放电率如果考虑散热损失可以加一个自损耗系数。第四层设备出力限制所有设备的出力都要限制在安全范围内0 ≤ P_eb(t) ≤ P_eb_max 0 ≤ P_es_ch(t) ≤ P_es_ch_max 0 ≤ P_es_dis(t) ≤ P_es_dis_max SOC_es_min ≤ SOC_es(t) ≤ SOC_es_max第五层热负荷需求约束这是保证调度方案可用的关键Q_heat(t) Q_hs_dis(t) ≥ Q_demand(t)含义是电锅炉直接供热 蓄热罐放热必须满足用户的热负荷需求。不等号的原因是允许“多供”比如蓄热罐里热量放不完多放一点也没关系实际运行时通过阀控调节但不能“少供”。我在实际建模时习惯把热需求约束写成不等号不仅更贴近物理实际而且比等式约束更容易找到可行解。2.4 模型类型从LP到MILP的进化如果不考虑储能充放电不能同时进行这个逻辑约束模型是线性的LP直接用linprog就能求。但这样有一个问题在分时电价下模型可能出现“同时充电又放电”的荒谬结果——因为从数学上看这样操作不会增加成本但不合理。解决办法有两种一是加约束P_es_ch(t) * P_es_dis(t) 0这是非凸约束会让问题变成MINLP求解困难不建议。二是引入二元变量令u_ch(t)和u_dis(t)为0-1变量分别表示充电状态和放电状态再加约束0 ≤ P_es_ch(t) ≤ u_ch(t) * P_es_ch_max 0 ≤ P_es_dis(t) ≤ u_dis(t) * P_es_dis_max u_ch(t) u_dis(t) ≤ 1这样就变成混合整数线性规划MILP用intlinprog或通过YALMIP调用Cplex/Gurobi求解。实际场景下我推荐用YALMIP因为建模语法简洁约束条件多的时候不容易出错。3. Matlab代码实现思路3.1 整体程序框架我习惯把代码分成四个模块模块之间用脚本串联这样调参、排查问题都方便参数初始化模块定义电价、负荷曲线、设备参数、时间参数。模型构建模块用YALMIP定义变量、目标函数、约束条件。求解模块设置求解器参数调用求解并检查求解状态。结果分析模块提取变量绘制功率平衡图、SOC曲线、成本对比表。3.2 关键参数设置先看基础参数怎么设置。以我用的算例为例%% 时间参数 T 24; % 调度周期小时 dt 1; % 时间步长小时 %% 分时电价元/kWh price zeros(1, T); price(1:6) 0.32; % 谷段 00:00-06:00 price(7:10) 0.78; % 峰段 07:00-10:00 price(11:14) 0.52; % 平段 11:00-14:00 price(15:18) 0.78; % 峰段 15:00-18:00 price(19:22) 0.52; % 平段 19:00-22:00 price(23:24) 0.32; % 谷段 23:00-24:00分时电价的时间段划分必须和当地实际政策对齐。我在做这个项目时参考的是北方某城市的一般工商业分时电价峰谷比大约是2.4:1这个比例直接决定了储能套利的空间。如果峰谷价差低于0.3元/kWh储能基本没有经济性。热负荷曲线需要根据建筑面积、采暖指标和天气情况估算。非居民用户的热负荷特征可以用下面的方式生成%% 热负荷曲线kW % 假设办公楼白天用热夜间保温 Q_demand zeros(1, T); Q_demand(1:7) 15; % 夜间保温热负荷 Q_demand(8:18) 55; % 白天正常采暖 Q_demand(19:24) 20; % 晚间降低这里把热负荷做了简化处理实际项目中应该用DeST或EnergyPlus这类建筑能耗软件先模拟再把模拟结果作为已知条件输入优化模型。3.3 核心代码片段解析这是整个工程的核心。我用YALMIP来建模因为它只需要三行定义变量、写约束、调用求解器。%% 定义决策变量YALMIP sdpvar P_buy sdpvar(1, T, full); % 购电功率 P_eb sdpvar(1, T, full); % 电锅炉电功率 P_es_ch sdpvar(1, T, full); % 储能充电功率 P_es_dis sdpvar(1, T, full); % 储能放电功率 u_es_ch binvar(1, T, full); % 充电状态 u_es_dis binvar(1, T, full); % 放电状态 SOC_es sdpvar(1, T, full); % 储能SOC Q_hs_ch sdpvar(1, T, full); % 蓄热罐蓄热功率 Q_hs_dis sdpvar(1, T, full); % 蓄热罐放热功率 Q_heat sdpvar(1, T, full); % 电锅炉直接供热功率 SOC_hs sdpvar(1, T, full); % 蓄热罐储热状态变量定义过程中有个值得注意的细节SOC(1)作为初始时刻的状态它的值从哪来如果是日循环调度一般要求调度周期结束时SOC回到初始值即SOC_end SOC_start这样才能保证第二天的调度有足够的灵活性。这个约束叫“周期边界约束”我在做的时候专门踩过这个坑后面会细说。约束条件写起来比较繁琐但逻辑很清晰%% 约束条件 Constraints []; % 电功率平衡 for t 1:T Constraints [Constraints, ... P_buy(t) P_load(t) P_eb(t) P_es_ch(t) - P_es_dis(t)]; end % 热平衡 for t 1:T Constraints [Constraints, ... eta_eb * P_eb(t) Q_hs_ch(t) Q_heat(t)]; end % 热需求 for t 1:T Constraints [Constraints, ... Q_heat(t) Q_hs_dis(t) Q_demand(t)]; end % 储能SOC动态 for t 2:T Constraints [Constraints, ... SOC_es(t) SOC_es(t-1) (eta_ch * P_es_ch(t) - P_es_dis(t)/eta_dis) / E_es]; end SOC_es(1) 0.3; % 初始SOC SOC_es(T) 0.3; % 周期边界保证日循环可持续 % 充放电互斥约束 for t 1:T Constraints [Constraints, ... P_es_ch(t) u_es_ch(t) * P_es_ch_max, ... P_es_dis(t) u_es_dis(t) * P_es_dis_max, ... u_es_ch(t) u_es_dis(t) 1]; end % 设备上下限 Constraints [Constraints, 0 P_eb 150]; Constraints [Constraints, 0 P_es_ch 80, 0 P_es_dis 80]; Constraints [Constraints, 0.1 SOC_es 0.9]; Constraints [Constraints, 0 Q_hs_ch 100, 0 Q_hs_dis 100]; Constraints [Constraints, 0.1 SOC_hs 0.95];目标函数和求解部分%% 目标函数最小化购电成本 Objective sum(price .* P_buy); %% 求解 ops sdpsettings(solver, cplex, verbose, 1, showprogress, 1); sol optimize(Constraints, Objective, ops); % 检查求解结果 if sol.problem 0 P_buy_opt value(P_buy); SOC_es_opt value(SOC_es); else disp([求解失败错误码: , num2str(sol.problem)]); end3.4 求解器选型这里多说一句求解器。YALMIP只是建模语言真正求解还是看底层求解器。我测试过三种方案求解器类型优缺点intlinprogMATLAB自带MILP无需额外安装但求解速度一般大规模问题吃力CplexIBMMILP速度快稳定但需要商业授权学术免费GurobiMILP性能与Cplex相当学术免费业界常用建议优先用Gurobi或Cplex。这个模型的规模很小T24时变量不超过150个约束不超过300条即使最普通的求解器都能几秒求出最优解。但如果把时间粒度细化到15分钟T96变量数量涨到600个带二元变量后求解时间会明显增加这时候Cplex/Gurobi的优势就出来了。4. 算例设计与结果分析4.1 基准场景参数我设计了一个典型的非居民园区场景一栋建筑面积10000平方米的办公楼冬季热负荷指标约40W/平米峰值热负荷约400kW考虑同时系数。为了让算例在普通电脑上跑得快我给了一些“缩水”的参数——实际工程中电锅炉和储能的容量会更大但模型结构完全一样。参数数值调度周期24小时步长1小时基础电负荷峰值120kW谷值30kW热负荷峰段55kW保温段15kW电锅炉额定电功率150kW效率0.95储能容量/功率100kWh / 80kW充放电效率0.95蓄热罐容量/功率200kWh / 100kW蓄放热效率0.9储能初始SOC0.3蓄热罐初始状态0.3基础电负荷取多少直接影响储能套利空间。如果用户全天的电负荷都很大储能放电的高峰时段正好是基础电负荷的高峰两者叠加会让变压器容量不够用这时候储能的“削峰”价值就体现出来了。我算例里把基础负荷和热负荷叠加之后系统峰值出现在10点和17点左右和电网晚高峰基本重合这也是为什么需要储能转移负荷。4.2 调度结果分析跑完模型之后我最关心的三张图第一张电功率平衡图。这张图能直观看到储能什么时候充电、什么时候放电。结果一点不意外储能集中在0点到6点谷电时段充电上午9点-11点峰值时段放电下午15点-18点第二次放电。一天两次“充放循环”这就是共享储能和单用户储能最大的不同——负荷曲线叠加之后峰值时段有了两个储能的利用次数从“一天一充一放”变成“一天两充两放”单位能量成本下降经济性更好。第二张SOC变化曲线。储能SOC的轨迹应该是平滑的“上升-下降-上升-下降”形态。如果曲线出现锯齿状频繁充放交替说明电价波动太频繁或者约束逻辑有问题。蓄热罐的SOC则和电锅炉的运行状态强相关夜间谷段电锅炉不但直接供热还把多余热量蓄进罐里白天电价高时电锅炉降功率蓄热罐放热补足差额。第三张成本对比柱状图。我做了三个方案对比无储能、无蓄热电锅炉直供全部负荷按实时电价购电。仅蓄热式电采暖夜间蓄热白天放热。蓄热式电采暖 共享储能两者协同调度。结果基于我设的参数方案2相比方案1节省电费约18%方案3相比方案1节省约29%。方案3比方案2多出来的那部分收益就是共享储能“套取峰谷价差”和“削峰转移负荷”贡献的。注意这个数字依赖峰谷价差和负荷曲线形态换成峰谷比1.5:1的场景储能收益会大幅缩水。4.3 敏感性分析做完基准场景我习惯再做两组敏感性分析这也是投论文或做项目汇报时审稿人和领导最爱问的问题。储能容量对成本的影响把储能容量从50kWh逐步增加到300kWh观察总成本变化。结果是成本先快速下降然后趋于平缓最后反而略有上升。平缓的原因是储能容量超过某阈值后边际充放循环次数已经饱和上升的原因更微妙——容量大了日均分摊成本增加但调度优化只能在“多充放多省钱”和“多分摊多花钱”之间找平衡。这个拐点就是储能最优容量。峰谷价差对调度策略的影响把峰谷价差从0.3元/kWh逐步调到1.0元/kWh观察储能充放电量变化。价差小的时候储能只在深夜充一次电、白天放一次电价差拉大之后模型会主动安排“上午放、午间充、下午再放”的两次循环。这说明储能运营策略不是固定不变的而是要跟着电价信号走。5. 常见问题与排查技巧实录5.1 求解失败或找不到可行解这是初学者最容易碰到的坑。我遇到过一次infeasible problem当时排查了很久最后定位到问题是热平衡约束里电锅炉效率和热功率单位不一致。电锅炉的η_eb 0.95热量单位是kW热而从电功率换算过去的时候老是差一个效率因子最终导致热需求约束在某些时段无法满足。排查思路先去掉上层约束比如去掉蓄热罐动态约束看模型能不能求解。如果能求解说明问题出在被去掉的那一组约束上再逐层加回来二分法定位问题约束。这个方法听着笨但非常有效。5.2 周期边界约束遗漏如果调度周期结束时SOC没有回到初值那么算出来的“最优策略”是短视的——它会倾向于在最后一个时段把储能能量全部耗光因为这样省当天的电费。但第二天储能就没电了实际运行中根本不可行。解决方案是设一个末端SOC约束例如SOC_es(T) SOC_es(1)或允许在一定范围内比如0.25~0.35。我实际项目中一般设0.3 ± 0.05的容差范围目的是给次日调度留一点灵活性。5.3 充放电同时出现的荒谬结果不加0-1互斥约束直接跑LP模型大概率会看到某一时段P_es_ch和P_es_dis同时大于零。这源于线性规划“不关心物理是否合理只关心成本最低”。加了互斥约束之后这个问题消失。不过要注意如果使用Cplex的MILP求解器0-1变量数量从24个增加到48个因为充放各需要一个状态变量求解时间会小幅度增加。对这个规模的问题完全无感但如果扩展到8760小时全年优化变量爆炸会非常严重那时候需要另想办法。5.4 YALMIP求解器配置常见的坑YALMIP报错solver not found十有八九是求解器没有正确添加到MATLAB路径或者求解器依赖的许可证没配置好。我建议Cplex/Gurobi安装完成后在MATLAB里运行yalmiptest会列出来当前可用的求解器。设置sdpsettings(solver, gurobi)之前先确认GUROBI_HOME环境变量已设置。如果不想折腾商业求解器intlinprog完全够用——刚才这个模型用intlinprog求解耗时不到1秒。5.5 热负荷数据的颗粒度问题项目初期我图省事用24小时分辨率的典型日热负荷曲线直接跑。结果发现夜间保温负荷和白天采暖负荷之间那个“爬坡”时段早上6点到8点之间热负荷陡然上升蓄热罐放热功率瞬时拉满造成约束撮合困难。后来把数据细化到15分钟间隔模型更精确但是求解时间明显增加。这个取舍要看项目的实际需求方案比选阶段用24小时粒度就够了做详细设计的时候再切到15分钟。没必要一开始就把模型做大调试起来非常痛苦。5.6 极端天气场景下模型失效有一个我在项目交流中听到的案例寒潮来袭那几天热负荷峰值远超典型日数据调度模型输出一个极端方案——电锅炉满负荷运行、蓄热罐放热功率拉满、储能也全力放电但仍然满足不了热需求。模型给出的“最优解”实际上是“尽力而为”的不可行解。这个问题在日前调度里很难完全避免工程上的标准做法是在热需求约束里允许一定比例的缺供加松弛变量但缺供量有惩罚成本或者用“最冷日”数据重新生成保守调度计划我的建议是在模型里同时保留硬约束和松弛变量两层结构正常天气用硬约束极端天气启用松弛变量这样程序不用改逻辑改参数就行。6. 延展思考与改进方向做完这个项目之后我复盘时想过几个可以继续深入的方向一是从“日前确定性优化”走向“鲁棒优化”或“随机规划”。热负荷预测不可能100%准确室外温度变化、人员密度变化都会影响实际热需求。如果把热负荷描述为不确定集用鲁棒优化做日前调度得到的方案会比确定性模型更抗风险。代价是模型规模和求解难度明显上升。二是共享储能的“多主体博弈”建模。我在这个项目里做了简化处理——所有用户作为一个联合体优化这是“合作博弈”的视角。实际园区里各用户的用能特性不同对储能服务的需求也不同怎么公平分配储能容量和费用涉及Shapley值或Nash均衡的问题。这个方向非常适合作为深入的学术研究课题。三是把电采暖建筑的热惯性直接建模进去。蓄热罐是显式热储能建筑本身的围护结构就是隐式热储能。把建筑室内温度作为状态变量在保证舒适度约束的前提下利用建筑热惯性“提前开、提前关”电锅炉相当于多了一层可调资源。这部分需要和建筑热工模型耦合逻辑会更加复杂但实际节能效果很可观。我在实际做这个模型的过程中最大的体会是优化的价值不在于把复杂问题算出一个漂亮的最优解而在于把业务逻辑用数学语言表达清楚。很多时候约束条件写得比目标函数更关键——约束决定了方案的可行性目标函数只是决定了方案的优劣。从“能跑出结果”到“结果符合实际”中间差的就是对物理机理的理解深度。最后分享一个非常实用的小技巧无论模型跑得多顺都要在代码里留一个“参数打印”的开关。把每次运行的电价、负荷、设备参数、求解状态、目标函数值都打印到日志文件里。这样后续调参的时候你能清楚地知道是哪一次改动导致结果变化而不是靠记忆和猜。我吃过一次亏改了一版电价数据结果忘了删旧的变量文件跑出来结果怎么都对不上白白浪费了一个下午排查。从那以后参数版本管理就刻进我的习惯里了。