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

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(dropTrue) 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_size0.2, random_state42 ) model RandomForestRegressor(n_estimators300, max_depth12, random_state42) 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.95MAE在小时粒度大概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是容量单价元/kWhE是储能额定容量kWhC_pow是功率单价元/kWP是储能额定功率kWC_om是年运维成本系数C_save是储能带来的年收益主要通过峰谷套利和减少弃光来体现。约束包括储能SOC连续变化SOC(t1) SOC(t) (Pch(t)*ηch - Pdis(t)/ηdis) * Δt / ESOC上下限0.1 ≤ SOC(t) ≤ 0.9充放电功率限制0 ≤ Pch(t) ≤ P0 ≤ 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_varn_var, n_obj1, xlnp.array([100, 50]), xunp.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_size100, eliminate_duplicatesTrue) res minimize(problem, algorithm, (n_gen, 200), seed42, verboseFalse) 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), catpulp.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[t1] 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(msgFalse)) print(Status:, pulp.LpStatus[prob.status]) for t in range(24): if Pch[t].value() 1e-4 or Pdis[t].value() 1e-4: print(fhour {t}: Pch{Pch[t].value():.1f}, Pdis{Pdis[t].value():.1f}, SOC{SOC[t1].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_varn_var, n_obj2, xlnp.concatenate(([100, 50], np.zeros(24 * self.pv.shape[0]))), xunp.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(axis1, keepdimsTrue) # 归一化 cost, curtail self._simulate(E, P, alpha) obj1[i] cost obj2[i] curtail out[F] np.column_stack([obj1, obj2]) algorithm NSGA2(pop_size80) res minimize(problem, algorithm, (n_gen, 300), seed42)看到那个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问多目标优化下午统一绘图、补摘要、整理附录。有一个我踩过的坑是绘图拖到最后才做结果发现模型输出的数据结构不匹配画图脚本返工浪费了两个小时。建议每问模型跑通后立刻画核心结果图哪怕只是草稿最后统一美化格式就好。附录代码我建议做一次“一键运行”检查也就是把数据路径改成相对路径删掉所有本机绝对路径。每年都有队伍因为代码换个电脑跑不起来被扣细节分这不该是实力问题导致的失分。代码命名也统一一下注释写清楚“第几问-实现-输入-输出”评卷老师看到整洁的代码会很舒服即便答辩环节提问也能快速定位。