本文代码已在 MATLAB R2025b 25.2.0.2998904、PCWIN64 环境原生运行,并完成独立原生复核。最终发布源码在 2026-11-06 01:06:40–01:07:13 UTC 再次完整回归。以下区分作者测试、独立测试和产物检查,不将重复运行叠加计数。
1. 固定系数会把“秒”变成“样本数”
采集程序收到一串数值和时间戳,写出 y = (1-alpha)*y + alpha*x 很容易。如果采样间隔时而 20 ms、时而 90 ms,同一个 alpha 对应的物理时间常数已经变了。图形仍然平滑,却可能在数据密集段响应过快,在稀疏段响应过慢。
本文只解决一个明确问题:已知一阶模型的时间常数 tau,怎样在不等间隔时间戳上更新状态,并把不可信的长时间缺口单独标出来。它不估计 tau,不自动修正时间戳,不插值补造丢失数据,也不设计报警阈值。
2. 先写清输入在哪个区间有效
模型是 tau * dy/dt = x - y,tau 的单位与时间戳相同。约定 x(k) 在 [t(k), t(k+1)) 内保持不变,y(k) 是 t(k) 时刻的状态。这是左端零阶保持,不是把刚收到的新数值回填进上一个区间。
对单个区间解微分方程:
y(k) = exp(-dt/tau)*y(k-1) + (1-exp(-dt/tau))*x(k-1),其中 dt=t(k)-t(k-1)。
这也解释了一个容易被误判为“慢一拍”的现象:当 x(k) 在 t(k) 突然变化时,连续系统状态 y(k) 不会瞬间跳到新输入;新输入影响接下来的区间。如果业务定义是用当前观测 x(k) 做一次离散平滑,那是另一种更新约定,应重新定义输出时刻,不能悄悄把这里的 x(k-1) 换成 x(k)。
当 dt/tau 很小时,直接算 1-exp(-dt/tau) 会做两个相近数相减。代码用 alpha=-expm1(-dt/tau)。MathWorks 的 expm1 文档说明了它对小实数输入的精度用途;这里的 ODE、区间语义和缺口策略由本文独立定义。
另一个方向也要小心:当 dt/tau 较大时,alpha 可能舍入成 1,直接用 1-alpha 会丢掉尚未为零的衰减系数。实现因此分别计算 beta=exp(-dt/tau) 和 alpha=-expm1(-dt/tau),而不是由其中一个相减构造另一个。定向测试使用 dt/tau=40、初值 1e100、输入为 0,检查剩余状态仍被保留。
3. 长缺口不是普通的大 dt
如果设备明确保证采样保持,即使没有新报告,旧输入仍然有效,可以令 maxGap=Inf,持续按这个物理模型推演。如果只是网络中断,无法确认期间发生了什么,把很大的 dt 代入公式只是一个假设,不是重建了缺失历史。
本文提供一个更保守且容易审计的策略:
- dt 小于或等于 maxGap:按左端保持模型积分。
- dt 大于 maxGap:舍弃旧状态,将 y(k) 重置为当前新样本 x(k),同时令 reset(k)=true。
- 第一项使用调用者明确给定的 y0,reset(1)=false;它不是缺口重置。
“重置到当前值”是新的起点,不是对缺口末端真实物理状态的估计。下游若要判断“已经稳定”,还需要另外定义重置后的有效期。本文的 reset 仅标记重置发生的位置。
输入含 NaN、时间戳重复或倒序时直接报错,不静默删点。删掉时间戳后再运行会改变每段 dt,尤其容易掩盖采集故障。时间戳建议先在保留原始整数/时间类型精度的阶段转换成相对秒;如果大绝对时间戳转 double 时已经丢掉微小间隔,后续相减不能把它救回来。
4. 哪些性质应该先测
第一组使用恒定输入 1、初值 0。解析解是 1-exp(-(t-t(1))/tau),与不规则网格上的更新逐点对照。这比“曲线看起来不错”更直接。
第二组把同一个恒定输入区间拆成多个子区间。只要保持假设不变,终点状态应在舍入误差内一致。第三组改变 x(3),检查 y(1:3) 不变而下一状态改变,用于抓出左右端输入混用。
边界组覆盖单点、很小 dt、很大 dt、恰好等于缺口门限和刚超过门限。负例是给全部区间套用同一个名义 dt 的系数,要求它在构造的不规则网格上显著偏离解析解。该负例用于验证测试能抓住目标错误,不是宣称固定系数永远不能使用。
5. 完整实现与可复现入口
把下面三个文件存到同一个空目录。本次原生环境为 MATLAB R2025b,版本 25.2.0.2998904,PCWIN64。对最终 11 个作者 .m 文件执行 requiredFilesAndProducts,静态依赖结果仅识别 MATLAB 产品;本机安装有其他工具箱,本次未进行仅安装 MATLAB 的干净环境试验,也未做许可证调用跟踪。因此这里只报告已执行环境与静态依赖结果,不扩大成所有环境兼容保证。官方在线文档当前页面标 R2026b,本文不把文档版本混作实际执行版本。
lowpass_zoh.m
function [y, reset] = lowpass_zoh(t, x, tau, y0, maxGap)
%LOWPASS_ZOH Exact interval update under a declared left-sample hold.
% x(k) holds on [t(k),t(k+1)); y(k) is the state at t(k).
% If dt > maxGap, discard the old state and set y(k)=x(k).
% Reset is a policy decision, not an estimate of the unobserved interval.
if ~(isa(t,'double') && ~issparse(t) && isreal(t) && isvector(t) && ~isempty(t) && ...
all(isfinite(t(:))) && isa(x,'double') && isreal(x) && ...
~issparse(x) && isvector(x) && ~isempty(x) && all(isfinite(x(:))))
error('lowpass:Input','t and x must be nonempty finite real full double vectors.');
end
if numel(t)~=numel(x), error('lowpass:Shape','t and x lengths must match.'); end
if ~(isa(tau,'double') && ~issparse(tau) && isreal(tau) && isscalar(tau) && isfinite(tau) && tau>0)
error('lowpass:Tau','tau must be a finite positive double scalar.');
end
if ~(isa(y0,'double') && ~issparse(y0) && isreal(y0) && isscalar(y0) && isfinite(y0))
error('lowpass:Initial','y0 must be a finite real double scalar.');
end
if ~(isa(maxGap,'double') && ~issparse(maxGap) && isreal(maxGap) && isscalar(maxGap) && ...
~isnan(maxGap) && maxGap>0)
error('lowpass:Gap','maxGap must be positive; Inf explicitly disables resets.');
end
t=t(:); x=x(:); dt=diff(t);
if any(~isfinite(dt) | dt<=0)
error('lowpass:TimeOrder','Time differences must be finite and strictly positive.');
end
y=zeros(size(t)); reset=false(size(t)); y(1)=y0;
for k=2:numel(t)
if dt(k-1)>maxGap
y(k)=x(k); reset(k)=true;
else
alpha=-expm1(-dt(k-1)/tau);
beta=exp(-dt(k-1)/tau);
y(k)=beta*y(k-1)+alpha*x(k-1);
if ~isfinite(y(k)), error('lowpass:Overflow','Nonfinite state update.'); end
end
end
end
test_lowpass_zoh.m
function result = test_lowpass_zoh()
% Tests for the declared left-held low-pass interface.
n = 0;
t = [0; 0.02; 0.11; 0.12; 0.8; 1.7; 3.0];
tau = 0.7;
[y, reset] = lowpass_zoh(t, ones(size(t)), tau, 0, Inf);
ref = -expm1(-t/tau);
check(max(abs(y-ref)) < 2e-14, 'constant-input analytic response');
check(~any(reset), 'no reset without a long gap');
check(all(diff(y) >= 0) && all(y >= 0 & y <= 1), 'convex bounds');
[a, ~] = lowpass_zoh([0; 1], [2; 2], tau, -3, Inf);
[b, ~] = lowpass_zoh([0; 0.1; 0.6; 1], 2*ones(4,1), tau, -3, Inf);
check(abs(a(end)-b(end)) < 2e-14, 'subdivision invariance for held input');
[yrow, ~] = lowpass_zoh(t.', ones(1,numel(t)), tau, 0, Inf);
check(isequal(yrow,y) && iscolumn(yrow), 'shape normalization');
x = [0; 2; -1; 5]; tt = [0; 0.1; 0.4; 0.7];
[a, ~] = lowpass_zoh(tt, x, tau, 0, Inf);
x(3) = 999;
[b, ~] = lowpass_zoh(tt, x, tau, 0, Inf);
check(isequal(a(1:3),b(1:3)) && a(4) ~= b(4), 'left-held sample timing');
[a, r] = lowpass_zoh([0; 0.5; 1.0001; 1.2], [1; 2; 7; 8], 1, 0, 0.5);
check(isequal(r,[false; false; true; false]), 'strict long-gap threshold');
check(a(3) == 7, 'long gap resets to newly observed sample');
check(a(2) > 0 && a(2) < 1, 'gap equal to threshold is integrated');
[a, r] = lowpass_zoh(5, 4, 1, -2, Inf);
check(a == -2 && ~r, 'single sample retains declared initial state');
[a, ~] = lowpass_zoh([0; 1e-18], [1; 1], 1, 0, Inf);
check(a(2) > 0 && abs(a(2)/1e-18-1) < 1e-14, 'expm1 small-step branch');
[a, ~] = lowpass_zoh([0; 1000], [1; 0], 1, -1, Inf);
check(a(2) == 1, 'large normalized step reaches held input');
[a, ~] = lowpass_zoh([0;40],[0;0],1,1e100,Inf);
tail=exp(-40)*1e100;
check(abs(a(2)/tail-1)<2e-14,'retain small decay times a large initial state');
expect_error(@()lowpass_zoh([0;0],[1;2],1,0,Inf),'lowpass:TimeOrder');
expect_error(@()lowpass_zoh([1;0],[1;2],1,0,Inf),'lowpass:TimeOrder');
expect_error(@()lowpass_zoh([0;1],[1;NaN],1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh([0;1],1,1,0,Inf),'lowpass:Shape');
expect_error(@()lowpass_zoh([0;1],[1;2],0,0,Inf),'lowpass:Tau');
expect_error(@()lowpass_zoh([0;1],[1;2],1,0,0),'lowpass:Gap');
expect_error(@()lowpass_zoh([-realmax;realmax],[1;2],1,0,Inf),'lowpass:TimeOrder');
expect_error(@()lowpass_zoh([],[],1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh(single([0;1]),[1;2],1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh(sparse([0;1]),[1;2],1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh([0;1],sparse([1;2]),1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh([0;1],[1;2i],1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh([0;Inf],[1;2],1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh(ones(2),ones(2),1,0,Inf),'lowpass:Input');
expect_error(@()lowpass_zoh([0;1],[1;2],NaN,0,Inf),'lowpass:Tau');
expect_error(@()lowpass_zoh([0;1],[1;2],Inf,0,Inf),'lowpass:Tau');
expect_error(@()lowpass_zoh([0;1],[1;2],1,Inf,Inf),'lowpass:Initial');
expect_error(@()lowpass_zoh([0;1],[1;2],1,1i,Inf),'lowpass:Initial');
expect_error(@()lowpass_zoh([0;1],[1;2],1,0,NaN),'lowpass:Gap');
expect_error(@()lowpass_zoh([0;1],[1;2],1,0,-Inf),'lowpass:Gap');
expect_error(@()lowpass_zoh([0;1],[1;2],sparse(1),0,Inf),'lowpass:Tau');
expect_error(@()lowpass_zoh([0;1],[1;2],1,sparse(0),Inf),'lowpass:Initial');
expect_error(@()lowpass_zoh([0;1],[1;2],1,0,sparse(1)),'lowpass:Gap');
% Deliberate wrong model: use nominal step for every irregular interval.
alpha = -expm1(-0.1/tau); wrong = zeros(size(t));
for k=2:numel(t), wrong(k)=(1-alpha)*wrong(k-1)+alpha; end
check(max(abs(wrong-ref)) > 0.2, 'fixed-alpha mutant is detected');
result = struct('status','PASS','checks',n,'analytic_max_error',max(abs(y-ref)), ...
'fixed_alpha_max_error',max(abs(wrong-ref)));
fprintf('lowpass_zoh PASS checks=%d\n',n);
function check(ok,label)
assert(ok,'test:Failed','%s',label); n=n+1;
end
function expect_error(fn,id)
found=false;
try, fn(); catch ME
if ~strcmp(ME.identifier,id), rethrow(ME); end
found=true;
end
check(found,['expected error ' id]);
end
end
demo_lowpass_zoh.m
function demo_lowpass_zoh(outdir)
% Generates native MATLAB figures only when actually executed.
t=[0; 0.02; 0.11; 0.12; 0.8; 1.7; 3.0]; tau=0.7;
[y,~]=lowpass_zoh(t,ones(size(t)),tau,0,Inf);
wrong=zeros(size(t)); alpha=-expm1(-0.1/tau);
for k=2:numel(t), wrong(k)=(1-alpha)*wrong(k-1)+alpha; end
tg=[0;0.1;0.2;2;2.1;2.2]; xg=[0;0;1;3;3;3];
[yg,reset]=lowpass_zoh(tg,xg,tau,0,0.5);
fig=figure('Visible','off','Color','w','Position',[100 100 1000 720]);
clean=onCleanup(@()close(fig));
subplot(2,1,1); fine=linspace(0,3,400);
plot(fine,-expm1(-fine/tau),'k-',t,y,'bo-',t,wrong,'rs--','LineWidth',1.3);
grid on; xlabel('Time (s)'); ylabel('State');
legend('Analytic held-input response','Actual dt','Fixed nominal dt','Location','southeast');
title('Same time constant, irregular observation times');
subplot(2,1,2); hInput=plot(tg,xg,'k.','MarkerSize',14); hold on;
for k=1:numel(tg)-1
if tg(k+1)-tg(k)<=0.5
plot(tg(k:k+1),[xg(k);xg(k)],'k-','HandleVisibility','off');
end
end
hState=plot(tg,yg,'bo','LineWidth',1.3);
hReset=plot(tg(reset),yg(reset),'rx','MarkerSize',12,'LineWidth',2);
grid on; xlabel('Time (s)'); ylabel('Value');
legend([hInput hState hReset],'Input / trusted left hold','State samples', ...
'Explicit reset','Location','best');
title('Long gaps are marked resets, not recovered history');
print(fig,fullfile(outdir,'01_irregular_lowpass.png'),'-dpng','-r160');
save(fullfile(outdir,'01_irregular_lowpass_data.mat'),'t','tau','y','wrong','tg','xg','yg','reset');
end
运行命令
在以上文件所在目录运行以下命令。使用新目录保留每次证据;出错时保留失败日志,不把失败记录改成通过。
outdir=fullfile(pwd,['native_run_' datestr(now,'yyyymmddTHHMMSS')]);
assert(exist(outdir,'dir')~=7,'Please use a new evidence directory.');
mkdir(outdir);
diary(fullfile(outdir,'native.log'));
fprintf('MATLAB: %s\nPlatform: %s\n',version,computer);
ver
result=test_lowpass_zoh();
demo_lowpass_zoh(outdir);
save(fullfile(outdir,'native_result.mat'),'result');
diary off
6. 图应该帮助判断哪件事
第一幅图画恒定输入的解析曲线、真实 dt 更新样本和固定名义 dt 的反例。应比较同一物理时间上的状态,而不是同一数组下标。样本点之间的连线仅作视觉辅助,黑线才是连续解析解。
第二幅图只在可信短区间画左端保持线段,长缺口不连输入轨迹。状态用采样点显示,重置位置用叉号标出,避免将连接离散样本的直线误认为真实 ODE 轨迹。本次原生运行已生成并回读图像,独立复核也核对了对应 MAT 数据和图示语义。本文保留完整生成图代码,不嵌入实验图片;读者运行 demo_lowpass_zoh 后可自行检查。
7. 原生结果与适用边界
- 作者原生检查:37 项 API 断言通过,另有 3 项导出检查通过。
- 独立原生检查:422 项 API 断言、9 项产物断言通过。独立解析对照报告的 maxLowpassError 为 3.1610696608321403e-16;这里只按报告字段记录,不将其扩写为全域误差上界。
- 独立变异:把 beta 改回 1-alpha 后,大初值衰减用例得到 0,正确参考为 4.2483542552915893e82,运行时断言按预期失败。最终实现的对应回归通过。
- 原生图像完成生成、回读和视觉复核,MAT 完成独立重算。最终发布副本的整批回归退出码为 0,stderr 为空。
本方法对声明的分段恒定输入给出区间精确解,但仍有浮点舍入,不是任意真实输入的精确重建。极小间隔可能小到 double 分辨率之外;极大的幅值也可能导致非有限中间结果,代码会拒绝非有限状态。更极端的 dt/tau 还可能使指数系数下溢;先算系数再乘巨大状态时,甚至可能遗漏本可表示的残余。本例不承诺跨越这一极端动态范围,需要时应缩放状态或使用专门的指数乘法。对于线性变化输入、多变量状态空间模型、状态协方差传播,应该另建模型,不应把这个单状态函数直接包装成通用滤波器。
工程上最先确认的不是 alpha 取多少,而是:旧采样值究竟从什么时候保持到什么时候,缺口期间是否还能信任这个假设。
417



被折叠的 条评论
为什么被折叠?



