ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

建筑光储系统双层优化建模:改进粒子群算法与Python复现

建筑光储系统双层优化建模:改进粒子群算法与Python复现

1. 先把问题拆清楚:这个优化模型到底在优化什么

刚看到“建筑集成光储系统规划运行综合优化”这个题目的时候,我第一反应是这东西听着挺绕,拆开以后其实逻辑很直接。建筑集成光储系统,说白了就是一栋楼或一个园区,屋顶铺了光伏,配电房里放了电池储能,同时还要从电网买电、可能还有余电上网。所谓“规划”,是你在建设之前要拍板的事情:光伏装多少千瓦、储能配多少容量、逆变器功率选多大;所谓“运行”,是系统建好之后每一天的管理策略:电池什么时候充电、什么时候放电、充放功率多少、要不要从电网多买点电存起来。

这篇论文的核心工作,就是把“规划”和“运行”两个问题叠在一起求解,而不是先拍一个规划方案,再去算运行策略。为什么这两件事要叠在一起?因为规划方案的好坏,必须通过运行效果来评价。你储能配大了,可每天的电价差和光伏出力根本跑不满这个容量,那这笔投资就是浪费;你储能配小了,光伏大发的时候存不下、晚上高价时段又不够放,削峰填谷的效果就打了折扣。所以规划方案是“因”,运行收益是“果”,这两者必须放在同一个优化框架里去算,才能得到真正经济性最优的方案。

解决这个问题的工具箱,题目里也点明了:改进粒子群算法。粒子群算法(PSO)在电力系统优化里算是家常便饭了,但直接拿标准版本去解这种高维、强约束、非线性的双层优化问题,效果往往不好。论文的贡献在于对PSO做了几个针对性改进,让它在“规划+运行”这种复杂搜索空间里能稳定找到高质量解。整篇博文我会围绕三件事来讲:第一,这个数学模型怎么搭;第二,改进粒子群算法到底改在哪些地方;第三,Python代码复现的时候有哪些细节值得较真。

这篇内容适合谁看?打算复现EI论文做课题研究的在校学生,做园区综合能源方案设计的工程师,以及想系统了解光储系统优化建模逻辑的入门者。我会尽量把这套东西从“论文黑话”翻译成“能直接上手的代码逻辑”。

2. 规划与运行为什么要放进同一个优化框架

2.1 两层决策变量的关系:上下层之间的数据流

先把这个问题的数学脉络捋一下。上层是规划层,决策变量是光伏装机容量、储能额定容量、储能额定功率,甚至可能包括柴发或者冷热电联供设备的容量,但光储系统场景下主要就是前面三个。下层是运行层,决策变量是每一个调度时段内的储能充放电功率、光伏上网比例、电网交互功率等。

这里的关键在于:下层的运行优化,需要把上层给出来的规划方案当作已知参数带进去;下层的运行结果(典型日运行成本、弃光率、购电成本等)又会反馈给上层,作为评价规划方案优劣的依据。这就形成了典型的嵌套式主从优化——决策变量之间不是你写你的、我写我的,而是存在明确的因果依赖。

可能有人觉得:那我先把规划方案定下来,然后单独跑运行优化不就行了?不行。问题在于规划方案空间很大,你不可能穷举所有容量组合再去跑运行优化。而且规划变量和运行变量的“耦合性”特别强,比如储能容量和充放电策略就是强耦合的:同样的容量,用“每天浅充浅放”和“深度充放一次”两种策略,寿命损耗和经济性差距非常大。论文用双层迭代同时搜这两类变量,本质上是把容量设计和运行策略看作一个联合搜索空间来处理。

2.2 为什么要用典型日而不用全年8760小时

如果你第一次接触这类问题,可能会犯一个错误:直接把全年8760小时的运行数据全部扔进优化模型。学生时代我也这么干过,结果就是模型规模爆炸,收敛速度感人,一个算例跑一晚上都不见得能出结果。

论文里的做法是用典型日来代表全年运行工况。常见的是按季节选四个典型日,每个典型日采用24个时段(也有人用96个时段,每15分钟一个点),每个典型日对应一个权重系数,模拟该季节在全年中的天数占比。这样上层每个规划方案只需要跑四个典型日的运行优化,计算量从8760小时降到了4×24个时段,削减了两个数量级。

选典型日这件事不能太随意。通常的做法是对光伏出力系数、负荷曲线做聚类分析,把全年数据聚成4类,每类聚类中心就是该季节的典型日。如果没有聚类工具,也可以手动选取每个季节具有代表性的晴天和阴天工况。但不管怎么选,都要注意典型日的运维时间尺度和目标函数里的全年指标(比如设备寿命损耗)需要做加权折算,否则全年经济性评估会出现偏差。

2.3 目标函数:经济性不止是“电费最低”

看原文目标函数的时候,我发现它的成本构成比想象中丰富,这也是这类论文“EI味”比较重、但实际很合理的地方。总成本通常由四块叠加:

一是初始投资年化成本。光伏组件、储能电池、逆变器等设备的采购和安装费用,按照设备寿命进行等年值折算。储能的循环寿命一般在3000到6000次,光伏寿命在25年左右,这两种设备的寿命不同,年化折算方式也不同,不能直接相加了事。

二是运行维护成本。光伏和储能的运维费用一般按装机容量和充放电量的比例估算,数值不大但必须写进目标函数里,因为论文评审专家会盯这部分。

三是购电成本。这是运行层最核心的部分,规则是按分时电价计算:高峰时段电贵、低谷时段电便宜,储能的价值正好体现在低价充电、高价放电的电价差套利。

四是弃光惩罚或环保惩罚项。有时候目标函数里还会加一个弃光率惩罚,用来防止优化结果为了省钱而随意把光伏出力浪费掉。这个权重项的设置非常敏感,我后面会专门讲。

最后目标函数是把“年化投资+年运维+年购电成本+惩罚项”全部累加求最小值。要注意的是:规划层的投资成本是一次性的,运行层的成本是多年重复发生的,两者必须统一到同一个时间尺度上(通常用等年值法折算),这是建模时最容易忽略但最容易出错的地方。

3. 改进粒子群算法:改进点到底在哪里

3.1 标准PSO在光储规划问题上的“无能为力”

要说清楚改进版做了什么,得先明白标准PSO在这个问题面前有多吃力。标准PSO的迭代公式是每个粒子根据个体历史最优(pbest)和群体历史最优(gbest)来更新速度和位置,收敛快、实现简单,但放在光储规划这个场景里有几个硬伤。

第一,搜索空间维度太高。一个粒子的位置要同时包含光伏容量、储能容量、储能功率,以及四个典型日每个时段蓄电池的充放电功率。如果按4个典型日×24时段算,一个粒子的维度大约是3+4×24=99维。标准PSO在这个维度下粒子之间信息交换效率会明显下降,种群容易陷入局部最优。

第二,约束条件又多又杂。规划层有容量上下限约束,运行层有蓄电池SOC连续性约束、充放电功率约束、功率平衡约束、电网交互功率上限约束。标准PSO本身不处理约束,传统做法是罚函数,但罚函数系数一调不好,要么“罚不动”——解明显越界了却依然被当作好解保留下来;要么“罚太狠”——几乎所有搜索方向都被拒绝,粒子动都不敢动。这一点我复现的时候深有体会,一开始用的整体罚函数法,跑出来全是不可行解。

第三,光储系统的运行调度具有明显的时段耦合特性——蓄电池今天的SOC会影响到明天凌晨的充放电空间,这一点和典型日之间的衔接还有额外约束。标准PSO完全是随机搜索,容易产生锯齿状、频繁充放的不合理策略,导致SOC曲线像心电图一样上下乱跳,显然不符合电池实际运行约束。

3.2 改进策略拆解:惯性权重衰减只是基础操作

大多数面向工程应用的改进PSO文章,第一个改进点都是惯性权重。标准PSO一般在0.4到0.9之间取一个固定值,改进算法通常让惯性权重随迭代次数线性递减,从0.9降到0.4。背后的逻辑是:迭代前期需要较大的“步长”去充分勘探全局解空间,后期则希望粒子在小范围精细搜索,让惯性权重收敛到小值。这个策略本身不新鲜,但胜在稳定,几乎所有光储优化论文都会用它,不容易翻车。

除了惯性权重,论文更核心的改进点我觉得在约束处理机制上,而不是传统PSO直接套罚函数。常用的做法有两种。一种是把约束条件改造成启发式变异修复策略:对蓄电池功率越界的个体,直接把功率值钳位到边界;对不满足功率平衡的解,通过电网交互功率补齐差额。这种“硬修复”比罚函数更符合物理实际——系统运行时本来就需要实时平衡,不能容忍目标函数被罚一下就蒙混过去。

第二种常见做法是可行性规则排序:比较两个粒子适应度时,先比可行性,可行解优于不可行解;两个都是可行解时再比适应度值;两个都不可行时,比较约束违反总量。这种做法避免了罚函数需要反复调整系数的麻烦,而且不会让搜索偏向某些特定方向。实测下来,可行性规则+边界修复的组合,比单纯罚函数收敛更稳、结果更可解释。

3.3 改进后的算法执行流程

把整个算法串起来,流程是这样的:

  1. 初始化种群,每个粒子的位置向量包含规划层变量和运行层变量,同时对规划变量做区域内随机生成,对运行变量确保SOC初始值和充放电功率不超过边界。
  2. 对每个粒子,先取出规划变量,代入运行层子函数,求解各典型日的最优运行策略(可以用枚举搜索、也可以用内层PSO或线性规划方法),返回各典型日运行成本和约束违反量。
  3. 把运行成本和年化投资成本加权求和,作为该粒子的适应度值,同时记录约束违反总量。
  4. 根据可行性规则更新个体历史最优和群体历史最优。
  5. 按PSO速度更新公式更新粒子位置,对越界变量做边界修复,然后进入下一轮迭代。
  6. 直到达到最大迭代次数,或者群体最优连续若干代不再显著变化,输出最优规划方案。

这里的内层运行优化是性能瓶颈。如果直接用暴力枚举每个时段的充放电功率,计算量会非常大;更聪明的做法是把运行层看作一个带功率平衡约束的线性规划或二次规划问题,用线性规划求解器或动态规划来处理。我在复现时用的是动态规划思路——光伏出力和负荷曲线已知,每个阶段只需决定蓄电池充放策略,目标是从电网购电成本最低。这样内层求解速度非常快,外层粒子群迭代几百代也只需几分钟。

我整理了标准PSO与改进PSO在这类问题上的对比,方便大家理解:

对比维度标准PSO改进PSO
惯性权重固定值,一般在0.4-0.9线性递减或自适应调整,前期勘探、后期收敛
约束处理整体罚函数,系数难调可行性规则+边界修复+动态惩罚
高维搜索容易陷入局部最优引入社会/认知学习因子自适应、变异机制增加多样性
内层运行嵌套直接计算,成本高用线性规划/动态规划解耦,显著提速
结果质量可能给出不可行解或SOC锯齿波动解可行、SOC曲线平滑、符合物理规律

4. Python代码实现:从数学公式到能跑的结果

4.1 程序的整体架构设计

复现时我建议把代码拆成四个模块:数据模块、优化模型模块、改进粒子群算法模块、结果可视化模块。不要让所有逻辑堆在一个主脚本里,否则后面调参和排查问题会非常痛苦。

数据模块负责读入分时电价、光伏出力系数、负荷曲线、典型日权重、设备成本参数和寿命参数。优化模型模块封装目标函数计算、约束条件判断、运行子问题的求解逻辑。改进粒子群算法模块是独立的,它不依赖具体问题——你给它一个“适应度计算函数”和“边界范围”,它就能跑优化。这样设计的好处是:如果以后想换算法(比如换成遗传算法或灰狼算法),只需要替换算法模块,不改动模型模块。

典型物理参数我按一个中等规模建筑来设置:光伏装机150-300 kW区间内寻优、储能容量200-800 kWh区间内寻优、储能功率50-200 kW区间内寻优。分时电价按当地工商业电价设置,比如峰时1.2元/kWh、平时0.75元/kWh、谷时0.35元/kWh。光伏成本按每千瓦3500-4500元估算,储能系统按每千瓦时800-1000元估算(不含PCS)。寿命方面,光伏按25年、储能按10年或6000次循环折算。

4.2 双层求解的控制流实现

外层粒子群是主循环,内层运行优化作为子函数。核心的控制流我简化成下面这段伪代码风格的Python代码,方便大家理解框架:

# 外层:粒子群搜索规划变量 for iteration in range(max_iter): for i in range(pop_size): # 从粒子i中提取规划变量:光伏容量、储能容量、储能功率 pv_capacity = particles[i, 0] battery_capacity = particles[i, 1] battery_power = particles[i, 2] # 将规划变量传入运行层,计算四个典型日的运行成本 annual_operation_cost = 0.0 total_violation = 0.0 for day_idx in range(num_typical_days): # 内层运行优化:动态规划求最小购电成本 daily_cost, violation = run_operation_schedule( pv_capacity, battery_capacity, battery_power, day_idx ) annual_operation_cost += day_weight[day_idx] * daily_cost total_violation += violation # 年化投资成本 + 运行成本 = 总成本 investment_cost = calculate_annualized_investment( pv_capacity, battery_capacity, battery_power ) fitness = investment_cost + annual_operation_cost # 可行性规则更新pbest和gbest particles[i].update_fitness(fitness, total_violation) # 更新惯性权重、粒子速度和位置 inertia = 0.9 - (0.9 - 0.4) * (iteration / max_iter) # ... 速度和位置更新代码

这里的run_operation_schedule是整个程序的关键。它接收光伏容量和储能参数,结合典型日的光伏出力系数和负荷曲线,计算24个时段内储能应该在什么时候充电、什么时候放电,以及每个时段从电网买多少电。注意到一个细节:运行层必须知道储能初始SOC是多少,这通常取自前一个典型日的末段SOC。如果四个典型日彼此独立求解,SOC无法衔接,就会导致优化结果低估了储能的实际可用容量。

4.3 内层运行调度的动态规划实现

运行层的本质是:光伏出力和负荷都是已知的(至少在典型日优化这个时间尺度上是确定的),储能是自己的可控资源,分时电价已知,求购电成本最低的充放电策略。

这个问题用动态规划来解,思路是把储能SOC离散化为若干个档位(比如0到100%,按5%一个间隔,共21个档位),然后从第1时段到第24时段逐步推进,记录每个时段在每个SOC档位下的最小累计购电成本。状态转移时,根据充放电功率计算SOC变化,进而推算出该时段的电网交互功率和购电成本。虽然SOC离散化会带来一定精度损失,但好处是稳定、快速、不依赖商用求解器,而且天然支持SOC约束。

def run_operation_schedule(pv_capacity, battery_capacity, battery_power, day_idx): # 读取典型日的光伏出力和负荷曲线 pv_output = pv_profiles[day_idx] * pv_capacity load = load_profiles[day_idx] # 定义SOC离散档位,例如0~100%,步长5% soc_levels = np.arange(0, 1.01, 0.05) # DP表:dp[t][i] 表示第t时段结束、SOC为第i档时的最小累计购电成本 dp = np.full((num_time_slots + 1, len(soc_levels)), np.inf) dp[0][10] = 0 # 初始SOC=50% for t in range(num_time_slots): for i in range(len(soc_levels)): if dp[t][i] == np.inf: continue current_soc = soc_levels[i] for charge_rate in possible_power_options: new_soc = current_soc + delta_soc(charge_rate, battery_capacity) # 将new_soc映射到最近离散档位 j = np.argmin(np.abs(soc_levels - new_soc)) net_load = load[t] - pv_output[t] + charge_rate if net_load > 0: cost = dp[t][i] + net_load * price[t] else: cost = dp[t][i] # 余电上网按较小收益处理 if cost < dp[t+1][j]: dp[t+1][j] = cost return np.min(dp[num_time_slots]), 0.0

这段代码表达的是DP的核心思想,实际应用时还要考虑充放电效率(通常在0.9到0.98之间)、SOC上下限约束(电池不建议放空也不建议充满,一般限制在10%-90%)、以及单位时段内最大充放电功率约束。

4.4 粒子群更新中的边界修复与自适应参数

速度更新公式不必多说,值得强调的是边界修复这个问题。储能功率为负表示充电、为正表示放电,粒子在更新的时候很容易产生一个既大于功率上限又小于功率下限的数值(比如600 kW,明显超出电池PCS能力)。这时候不能简单把数值截断到边界就完了,因为数值截断会导致大量粒子聚集在边界,多样性急剧下降。我采用的修复方式是:如果越界,则将对应维度的数值置为边界,同时以一定概率对该维度做随机重置,保证粒子还有机会跳出“边界陷阱”。

另一个实用的改进是让学习因子也自适应变化。迭代前期让c1偏大,鼓励粒子向自己的历史最优学习,保持个体探索能力;迭代后期让c2偏大,鼓励粒子向全局最优靠拢,加快群体收敛速度。我实测的自适应公式是:

def update_velocity(particle, pbest, gbest, iteration, max_iter): w = 0.9 - 0.5 * (iteration / max_iter) c1 = 2.5 - 1.5 * (iteration / max_iter) c2 = 0.5 + 1.5 * (iteration / max_iter) r1 = np.random.rand(n_dim) r2 = np.random.rand(n_dim) velocity = (w * particle.velocity + c1 * r1 * (pbest.position - particle.position) + c2 * r2 * (gbest.position - particle.position)) return velocity

这些参数看起来简单,但在复现论文的时候是最容易和原结果对不上的地方。不同论文里c1、c2的初值和终值差异很大,复现时不要死搬硬套,应该结合自己的算例规模做敏感性分析。

5. 复现过程中最值得记录的四个大坑

5.1 光伏出力曲线与负荷曲线的时间轴必须对齐

这个坑我在第一次复现的时候踩得很惨。论文给的光伏出力典型曲线可能是以“每小时的太阳辐照度”标定的,而负荷曲线可能是以“15分钟一个采样点”给出来的。如果不做插值重采样直接把两组数据相乘,出来的“净负荷”曲线锯齿严重,储能的充放电策略也会被带着乱跳。复现时务必要统一时间分辨率。时间分辨率的选择会影响储能循环寿命损耗的评估精度——15分钟一个时段的SOC波动比1小时一个时段的SOC波动更剧烈,折算出来的寿命衰减可能差出30%。

5.2 目标函数里投资成本与运行成本的“时间口径”必须一致

这是另一个很容易在复现结果里出现数量级错误的地方。初始投资是“一次性投入”,而购电成本是“每年都发生的”。正确做法是把初始投资按等年值法折算成“每年均摊成本”,再和年运行成本相加。折算公式是A = P * r * (1+r)^n / ((1+r)^n - 1),其中r是折现率,n是设备寿命。

如果你不折算,直接把“一次性投资”和“年运行成本”放在一起比较,得出的最优方案一定是储能容量越小越好,因为储能投资高得离谱,运行节省的电费根本cover不了投资。这也是为什么很多新手复现光储优化论文时算出来的最优容量总是贴着下边界的原因——不是算法不行,是口径错了。

5.3 SOC的“跨日衔接”问题

论文里如果特意描述了每个典型日如何衔接,你要特别留意。按照物理常识,典型日1的结束SOC应该等于典型日2的起始SOC。但很多复现代码偷懒,把每个典型日都当成独立问题、初始SOC固定设为50%,这样算出来的运行策略会高估储能的可用容量,导致储能容量优化结果偏高。我处理的方案是:先以多个典型日为一个“连续性周期”,让最后一个典型日的结束SOC回到初始值,这样既保证能量守恒,又让储能容量优化有一个合理的物理边界。

5.4 “假收敛”与粒子多样性丧失

PSO的早期收敛问题非常隐蔽。有时候程序跑得很好,目标函数值连降几十代,你以为快收敛了,结果把gbest拿出来一看,储能容量竟然停在初始值附近——其实是粒子速度过早收敛到0,整个种群已经“僵化”了,而不是真的找到了最优解。排查方法很简单:在迭代过程中把粒子的位置标准差打印出来,如果标准差在迭代中期就趋近于0,说明种群失去多样性,需要增加变异操作、重新初始化部分粒子、或者增大惯性权重。实测下来,每迭代10代随机重置5%的粒子,能明显缓解这种早熟收敛。

6. 复现论文的边界和可以继续做的事

6.1 论文复现不等于“完全复刻结果”

很多人第一次复现EI论文,总盼着能和原文结果完全一致。以我多次复现的经验,这个想法基本要放弃。论文里很多关键参数并不一定在正文中完整公开——比如典型日选取的具体细节、储能SOC上下限的精确取值、价格曲线的时段划分、甚至光伏组件效率的温度折减系数,都可能被省略。复现的目标应该是“复现方法框架”和“复现趋势规律”,而不是逐位对齐结果。

如果计算出的最优容量和原文在同一数量级、光储经济性优化带来的成本降幅在相近百分点区间、储能的充放电策略在时间分布上吻合,那这篇文章的复现就是成功的。我见过很多同行因为一个系数对不上,耗了一两周在原地打转,不值得。

6.2 在复现基础上可以扩展的三个方向

这个项目的扩展空间其实比论文本身的篇幅大得多。一个方向是引入不确定性——光伏出力和负荷预测都存在偏差,可以在运行层加入鲁棒优化或场景随机规划,让储能容量配置方案在面对预测误差时依然有良好的经济性表现。另一个方向是引入多智能体博弈——如果建筑不止一栋,而是形成微电网群,各楼栋之间可以进行电量交易,规划问题和运行问题就从单主体优化变为多主体博弈,技术难度和论文发表潜力都会上一个台阶。还有一个很实际的方向是把电池寿命模型做得更细——用更准的循环深度折算模型代替简单的吞吐量模型,这样储能收益的评价会更贴近工程实际。

6.3 最后分享一点个人体会

复现这类论文,最大的收获不是“跑通了别人的代码”,而是真正理解了一道优化题目的完整解题思路:从物理系统到数学建模、从模型求解到代码工程化,中间每一步都有大量的隐性知识和工程细节。如果你现在正在复现同类论文,建议先花两周时间把双层模型的数学表达式写得滴水不漏,再开始写代码——写清楚公式的时间投入,一定会在后面调试代码的时候加倍省回来。也建议每一步改动都单独保存一个版本,别一口气改完最后发现结果变差了却找不到是哪一步造成的。踩过几次坑之后我最大的体会是:好的优化代码,一半靠算法,一半靠约束处理和对物理过程的理解,算法反而是投入产出比最低的那个环节。

RELATED READING

延伸阅读

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