本文承接滑动 RMS 如何接报警:窗口未满和缺样不能当成正常中的有效性接口:旧文负责指标何时有效,这次新增双阈值、按时间持续、UNKNOWN 与重新恢复的完整离线状态机。
前面的 RMS 有效性讨论解决"这个测量值当前能否使用";接到报警逻辑后,还要回答"它已满足阈值多久,以及证据中间是否断过"。本文从这个有效性接口继续到时间状态机,不再计算或重复 RMS。输入 x 可以是已经验证质量的温度、振动指标或电流,本例使用无量纲的合成观测。
用"连续三个点超限"代替"一秒钟超限",隐含了固定采样周期。通信抖动、密集补发或短时停顿会破坏这种假设。本实验根据时间戳累计被观测且连续通过质量检查的合格区间,输出状态、有效性与事件表。
1. 明确状态与输入契约
状态为 UNKNOWN=-1、NORMAL=0、ALARM=1。初始 UNKNOWN,不能在没有证据时默认正常。输出 Alarm 只在已知状态下为 0/1;UNKNOWN 时为 NaN,同时 StateValid=false。有效样本不一定意味着状态已知,例如恢复后的第一个高值仍未达到进入持续时间。
t 是严格递增、有限的 double 相对秒数,abs(t)<=1e9;x 为同长度的实 double 向量,允许 NaN/Inf 表示无效,有限幅值不超过 1e6,长度 1~100000。阈值 low、high 为有限 double 标量且幅值不超过 1e6,必须 low<high;进入、退出持续时间和最大间隔均为 double 标量,范围 (0,1e6] 秒。本示例不接受零持续时间,也不会排序重复时间戳。
示例参数为 low=3、high=5、进入持续 1 s、退出持续 0.5 s、最大接纳间隔 1 s。相邻两条都有效且 dt<=maxGap 才构成可连续计时的连接;等于间隔阈值时允许连接。
2. 等号、滞回带和恢复策略
| 当前观测/质量 | 状态处理 | 计时处理 |
|---|---|---|
| 无效值 | 立即 UNKNOWN | 清除候选与起点 |
| 相邻连接不合格 | 先 UNKNOWN | 清除旧计时;当前有效点可作为新起点 |
x>=high,当前不是 ALARM |
考察进入 ALARM | 同一高值合格段持续达到 enterHold 才进入 |
x<=low,当前不是 NORMAL |
考察进入 NORMAL | 同一低值合格段持续达到 exitHold 才进入 |
low<x<high |
保留当前已知状态;UNKNOWN 继续 UNKNOWN | 清除候选,不暂停累计 |
| 已 ALARM 且仍在高侧,或已 NORMAL 且仍在低侧 | 保留状态 | 不重复产生进入事件 |
高阈值与低阈值的等号都算合格。从 UNKNOWN 恢复也必须重新满足高侧或低侧持续时间;不会因为收到一个有限值就恢复正常。已在 ALARM 时,短暂低值不足退出时间则继续报警;处于 NORMAL 时,短暂高值不足进入时间则继续正常。
候选从首个合格样本时刻 t_start 开始,在同一连续合格段的当前行计算 D=t_k-t_start,并在 D>=hold 的首个观测行完成转换。这里不会把事件倒签成 t_start+hold:采样不一定恰好落在该时刻。若高侧计时中进入滞回带或低侧,原高侧计时清除,不能把分散的高值时间拼起来。
这种"持续"是业务约定:仅依据已观察且连续通过质量检查的区间计时,不能证明采样间信号从未越阈。图中的折线或阶梯线也不提供这种证明。
3. 手算时序:三个高点为什么不能算一秒
0 和 0.5 s 的低值让初始 UNKNOWN 在 0.5 s 恢复 NORMAL。1.25~1.5 s 的高值只有 0.25 s,2 s 的中间值清除候选。第二轮高值出现在 2.5、2.75、3、3.5 s;前三个点只覆盖 0.5 s,所以 3 s 仍 NORMAL,到 3.5 s 才 ALARM。
4 s 的中间值保持 ALARM;4.5、5 s 的值恰好等于 low,满足 0.5 s 后转 NORMAL。5.5 s 的 NaN 立即使状态 UNKNOWN。6 s 的高值开始候选,但下一条到 8 s 才来,间隔 2 s 超限,旧候选作废;8、8.5、9 s 重新累计,9 s 才 ALARM。10、10.5 s 的低值最终使其退出。
| 事件时刻 (s) | 从状态 → 到状态 | 原因 | 本轮合格起点 (s) |
|---|---|---|---|
| 0.5 | UNKNOWN → NORMAL | 低侧持续完成 | 0 |
| 3.5 | NORMAL → ALARM | 高侧持续完成 | 2.5 |
| 5 | ALARM → NORMAL | 低侧持续完成 | 4.5 |
| 5.5 | NORMAL → UNKNOWN | 无效观测 | 不适用 |
| 9 | UNKNOWN → ALARM | 恢复后高侧持续完成 | 8 |
| 10.5 | ALARM → NORMAL | 低侧持续完成 | 10 |
故意错误控制是"不看时间,连续三个高值就置真"。它在 3 s 提前置真;3.5 s 才满足本实验的 1 s 规则。该控制只用于暴露计时错误,不是一套完整滞回状态机。
4. 完整可运行源码
保存为 time_alarm_demo.m 并执行 time_alarm_demo。它生成输入、进行手算事件断言、导出表格/MAT 和图片;本地状态机函数随文件完整提供,不需要 Simulink、Stateflow 或其他工具箱。
matlab
function r=time_alarm_demo(outDir)
% Timestamp-based sampled persistence, explicit UNKNOWN; no fixed sample period.
if nargin<1,outDir=fullfile(fileparts(mfilename('fullpath')),'results');end
if ~exist(outDir,'dir'),mkdir(outDir);end
t=[0;0.5;1;1.25;1.5;2;2.5;2.75;3;3.5;4;4.5;5;5.5;6;8;8.5;9;9.5;10;10.5;11];
x=[2;2;2;6;6;4;6;6;6;6;4;3;3;NaN;6;6;6;6;6;3;3;3];
low=3;high=5;enterHold=1;exitHold=0.5;maxGap=1;
a=timed_hysteresis(t,x,low,high,enterHold,exitHold,maxGap);
expectedTimes=[0.5;3.5;5;5.5;9;10.5];
assert(isequal(a.events.Time_s,expectedTimes),'ALARM:Timeline','Hand transition timeline differs.');
assert(isequal(a.events.ToState,[0;1;0;-1;1;0]),'ALARM:State','Hand transition states differ.');
% Wrong shortcut: 3 consecutive high samples -> alarm, using sample count.
count=0;wrong=false(size(x));
for k=1:numel(x)
if isfinite(x(k))&&x(k)>=high,count=count+1;else,count=0;end
wrong(k)=count>=3;
end
assert(wrong(t==3)&&a.state(t==3)==0,'ALARM:CountTrap','Sample-count counterexample differs.');
assert(a.state(t==8.5)==-1 && ~a.stateValid(t==8.5),'ALARM:Gap','Long-gap dwell must restart.');
tab=table(t,x,a.sampleValid,a.linkValid,a.state,a.stateValid,a.alarm,a.evaluatedTarget,a.dwell_s,wrong, ...
'VariableNames',{'Time_s','Input','SampleValid','IncomingLinkValid','State','StateValid','Alarm','EvaluatedTarget','Dwell_s','WrongThreeHighSamples'});
writetable(tab,fullfile(outDir,'time_alarm_samples.csv'));
writetable(a.events,fullfile(outDir,'time_alarm_events.csv'));
writetable(a.qualityEvents,fullfile(outDir,'time_alarm_quality.csv'));
f=figure('Visible','off','Color','w','Position',[80 80 1100 800]);
subplot(3,1,1);plot(t,x,'bo-');hold on;yline(high,'r--');yline(low,'g--');grid on;
xlabel('Time (s)');ylabel('Input');legend('Observed samples','Enter >=5','Exit <=3','Location','eastoutside');
title('Lines connect samples for display; unseen behavior is not proved');
subplot(3,1,2);stairs(t,a.state,'b-','LineWidth',1.4);hold on;
plot(a.events.Time_s,a.events.ToState,'ko','MarkerFaceColor','k');grid on;
yticks([-1 0 1]);yticklabels({'UNKNOWN','NORMAL','ALARM'});ylim([-1.3 1.3]);xlabel('Time (s)');
title('State at observation times only; gap reset is detected at next arrival');
subplot(3,1,3);plot(t,a.dwell_s,'bo-');hold on;stairs(t,double(wrong),'r--');grid on;
xlabel('Time (s)');ylabel('Seconds / wrong Boolean');legend('Evaluated dwell (s)','Wrong 3-high-sample rule','Location','eastoutside');
title('At 3 s: 3 high samples span only 0.5 s; enter at 3.5 s');
print(f,fullfile(outDir,'time_alarm.png'),'-dpng','-r150');close(f);
r=struct('low',low,'high',high,'enterHold_s',enterHold,'exitHold_s',exitHold,'maxGap_s',maxGap, ...
'transitionTimes_s',a.events.Time_s,'transitionStates',a.events.ToState, ...
'wrongCountTriggersAt_s',3,'correctFirstAlarmAt_s',3.5,'unknownSamples',nnz(~a.stateValid));
save(fullfile(outDir,'time_alarm.mat'),'r','t','x','a','tab','wrong');
fprintf('ALARM_PASS transitions=%d wrong_count_at=3 actual_first_alarm=3.5 unknown_samples=%d\n',height(a.events),nnz(~a.stateValid));
end
function a=timed_hysteresis(t,x,low,high,enterHold,exitHold,maxGap)
% -1 UNKNOWN, 0 NORMAL, 1 ALARM. Low/high equality qualifies. Positive holds.
validateattributes(t,{'double'},{'real','finite','vector','nonempty'});
validateattributes(x,{'double'},{'real','vector','nonempty'});
validateattributes(low,{'double'},{'real','finite','scalar'});
validateattributes(high,{'double'},{'real','finite','scalar'});
validateattributes(enterHold,{'double'},{'real','finite','scalar','positive','<=',1e6});
validateattributes(exitHold,{'double'},{'real','finite','scalar','positive','<=',1e6});
validateattributes(maxGap,{'double'},{'real','finite','scalar','positive','<=',1e6});
t=t(:);x=x(:);N=numel(t);
assert(N==numel(x)&&N<=1e5,'ALARM:Length','Equal lengths with at most 1e5 samples required.');
assert(low<high && max(abs([low high]))<=1e6,'ALARM:Threshold','Require bounded low<high.');
assert(all(abs(t)<=1e9),'ALARM:Range','Use relative seconds with magnitude <=1e9.');
dt=diff(t);assert(all(isfinite(dt)&dt>0),'ALARM:Time','Timestamps must strictly increase.');
sampleValid=isfinite(x);assert(all(abs(x(sampleValid))<=1e6),'ALARM:Range','Finite input magnitude must be <=1e6.');
link=[false;sampleValid(1:end-1)&sampleValid(2:end)&dt<=maxGap];
state=-ones(N,1);pending=nan(N,1);dwell=zeros(N,1);
s=-1;target=NaN;start=NaN;ev=cell(0,6);quality=cell(0,3);
for k=1:N
previous=s;reason='';qualificationStart=NaN;
if k==1,quality(end+1,:)={k,t(k),'initial_unknown'};end %#ok<AGROW>
if ~sampleValid(k)
s=-1;target=NaN;start=NaN;reason='invalid_sample';
quality(end+1,:)={k,t(k),'invalid_sample'}; %#ok<AGROW>
else
if k>1 && ~link(k)
s=-1;target=NaN;start=NaN;reason='continuity_reset';
quality(end+1,:)={k,t(k),'continuity_reset'}; %#ok<AGROW>
end
candidate=NaN;holdTime=NaN;
if x(k)>=high && s~=1,candidate=1;holdTime=enterHold;
elseif x(k)<=low && s~=0,candidate=0;holdTime=exitHold;end
if isnan(candidate)
target=NaN;start=NaN; % Midband resets pending but retains known state.
else
if isnan(target)||target~=candidate,target=candidate;start=t(k);end
% These row outputs record the evaluated candidate, including completion.
dwell(k)=t(k)-start;pending(k)=target;
if dwell(k)>=holdTime
s=target;qualificationStart=start;reason='dwell_complete';
target=NaN;start=NaN;
end
end
end
if s~=previous
ev(end+1,:)={k,t(k),previous,s,reason,qualificationStart}; %#ok<AGROW>
end
state(k)=s;
end
if isempty(ev)
events=table(zeros(0,1),zeros(0,1),zeros(0,1),zeros(0,1),cell(0,1),zeros(0,1), ...
'VariableNames',{'Sample','Time_s','FromState','ToState','Reason','QualificationStart_s'});
else
events=cell2table(ev,'VariableNames',{'Sample','Time_s','FromState','ToState','Reason','QualificationStart_s'});
end
qualityEvents=cell2table(quality,'VariableNames',{'Sample','Time_s','Reason'});
valid=state~=-1;alarm=nan(N,1);alarm(valid)=double(state(valid)==1);
a=struct('state',state,'stateValid',valid,'alarm',alarm,'sampleValid',sampleValid, ...
'linkValid',link,'evaluatedTarget',pending,'dwell_s',dwell,'events',events,'qualityEvents',qualityEvents);
end
5. 实测、事件表与有效性字段
2026-10-21 在 MATLAB 25.2.0.2998904 (R2025b),PCWIN64 实际执行,v2 主批处理和独立检查批处理的退出码均为 0。六个状态转换与上表逐项一致,22 条观测中有 5 条 UNKNOWN。错误样本计数控制在 3 s 置真,正式逻辑首次 ALARM 为 3.5 s。

上图阈值等号可直接对照观测;中图 UNKNOWN 独立绘出;下图比较当行评估持续时间与错误的样本计数控制。图片已打开检查。状态连线只是观测行的显示方式,不意味着缺口内仍持续有效。
time_alarm_samples.csv 包含 SampleValid、IncomingLinkValid、State、StateValid、Alarm、EvaluatedTarget 和 Dwell_s。第一行没有入边,连接标志为 false。EvaluatedTarget 与 Dwell_s 记录当行被评估的候选和证据持续时间,包括完成转换的那一行;内部候选在转换后已清除,所以它们不能解释为"处理完该行后仍待完成的任务"。没有候选时目标为 NaN、持续时间为 0。
time_alarm_events.csv 只记录状态改变;time_alarm_quality.csv 另记录初始未知、无效值、连续性重置,即使状态本来已是 UNKNOWN 也保留质量记录。无事件是合法输出,数值列仍应为 double 类型的空列,不能随数据情况变成 cell。该空表问题在独立复核后修复,并被实际边界测试覆盖。table 官方文档
独立参考先按无效点/长间隔切成质量段,再枚举高侧、低侧的最大连续合格段,寻找首个达到持续时间的观测,最后按完成事件填充状态。它不复用主实现的候选计时变量。对 5 个不等间隔时刻、四类观测 {低值,中间值,高值,NaN} 的全部 4^5=1024 组序列,结果逐样本一致;固定时刻还包含一个长间隔。另有阈值等号、中间带清候选、无效值打断待定/已报警状态、长间隔打断已报警状态、单样本、无事件表类型与不合法参数检查。三篇总计 47 个命名检查通过;这不替代所有时间排列和实物工况验证。
6. 离线记录分析如何接到在线系统
可按"传感器/已有 RMS 指标 → 时间戳和质量位 → 本状态机 → {状态,有效性,事件} → 看板或控制层"的接口组织。质量位失败时保留记录并置 x=NaN,不能把 UNKNOWN 映射成 false 后当正常。显示可采用灰色未知、绿色正常、红色报警,同时保留恢复原因。硬件接线、电气隔离与输出联锁仍应依据实际设备设计,本例没有操作任何 I/O,也不是安全联锁认证软件。
离线复用可把 timed_hysteresis 本地函数提取到同名文件,调用 a=timed_hysteresis(t,x,low,high,enterHold,exitHold,maxGap)。此函数每次从 UNKNOWN 开始;连续数据不能简单按任意块独立调用后假装状态连续。真正的在线版本需要显式保存状态、候选目标、候选起点、上一条时间与质量。
还需区分样本到达驱动 和独立超时检测。本 demo 只在收到下一条记录时发现长间隔:6→8 s 的缺口在 8 s 被识别。若设备永久断流,就没有下一条记录可驱动此函数。在线系统应另有基于单调时钟的 watchdog,在 now-lastTime>maxGap 时产生 UNKNOWN/超时事件并清除候选;不能等补发数据到来才宣称期间已实时失效。这里提供的是可审查的离线批处理模型,不声称实现了在线定时器。
选择进入/退出持续时间需要考虑采集周期、允许抖动、业务响应速度与传感器质量。数值范围限制不能消除时间戳量化和浮点舍入;严格等号按存储的 double 值判断,若业务需要容差,应先制定明确的容差策略再测试,不能随意加 epsilon。也不能用更长持续时间掩盖质量标志缺失。
本图使用 stairs 官方接口 在给定时间坐标上绘制状态;图形本身不增加观测证据。输入验证参考 validateattributes 官方文档。官方页面于 2026-10-21 核对,当前标注 R2026b;本次代码真实运行于 R2025b。
本篇代码的执行凭据
以下是便于复用的命令模板,不是原测试机绝对路径。只复现本篇时,将上文完整源码保存到新的工作目录,将 MATLAB 可执行文件加入 PATH;把下例 你的新目录 换成实际路径。命令的标准输出与退出码应保存,以便复核。
powershell
matlab -wait -sd "你的新目录" -batch "time_alarm_demo"
本次实际命令、源快照与时间/退出码原文均保留在作者归档实验包的 matlab/execution,最终数值来自其中 20261021-082339-demos-v2 运行,并非模板命令的假定结果。
主运行退出码 0,独立检查退出码 0。作者归档中保留 matlab/execution/20261021-082339-demos-v2 与 matlab/execution/20261021-082407-tests-v2 下的 stdout.txt、run.json。这些是归档路径,不是本网页的下载链接;本篇不依赖该归档即可运行上文完整示例。
本篇完整 MATLAB 源码 SHA256:d770d8e3781fb3927d2f4b190e32f5ebc60593125b329d7f8c710764857d3744。文内代码、final 源码与真实执行快照已逐字节核验一致。独立复核由不同于主作者的 AI 子代理完成,身份、发现、修复和范围记录保存在作者实验归档的 reviews;不是人工认证。
留一个工程问题
系统已处于 ALARM,随后出现一条无效观测,恢复后的首个有效值又落在 low 与 high 之间。按本文契约,它应显示 ALARM、NORMAL 还是 UNKNOWN,什么后续证据才能使状态重新有效?