综合能源系统日前日内两阶段调度:Matlab+Yalmip建模与滚动优化实现
2026/9/15 4:26:08 网站建设 项目流程

做综合能源系统优化调度这些年,我最大的感受是:模型不复杂,真正复杂的是“怎么把工程问题翻译成数学问题,再用代码稳稳当当地跑出结果”。日前日内两阶段调度就是这个领域里最经典、也最实用的一套思路。我基于Matlab加Yalmip把完整流程实现了一遍,从日前计划到日内滚动修正,从数学建模到求解器调优,来龙去脉都记录在本文里。如果你正在做微电网、园区综合能源、低碳楼宇的调度研究或工程验证,这套内容可以直接当作搭建初版模型的脚手架。

1. 为什么综合能源系统要做“日前+日内”两阶段调度

1.1 单阶段调度在工程中的短板

先看一个最常见的场景。园区里装了光伏、一台热电联产机组、燃气锅炉和储能电池,还和配电网相连。传统做法是“前一天算好未来24小时的计划,第二天照着执行”,这在负荷和新能源出力都稳定的时候没有问题。但现实是,光伏出力受云层遮挡波动剧烈,用户负荷也有很强的随机性,如果只按日前计划执行,实际运行时刻就会出现两种尴尬局面:一种是设备出力不够,缺电时只能高价从电网买;另一种是出力安排过多,产生浪费甚至需要倒贴成本把电卖回去。

把日前计划直接当运行指令,本质上是用“预测值”代替“真实值”做决策。预测有误差不可怕,可怕的是运行过程中没有纠偏机制。单阶段调度恰恰缺少这种纠偏,它把所有决策都押在一次预测上,误差带来的成本就全部变成了系统的实际损失。

1.2 两阶段调度的核心思想与架构

两阶段调度把决策拆成两层,对应不同时间尺度上的信息质量。日前阶段基于短期预测(24小时负荷预测、光伏预测、电价信息),提前安排各机组启停计划、储能充放电计划、与电网的购售电计划;日内阶段则使用更接近实际运行时刻的“超短期预测”数据,在日前计划的基础上做滚动修正,只调整一些可调设备的出力增量,尽量避免大幅度改变既定的启停安排。

这种架构的思路,和“先定大方向,再微调细节”的做事逻辑一样。大方向定得太死,遇到变化就手足无措;完全不提前规划,运行现场又会因为决策滞后而手忙脚乱。两阶段的好处就在中间:既有提前计划带来的经济性,又有滚动修正带来的鲁棒性。尤其对储能设备来说,日内阶段能利用最新信息调整充放电策略,把预测误差的冲击缓冲掉,系统整体运行成本会明显优于“日前后不做修正”的单阶段方案。

调度框架可以简单归纳如下:

  • 日前阶段:时间分辨率为1小时,调度周期24小时,基于日前预测形成基准计划;
  • 日内阶段:时间分辨率缩短到15分钟,滚动窗口通常取2至4小时,每15分钟或每30分钟重算一次;
  • 信息更新:日内阶段每次滚动都使用最新的超短期预测数据,并对日前计划中涉及的可调量设偏差惩罚,防止计划被过度修改。

2. 日前调度的数学模型搭建

2.1 目标函数的设计原则

日前阶段的目标很直接,最小化系统未来24小时的总运行成本。这里的运行成本包含四个部分:购气成本(供给热电联产机组和燃气锅炉)、从电网购电的成本、向电网售电的收入,以及储能电池的循环损耗成本。

目标函数可以写成如下形式:

  • 总成本 = 购气成本 + 购电成本 - 售电收益 + 储能损耗成本

其中购气成本由机组消耗的气量与气价乘积得到。燃气消耗量取决于机组的电出力和热出力,分别除以对应的效率后折算成燃料输入。这里需要注意,热电联产机组的燃料消耗计算与普通发电机组不同,消耗的气量同时用于供热和供电,计算时要把电出力和热出力都折算进输入侧。

购电成本是电价乘以从电网买入的功率,售电收益则是上网电价乘以卖给电网的功率。如果当地执行峰谷电价,日内不同时段的电价差异会直接影响储能“低充高放”策略的形成,所以这块建模要仔细读清楚当地电价结构。

储能损耗成本可以用充放电功率的线性函数近似表示,虽然真实电池寿命损耗是非线性的,但在日前计划的场景下,用线性化处理可以保持整个优化模型为线性规划或混合整数线性规划,这对求解速度有直接影响。

2.2 功率平衡约束与机组出力范围约束

不管目标函数怎么设计,优化模型的核心约束都是功率平衡。对综合能源系统来说,最少需要满足电功率平衡和热功率平衡两个方程。

电功率平衡约束写成:

  • 光伏出力 + 风电出力 + 热电联产电出力 + 购电功率 + 电池放电功率 = 电负荷 + 售电功率 + 电池充电功率

热功率平衡约束写成:

  • 热电联产热出力 + 燃气锅炉热出力 = 热负荷

这两类约束是所有调度模型的地基。实际工程中如果系统还包含电锅炉、热泵、蓄热罐等设备,就需要在等式两侧追加对应项,但基本原理完全一致。

机组出力范围约束包括三项内容:

  • 热电联产机组的电出力上下限;
  • 燃气锅炉的热出力上下限;
  • 机组爬坡速率约束,即相邻时段出力变化幅度不能超过设定值。

对于热电联产机组,电出力和热出力之间存在耦合关系,常见处理方式是采用热电联产运行特征区域。工程简化时可以用“电出力决定热出力区间”的形式表达,如果追求更高精度,则使用多边形运行区域,配合二进制变量实现。

2.3 储能运行约束与SOC递推

储能建模是综合能源系统中最容易出现低级错误的环节,因为涉及连续时间尺度的状态变量,约束式推导容易漏项。

电池的荷电状态递推关系为:

  • 当前时段的SOC = 上一时段SOC + 充电功率 × 充电效率 - 放电功率 / 放电效率

这里注意两个细节。第一,充电和放电的效率位置不同,充电时是乘以效率,放电时是除以效率,很多初学者的错误就在这里,效率位置一颠倒,优化结果会明显偏离实际。第二,充电功率和放电功率不能同时为正,这在数学模型里需要通过二进制变量强制约束,或者用一组互补约束表达。如果不加这个约束,模型会出现“一边充电一边放电”的荒谬解,相当于零成本传输能量,目标函数会利用这个漏洞。

SOC本身也有上下限约束,通常设置在20%至90%之间,这取决于电池的实际运维要求。最后,如果日前和日内阶段使用同一套电池模型,SOC的末值还需要作为状态量传给日内阶段作为初始状态,否则两个阶段之间会出现“电量不连续”的问题。

2.4 日前阶段的输出形式

日前阶段求解完成后,输出的不是一条简单的出力曲线,而是一组完整的基准运行计划,包括:

  • 各时段热电联产机组启停状态与电热出力基准值;
  • 燃气锅炉的供热出力基准值;
  • 储能电池的充放电功率和SOC轨迹基准值;
  • 从电网的购电和售电计划。

这些基准值不会直接作为日内阶段的刚性指令,而是作为日内滚动优化的“参考轨迹”。日内阶段的目标并不是完全复现日前计划,而是在跟随日前计划的大前提下,利用最新预测信息做局部修正。正是这种“参考-修正”的关系,让两阶段调度从一套数学公式变成了一套可落地的运行策略。

3. 日内滚动调度的建模思路

3.1 滚动优化窗口与更新频率怎么定

日内阶段采用滚动时域优化,也就是在每个调度时刻,系统会基于当前最新的预测数据,对未来的一个有限时间窗口重新做一次优化决策。这个窗口通常是2到4小时,窗口末端与当前时刻之间每一步都有完整的模型约束。

滚动频率的选择与预测系统的时间分辨率直接相关。如果超短期负荷预测是15分钟更新一次,那么滚动优化也建议15分钟执行一次;如果只有30分钟更新的预测数据,那么30分钟执行一次就够了,过高的频率反而会因预测信息没有更新而白算。这里的原则是:滚动频率与预测更新频率匹配,力求每一次优化都能纳入新的信息。

每个优化周期内只执行第一步的调度指令,等运行到下一时刻,拿到新的预测数据后再重新优化。这种“走一步看一步”的方式,本质上是把长时间跨度的不确定性控制问题,分解为若干个短时间跨度的确定性优化问题。

3.2 偏差惩罚项的设计方法

日内阶段和日前阶段模型结构基本一致,最大的区别在于目标函数中多了偏差惩罚项。如果不加偏差惩罚,滚动优化很容易因为预测波动而大幅改变机组出力,导致实际运行曲线剧烈抖动,一方面增加机组磨损,另一方面会破坏日前签订的购售电协议。

偏差惩罚项的常用设计方法是:对日前计划的偏差变量的绝对值乘以一个惩罚系数并求和。例如:

  • 电出力偏差:日内优化得到的机组电出力,相对日前计划值的绝对差值;
  • SOC偏差:日内优化得到的电池末时段SOC,相对日前计划轨迹的绝对差值。

惩罚系数的量级需要仔细标定。如果系数设得太大,日内修正能力被压制,调度结果接近日前计划,失去了滚动修正的意义;如果设得太小,日内结果又会剧烈偏离日前计划,两阶段调度的“稳定性”就被破坏了。我一般先取成本系数量级的1.2到2倍作为惩罚系数的初值,再根据机组实际调节能力微调,直到出力曲线平滑度与跟踪及时性之间取得平衡。

3.3 日内阶段的约束修正细节

日内阶段虽然和日前阶段共用设备模型,但某些约束的形态需要调整。

对于启停状态这种整数变量,日前计划已经确定了机组的启停序列,日内阶段一般不再重新优化启停状态,而是直接沿用日前计划的启停结果,只对在线机组的出力大小做连续调整。这样处理一方面减少了整数变量,大幅降低了求解难度;另一方面避免了机组频繁启停对设备寿命的影响。

对于储能设备,日内阶段会把日前计划在该时段的SOC轨迹作为参考值,并把SOC的偏差纳入惩罚目标,这样电池就能在日内阶段修正充放电行为,又不会偏离日前太远。对于电网交互功率,如果日前已经签订了固定电量的购售电合同,日内阶段就需要把合同电量作为硬约束,超出合同的部分按现货价格处理,目标函数里就会多一项不平衡费用项。

4. Matlab与Yalmip的完整代码实现

4.1 环境配置:工具箱安装与求解器选择

在写代码之前,先把环境搭好。Yalmip是一个建模工具箱,它本身不求解优化问题,而是把优化模型翻译成求解器能识别的标准格式,相当于是建模语言和求解器之间的“翻译官”。所以用Yalmip建模之前,必须先安装好一个后端求解器。

我在这个项目里选择Gurobi作为求解器,原因是综合能源调度模型通常带有二进制变量(比如启停状态、充放电状态),属于混合整数线性规划问题,Gurobi在MILP场景下的求解速度明显优于默认的线性规划求解器。如果只是纯线性规划问题,内置的linprog就够用了,但一旦加了integer变量,linprog就不再适用,这也是很多初学者跑代码报错的常见原因。

安装时需要注意版本匹配。Yalmip本身是纯Matlab代码,安装方式就是把它所在的文件夹加入Matlab搜索路径:

addpath(genpath('D:\tools\yalmip')); savepath;

Gurobi安装完成后,要确保Matlab能找到Gurobi的接口文件。我遇到过许多次“solver not found”报错,最后排查下来都是因为Gurobi的Matlab接口路径没有加入搜素路径。安装完成后可以在Matlab里运行:

yalmiptest

看到输出中Gurobi那一行显示OK,说明环境已经打通。

4.2 日前调度核心代码实现

下面是用Yalmip构建日前调度模型的核心代码。为了便于阅读,我把系统简化为:光伏、热电联产机组、燃气锅炉、储能、电网交互。

%% 数据准备 T = 24; % 优化周期24小时 P_load = load_data(1:T); % 电负荷预测 H_load = heat_data(1:T); % 热负荷预测 P_pv = pv_data(1:T); % 光伏出力预测 price_buy = []; % 各时段购电价 price_sell = []; % 各时段售电价 gas_price = 2.5; % 单位气价,元/m3 %% 决策变量定义 P_chp = sdpvar(1, T); % 热电联产电出力 H_chp = sdpvar(1, T); % 热电联产热出力 H_gb = sdpvar(1, T); % 燃气锅炉热出力 P_buy = sdpvar(1, T); % 购电功率 P_sell = sdpvar(1, T); % 售电功率 P_ch = sdpvar(1, T); % 电池充电功率 P_dis = sdpvar(1, T); % 电池放电功率 SOC = sdpvar(1, T); % 电池荷电状态 u_chp = binvar(1, T); % 热电联产启停 u_ch = binvar(1, T); % 充电状态 u_dis = binvar(1, T); % 放电状态 %% 目标函数 % 燃气耗量 = 电出力折算 + 热出力折算 V_chp = (P_chp / 0.35 + H_chp / 0.45) / 9.7; V_gb = (H_gb / 0.90) / 9.7; objective = gas_price * sum(V_chp + V_gb) ... + sum(price_buy .* P_buy - price_sell .* P_sell) ... + 0.01 * sum(P_ch + P_dis); % 电池损耗项 %% 约束条件 constraints = []; % 电功率平衡 for t = 1:T constraints = [constraints, ... P_pv(t) + P_chp(t) + P_buy(t) + P_dis(t) == ... P_load(t) + P_sell(t) + P_ch(t)]; end % 热功率平衡 for t = 1:T constraints = [constraints, ... H_chp(t) + H_gb(t) == H_load(t)]; end % 机组出力范围与启停逻辑 constraints = [constraints, ... 0 <= P_chp <= 500 * u_chp, ... 0 <= H_chp <= 200 * u_chp, ... 0 <= H_gb <= 800]; for t = 2:T constraints = [constraints, ... -100 <= P_chp(t) - P_chp(t-1) <= 100]; % 爬坡约束 end % 储能约束 SOC_init = 0.5; SOC_min = 0.2; SOC_max = 0.9; P_bat_max = 100; Cap = 200; for t = 1:T if t == 1 constraints = [constraints, ... SOC(1) == SOC_init + (P_ch(1)*0.95 - P_dis(1)/0.95)/Cap]; else constraints = [constraints, ... SOC(t) == SOC(t-1) + (P_ch(t)*0.95 - P_dis(t)/0.95)/Cap]; end constraints = [constraints, ... SOC_min <= SOC(t) <= SOC_max, ... 0 <= P_ch(t) <= P_bat_max * u_ch(t), ... 0 <= P_dis(t) <= P_bat_max * u_dis(t), ... u_ch(t) + u_dis(t) <= 1]; % 禁止同时充放电 end % 电网交互上限 constraints = [constraints, 0 <= P_buy <= 1000, 0 <= P_sell <= 500]; %% 求解配置与执行 ops = sdpsettings('solver', 'gurobi', 'verbose', 1, ... 'gurobi.TimeLimit', 120, 'gurobi.MIPGap', 0.01); result = optimize(constraints, objective, ops); if result.problem == 0 P_chp_da = value(P_chp); H_chp_da = value(H_chp); SOC_da = value(SOC); else disp('求解失败,请检查问题类型和求解器配置'); disp(result.info); end

这段代码的求解逻辑很清晰,但有几个细节值得专门说明。

第一,燃气耗量折算公式中的热值取9.7千瓦时每立方米,这是一个常见的天然气热值近似值,实际工程中需要根据当地气源成分标定,否则购气成本的计算会有系统性偏差。

第二,储能损耗项我用了充放电功率的线性函数,这是一个简化处理。如果要做更精细的电池老化建模,可以把损耗项改成二元二次函数,但那样模型性质会从线性规划变成二次约束规划,求解难度显著上升。工程初版建议先用线性项,确认整体框架跑通后再逐步精细化。

第三,充放电功率的二进制变量u_ch和u_dis是防止“同时充放电”的关键。有些优化问题即使不加这个约束,最优解也不会出现同时充放电,但加上之后可以让问题性质更明确,尤其在求解器数值处理不佳时能避免很多奇怪解。

4.3 日内滚动调度核心代码实现

日内阶段的代码核心是滚动循环。这里以1小时为时间步长、4小时为滚动窗口作为示例。实际工程中如果时间分辨率为15分钟,只需要把窗口长度对应改成16个周期即可,逻辑完全一致。

%% 日内滚动优化参数 W = 24; % 日前计划的总周期 horizon = 4; % 滚动窗口长度 lambda = 1.5; % 偏差惩罚系数 % 超短期预测数据(实际使用中每个滚动时刻更新) P_pv_st = intraday_pv_data; % 尺寸为 W x horizon 的预测序列 P_load_st = intraday_load_data; % 存储每个滚动时刻的执行结果 exec_P_chp = zeros(1, W); exec_P_gb = zeros(1, W); exec_P_buy = zeros(1, W); exec_SOC = zeros(1, W+1); exec_SOC(1) = 0.5; for k = 1 : W - horizon + 1 t_idx = k : k + horizon - 1; % 决策变量:只优化窗口内的连续出力,整数变量沿用日前计划 P_chp_m = sdpvar(1, horizon); H_chp_m = sdpvar(1, horizon); H_gb_m = sdpvar(1, horizon); P_buy_m = sdpvar(1, horizon); P_ch_m = sdpvar(1, horizon); P_dis_m = sdpvar(1, horizon); SOC_m = sdpvar(1, horizon); % 偏差变量 delta_p = sdpvar(1, horizon); % 电出力偏差绝对值 delta_soc = sdpvar(1, horizon); % SOC偏差绝对值 % 目标函数:运行成本 + 偏差惩罚 V_chp_m = (P_chp_m / 0.35 + H_chp_m / 0.45) / 9.7; V_gb_m = (H_gb_m / 0.90) / 9.7; objective = gas_price * sum(V_chp_m + V_gb_m) ... + sum(price_buy(t_idx) .* P_buy_m) ... + lambda * (sum(delta_p) + sum(delta_soc)) ... + 0.01 * sum(P_ch_m + P_dis_m); constraints = []; for j = 1 : horizon t = t_idx(j); constraints = [constraints, ... P_pv_st(k, j) + P_chp_m(j) + P_buy_m(j) + P_dis_m(j) == ... P_load_st(k, j) + P_ch_m(j)]; constraints = [constraints, ... H_chp_m(j) + H_gb_m(j) == H_load_st(k, j)]; constraints = [constraints, ... delta_p(j) >= P_chp_da(t) - P_chp_m(j)]; constraints = [constraints, ... delta_p(j) >= P_chp_m(j) - P_chp_da(t)]; end % 机组范围与爬坡约束,启停状态沿用日前计划 constraints = [constraints, ... 0 <= P_chp_m <= 500, ... 0 <= H_chp_m <= 200, ... 0 <= H_gb_m <= 800]; for j = 2 : horizon constraints = [constraints, ... -100 <= P_chp_m(j) - P_chp_m(j-1) <= 100]; end % 储能递推与偏差惩罚 for j = 1 : horizon if j == 1 constraints = [constraints, ... SOC_m(1) == exec_SOC(k) + (P_ch_m(1)*0.95 - P_dis_m(1)/0.95)/200]; else constraints = [constraints, ... SOC_m(j) == SOC_m(j-1) + (P_ch_m(j)*0.95 - P_dis_m(j)/0.95)/200]; end constraints = [constraints, ... 0.2 <= SOC_m(j) <= 0.9, ... 0 <= P_ch_m(j) <= 100, ... 0 <= P_dis_m(j) <= 100]; constraints = [constraints, ... delta_soc(j) >= SOC_da(t_idx(j)) - SOC_m(j)]; constraints = [constraints, ... delta_soc(j) >= SOC_m(j) - SOC_da(t_idx(j))]; end % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 0, ... 'gurobi.TimeLimit', 60); result = optimize(constraints, objective, ops); if result.problem ~= 0 disp(['滚动优化失败,窗口起始点:', num2str(k)]); break; end % 只执行窗口内的第一个调度指令 exec_P_chp(k) = value(P_chp_m(1)); exec_P_gb(k) = value(H_gb_m(1)); exec_P_buy(k) = value(P_buy_m(1)); exec_SOC(k+1) = value(SOC_m(1)); end

这段代码展示的滚动框架有一点很重要:整数变量没有出现在日内阶段,启停状态完全继承日前结果。这个设计让每个滚动窗口的求解规模显著缩小,因为去掉了二进制变量,模型从混合整数线性规划退化为纯线性规划,Gurobi求解几乎瞬间完成。

执行策略只取窗口内第一个时段的指令,这也是滚动时域优化的标准做法。有人会问,既然算了4个小时的计划,为什么不把后面几小时的指令也一起执行?原因是预测在滚动更新,后几步的预测精度会随着时间推移快速下降,提前执行反而可能把误差放大。

SOC的递推初始值用的是上一时刻的实际执行SOC,而不是优化计算得到的窗口内SOC值。这一步如果不小心,多轮滚动之后SOC会发生不可控的漂移,最终导致电池“凭空多电”或“凭空少电”,结果完全失真。这是我在实际调试中踩过最隐蔽的坑之一。

4.4 结果后处理与可视化

调调度的最终结果不能只看一行数字,我习惯把运行曲线一次性画出来,包括电平衡图、热平衡图、SOC变化图、购售电情况。可视化图纸既是给团队评审用的交付物,也是自查模型逻辑是否合理的工具。

%% 结果绘图 time_axis = 1:24; figure; subplot(2,2,1); bar(time_axis, [P_pv; P_chp_da; P_buy; P_dis], 'stacked'); hold on; plot(time_axis, P_load, 'k-', 'LineWidth', 2); legend('光伏', '热电联产', '购电', '放电', '负荷'); title('日前电功率平衡'); xlabel('时段/h'); ylabel('功率/kW'); subplot(2,2,2); bar(time_axis, [H_chp_da; H_gb_da], 'stacked'); hold on; plot(time_axis, H_load, 'k-', 'LineWidth', 2); legend('热电联产供热', '燃气锅炉', '热负荷'); title('日前热功率平衡'); xlabel('时段/h'); ylabel('功率/kW'); subplot(2,2,3); plot(time_axis, SOC_da, 'b-o', 'LineWidth', 1.5); ylim([0 1]); title('储能SOC变化曲线'); xlabel('时段/h'); ylabel('SOC'); subplot(2,2,4); stairs(time_axis, price_buy, 'r', 'LineWidth', 1.5); hold on; stairs(time_axis, price_sell, 'b--', 'LineWidth', 1.5); legend('购电价', '售电价'); title('分时电价'); xlabel('时段/h'); ylabel('电价/元/kWh');

画图之后要重点核对几件事:电平衡图中各柱子的累加值是否恰好等于负荷曲线,SOC是否始终落在上下限内,以及电价峰谷时段储能是否出现了“逆峰充电、谷段放电”这类违反直觉的结果。如果出现违背常识的曲线,大概率是约束方向写错或者数据单位不一致。

5. 常见问题与排查技巧实录

5.1 Yalmip报错排查:从“无法求解”到“结果异常”

用Yalmip最容易遇到的第一类报错是“No suitable solver for the problem type”或者“solver not found”。出现这类提示,先检查Yalmip是否识别到了Gurobi,再检查模型里是不是出现了求解器不支持的变量类型。

我在实际调试中遇到过一种情况:只是加了一个二进制变量进去,但忘了把求解器从linprog切换到Gurobi,结果Yalmip直接报错。原因很简单,linprog只支持线性规划,不支持混合整数规划,这类问题在Yalmip里必须由专门支持混合整数规划的求解器处理。所以看到这类报错时,优先排查的不是模型,而是求解器的选择。

第二类常见的坑是“Infeasible problem”,也就是模型无解。这种情况90%的根源在约束条件上自相矛盾。我常遇到的一个典型问题是:电池容量固定,但需求要求SOC从20%充到90%,而充电功率上限和充电时间根本不够完成这个变化量,结果模型无解。排查思路是先从电池约束下手,逐条注释掉部分约束,观察哪两条约束放在一起时就报无解,再用小规模数据手工验证。

第三类是结果“看起来很怪但不报错”,比无解更隐蔽。这类问题往往出在单位不统一或者数据量级差异过大上。比如电功率单位是kW,储能容量单位却是MWh,数值一混,约束就变得极其宽松或极其严格,求解结果自然不可信。我在初期做数据准备时专门写了一个单位校验函数,对每一项输入数据都检查量纲一致性,这招帮我节省了大量排查时间。

5.2 数值尺度与求解性能调优

优化模型的数值尺度对求解性能影响极大。如果模型里的数据量级从1e-6到1e6都有,求解器在数值上会很难收敛,或者收敛到很差的结果。综合能源系统里最容易出现量级失衡的地方就是能量换算:电功率几百千瓦,购气成本几十万元每小时计,SOC却在0到1之间。三个量级的数字放在同一个目标函数里,如果不做归一化,求解器的数值稳定性会非常差。

我常用的处理方式是在建模前先把所有数据的单位统一到一个基准。功率统一用kW,能量统一用kWh,价格统一用元/kWh或元/m3,SOC作为标幺值保持在0到1之间。然后目标函数中的各项按量级估算一下,如果某一项比其他项大出三个数量级以上,就需要为这一项单独设计权重因子。这个步骤和偏差惩罚系数的标定属于同一类工作,本质都是让目标函数的数值尺度处于合理范围。

求解速度方面,最有效的优化手段是减少整数变量的数量。日前阶段有24个时段的启停变量,如果每个机组都用二进制表示启停状态,系统的整数变量数量会爆炸。缓解方法包括:把运行模式相近的多台机组聚合为一台等效机组;日内阶段完全剔除整数变量;用sdpvar定义变量时尽量复用矩阵表达式,不要写一个循环建24次变量。

5.3 我对“两阶段”落地的几点个人体会

这套代码跑通之后,我回头看整个项目,最大的认知变化是:两阶段调度的价值不在于“计划算得多准”,而在于“计划被偏离时系统能否以可接受的成本快速修正”。日前阶段解决的是经济性优化问题,日内阶段解决的是可行性与鲁棒性修正问题,两个阶段的目标函数和约束形态天然就不同,硬要把两套模型合并成一套,反而失去了滚动修正的意义。

在调度策略与工程设备衔接上,还有一个容易被忽略的点:日内阶段输出的控制指令必须考虑设备实际调节能力,包括调节速率、最小稳定出力时间、启停次数限制等。数学模型里这些约束写得再精细,如果设备现场的控制器无法执行,整套系统就是纸面文章。所以我在实际项目中始终会保留一个“指令校验层”,在滚动优化结果下发前,对机组调节速率和储能功率增量做一次硬件层面的可行性检查,避免出现理论上最优、实际上无法执行的调度指令。

如果你也要做类似的工程或研究项目,我建议先从单阶段日前调度跑通,确认电热平衡、储能递推、罚项设计都正确之后,再加入日内滚动层。一次性把两阶段全部建好,出了问题时很难判断是日前模型的锅还是日内模型的锅。分阶段搭建,每层都验证通过再连接,效率反而最高。就我个人的经验来说,这套“Matlab加Yalmip求解器”的组合,既能满足学术研究对可复现性的要求,又能快速迁移到工程原型验证中,值得在这个方向上多花时间打磨。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询