news 2026/9/18 3:32:38

2025国赛A题光储系统建模全流程:预测、优化、调度与多目标扩展实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2025国赛A题光储系统建模全流程:预测、优化、调度与多目标扩展实战

2025国赛A题很多人第一眼看到“光储系统”几个字就慌了,觉得又得啃物理、又得搞优化、还得处理一堆气象数据。其实把题目拆开看,这套题就是典型的“预测—优化—调度—扩展”四步走:第一问让你预测光伏出力,第二问在给定约束下配置储能容量,第三问把储能放进动态电价环境里做运行策略,第四问再扩展到多电站甚至多目标。每一问之间都是递进关系,前一问的输出正好是后一问的输入。这篇文章我会把1到4问的建模思路、完整代码、论文写法全部过一遍,包括我复现时踩过的坑和最后总结出来的时间分配方案,适合正在备战国赛、或者想用A题练手找节奏的同学直接抄作业。

1. 2025国赛A题到底在考什么:先把题目逻辑理清楚

1.1 这类题目的本质与1-4问的递进关系

A题这么多年一直有个共性:问题背景听起来复杂,但核心数学工具其实很固定。2025年这道题从回忆版来看,核心场景是光伏电站加储能系统的容量配置与调度优化,本质是带约束的优化问题,只是把数据预测、运行模拟、经济性分析全揉在一起了。

我拿到题第一件事不是写代码,是把四问的输入输出列成一张表:

问题输入输出核心方法
第1问历史气象数据、历史发电功率光伏出力预测模型、典型日发电曲线回归/时序预测
第2问负荷曲线、电价、储能参数储能额定容量与额定功率单目标优化(遗传算法)
第3问动态电价、预测出力、储能配置储能逐时充放电策略混合整数线性规划
第4问多电站数据、共享储能约束Pareto前沿、容量配置方案多目标优化

这样列完就清楚多了。第1问是打底的数据建模,第2问是静态容量优化,第3问把时间维度加进来做运行策略,第4问再加一个“多个电站共享一套储能”的现实约束。评卷时每一问都是独立给分点,所以哪怕第四问没做完,前三问做得干净完整,成绩也不会差。

1.2 整体方案选型:为什么用“预测+优化+调度+扩展”

很多参赛队喜欢一上来就堆深度学习、堆复杂算法,觉得模型越花哨分越高。这个思路在国赛里容易翻车。A题评卷更看重的是逻辑链条是否自洽,模型能不能解释清楚,结果是否合理,而不是算法名词多不多。

所以我推荐的路线是:第1问用随机森林做主要预测模型,再用LSTM做对比;第2问用遗传算法求储能容量,做一版粒子群对照;第3问用整数线性规划,直接调求解器;第4问用NSGA-II,或者退一步用加权法把多目标转成单目标,保证代码能跑出结果。这套组合的优点是每问都有成熟库能调、代码量适中、论文里也方便写清楚“为什么这么选”。

1.3 工具链准备与代码框架

我用的是Python 3.9,核心库是pandas、numpy、scikit-learn、pymoo、PuLP,画图用matplotlib。如果机器性能一般,LSTM可以用TensorFlow的CPU版本,预测数据量不大,跑得动。

代码建议按“data_preprocess.py、q1_forecast.py、q2_capacity.py、q3_dispatch.py、q4_multi_objective.py”这样一个文件对应一问,统一用一个config.py存路径和参数。这么做的好处是论文写“模型建立”章节时,可以顺着代码文件去截图和贴关键逻辑,不会到写论文时翻半天找不到代码。

项目里的数据文件就两个:一个气象历史数据表(含辐照度、温度、湿度、风速),一个电站实际发电功率表(15分钟粒度,连续一年)。拿到数据第一件事就是看时间范围和缺失值分布,别急着建模。

2. 第1问:光伏出力预测——数据预处理与模型选型

2.1 数据清洗与特征构建

这一步看似基础,但做得好不好直接影响后面所有模型的精度。原始数据里常见三个问题:时间戳不连续、辐照度出现负值、发电功率在夜间出现非零值(传感器漂移)。

我的处理流程是先把15分钟数据重采样成小时数据,减少噪声,再对缺失值做线性插值。辐照度小于0的直接置0,功率小于0的也置0,但要做个标记,看一眼是不是系统性故障,如果某个连续时间段全是异常值,那段直接剔除比插值更安全。

特征构建上,除了温度、湿度、风速、辐照度这四个直接气象特征,还加了“前一小时功率”“前两小时功率”“太阳高度角正弦值”这三个衍生特征。为什么加滞后项?因为光伏系统有热惯性,前一小时发电量对当前小时有强相关作用。太阳高度角这个特征在晴天场景下尤其重要,能帮助模型区分早晚低出力、中午高出力的日变化形态。

2.2 预测模型选型与代码实现

随机森林做光伏预测是性价比很高的选择,不需要归一化、不需要调太多超参数,对辐照度和滞后项这类数值特征的处理也稳定。下面是第1问的核心训练代码,我在关键位置加了注释:

import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score data = pd.read_csv('data/pv_data.csv', parse_dates=['time']) data['hour'] = data['time'].dt.hour data['month'] = data['time'].dt.month # 构造滞后特征 data['power_lag1'] = data['power'].shift(1) data['power_lag2'] = data['power'].shift(2) # 太阳高度角正弦近似:用hour做简化映射 data['sin_hour'] = np.sin((data['hour'] - 6) / 12 * np.pi) data = data.dropna().reset_index(drop=True) features = ['irradiance', 'temperature', 'humidity', 'wind_speed', 'power_lag1', 'power_lag2', 'sin_hour', 'hour', 'month'] X = data[features] y = data['power'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = RandomForestRegressor(n_estimators=300, max_depth=12, random_state=42) model.fit(X_train, y_train) y_pred = model.predict(X_test) print('R2:', r2_score(y_test, y_pred)) print('MAE:', mean_absolute_error(y_test, y_pred)) print('RMSE:', np.sqrt(mean_squared_error(y_test, y_pred)))

注意这里的sin_hour不是严格的天文计算,但做特征够用了。如果你想做得更精细,可以用pysolar库计算当地纬度和时刻对应的太阳高度角,但对预测精度的提升有限,比赛时间紧的话不做也行。

2.3 精度评估与可能的评分点

随机森林在这个数据集上R2能做到0.92到0.95,MAE在小时粒度大概5到8千瓦。这就够用了,不用费劲调参。

真正能拉开差距的是两个对比实验:一是晴天、多云、雨天三类天气分别预测,看模型在哪种场景下误差大;二是把随机森林结果与LSTM结果对比,画一张预测曲线对照图,证明“我考虑过多种方法,最后基于xx选择了随机森林”。

我还建议做误差的按小时分布图,比如计算每个月、每个小时的MAE热力图。这类图放进论文里非常加分,因为能直观体现你已经做了细致的结果分析,而不是只丢一个R2就完事。

3. 第2问:储能容量怎么定——遗传算法求最小年成本

3.1 目标函数与约束的数学化

第2问要把工程问题翻译成数学优化问题。题目给的背景一般是:已知第1问预测出的典型日发电曲线,已知负荷曲线,已知储能单位容量造价、单位功率造价、寿命年限,求储能的额定容量和额定功率,使年综合成本最小。

我建的目标函数是:

min C = C_cap * E + C_pow * P + C_om * E - C_save

其中C_cap是容量单价(元/kWh),E是储能额定容量(kWh),C_pow是功率单价(元/kW),P是储能额定功率(kW),C_om是年运维成本系数,C_save是储能带来的年收益,主要通过峰谷套利和减少弃光来体现。

约束包括:

  • 储能SOC连续变化:SOC(t+1) = SOC(t) + (Pch(t)*ηch - Pdis(t)/ηdis) * Δt / E
  • SOC上下限:0.1 ≤ SOC(t) ≤ 0.9
  • 充放电功率限制:0 ≤ Pch(t) ≤ P,0 ≤ Pdis(t) ≤ P
  • 功率平衡:Ppv(t) + Pdis(t) - Pch(t) = Pload(t)

为什么SOC下限设0.1而不是0?因为锂电池深度放电会缩短寿命,论文里写这句能体现你考虑到了工程实际。为什么收益要算进目标函数?因为如果不考虑收益,最优解永远是“不装储能”,题目就没意义了。

3.2 遗传算法求解实现

这个优化问题有约束、有非线性,直接用scipy的minimize容易陷入局部最优,所以我用遗传算法在更大空间里搜。

import numpy as np from pymoo.core.problem import Problem from pymoo.algorithms.soo.nonconvex.ga import GA from pymoo.optimize import minimize class StorageProblem(Problem): def __init__(self, pv_curve, load_curve, price, params): self.pv = pv_curve self.load = load_curve self.price = price self.params = params # eta_ch, eta_dis, cost_cap, cost_pow, ... n_var = 2 # E, P super().__init__(n_var=n_var, n_obj=1, xl=np.array([100, 50]), xu=np.array([2000, 500])) def _evaluate(self, x, out, *args, **kwargs): E = x[:, 0] P = x[:, 1] n = len(self.pv) cost = np.zeros(len(x)) for i, (Ei, Pi) in enumerate(zip(E, P)): SOC = 0.5 # 初始SOC save = 0.0 for t in range(n): net = self.pv[t] - self.load[t] # 盈余为正 # 盈余时充电,缺电时放电 if net > 0: Pch = min(net, Pi, (0.9 - SOC) * Ei / self.params['eta_ch']) SOC += Pch * self.params['eta_ch'] / Ei else: Pdis = min(-net, Pi, (SOC - 0.1) * Ei * self.params['eta_dis']) SOC -= Pdis / (self.params['eta_dis'] * Ei) save += Pdis * self.price[t] cap_cost = self.params['cost_cap'] * Ei + self.params['cost_pow'] * Pi om_cost = self.params['cost_om'] * Ei cost[i] = cap_cost + om_cost - save out["F"] = cost problem = StorageProblem(pv_curve, load_curve, price, params) algorithm = GA(pop_size=100, eliminate_duplicates=True) res = minimize(problem, algorithm, ('n_gen', 200), seed=42, verbose=False) print('Best E:', res.X[0], 'Best P:', res.X[1])

这里有个关键逻辑我要重点说:SOC的更新用的是一阶差分,本质上是把连续时间过程离散化成了小时步长。这一步的代码写对了,第3问的调度模型就是它的扩展版。如果这里SOC算错,后面的结果全废,调试时一定要单独打印一天24小时的SOC变化曲线人工看一眼。

3.3 结果解读与灵敏度分析

遗传算出来的最优配置要结合工程实际检查。如果结果出现E特别大、P特别小的“铅酸电池型”配置,可能是成本系数设置有问题,回数据里检查峰谷价差。反过来如果E很小,说明储能主要起削峰作用,套利空间有限。

灵敏度分析建议做三个维度:储能单位造价下降20%、峰谷价差扩大30%、光伏装机容量增加15%,分别记录最优E、P和总成本的变化。把结果做成柱状图或表格,论文里写“当储能成本下降20%时,最优容量配置提升x%,系统年收益增加y%”,这一小节基本就是送分题。

4. 第3问:动态电价下的充放电调度——MILP模型与求解

4.1 问题从静态到动态的升级

第2问是把储能容量定下来,第3问则是在容量已定的前提下,面对一天24小时波动的电价和光伏出力,求储能的充放电策略。这就从容量优化问题变成了调度问题。

这里要引入0-1变量,因为同一时刻储能不能既充电又放电。为什么不能?物理上一个储能单元同时充放电就是能量浪费,虽然实际系统可能有不同子模块,但模型里必须排除。加了二进制变量后,问题就变成混合整数线性规划MILP,用PuLP或Gurobi这类求解器处理。

4.2 PuLP求解MILP的核心代码

import pulp E = 800 # kWh 额定容量 P = 200 # kW 额定功率 eta_ch, eta_dis = 0.95, 0.95 SOC_min, SOC_max = 0.1, 0.9 pv = pv_curve_24 # 长度为24的预测出力 load = load_curve_24 price = price_24 prob = pulp.LpProblem("Storage_Dispatch", pulp.LpMinimize) Pch = pulp.LpVariable.dicts("Pch", range(24), 0, P, pulp.LpContinuous) Pdis = pulp.LpVariable.dicts("Pdis", range(24), 0, P, pulp.LpContinuous) u = pulp.LpVariable.dicts("u", range(24), cat=pulp.LpBinary) SOC = pulp.LpVariable.dicts("SOC", range(25), SOC_min, SOC_max, pulp.LpContinuous) # 目标:购电成本最小 - 售电收益最大 prob += pulp.lpSum([price[t] * (load[t] - pv[t] - Pdis[t] + Pch[t]) for t in range(24)]) prob += (SOC[0] == 0.5) for t in range(24): prob += (SOC[t+1] == SOC[t] + (Pch[t] * eta_ch - Pdis[t] / eta_dis) / E) prob += (Pch[t] <= u[t] * P) prob += (Pdis[t] <= (1 - u[t]) * P) prob += (load[t] - pv[t] - Pdis[t] + Pch[t] >= 0) # 不允许向电网反送电(或按题目要求限制) prob.solve(pulp.PULP_CBC_CMD(msg=False)) print("Status:", pulp.LpStatus[prob.status]) for t in range(24): if Pch[t].value() > 1e-4 or Pdis[t].value() > 1e-4: print(f"hour {t}: Pch={Pch[t].value():.1f}, Pdis={Pdis[t].value():.1f}, SOC={SOC[t+1].value():.3f}")

注意那个“不允许向电网反送电”的约束,不是每个题目都有,但如果题目背景是“自发自用、余电不上网”,这个约束必须有。如果没有这个约束,模型会在电价高时疯狂放电卖给电网,解出来的收益会乐观得离谱,评卷老师一看就知道你没考虑实际限制。

4.3 不同场景的策略对比与图表呈现

第3问的产出不能只有一个调度方案,要拿不同场景对比。我用三种典型天气做对照:晴天(光伏充足)、多云(光伏波动)、雨天(光伏不足)。每种场景下,把24小时的Pch、Pdis、SOC、电网交互功率四张子图画在一起,用matplotlib的subplots就能实现。

画SOC曲线时有个细节,曲线应该平滑爬升和下降,充电时段SOC上升斜率等于Pch*η/E。如果你算出的SOC曲线出现锯齿状跳变,大概率是SOC更新公式里η放错了位置,赶紧查代码。

论文里要把“低谷充电、高峰放电”的运行规律描述清楚,并比较三种天气下储能日循环次数和日收益。这类对比是评卷老师快速判断你模型是否有效的重要证据,图一定要画得清爽,坐标轴标签和单位不能漏。

5. 第4问:多电站共享储能的Pareto最优——多目标优化写法

5.1 从单目标到多目标的建模变化

第4问通常是把单个光伏电站升级成多个电站共享一套储能。这里的核心矛盾来了:共享储能的总容量有限,A电站多用了,B电站就可能不够;同时不同电站的光伏出力曲线有差异,电价时段一样,怎么分配充放电功率变成一个新的决策维度。

目标函数也从单一的年成本最小,变成至少两个互相冲突的目标:比如系统总成本最小和储能利用率最大。两个目标往往不能同时达到最优,所以解不再是单个点,而是一个Pareto前沿面。

5.2 NSGA-II实现要点

多目标优化里NSGA-II是最稳的选择,pymoo库封装得很好,直接可以调用。

import numpy as np from pymoo.core.problem import Problem from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.optimize import minimize class MultiStorageProblem(Problem): def __init__(self, pv_data, load_data, price, params): self.pv = pv_data # shape: (station_count, 24) self.load = load_data self.price = price self.params = params # 决策变量:共享储能容量E,功率P,每个电站各时段的放电分配比例alpha n_var = 2 + 24 * self.pv.shape[0] super().__init__(n_var=n_var, n_obj=2, xl=np.concatenate(([100, 50], np.zeros(24 * self.pv.shape[0]))), xu=np.concatenate(([2000, 500], np.ones(24 * self.pv.shape[0])))) def _evaluate(self, x, out, *args, **kwargs): obj1 = np.zeros(len(x)) # 总成本 obj2 = np.zeros(len(x)) # 弃电率/储能利用率相关 for i, row in enumerate(x): E, P = row[0], row[1] alpha = row[2:].reshape(24, self.pv.shape[0]) alpha = alpha / alpha.sum(axis=1, keepdims=True) # 归一化 cost, curtail = self._simulate(E, P, alpha) obj1[i] = cost obj2[i] = curtail out["F"] = np.column_stack([obj1, obj2]) algorithm = NSGA2(pop_size=80) res = minimize(problem, algorithm, ('n_gen', 300), seed=42)

看到那个alpha归一化的处理了吗?决策变量里alpha是24小时×电站数量,表示每个小时储能放电功率在各个电站之间的分配比例。归一化这一步很关键,如果不做,求解器生成的一组alpha加起来不等于1,物理意义就崩了。当然你也可以用带约束的处理方式,但归一化最简单、出错概率最低。

5.3 Pareto前沿图与决策建议

跑完之后,画Pareto前沿就是标准的散点图,横轴是弃电率,纵轴是总成本,前沿上的点落在左下角那条凹曲线上。如果画出来的点分布特别乱,没有明显的凹形,大概率是alpha的决策变量维度过高导致搜索不充分,可以增大种群或者增加遗传代数。

论文收尾时,要从Pareto前沿里选出一个推荐方案。选择依据可以写“取曲率最大点”,也就是经济学里的“肘部法则”。把这个点的E、P、alpha还原成实际问题里的储能配置和调度规则,再和单电站独立储能方案对比,说明共享储能带来了多少成本下降。这就是第4问的完整闭环。

6. 论文怎么把代码和模型写成评委能看懂的样子

6.1 摘要与符号说明的写法

很多队代码写得很好,论文却像应付差事。国赛评卷强度很大,摘要基本决定了评委对论文的第一印象。摘要一定要按“问题重述→模型方法→关键结果”三段写,每问一行结果数据,比如“第1问预测模型RMSE为6.8kW,第2问最优储能配置为800kWh/200kW,年节约成本xx万元”。数据必须具体,不能写“取得了较好的效果”这种空话。

符号说明表的排版也容易被忽略。国赛模板里和正文之间最好用一个双栏表格列符号,按照“字母-含义-单位”三列排列,E、P、SOC、η这些核心符号一个都不要漏。注意全文符号必须统一,不要正文里一会用E_t一会用E(t)。我见过好多论文因为这被扣分,实在冤枉。

6.2 模型建立与求解过程的结构

每个问题的标准结构是:问题分析→模型建立→约束解释→模型求解→结果分析。模型建立部分不要直接堆公式,先用中文把思路说清楚,再给出数学公式。公式用Word的公式编辑器写,不要截图。代码不要整段贴进正文,论文里放关键代码段(10行以内),说明核心逻辑,完整代码放附录。

另外,每个模型求解完,建议加一小段“模型检验”,把灵敏度分析结果整理成表贴出来。评卷老师看到你主动做了稳健性检验,会认为你对模型的理解是完整的,这部分在评分细则里往往是优等论文的区分点。

6.3 把代码逻辑“翻译”成数学语言

这是最容易卡壳的地方。很多同学代码能跑,但论文里写不清楚模型。我的做法是每一段代码对应写两个东西:数学表达式和参数表。以第3问为例,MILP的目标函数、SOC递推式、功率约束在代码里就是几行,但论文里要展开成一组带编号的公式。

当你发现代码里的某个判断条件写不进数学公式时,说明这个条件可能是数据清洗层面的临时处理,不该出现在模型里。模型和代码要严格对应,这个对演讲清楚,答辩时也会非常从容。

7. 踩坑实录与问题排查:这些坑我在复现时都踩过

7.1 数据处理阶段的常见坑

第一个坑是时间对齐。光伏出力数据是15分钟粒度,气象数据是小时粒度,不统一就合并。我先用resample把光伏出力聚合成小时均值再与气象数据merge,避免了索引错位。第二个坑是时区问题,数据里的时间如果是UTC,直接建模会导致中午预测成凌晨,功率曲线整体平移12小时。这个问题特别隐蔽,画一遍24小时平均功率曲线就能看出来,如果曲线峰在深夜,第一反应应该是检查时区而不是调模型。

7.2 优化求解阶段的报错与定位

遗传算法跑不出合理结果时,先别调参数,打印每一代的最优值和均值,看收敛曲线。如果均值不降、最优值跳变,大概率是约束没处理好,比如SOC越界。把SOC的上下限约束加成一个惩罚项,比在初始化里硬限制更稳定。

PuLP求解MILP最常见的报错是infeasible,也就是模型无可行解。先别怀疑求解器,回头检查是不是同时加了Pch和Pdis都大于0的约束、却没加互斥变量u。其次是检查负荷约束里是否出现了“某小时必须从电网买电”同时又“收益必须为正”这种矛盾假设。把约束逐个注释掉试跑一遍,很快能定位到哪条约束出的问题。

7.3 时间管理与论文定稿的经验

最后说点非技术层面的经验。三天比赛时间,我的分配是:第一天下午到晚上完成第1问预测模型并出图;第二天上半天完成第2问遗传算法,中下午完成第3问MILP,晚上开始写论文前两问;第三天上午冲刺第4问多目标优化,下午统一绘图、补摘要、整理附录。

有一个我踩过的坑是绘图拖到最后才做,结果发现模型输出的数据结构不匹配,画图脚本返工浪费了两个小时。建议每问模型跑通后,立刻画核心结果图,哪怕只是草稿,最后统一美化格式就好。

附录代码我建议做一次“一键运行”检查,也就是把数据路径改成相对路径,删掉所有本机绝对路径。每年都有队伍因为代码换个电脑跑不起来被扣细节分,这不该是实力问题导致的失分。代码命名也统一一下,注释写清楚“第几问-实现-输入-输出”,评卷老师看到整洁的代码会很舒服,即便答辩环节提问也能快速定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 3:31:53

闲置盒子变服务器:RK3568 刷 Armbian 的 4 步改造攻略

闲置盒子变服务器&#xff1a;RK3568 刷 Armbian 的 4 步改造攻略 【免费下载链接】amlogic-s9xxx-armbian Supports running Armbian on Amlogic, Allwinner, and Rockchip devices. Support a311d, s922x, s905x3, s905x2, s912, s905d, s905x, s905w, s905, s905l, rk3588, …

作者头像 李华
网站建设 2026/9/18 3:29:52

FDM打印的因果互锁盖板设计:机械互锁成败的关键

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:28:30

CUDA Samples 快速上手指南:在自己机器上跑通第一个 GPU 示例

CUDA Samples 快速上手指南&#xff1a;在自己机器上跑通第一个 GPU 示例 【免费下载链接】cuda-samples Samples for CUDA Developers which demonstrates features in CUDA Toolkit 项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples CUDA Samples 是 N…

作者头像 李华
网站建设 2026/9/18 3:28:24

Postman接口文档流水线:构建可验证、可追溯的API契约

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华