水质预测MATLAB代码实战:机理模型、参数辨识与MPC控制策略
简介这份资源是论文《模型预测控制在实时水质调节中有多有效状态空间建模和可扩展控制》的配套MATLAB源代码面向给排水、市政供水与自动控制方向的研究生、科研人员及工程师用于解决供水网络中消毒剂水平实时调节这一难题。代码提出水质控制问题的状态空间表示明确氯投加量与管网氯浓度之间的关系并设计可扩展的模型预测控制算法兼顾响应速度与不确定性适应能力。压缩包共542个文件约12.14MB包含130个m脚本、110个inp管网拓扑、75个dat数据、21个msx水质模型及若干shp、xlsx、pdf等覆盖建模、仿真与结果分析全流程。资源依赖EPANET-Matlab-Toolkit 2.1.8.1加载main.m后即可运行配置集中于Constants4Concentrat相关文件。目前已有853人学习适合希望复现论文实验、掌握管网水质建模与MPC控制实现细节的读者参考。1. 水质预测 MATLAB 代码从数据到控制策略的完整落地路径做环境工程或过程控制的朋友大概率遇到过这种场景手里有一堆监测站的历史数据领导要你下周给出未来三天的水质预判还要顺带说清楚该怎么调曝气量。用 Excel 硬拟合吧精度不够上深度学习吧数据量又撑不起来。这套 Water-Quality-Modeling-and-Control 的 MATLAB 代码包恰好卡在中间那个位置——它用机理模型加系统辨识的思路把水质预测和后续控制策略串成了一条线。代码结构清晰主脚本加函数模块的经典布局适合有 MATLAB 基础、想快速搭出可运行原型的人。你不需要从头推公式但得知道每个模块在算什么不然调参时就是盲人摸象。2. 水质模型选型与数据预处理为什么不能直接上神经网络2.1 机理模型和黑箱模型的边界在哪水质系统有个特点变量之间不是纯统计关系而是有物质守恒约束的。比如溶解氧的消耗和有机物降解速率挂钩总磷的变化受底泥释放影响。如果你直接拿 LSTM 去拟合数据量少的时候它会把噪声当规律学换一个季节就崩。这套代码走的是灰箱路线——用一阶动力学方程搭骨架再用最小二乘辨识关键系数。常见做法是先把 BOD、COD、氨氮、溶解氧这几个核心指标的变化写成微分方程然后离散化求解。代码里model_definition.m这个文件定义了状态变量和速率方程。我一般会先打开它看三件事状态变量有几个、每个方程里哪些参数是待辨识的、有没有饱和项。饱和项很关键比如溶解氧低于某个阈值时硝化反应速率会骤降这个非线性特征如果被忽略预测曲线在低氧区间会偏得离谱。% model_definition.m 片段状态变量与速率方程 % 状态向量 x [BOD; NH4; DO; TP] % 参数向量 p [k_d; k_n; k_r; theta] function dxdt water_quality_ode(t, x, p, u) % x(1): BOD浓度, x(2): 氨氮浓度, x(3): 溶解氧, x(4): 总磷 % p(1): BOD降解系数, p(2): 硝化速率常数 % p(3): 复氧系数, p(4): 温度修正系数 % u: 外部输入如曝气量、进水流量 T 20; % 参考温度实际使用时从数据里读 theta p(4); k_d p(1) * theta^(T - 20); k_n p(2) * theta^(T - 20); k_r p(3); % 溶解氧饱和浓度用温度经验公式 DO_sat 14.6 - 0.4 * T 0.008 * T^2; % 速率方程 r_d k_d * x(1); % BOD降解耗氧 r_n k_n * x(2) * x(3) / (x(3) 2); % 硝化反应带半饱和项 r_r k_r * (DO_sat - x(3)); % 复氧 dxdt zeros(4,1); dxdt(1) -r_d u(1); % BOD: 降解 进水 dxdt(2) -r_n u(2); % 氨氮: 硝化消耗 进水 dxdt(3) -r_d - 4.57*r_n r_r u(3); % 溶解氧: 消耗 复氧 曝气 dxdt(4) -0.1*r_d u(4); % 总磷: 与BOD耦合释放 进水 end这段代码的逻辑是先算温度修正后的速率常数再算三个核心反应速率最后按质量平衡写微分方程。参数theta一般取 1.02 到 1.08 之间如果你拿到的数据跨越了冬夏两季这个值不调准预测曲线在温度突变时会明显滞后。u向量是外部输入实际用的时候需要把监测站的进水流量和曝气量按时间对齐后插值到求解步长上。2.2 数据清洗缺失值和异常值的处理顺序不能反原始监测数据里缺失值和异常值往往同时存在。我见过有人先填缺失再剔异常结果把插值出来的假数据当真实值保留下来。正确顺序是先识别异常并置为 NaN再统一插值。异常识别用滑动窗口的 3σ 准则窗口宽度取 24 小时比较稳太短会误杀真实波动太长会漏掉突发排放。% data_preprocess.m 片段异常检测与插值 function data_clean preprocess_raw(data_raw, window_hours) % data_raw: 时间戳 各指标浓度 % window_hours: 滑动窗口小时数 data_clean data_raw; vars {BOD,NH4,DO,TP}; for i 1:length(vars) col data_raw.(vars{i}); % 滑动窗口3σ检测 win window_hours; mu movmean(col, win, omitnan); sigma movstd(col, win, omitnan); outlier abs(col - mu) 3 * sigma; col(outlier) NaN; % 线性插值限制最大连续缺失长度 col fillmissing(col, linear, MaxGap, 6); data_clean.(vars{i}) col; end end参数MaxGap设成 6 的意思是连续缺失超过 6 个采样点就不插值了直接留 NaN后面建模时跳过这段。这个阈值根据你的采样频率来定如果是小时数据6 小时以上的缺失用线性插值就是自欺欺人。滑动窗口的movstd计算量不小数据超过十万条时建议先降采样再检测或者改用中位数绝对偏差对异常值更鲁棒。3. 参数辨识与预测验证最小二乘的坑比你想的多3.1 用 lsqnonlin 做参数辨识的实操配置参数辨识的本质是找一组 p 让模型输出和实测值的残差平方和最小。MATLAB 里lsqnonlin是常用选择但默认配置在 Wasserstein 距离意义下可能收敛到局部极小。我一般会做三件事设合理的上下界、给初值、开并行计算。% parameter_estimation.m 片段 function p_est estimate_params(data_clean, p0, lb, ub) % 提取输入输出 t_span data_clean.time; y_meas [data_clean.BOD, data_clean.NH4, data_clean.DO, data_clean.TP]; u_input [data_clean.inflow, data_clean.ammonia_load, ... data_clean.aeration, data_clean.phosphorus_load]; % 目标函数仿真输出与实测的残差 obj_fun (p) simulate_and_residual(p, t_span, y_meas, u_input); % 配置优化选项 options optimoptions(lsqnonlin, ... Display, iter, ... Algorithm, trust-region-reflective, ... MaxFunctionEvaluations, 5000, ... UseParallel, true); % 多初值尝试避免局部极小 p_est []; best_resnorm inf; for k 1:5 p_try p0 .* (1 0.2 * randn(size(p0))); % 初值扰动 [p_temp, resnorm] lsqnonlin(obj_fun, p_try, lb, ub, options); if resnorm best_resnorm best_resnorm resnorm; p_est p_temp; end end endAlgorithm选trust-region-reflective是因为它能处理边界约束比levenberg-marquardt更适合有物理意义的参数范围。MaxFunctionEvaluations设 5000 是经验值水质模型一般 2000 到 3000 次就能收敛设太大浪费时间。多初值扰动那步很关键我遇到过 BOD 降解系数在 0.1 和 0.5 两个初值下收敛到完全不同的结果残差差了三倍。扰动幅度 0.2 是试出来的太大容易跑到不合理区域太小起不到探索作用。3.2 预测验证别只看 RMSE很多人验证模型就报一个 RMSE这在水质预测里不够。因为水质指标的量纲和波动范围差异大BOD 可能几百溶解氧只有个位数同一个 RMSE 对两者的意义完全不同。我习惯用三个指标NSE、PBIAS 和 RSR。NSE 看整体拟合PBIAS 看系统性偏差RSR 是 RMSE 和标准差的比值能反映模型是否比直接用均值预测更好。% validate_model.m 片段 function metrics calc_metrics(y_true, y_pred) % Nash-Sutcliffe效率系数 NSE 1 - sum((y_true - y_pred).^2) / sum((y_true - mean(y_true)).^2); % 百分比偏差 PBIAS 100 * sum(y_true - y_pred) / sum(y_true); % RMSE-标准差比值 RSR sqrt(mean((y_true - y_pred).^2)) / std(y_true); metrics struct(NSE, NSE, PBIAS, PBIAS, RSR, RSR); end判断标准NSE 大于 0.65 算可接受大于 0.8 算好PBIAS 绝对值小于 15% 说明没有明显系统偏差RSR 小于 0.7 表示模型优于均值预测。如果 NSE 高但 PBIAS 也大说明模型在某个区间系统性偏高或偏低这时候要回去检查速率方程里有没有漏掉某个源汇项。4. 控制策略实现从预测到执行的最后一公里4.1 模型预测控制框架的简化实现预测做完了接下来是怎么用。这套代码里带了一个简化版的模型预测控制MPC框架核心思路是用辨识好的模型往前预测 N 步在控制步长内优化曝气量等操作变量让溶解氧维持在目标区间同时 minimize 能耗。% mpc_controller.m 片段 function u_opt mpc_step(x_current, p_est, target_DO, horizon) % x_current: 当前状态 % target_DO: 溶解氧目标值 % horizon: 预测时域步数 n_u 1; % 控制变量曝气量 u_min 0; u_max 100; % 曝气量范围 % 目标函数跟踪误差 控制增量惩罚 cost_fun (u_seq) mpc_cost(u_seq, x_current, p_est, target_DO, horizon); % 用fmincon求解带约束优化 options optimoptions(fmincon, Display, off, ... Algorithm, sqp, MaxIterations, 100); u0 50 * ones(horizon, 1); % 初值 u_opt fmincon(cost_fun, u0, [], [], [], [], ... u_min*ones(horizon,1), u_max*ones(horizon,1), [], options); u_opt u_opt(1); % 只取第一步执行 end function J mpc_cost(u_seq, x0, p, target, N) J 0; x x0; for k 1:N % 用欧拉法前进一步 dxdt water_quality_ode(0, x, p, [0;0;u_seq(k);0]); x x dxdt * 0.1; % 步长0.1小时 % 跟踪误差 J J (x(3) - target)^2; % 控制平滑惩罚 if k 1 J J 0.1 * (u_seq(k) - u_seq(k-1))^2; end end endhorizon一般取 10 到 20 步对应未来 1 到 2 小时。步长 0.1 小时是折中太小计算量大太大精度不够。控制增量惩罚系数 0.1 是调出来的设大了曝气量变化太慢跟不上水质波动设小了阀门频繁动作影响设备寿命。fmincon用sqp算法是因为目标函数有非线性约束虽然这里只写了边界约束但实际工程中可能还要加溶解氧变化率限制。4.2 控制效果评估别忽略执行器延迟仿真里控制效果很好一上实际系统就振荡八成是执行器延迟没建模。鼓风机从收到信号到实际出气量变化通常有几十秒到几分钟的滞后。代码里actuator_delay.m提供了一个一阶惯性加纯延迟的模型我一般会把延迟时间设成实际设备说明书上的响应时间惯性时间常数取延迟的三分之一到二分之一。% actuator_delay.m 片段 function u_actual apply_delay(u_cmd, delay_time, tau, dt, state) % 一阶惯性 纯延迟 % state: 保存延迟队列和当前输出 % 延迟队列 state.buffer [state.buffer(2:end); u_cmd]; u_delayed state.buffer(1); % 一阶惯性 u_actual state.y (dt/tau) * (u_delayed - state.y); state.y u_actual; enddelay_time和tau这两个参数如果拍脑袋填控制效果会差很多。我的做法是先做一次阶跃响应实验记录从给信号到溶解氧开始变化的时间那就是纯延迟然后看溶解氧变化到新稳态的 63.2% 用了多久减去纯延迟就是惯性时间常数。没有实验条件的话保守一点延迟取 2 分钟惯性取 1 分钟至少不会振荡。5. 避坑与排查那些让我熬夜的报错和异常5.1 求解器报错“无法满足积分容差”现象运行ode45时提示Unable to meet integration tolerances without reducing the step size。原因通常是速率方程在某个参数组合下出现了刚性比如硝化速率常数辨识得太大导致氨氮浓度在一步内变成负数下一时刻又反弹。解决换ode15s求解器或者在速率方程里加非负约束浓度低于零时强制截断。5.2 参数辨识结果物理意义不合理现象辨识出的复氧系数是负值或者温度修正系数大于 2。原因初值离真实值太远优化跑到了无物理意义的区域。解决在lsqnonlin里设紧的上下界复氧系数下界设 0.01温度修正系数限制在 1.0 到 1.1 之间。如果还是跑偏检查输入输出数据的时间对齐错位一小时就能让辨识结果完全失真。5.3 预测曲线在雨天突变现象晴天预测很准一到降雨事件就偏得离谱。原因模型没有考虑降雨带来的面源污染冲击和稀释效应。解决在输入向量里加入降雨量作为前馈变量或者在检测到降雨时切换到另一组辨识参数。代码里预留了rainfall_flag接口但默认没启用需要自己把降雨数据接进去。5.4 MPC 控制量频繁抖动现象曝气量指令在相邻时刻反复跳变。原因目标函数里控制增量惩罚系数太小或者预测时域太短。解决把增量惩罚系数从 0.1 调到 0.5 以上同时把horizon从 10 增加到 15。如果还抖检查状态反馈有没有噪声加一个一阶低通滤波再送进控制器。5.5 仿真速度慢到无法接受现象一次参数辨识跑半小时以上。原因ode45在每次目标函数调用时都重新求解整个时间序列而lsqnonlin默认要调用几百次。解决把UseParallel打开同时把仿真时间分段用上一次的末状态作为下一段的初值减少重复计算。数据量特别大时先降采样到小时均值再辨识精度损失很小但速度快一个数量级。6. 进阶技巧用滚动窗口辨识应对水质季节性变化水质系统的参数不是恒定的。温度、微生物群落结构、底泥特性都会随季节变一套参数用全年春秋季的预测误差会明显大于冬夏。我后来养成的习惯是每 30 天用最近 90 天的数据重新辨识一次参数然后对比新旧参数的 NSE如果新参数在验证集上 NSE 下降超过 0.05就说明系统发生了结构性变化需要检查是不是有新的排放源接入。具体操作上把parameter_estimation.m包一层循环按时间窗口切片调用。注意每次辨识的初值用上一窗口的结果这样收敛快而且参数变化轨迹平滑不会出现某个月突然跳变的情况。% rolling_identification.m 片段 function p_history rolling_identify(data_clean, window_days, step_days) % window_days: 辨识窗口长度 % step_days: 滚动步长 t_start data_clean.time(1); t_end data_clean.time(end); p_history []; p_init [0.3; 0.1; 0.5; 1.05]; % 初始猜测 current t_start; while current days(window_days) t_end idx data_clean.time current ... data_clean.time current days(window_days); data_win data_clean(idx, :); p_est estimate_params(data_win, p_init, ... [0.01; 0.01; 0.01; 1.0], [2.0; 1.0; 5.0; 1.1]); p_history [p_history; current, p_est]; p_init p_est; % 用当前结果作为下一窗口初值 current current days(step_days); end endwindow_days取 90 是因为水质系统的记忆长度大概在一个季度太短辨识不稳定太长跟不上变化。step_days取 30 是折中每周滚动一次计算量太大每季度滚动一次又太滞后。跑完这个循环把p_history画出来如果某个参数在特定月份持续漂移那就是系统在告诉你该去现场看看是不是有新的污染源了。从那以后我每次拿到新的监测数据都强制走一遍滚动辨识哪怕只是看看参数有没有异常漂移。这套代码给的是一个骨架真正让它在你手里跑出价值靠的是把现场工况和模型参数对应起来的那点直觉。希望帮到你。本文还有配套的精品资源点击获取