拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Julia复现电网经济调度与频率控制分层耦合模型全记录

Julia复现电网经济调度与频率控制分层耦合模型全记录 电网调度和频率控制在我刚入行那几年一直被当成两个“战壕”里的工作做经济调度的人天天盯机组负荷率、煤耗曲线、启停顺序追求的是每一度电发得够便宜做频率控制的人则盯着AGC、一次调频死区、系统惯量追求的是电压和频率别越线。两条线的工具、模型、时间尺度完全不是一个量级以前开会都各讲各的。直到后来做新能源高比例接入的项目我才意识到这两个层必须放在同一个框架里建模——调度是小时级的经济决策频率响应是秒级到毫秒级的物理动态中间还夹着分钟级的滚动调度。要把这三个层次同时塞进一个可复现的模型框架里还要把代码从论文变成能跑的工程实现我选择用Julia从零开始做复现。这篇文章就是完整的过程记录包括模型拆分、关键代码、性能调优以及我踩进去又爬出来的那些坑。1. 电网经济与频率控制为什么必须放在一个框架里1.1 经济调度与频率控制之间的“尺子”不一样传统电力系统运行里经济调度解决的是“哪些机组出力、出多少力”的问题优化周期通常是15分钟到24小时物理上对应的是机组爬坡、启停、煤耗这些慢过程。频率控制则解决的是“负荷突变后转速怎么稳住”的问题时间常数是秒级甚至毫秒级背后是转子惯量、调速器响应、AGC闭环这些快过程。两者的动态特性差了大约四个数量级。要是用同一个时间步长去仿真那调度问题没法算——秒级步长跑24小时迭代次数太多要是忽略快过程那调度结果又失真——频率掉得厉害的时候调度给的出力基点早就失效了。所以必须做分层建模每一层用自己合适的时间尺度去建模和求解然后再把层与层之间的边界条件衔接起来这就构成了所谓的多层多时间尺度模型。1.2 多层多时间尺度模型到底在解决什么问题新能源渗透率上来以后原来的“先调度、再调频”串行思路就不够用了。光伏和风电的出力波动大既影响经济性也影响频率质量。过度追求经济最优可能导致系统惯量变低一次调频能力不足反过来过度保守的调度会压着机组出力让经济性变差。要同时考虑经济性和频率安全就需要一个能够体现它们之间约束关系的模型。多层多时间尺度模型的本质是把这两个问题拆成层次结构而不是简单拼接。上层做经济规划和调度给出各机组的基准出力下层做频率闭环控制根据实时频率偏差修正出力。上下层之间通过机组出力的设定值、可调容量、调节速率这些物理量互相约束。数据是双向流动的上层给下层提供运行基点下层给上层反馈调节余量。这样才算真正把“经济决策”和“动态控制”串成了一个整体。2. 模型架构三层三尺度怎么划分2.1 顶层经济调度小时级的机组出力分配顶层模型的输入是负荷预测、新能源出力预测和各机组参数输出是未来24小时甚至更长时间段内各机组的出力计划。目标函数是让系统总发电成本最小包括燃料成本、启停成本和必要的备用成本。约束条件除了功率平衡、机组上下限、爬坡速率、最小启停时间之外还必须加入频率安全相关的约束最典型的做法是给系统设定一个最低惯量水平或一次调频备用容量约束。在Julia里这个模型我直接用JuMP.jl搭求解器用HiGHS。HiGHS是开源求解器里对线性规划和凸二次规划支持很稳的不需要商业授权复现的时候也没有版权负担。模型架构上先按机组类型分组再按时段展开最终形成一个带机组组合变量的混合整数规划问题。对于纯经济调度不考虑机组启停的简化场景只需要把机组组合变量去掉退化成纯粹的连续优化问题求解速度和稳定性都会大幅度提升。2.2 中层滚动调度分钟级的负荷跟踪中层解决的问题是顶层给出的小时级出力计划太粗糙负荷在15分钟甚至5分钟尺度上有明显波动如果不跟踪频率控制层会承受过大的调节压力。所以中层模型采用滚动时域控制的思想每15分钟刷新一次使用最新的负荷预测和新能源预测将未来4小时内的机组出力基点重新优化一遍。这一层的模型和顶层结构类似但多了两个关键变化一是时间步长更细成本函数要考虑更精细的爬坡约束二是需要把上一轮优化结果中已确定的部分作为初始状态不能随意跳变。实际代码里这里我会用一个滚动循环每次循环都重新构建并求解一次JuMP模型然后只取第一个或前几个时段的解作为下一层频率控制器输入的基点。这其实就是标准MPC流程难点不在模型本身而在求解速度和数值稳定性。2.3 底层频率控制秒级到毫秒级的动态响应底层模型面向的是系统频率的动态过程。这里我采用集中惯量模型也就是把所有发电机组的转子运动方程合并成一个等效方程再用一个一阶惯性环节来近似调速器和汽轮机的响应延迟。系统频率偏差的微分方程可以写成2H_total * dΔf/dt ΔP_gen - ΔP_load - D * Δf其中H_total是系统等效惯量D是负荷频率调节系数ΔP_gen是发电机机械功率增加量ΔP_load是负荷扰动。一次调频由调速器自动完成二次调频由AGC参考频率偏差和区域控制误差去调整功率设定值。这里我还给调速器加了一个限幅避免出力调节超过机组爬坡能力。底层仿真用DifferentialEquations.jl来做时间范围取30到60秒足以观察一次调频的暂态过程和二次调频的恢复趋势。这一层的输出是频率偏差曲线、机组调节功率曲线通过这些曲线反过来定量评估顶层调度方案是否满足频率安全要求。3. Julia源代码复现从模型到可运行代码3.1 为什么我选Julia而不是Python说得直接一点这个任务放在Python里也能做但非常别扭。调度建模用PuLP或者Pyomo倒是没问题可一旦要做秒级动态仿真加上滚动优化循环Python的解释器开销和内存分配问题就让人头疼。我试过用Python实现一个简化版同一个案例仿真跑下来要十几秒而Julia版本冷启动之后基本能压到一秒以内。Julia的优势在于它既是动态语言又是编译型语言。写起来脚本感很强变量不用声明类型理解成本低但运行时通过LLVM编译成机器码数值计算性能接近C和Fortran。再加上JuMP建模语法和Python的PuLP非常接近迁移成本很低。对于这种既要搭优化模型、又要跑动态仿真、还得分层循环计算的任务Julia是当下最顺手的选择。3.2 项目结构、依赖与求解器选型代码我按模块分文件组织结构清晰一些后面换机组数据、改参数也方便。项目结构大概是这样pkg/ Project.toml src/ model/ Economy.jl Frequency.jl Rolling.jl data/ units_5gen.csv run_pipeline.jl utils.jl核心依赖只有四个JuMP.jl负责优化建模HiGHS.jl作为开源求解器DifferentialEquations.jl负责频率动态仿真CSV.jl和DataFrames.jl负责读取机组参数。性能分析的时候再加BenchmarkTools.jl不需要一上来就引入一堆重型依赖。求解器选型上HiGHS适合中等规模线性规划和二次凸规划。如果你要跑几千台机组的机组组合可以换Gurobi或者CPLEX但复现阶段HiGHS足够。频率动态仿真选Tsit5方法这属于自适应步长的龙格库塔法精度和速度的平衡比较好不需要手动调步长。3.3 经济调度层的核心实现经济调度的核心代码不算长我直接贴最核心的部分using JuMP, HiGHS function build_dispatch(gen, load_curve; T24) n length(gen) model Model(HiGHS.Optimizer) set_silent(model) variable(model, P[1:n, 1:T] 0.0) for t in 1:T constraint(model, sum(P[i, t] for i in 1:n) load_curve[t]) end for i in 1:n, t in 1:T constraint(model, P[i, t] gen[i].Pmax) constraint(model, P[i, t] gen[i].Pmin) if t 1 constraint(model, P[i, t] - P[i, t-1] gen[i].ramp_up) constraint(model, P[i, t-1] - P[i, t] gen[i].ramp_dn) end end objective(model, Min, sum(gen[i].c2 * P[i, t]^2 gen[i].c1 * P[i, t] gen[i].c0 for i in 1:n, t in 1:T) ) optimize!(model) assert termination_status(model) OPTIMAL return value.(P), objective_value(model) end这里有几个关键细节。第一成本函数我用的是二次函数但HiGHS默认处理的是线性规划二次目标函数它会自动调用对应的QP算法实测没有问题。第二爬坡约束是相邻时段之间的出力差限制这个约束对中层滚动模型尤其重要否则调度结果会让机组频繁大幅度变出力。第三我用set_silent(model)关掉了求解器日志不然跑滚动优化的时候控制台会刷爆炸。3.4 频率动态层的核心实现频率动态层我不需要复杂的系统模型重点是把转子运动方程和调速器响应组在一起using DifferentialEquations function freq_dynamics!(du, u, p, t) df u[1] # 频率偏差 pg u[2] # 调速器机械功率增量 # 一次调频按频率偏差比例调整功率带限幅 pg_ref -p.R * df du[2] (clamp(pg_ref, -p.limit, p.limit) - pg) / p.tau_g # 二次调频AGC按区域控制误差积分 pagc p.Ki * df # 功率平衡方程 du[1] (pg pagc - p.disturbance - p.D * df) / (2.0 * p.H) end function run_frequency(p) u0 [0.0, 0.0] tspan (0.0, 60.0) prob ODEProblem(freq_dynamics!, u0, tspan, p) sol solve(prob, Tsit5(); reltol1e-6, abstol1e-8) return sol end参数p是一个结构体用Base.kwdef定义字段包括惯量H、调速器下垂系数R、调速器时间常数tau_g、AGC积分系数Ki、负荷扰动disturbance等。这个结构体还有一个好处Julia对参数类型的推断会非常精确仿真性能比用Dict存参数快得多。这一层最容易出问题的地方在clamp函数。调速器出力是有物理上限的如果不加限幅大扰动下仿真会出现频率又飞、AGC又狂拉功率的失真现象。我在实际复现时发现加限幅之后频率最低点会偏低一些但这个结果才是符合物理实际的。3.5 多时间尺度耦合的关键代码三层模型不应该是三个孤立脚本真正的联动在耦合逻辑里。我的实现思路是先跑顶层调度得到24小时的基准出力然后按15分钟粒度插值得到中期基点中层滚动模型根据最新的负荷修正基点最后把修正后的基点作为频率仿真里的初始机械功率初值跑一段大扰动仿真看频率曲线。function run_pipeline(data, load_day) # step 1: 日前经济调度 P_day, cost build_dispatch(data.gen, load_day; T24) # step 2: 日内滚动修正15分钟步长窗口4小时 P_roll rolling_dispatch(data, P_day, load_day) # step 3: 频率仿真在第10秒施加0.05 p.u.负荷扰动 p data.params p.disturbance 0.05 p.Pset0 P_roll[1] # 取滚动结果第一个时段作为初始基点 sol run_frequency(p) # step 4: 评估频率安全指标 nadir minimum(sol[1, :]) return (dispatchP_day, rollingP_roll, freqsol, nadirnadir) end三层之间的数据流就这样串起来了。实际运行中我还检查了一个关系顶层调度给的备用容量如果不够频率仿真里纳底就会很低甚至低于49.5Hz的安全线。这就是经济调度和频率控制耦合的直观体现——只看经济性不检查频率安全方案可能非常危险。4. 运行效果与性能调优实录4.1 三个时间尺度跑通后的实测结果我用的测试系统是一个5机组模型包含两台火电、一台燃气轮机、一个风电场和一个储能电站。负荷曲线取典型夏季峰荷日最大负荷650MW风电处理成负的净负荷参与平衡。运行完整pipeline之后日前调度给出的小时级出力曲线比较平滑但15分钟滚动调度会明显跟踪到负荷的小幅波动。对频率层施加一个0.05 p.u.的负荷扰动后系统频率在扰动后3秒左右到达最低点经过一次调频粗调然后AGC缓缓拉回接近50Hz。整个仿真60秒频率最低点、稳态偏差这两个安全指标都能直接读出来。这个流程的价值在于顶层调度方案改了哪怕一个参数比如把某台机组的Pmax调低频率层的纳底立刻会变。这就验证了多层模型之间确实存在强耦合关系单层模型根本发现不了这种连锁影响。4.2 Julia性能优化的几个关键动作第一版代码非常慢慢在哪慢在JuMP模型的反复构建。中层滚动优化每15分钟构建一次模型内层还有变量索引的各种操作一旦模型规模上来构建时间会远超求解时间。我把模型构建封装成了独立函数并确保每次循环时复用固定的变量索引结构构建时间显著下降。第二个关键动作是函数壁垒。Julia的JIT编译是按函数粒度触发的我在所有核心计算外面套了一层长函数比如把整个滚动优化循环包进rolling_dispatch函数里避免在全局作用域写循环。全局变量会让类型推断失效每次循环都触发编译或动态派发性能会掉一个数量级。第三是循环里避免分配。我检查了滚动调度里的临时数组把不必要的重建改成了预分配缓冲用views操作切片。Julia里数组切片默认是复制多复制几轮之后GC开销非常大这点和Python的习惯完全不同。4.3 内存管理不求极致但求稳定Julia的内存管理核心是GC机制但GC不会自动帮你避免分配。实测中发现一个现象跑完整pipeline时如果没有手动控制中间变量生命周期内存会一点一点涨上去。原因是JuMP模型的内部对象在滚动循环里不断创建前一轮模型虽然不再引用但GC触发时机不可控。我的做法是在滚动循环末尾显式置空引用然后定期调用GC.gc()。这不是最优雅的方案但在复现阶段非常有效。另一个经验是把不依赖数据的模型参数提升为常量例如机组数量、时段数量用const声明这会帮助编译器进行常量传播。如果要做超大规模仿真可以用--heap-size-hint参数控制堆大小或者考虑用PackageCompiler预编译成系统镜像可以把启动时间从几秒压到零点几秒。对于一次跑几十个算例的分析需求这个优化非常值得做。5. 复现过程中踩过的坑5.1 经济调度模型无解多半不是求解器的问题我遇到的第一类坑是模型无解。最初我还以为是HiGHS对某些极端约束处理不好查了半天发现完全是自己建模的问题。典型错误是机组总容量刚好等于负荷但没有留出备用容量或者爬坡约束太紧导致相邻时段无法满足负荷上升速度。解决办法很朴素先把机组总容量加大10%到15%或者把爬坡速率放宽再逐步收紧看影响。建模时先跑通再调参别一上来就追求高精度。另外要注意约束的物理单位一致性。负荷曲线我习惯用MW而建立爬坡约束时用的是“每个时段内的变化量”如果不除以时间步长就等于假设爬坡能力是1小时内完成的实际却是15分钟尺度数值上会差出好几倍。5.2 频率控制仿真不收敛或发散频率动态仿真初期遇到过发散。找了半天原因是AGC积分系数Ki给得太大频率偏差小的时候反馈很轻微一旦扰动稍大积分项快速积累功率设定值就会超出物理极限仿真自然就爆了。解决办法不只是调Ki数值还要在控制器输出侧再加一个限幅器这个经验说白了就是控制工程里的抗饱和。微分方程求解器报错其实也是一种提示。我在把扰动从0.02改成0.10时Tsit5的默认容差开始出现明显振荡。后来把reltol和abstol分别压到1e-6和1e-860秒的仿真时间只增加了一点点但曲线光滑多了。频率动态问题对精度不敏感但容差太低会产生锯齿影响对纳底的判断。5.3 Julia初印象陷阱纯Julia风格版本从Python转过来的第一版代码处处都有Python的影子大量Dict传参、大量中间数组、到处是global变量。运行起来并不出彩比Python还慢。后来我理解Julia是一门“可以写得像Python但本质是编译型”的语言于是开始改成结构体传参、函数屏障、类型稳定。改完后的版本和初版对比速度提升了差不多十倍内存分配减少到原来的三分之一。Julia里还有一个让人迷惑的地方是首调编译时间。第一次跑run_pipeline需要等十几秒甚至二十秒中间还会出现编译日志。这很正常不算性能问题。我自己会在批量跑算例之前先跑一个小规模预热把相关方法都编译一遍后面的重复调用就会很顺畅。6. 后续扩展与个人体会6.1 可以往哪些方向扩展这套框架的扩展空间非常大。最直接的方向是把顶层经济调度升级成完整的机组组合模型加入启停变量和最小启停时间约束模型变成混合整数规划。频率控制层也可以从集中惯量模型扩展成多机分布式模型每台机组单独建转子方程能研究机电振荡和区域间低频振荡。再者滚动调度层还可以加入不确定性集合做鲁棒优化或者随机优化。如果对Julia本身更感兴趣还可以把整个模型封装成包定义简洁的输入输出接口做成一个开源的复现工具。到那一步Julia的语言优势会进一步体现——文档、测试、包管理都是工程级别的体验。6.2 一点私货经验回头来谈一点个人的实际体会。能源领域做研究很多论文给出的模型都特别漂亮但复现时最难受的往往不是算法本身而是模型假设和代码实现之间的那条鸿沟。这篇复现做下来我对多层多时间尺度的理解比看十篇综述都要深。频率控制层的惯性常数是怎么受机组组合影响的、调度方案为何不能忽略动态约束这些抽象描述最终变成了我眼前的一条频率下降曲线。我特别建议有条件的人都去亲手跑一遍这样的复现尤其用Julia。它的上手速度比想象中快调试体验又比传统编译型语言友好非常适合把理论模型转化成可运行、可重复实验的工程代码。循环迭代模型的时候Julia带来的快速反馈会让你有一种在实验室里调设备的实感。这个体验是看任何代码片段都没法替代的。
返回列表