简介:面向电力系统谐波状态估计研究人员与技术人员的完整算法实现包,主要解决传统无迹卡尔曼滤波(UKF)在时变噪声和异常数据环境下估计精度不足的问题,提出并实现了基于自适应平方根无迹卡尔曼滤波(SRUKF)的改进方案。整个资源以1个docx文档交付,压缩包大小约60KB,文档内包含可直接运行的Python代码、详细注释与复现说明,覆盖类初始化、sigma点生成、预测与更新步骤、改进Sage-Husa噪声估计、Cholesky更新等核心组件,并给出IEEE14节点系统上的动态谐波状态估计仿真验证案例。通过该文档,读者既能理解自适应SRUKF的推导思路,又能动手调试代码,掌握噪声协方差实时估计和异常数据修正系数的设计方法,提升在非理想量测环境下的鲁棒估计性能。已有69人学习,适合具备一定编程基础、对电力系统谐波状态估计和卡尔曼滤波算法有初步了解的研究人员与技术开发者。
1. 动态谐波状态估计为什么要换掉传统UKF:时变噪声和异常数据下的精度短板
电力系统动态谐波状态估计,说白了就是在正常运行和扰动过程中,实时推算各节点的谐波电压幅值和相位。传统做法是用无迹卡尔曼滤波(UKF)跑状态估计,场景理想时精度还行,但实际工程里噪声统计特性是时变的——比如负荷波动、逆变器启停都会改变过程噪声和量测噪声的统计特性,而UKF一旦噪声先验给错,估计值就开始飘。更麻烦的是,采集终端偶尔会吐出异常数据,一个坏数据点就能把滤波结果拉偏好几个周期。这个资源复现的是一篇论文里的改进算法:把平方根无迹卡尔曼滤波(SRUKF)作为骨架,引入改进的Sage-Husa噪声估计器在线更新噪声协方差,再用马氏距离做异常数据检测和修正。适合正在做谐波监测、故障诊断或者状态估计的从业者,也适合读论文读不懂、需要一份能跑起来的参考代码的研究人员。
2. 自适应SRUKF的改进机理:平方根滤波、Sage-Husa噪声估计与马氏距离修正
2.1 为什么SRUKF比UKF更适合谐波状态估计
UKF的核心思想是用一组确定性采样的sigma点去逼近非线性函数的传播,绕开了雅可比矩阵的推导。这在理论上很漂亮,但工程实现里有个老大难问题:协方差矩阵P在迭代过程中容易失去正定性。一旦P非正定,Cholesky分解直接失败,滤波就算不下去了。我见过不少人在做高维状态估计时翻车,状态维度一高,数值误差累积得很快,P矩阵的特征值慢慢变成负的,然后整条估计曲线开始震荡。
SRUKF的改进思路很直接:不直接维护P,而是维护P的Cholesky因子S,让P = S·S^T。每次迭代只用S参与计算,数值稳定性比直接操作P高一个量级。具体到代码里,预测步和更新步都用QR分解来计算平方根协方差,计算量没有明显增加,但正定性问题被根治了。对于IEEE 14节点这种28维状态(14节点×幅值+相位)的估计问题,这个改进是刚需,不是锦上添花。
2.2 改进的Sage-Husa噪声估计器做了什么
Sage-Husa噪声估计器的思路是:既然噪声统计未知,那就从残差序列里在线估计。它的核心公式是用残差的外积去修正R和Q。但传统Sage-Husa有个明显的坑——它假设噪声是平稳的,估计出来的R和Q在工况稳定时还行,一旦噪声统计发生变化,它跟不上。
这个资源里的改进有两点。第一点是引入遗忘因子d(代码里默认0.95),让新息对估计结果的影响随时间衰减,旧数据的影响逐渐被遗忘。这样噪声统计变化时,估计器能更快跟上。第二点是加了滑窗统计:用最近若干个残差算均值和协方差,再和当前R做加权融合,而不是只用当前一步的残差。这个细节很实用,单步残差噪声太大,直接用来更新R会让R剧烈抖动,滑窗平滑之后R的变化就稳定多了。
2.3 异常数据修正的马氏距离方法
异常数据检测用的是马氏距离,公式是√(residual^T · S_z^{-1} · residual)。这里的S_z是观测残差协方差的平方根因子,马氏距离本质上衡量了残差相对于其协方差的偏离程度。和欧氏距离不同,马氏距离考虑了各个维度之间的相关性和量纲差异,比单纯的残差阈值判断更合理。
检测阈值设为3,对应3σ准则。残差的马氏距离如果超过3,代码就把残差乘以一个修正系数b/Mahalanobis_dist,把过大残差的幅值压小。这个做法的好处是不需要事先知道异常数据的模式——不管是量测突变、通信丢包还是传感器故障,只要残差统计上离群,就会被压回正常范围。需要注意的是,这个修正是在卡尔曼增益应用之前做的,也就是说,异常数据对状态更新的影响被直接削弱了,而不是等污染进状态之后再去清洗。
2.4 数据流完全理顺
整个滤波循环是这样的:先用状态转移函数传播sigma点得到预测状态x_pred和平方根协方差S_pred,然后用观测函数传播sigma点得到预测观测z_pred,接着计算残差、做马氏距离异常检测,修正残差后用卡尔曼增益更新状态和协方差,最后用改进的Sage-Husa估计器更新R和Q。每一步用的都是当前时刻的信息,没有未来数据的泄漏,符合在线估计的要求。
3. AdaptiveSRUKF类拆解:从sigma点生成到Cholesky更新的实现细节
3.1 初始化:无迹变换参数与权重计算
class AdaptiveSRUKF: def __init__(self, dim_x, dim_z, alpha=1e-3, beta=2, kappa=0): self.dim_x = dim_x self.dim_z = dim_z self.alpha = alpha self.beta = beta self.kappa = kappa self.lambda_ = alpha**2 * (dim_x + kappa) - dim_x self.Wm = np.full(2*dim_x+1, 1/(2*(dim_x + self.lambda_))) self.Wc = np.copy(self.Wm) self.Wm[0] = self.lambda_ / (dim_x + self.lambda_) self.Wc[0] = self.Wm[0] + (1 - alpha**2 + beta)初始化部分的关键在于lambda_的计算和权重的分配。alpha控制sigma点的散布范围,论文里推荐1e-3,这个值越小,sigma点越靠近均值,对强非线性函数的逼近能力越强——但太小也会导致高阶项权重过大,反而引入误差。beta在高斯分布假设下取2是最优的,kappa一般取0。Wm[0]和Wc[0]是第一个sigma点(即状态均值点)的权重,注意Wc[0]比Wm[0]多了一项(1 - alpha^2 + beta),这是为了在高斯分布下让协方差估计更准确。
这里的Q和R初始化都是单位矩阵,实际工程中需要根据谐波幅值和相位的典型波动范围来设。我一般会先用一段历史数据跑一次离线估计,统计残差来初始化和Q,比直接拍脑袋设单位矩阵靠谱得多。
3.2 平方根预测步:QR分解用法
def predict(self, f, Q=None): if Q is not None: self.Q = Q sigma_points = self.generate_sigma_points() n = self.dim_x X = np.zeros((2*n+1, n)) for i in range(2*n+1): X[i] = f(sigma_points[i]) self.x_pred = np.dot(self.Wm, X) X_diff = X - self.x_pred S_pred = np.linalg.qr(np.hstack([ np.sqrt(self.Wc[1]) * X_diff[1:], cholesky(self.Q).T ])).R这段代码里最值得注意的地方是QR分解的输入构造。np.hstack把两个矩阵横着拼在一起:左边是sigma点偏差的加权版本,右边是过程噪声协方差的Cholesky分解转置。这里的数学逻辑是:预测协方差 = sigma点传播带来的散布 + 过程噪声,两者叠加之后做QR分解,得到的R矩阵就是平方根协方差。
有个容易忽略的问题:QR分解得到的R不保证对角线都为正,所以代码里加了一步对角拉正的操作。这个细节看起来小,但不处理的话,后面做Cholesky更新时正负号会出错,状态估计会在某些维度上出现符号翻转。
3.3 平方根更新步:卡尔曼增益与残差修正
Z_diff = Z - z_pred S_z = np.linalg.qr(np.hstack([ np.sqrt(self.Wc[1]) * Z_diff[1:], cholesky(self.R).T ])).R P_xz = np.dot(self.Wc * X_diff.T, Z_diff) K = np.dot(P_xz, inv(S_z.T @ S_z))这里的逻辑顺序是:先算预测观测的平方根协方差S_z,再算交叉协方差P_xz,最后求卡尔曼增益K = P_xz · (S_z^T · S_z)^{-1}。注意S_z矩阵的维度是m×m,但它的平方根形式使得S_z^T · S_z等价于完整的协方差矩阵,所以实际求逆的对象是m×m矩阵,计算量比直接用P_z求逆略小,数值稳定性则好得多。
残差修正的代码在卡尔曼增益计算之后:
if anomaly_detection: S_z_inv = inv(S_z.T @ S_z) mahalanobis_dist = np.sqrt(residual.T @ S_z_inv @ residual) if mahalanobis_dist > 3: correction_factor = self.b / mahalanobis_dist residual *= correction_factor这个修正的逻辑是:如果马氏距离超了3,就用一个小于1的系数把残差压小。correction_factor = 0.98 / mahalanobis_dist,残差越大压得越狠。这里我没直接照抄阈值3,实际调试时发现阈值和数据噪声尺度有关,噪声偏大时3σ会被频繁触发,导致正常数据也被修正,状态估计反而出现钝化。这个排错过程我在第5章详细展开。
3.4 自适应噪声更新的实现细节
if adaptive: e = z - z_pred self.R = (1-self.d) * self.R + self.d * np.outer(e, e) - np.dot(K, P_xz.T) self.Q = (1-self.d) * self.Q + self.d * ...Sage-Husa更新式里有个容易被忽视的项——np.dot(K, P_xz.T)。这一项的意义是从观测更新中反推过程噪声对状态估计的影响。如果不减这一项,R的估计值会偏大,因为残差外积里有一部分其实是由过程噪声引起的,而这些已经被状态更新吸收了。
更新完之后强制对称化和加最小对角元素,这两步是稳健性的最后防线。np.maximum(self.R, 1e-6 * np.eye(m))保证所有对角线元素至少是1e-6,防止R退化到奇异。
3.5 cholupdate:协方差平方根的增量更新
def cholupdate(self, A, x, sign='+'): p = len(x) for k in range(p): r = np.sqrt(A[k,k]**2 + x[k]**2) c = r / A[k,k] s = x[k] / A[k,k] A[k,k] = r if k < p - 1: A[k+1:p,k] = (A[k+1:p,k] + s * x[k+1:p]) / c x[k+1:p] = c * x[k+1:p] - s * A[k+1:p,k] return A这个函数实现的是对Cholesky因子的一阶更新,复杂度只有O(n²),而重新做一次Cholesky分解是O(n³)。协方差更新步S = cholupdate(S, -U),这里的符号是负的,因为观测更新会减少不确定性。这个函数的数值稳定性很好,但也需要注意:如果x的长度和A的维度不匹配,索引越界是难免的——我在调试时就栽过这个跟头,后来加了维度断言才定位到问题。
4. 电力谐波系统建模:状态转移、测量方程与仿真数据生成
4.1 谐波状态空间的构造方式
class PowerSystemHarmonicModel: def __init__(self, num_buses=14): self.num_buses = num_buses self.Z = {h: np.random.rand(num_buses, num_buses) * (1 + 0.1j) for h in [3, 5, 7]} self.harmonic_sources = [3, 7]状态向量的组织方式是把每个节点的谐波电压分成幅值和相位两个分量,14个节点就是28维状态。3、5、7次谐波在电力系统里最常见——3次来自变压器励磁电流,5次和7次主要来自整流器和逆变器。谐波源设置在3号和7号节点,模拟的是两个工业负荷点。
谐波阻抗矩阵Z在资源里用了随机复数矩阵代替,实际工程应用时,这里要填的是从谐波潮流计算或实测数据辨识出来的网络阻抗参数。随机矩阵只适合验证算法流程,要得到可信的估计精度指标,必须换成真实电网参数。
4.2 状态转移方程:幅值慢变与节点耦合
def state_transition(self, x, dt=0.02): n = len(x) A = np.eye(n) for i in range(0, n, 2): A[i,i] = 0.95 + 0.05*np.sin(dt) A[i+1,i+1] = 0.97这里把幅值通道的动态建模为0.95的衰减率加正弦扰动,相位通道是0.97的衰减率。含义是:幅值随时间缓慢波动,相位变化比幅值更快一些。节点间耦合的代码里,谐波源节点的状态会以0.03的系数传导到相邻节点,模拟谐波在电网中的传播特性。
这个模型是对真实电力系统动态的简化。实际系统中,幅值和相位的动态还受负荷变化、网络拓扑切换、无功补偿设备投切等因素影响。如果你想用在真实数据上,这个状态转移模型需要扩充——至少要加入电压不平衡度和频率偏差的耦合项。
4.3 测量方程:电压测量和关键线路电流
def measurement_model(self, x): measurements = [] for i in range(0, len(x), 2): mag, phase = x[i], x[i+1] measurements.append(mag * np.cos(phase)) measurements.append(mag * np.sin(phase)) critical_lines = [(1,2), (2,3), (7,8)] for from_bus, to_bus in critical_lines: V_from = x[2*(from_bus-1)] * np.exp(1j*x[2*(from_bus-1)+1]) V_to = x[2*(to_bus-1)] * np.exp(1j*x[2*(to_bus-1)+1]) I_line = (V_from - V_to) / (0.1 + 0.5j) measurements.extend([I_line.real, I_line.imag]) return np.array(measurements) + np.random.normal(0, 0.02, len(measurements))观测模型有两类量测量:所有节点的电压相量(实部+虚部形式)和三条关键线路的电流。电压测量部分先把幅值相位转成实部虚部,这是为了匹配电力系统相量测量的标准输出格式。线路电流部分用简化阻抗模型算出来,加入高斯白噪声模拟测量误差。
4.4 仿真数据生成与时变噪声注入
true_all = [] est_all = [] for i in range(100): if i == 0: true_state = ukf.x.copy() else: true_state = state_transition(true_state) if 20 < i < 40: true_state += np.random.randn(dim_x) * 0.5 z = observation_function(true_state) if i % 10 == 0: z += np.random.randn(dim_z) * 5.0仿真设计了一个分段场景:20到40步之间,真实状态叠加了大幅噪声,模拟谐波源突变;每10步叠加一次大偏移,模拟异常量测。这个场景设计很巧妙——时变噪声出现在中间段,异常数据周期性出现,可以清晰对比算法在正常段、噪声段、异常点三个区间的表现差异。
4.5 性能评估:直接看误差的均值与最大值
errors = np.abs(true_states - estimates) print("平均估计误差:", np.mean(errors)) print("最大估计误差:", np.max(errors))用绝对误差的均值和最大值做评估指标,简洁但有效。均值反映整体估计精度,最大值反映最坏情况下的鲁棒性。实际应用时,我还会额外计算RMSE(均方根误差)和最大单点误差出现的时刻,用来判断误差是均匀分布还是集中在某个工况段——这个信息对算法调参很有价值。
5. 避坑记录:参数设置、数值稳定性和噪声估计的常见问题
5.1 Sigma点权重计算不正确导致滤波发散
现象:滤波运行几步后,状态估计开始剧烈震荡,最后发散到不可用。
原因:初始化时权重计算有个隐蔽的精度问题。lambda_ = alpha^2 * (dim_x + kappa) - dim_x,当dim_x较大时(比如28维),alpha^2可能让lambda_变成负数。如果lambda_接近-dlm_x,权重分配会出现极端值,sigma点分布不合理,滤波自然发散。
解决:检查lambda_的值,确保它大于0。如果dim_x较大,适当增大alpha(比如从1e-3调到1e-2),让sigma点分布更宽松。这个调参过程需要跑几组对比实验,不能拍脑袋。我在复现时就吃过这个亏,alpha设得太小,28维状态下lambda_变成负的,状态估计前几步就崩了。
5.2 S矩阵对角线元素不为正,Cholesky更新报错
现象:程序运行到cholupdate时抛出数值错误,或者S矩阵中出现NaN。
原因:QR分解得到的R矩阵不保证对角线元素全为正,代码里虽然做了拉正的补救,但这个操作只处理了对角线,没有处理S_z和S_pred内部的符号一致性。如果S_z的对角线被强行改成正的,而矩阵其他元素没同步调整,交叉协方差P_xz和增益K都跟着错。
解决:在QR分解之后,不仅要对角线取绝对值,还要检查所有对角线元素的符号是否一致,不一致时对整个行做符号翻转。另外,我发现用scipy.linalg.qr的mode='economic'参数可以减少很多边界问题——返回的R矩阵形状更整齐,不容易出现维度不匹配。
5.3 遗忘因子d设得过大会导致噪声估计震荡
现象:自适应更新开启后,R矩阵的数值在相邻两步之间剧烈跳变,状态估计出现周期性抖动。
原因:遗忘因子d过大(比如0.99以上),表示对当前残差的权重极大,旧信息的平滑作用被削弱,残差里的随机波动被直接映射到R的更新里。R一会儿大一会儿小,滤波器增益跟着忽大忽小。
解决:把d从0.99调回0.95。如果数据噪声波动确实很大,还可以在平滑公式里加分母因子,让R的变化更缓慢。一个可供参考的调参规律是:噪声平稳时d设大(0.97-0.99),噪声时变剧烈时d设小(0.90-0.95)。我在仿真里用0.95表现最稳定,但换到真实数据后需要重新扫描这个参数。
5.4 马氏距离阈值过小导致正常数据被误修正
现象:开启异常检测后,状态估计误差反而比不开时更大,尤其在噪声较大的时段。
原因:阈值3对应的是高斯分布下的3σ准则。但谐波状态估计的残差分布并不严格服从高斯分布——尤其是存在模型失配时,残差的标准差本身就被低估了,导致正常残差的马氏距离很容易超过3,修正系数把有效的观测信息也压掉了,状态估计就偏保守。
解决:先用一段无异常的历史数据跑一次滤波器,统计马氏距离的分布,然后取95%分位数作为阈值。这个做法比直接套3要稳妥很多。另外,修正系数可以加一个下限限制,比如最小取0.2,防止异常值被压得太狠导致状态更新量过小。
5.5 状态维度和观测维度不匹配时维度报错
现象:运行时出现矩阵维度不匹配的报错,通常是P_xz的维度是零或者S_z^T@S_z不可逆。
原因:观测函数的输出维度和初始化时传入的dim_z不一致。比如代码里dim_z=56,但观测函数实际输出的向量长度因为循环条件设置不当,变成了54或者58。维度差一两个,矩阵运算不报错,但卡尔曼增益的形态是错的,估计值偏到十万八千里。
解决:在初始化之后、滤波之前,先打印一次观测函数的输出维度,核对和dim_z是否一致。我在工程习惯里会写一个assert语句,维度不匹配直接抛异常,而不是等滤波跑起来之后再看图发现曲线不对。
6. 把算法跑起来之后:验证估计效果的三个指标与噪声估计参数调优技巧
用这份资源里的代码把滤波器跑通之后,直接看平均误差和最大误差是远远不够的。我在复现过程中逐步建了一套验证流程,包含三个指标和三组调优技巧。
第一个指标是RMSE(均方根误差),计算公式是np.sqrt(np.mean(errors**2))。平均绝对误差会把正负误差抵消掉,RMSE则对大的单点误差更敏感,能暴露滤波器在某些工况段的性能滑坡。第二个指标是把误差按时间画出来,看它是否在20到40步的时变噪声区间内明显变大。如果误差在噪声区间暴涨,说明Sage-Husa估计器的遗忘因子偏大,噪声追踪速度跟不上;如果误差在噪声区间之外也偏大,说明初始R和Q设置就不合理。第三个指标是检查R和Q的收敛曲线,看它们在滤波后期是否稳定在一个合理范围内。如果R持续增长不停下来,说明遗忘因子太大或者异常检测阈值过窄。
调优的第一组是遗忘因子d的扫描。我会在0.90到0.99之间按0.01步进做一组对比实验,画出RMSE随d变化的曲线。通常曲线会有一个明显的凹谷,谷底对应的d就是当前数据下的最优值。第二组是异常检测阈值的调整。前面说过用马氏距离的95%分位数做阈值,但更精细的做法是同时统计误差的假阳性率——阈值设大了,异常数据漏检;阈值设小了,正常数据被误修正。我一般会画ROC曲线来平衡这两个指标。第三组是初始R和Q的设置。不要直接用单位矩阵,而是跑一段开环仿真记录残差,拿残差的标准差作为R的初始对角元素。
从那以后,我每次拿到新的谐波数据,都强制走一遍这套验证流程:先算RMSE和峰度,再看误差时间曲线,最后扫一遍遗忘因子和阈值参数。这个过程看起来繁琐,但能避免把一组看起来不错的仿真指标误当成真实性能。希望帮到你。
本文还有配套的精品资源,点击获取