把两路日志放到同一张表里,有两种不同任务:一种是在公共时间网格上估计信号,另一种是回答"在这个查询时刻之前,最后一条记录是什么,现在还能不能用"。本文处理第二种,不重做公共网格插值。
已有文章《MATLAB 多传感器时间对齐 用 interp1 建立公共时间网格并限制外推》可以与本文对照阅读:这里把任务收窄为事件时间上的 latest-before/as-of 历史匹配,不要求重建采样点之间的连续信号。旧文用 previous 与 next 找到查询两侧的采样时刻,再用两者间距限制线性插值跨长缺口;本文检查的是查询时刻减去最近历史记录时刻。两种时间差对应不同的可用性规则,不能把"附近将有新样本"当作当前已经有新数据。
例如一路传感器更新很快,另一路更新较慢。主通道每到一个时间戳,都希望带上慢速通道的最近记录,再计算差值、画趋势或送进报警逻辑。
这里有两个容易被连续曲线掩盖的问题。第一,补进去的数值是否用到了查询时刻之后的样本?第二,即使它确实来自过去,这条数据是否已经太旧?设备在最后一次正常读数之后断流,单纯向前保持仍会给出一个看起来正常的数字。
本文把快速通道的时间戳作为主时间轴 tq,只处理慢速通道的时间戳 ts 和数值 xs。规则很明确:先选择不晚于查询时刻的最新样本,再检查该样本的年龄和有效性。 函数同时返回值、来源索引、数据年龄及状态,避免下游只看见一个 NaN,却不知道是从未收到、已经过期,还是新样本本身无效。
这里的"因果"只约束事件时间,不保证记录在查询时已经到达;严格在线回放还需要按到达或可获得时间筛选,具体边界见第 8 节。
本次实测环境与验证范围: MATLAB 25.2.0.2998904 (R2025b),PCWIN64。2026-10-22 08:06:58--08:07:49 UTC 完成原生验证,退出码为 0;该时间段包含完整验证流程,不是算法性能基准。
- 独立测试通过 21,890 个合法输入案例、42 个非法输入案例、31 项性质/范围检查,包括未来样本追加不变量和代表性 ULP 边界。
- 四份实测
.m文件与待测源码哈希匹配,源码未改;PNG、FIG、CSV、MAT 均已实际生成并回读,独立运行与图像复核已完成。 - Code Analyzer 对核心函数与演示无诊断;运行入口和独立测试脚本另有 5 条维护建议,已记录,不能将本次结果概括为"全包零诊断"。
1 先固定输入与输出契约
本例处理已经处于同一时钟基准下的事件时间。时间单位统一为秒,并使用相对于同一个原点的非负数值。
| 输入 | 约束 | 这样约束的原因 |
|---|---|---|
ts |
实数 double 向量,有限、非负、严格递增 |
每个源时间唯一,扫描指针可以只向前移动 |
xs |
与 ts 等长的实数 double 向量;允许 NaN,拒绝正负 Inf |
NaN 明确表示一次无效测量 |
tq |
实数 double 向量,有限、非负、非递减,允许重复 |
支持多个记录共享查询时刻,但不在内部重排记录 |
maxAge |
有限、非负的实数 double 标量 |
给出这一路传感器允许保持的最大数据年龄 |
向量可以是行向量或列向量,也接受空输入;输出统一为列向量。空源数据和只有一个源样本的情况都有明确行为,不需要凑够两个点。
isfinite 用于识别有限数,NaN 和正负无穷都不属于有限值。本实现对时间要求有限,对测量值则单独允许 NaN,因此没有用一次"删除所有非有限行"来清洗全部输入。MathWorks:isfinite
输出含义如下:
| 输出 | 含义 |
|---|---|
idx |
选中的源样本索引;没有历史样本时为 0,不能拿它直接索引 xs |
age |
查询时刻减去选中样本的时刻;没有历史时为 NaN |
missing |
存在历史样本,且最新历史样本的值为 NaN |
stale |
存在历史样本,且 age > maxAge |
valid |
有历史、值有效、未过期时为真 |
y |
仅在 valid 为真时输出样本值,否则为 NaN |
这里的 missing 特指"选中的测量值缺失",不包括"从未有过历史样本"。后者由 idx == 0 单独表示。没有历史时,missing、stale 和 valid 都为假,因此不能仅用 ~missing & ~stale 在函数外重建有效状态,还必须检查 idx > 0。
2 把选择与判定写成两个步骤
对第 k 个查询时刻,先定义候选集合:
H k = { i ∣ t s ( i ) ≤ t q ( k ) } . H_k=\{i\mid t_s(i)\leq t_q(k)\}. Hk={i∣ts(i)≤tq(k)}.
如果集合为空,返回 idx(k)=0、age(k)=NaN、y(k)=NaN。否则,因为源时间严格递增,最新历史样本就是
j k = max H k , a k = t q ( k ) − t s ( j k ) . j_k=\max H_k,\qquad a_k=t_q(k)-t_s(j_k). jk=maxHk,ak=tq(k)−ts(jk).
最终输出为
y ( k ) = { x s ( j k ) , x s ( j k ) 有限且 a k ≤ τ , N a N , 其他情况 , τ = maxAge . y(k)=\begin{cases} x_s(j_k), & x_s(j_k)\text{ 有限且 }a_k\leq\tau,\\ \mathrm{NaN}, & \text{其他情况}, \end{cases} \qquad \tau=\texttt{maxAge}. y(k)={xs(jk),NaN,xs(jk) 有限且 ak≤τ,其他情况,τ=maxAge.
选择 j_k 时只看时间,不先过滤 NaN。如果新样本报告无效,它就应当使当前保持值失效。若先删除这一行,程序可能悄悄退回更早的正常值,把"刚刚收到的无效测量"变成"继续正常输出"。
这是一种明确的工程策略:最新记录负责描述当前数据状态,不能因为它不好看就绕过它。 如果业务确实需要"最近一次有效测量",应另建接口和名称,并同时暴露它距离当前时刻的年龄,不能与本文接口混用。
3 手算十个查询点
设源数据为:
matlab
ts = [1; 3; 7; 8];
xs = [10; 30; NaN; 80];
tq = [0; 1; 2; 3; 5; 6; 7; 8; 10; 11];
maxAge = 2;
下面的表先按上述定义逐行手算,再由本次原生 MATLAB 断言逐项核对,六个输出字段均与手算一致。T、F 分别表示真、假。推导给出预期,原生断言则检查实现是否满足预期。
tq / s |
idx |
age / s |
missing |
stale |
valid |
y |
状态 |
|---|---|---|---|---|---|---|---|
| 0 | 0 | NaN | F | F | F | NaN | NO_HISTORY |
| 1 | 1 | 0 | F | F | T | 10 | VALID |
| 2 | 1 | 1 | F | F | T | 10 | VALID |
| 3 | 2 | 0 | F | F | T | 30 | VALID |
| 5 | 2 | 2 | F | F | T | 30 | VALID |
| 6 | 2 | 3 | F | T | F | NaN | STALE |
| 7 | 3 | 0 | T | F | F | NaN | MISSING |
| 8 | 4 | 0 | F | F | T | 80 | VALID |
| 10 | 4 | 2 | F | F | T | 80 | VALID |
| 11 | 4 | 3 | F | T | F | NaN | STALE |
这组输入的手算计数为 6 个有效查询、1 个无历史查询、2 个过期查询、1 个测量缺失查询,本次原生运行得到相同计数。
几个边界值得单独看:
tq=5和tq=10的年龄恰好为 2 秒,按age <= maxAge接受。tq=7选中的是时间 7 秒的NaN,不能回退到时间 3 秒的数值 30。tq=10已超过最后一个源时间 8 秒,但最后样本仍在允许年龄内,因此可以输出 80。tq=0没有源样本可选。此时既不计算一个虚构年龄,也不借用第一个未来样本。
missing 与 stale 是两个独立条件,并不互斥。例如单点源数据 ts=7、xs=NaN,在 tq=10、maxAge=2 时,最新样本既缺失又过期,两项都应为真。演示里的 STALE_MISSING 只是便于查看的组合标签,不替代这两个布尔量。
4 为什么 nearest 和 linear 不能直接替代
只看两个样本 (1,10)、(3,30),查询时刻为 2.6 秒:
| 方法 | 解析结果 | 此查询使用的数据 |
|---|---|---|
linear |
26 | 1 秒与 3 秒的样本 |
nearest |
30 | 3 秒的样本 |
previous |
10 | 1 秒的样本 |
线性结果可以直接算出:
10 + 2.6 − 1 3 − 1 ( 30 − 10 ) = 26. 10+\frac{2.6-1}{3-1}(30-10)=26. 10+3−12.6−1(30−10)=26.
3 秒的样本晚于 2.6 秒,因此前两种结果都用了查询时刻之后的信息。它们可以用于允许双侧信息的离线插值,但不满足本文"只取历史"的选择规则。interp1 的这些方法定义和 previous 至少需要两个样本的要求,可查阅 MathWorks:interp1(R2025b)。
interp1(...,'previous') 可以作为至少两个源点、源时间范围内 的历史选择交叉检查,但它不会替我们执行这里的数据年龄策略。它默认在源时间范围外返回 NaN;本文函数则允许在右侧继续保持,直到最后样本过期。以手算表的 tq=10 为例,两者的边界契约就不同。本文还支持只有一个源样本,不能把该情况直接交给 interp1 对比。MathWorks:interp1 的外推与方法要求
交叉检查时要先限定共同适用范围,而不是发现右端结果不同,就给其中一个实现判错。
5 一个只向前移动的指针就够了
核心循环保留指针 j,初始为 0。处理查询 tq(k) 时,只要下一个源时间仍满足 ts(j+1) <= tq(k),就继续向前推进。
循环退出后恰好有两种情形:
j=0,说明源数据为空,或第一个源样本晚于查询,均没有历史;j>0,当前样本不晚于查询,而下一个样本不存在或晚于查询,因此j正是最新历史样本。
为什么下一次查询不用从头找?因为 tq 非递减。已经不晚于当前查询的源样本,也不会晚于下一次查询;指针无需后退。重复查询时刻同样成立。
令源点数为 N、查询点数为 M。外层执行 M 次,指针在整个过程中最多前进 N 次,核心扫描时间复杂度为 O(N+M) 。六个输出按查询长度分配,为 O(M) ;扫描本身只增加常数个指针和标量状态,即 O(1) 额外状态。输入校验使用 diff 与逻辑临时数组,其临时空间可达 O(N+M),因此不能把整个函数的额外内存宣称为严格 O(1)。
追加未来样本不能改写已有查询
这个选择规则还带来一个可以独立验证的不变量:固定原查询序列,只追加严格晚于所有查询时刻的源样本,并继续满足源时间严格递增,原查询的全部输出应保持不变。
证明很直接。新追加的时间均不满足任一原查询的 ts <= tq 条件,所有候选集合 H_k 都没有变化;选中索引、年龄、缺失标记、过期标记以及最终值也就不变。新增样本是正常值还是 NaN,都不应影响这些过去查询。
以十行手算输入为例,可在源数据末尾追加时间 12 秒、数值 120,仍只查询原来的 0 到 11 秒,再比较六个输出。含 NaN 的输出应使用能按位置比较 NaN 的方式检查。本次独立测试已通过"严格追加未来样本不改变原查询输出"的性质检查;这里的 12 秒示例用于解释不变量,不另充当一条运行记录。
条件中的"晚于所有查询"不能删掉。如果新增的是旧事件时间的迟到记录,它可能进入原查询的候选集合,离线结果就可能改变;这已经涉及迟到数据策略,不属于上述不变量。
6 完整核心函数
保存为 asof_age.m。输入检查会直接拒绝乱序、重复源时间、无穷测量值和不符合类型的输入,不在内部隐藏排序或去重。
matlab
function [y, age, idx, valid, missing, stale] = asof_age(ts, xs, tq, maxAge)
%ASOF_AGE Latest sample at/before query time, gated by age and finite value.
% All time inputs are nonnegative relative seconds in real double vectors.
% ts must be strictly increasing; tq may repeat but must not decrease.
% xs may contain NaN (invalid measurement), but not Inf. Empty vectors work.
% All outputs are columns. idx=0 means no historical sample; never index xs(0).
% A newer NaN invalidates the hold: this function does not seek older good data.
% This is event-time alignment, not an arrival-time availability guarantee.
checkVector(ts, 'ts');
checkVector(xs, 'xs');
checkVector(tq, 'tq');
ts = ts(:); xs = xs(:); tq = tq(:);
if numel(ts) ~= numel(xs)
error('Asof:Length', 'ts and xs must have the same number of elements.');
end
if any(~isfinite(ts)) || any(ts < 0) || any(diff(ts) <= 0)
error('Asof:SourceTime', 'ts must be finite, nonnegative and strictly increasing.');
end
if any(~isfinite(tq)) || any(tq < 0) || any(diff(tq) < 0)
error('Asof:QueryTime', 'tq must be finite, nonnegative and nondecreasing.');
end
if any(isinf(xs))
error('Asof:Value', 'xs may contain NaN but not Inf.');
end
if ~(isa(maxAge, 'double') && isreal(maxAge) && isscalar(maxAge) ...
&& isfinite(maxAge) && maxAge >= 0)
error('Asof:MaxAge', 'maxAge must be a finite nonnegative double scalar.');
end
nq = numel(tq); ns = numel(ts);
y = nan(nq, 1); age = nan(nq, 1); idx = zeros(nq, 1);
valid = false(nq, 1); missing = false(nq, 1); stale = false(nq, 1);
j = 0;
for k = 1:nq
while j < ns && ts(j + 1) <= tq(k)
j = j + 1;
end
if j == 0
continue
end
idx(k) = j;
age(k) = tq(k) - ts(j);
missing(k) = isnan(xs(j));
stale(k) = age(k) > maxAge;
valid(k) = ~missing(k) && ~stale(k);
if valid(k)
y(k) = xs(j);
end
end
end
function checkVector(a, name)
if ~(isa(a, 'double') && isreal(a) && ismatrix(a) ...
&& (isvector(a) || isempty(a)))
error('Asof:Type', '%s must be a real double vector or empty array.', name);
end
end
7 完整演示与原生图生成
将下面文件保存为 asof_age_demo.m,与核心函数放在同一目录。演示包含手算期望断言、插值对照、三幅子图及 CSV/MAT 导出。代码采用基础 MATLAB 功能,本次已在 R2025b 原生环境执行通过。
三个绘图区分别用于观察:被接受的历史值与被拒绝的过期保持值、每个查询点的数据年龄、2.6 秒处三种插值方法的信息来源。图中的阶梯线只帮助定位源记录;有效查询点和状态输出才是接口判定结果。

图 1 本次 MATLAB 原生导出的三联图。上图区分接受值与过期保持值,中图显示数据年龄,底图比较 2.6 秒处三种插值方法;图像已完成独立复核。
PNG 使用 exportgraphics 从 MATLAB 图形对象导出,并设置分辨率;同时保留可重新打开的 FIG。关于图形对象导出和分辨率参数,见 MathWorks:exportgraphics。
matlab
function r = asof_age_demo(outDir)
%ASOF_AGE_DEMO Reproducible base-MATLAB example; no toolbox is required.
% Running this function generates native MATLAB figures, CSV and MAT files.
if nargin < 1
outDir = fullfile(fileparts(mfilename('fullpath')), 'results');
end
if ~exist(outDir, 'dir'), mkdir(outDir); end
ts = [1; 3; 7; 8];
xs = [10; 30; NaN; 80];
tq = [0; 1; 2; 3; 5; 6; 7; 8; 10; 11];
maxAge = 2;
[y, age, idx, valid, missing, stale] = asof_age(ts, xs, tq, maxAge);
expectedIdx = [0; 1; 1; 2; 2; 2; 3; 4; 4; 4];
expectedAge = [NaN; 0; 1; 0; 2; 3; 0; 0; 2; 3];
expectedY = [NaN; 10; 10; 30; 30; NaN; NaN; 80; 80; NaN];
assert(isequal(idx, expectedIdx), 'Demo:Index', 'Hand-derived index mismatch.');
assert(isequaln(age, expectedAge), 'Demo:Age', 'Hand-derived age mismatch.');
assert(isequaln(y, expectedY), 'Demo:Value', 'Hand-derived value mismatch.');
assert(nnz(valid) == 6 && nnz(idx == 0) == 1 && nnz(stale) == 2 ...
&& nnz(missing) == 1, 'Demo:Counts', 'State counts mismatch.');
status = repmat("VALID", size(tq));
status(idx == 0) = "NO_HISTORY";
status(stale) = "STALE";
status(missing) = "MISSING";
status(stale & missing) = "STALE_MISSING";
resultTable = table(tq, idx, age, missing, stale, valid, y, status);
disp(resultTable);
writetable(resultTable, fullfile(outDir, 'asof_hand_case.csv'));
% A deliberately tiny example reveals interpolation's information boundary.
smallT = [1; 3]; smallX = [10; 30]; q = 2.6;
linearValue = interp1(smallT, smallX, q, 'linear');
nearestValue = interp1(smallT, smallX, q, 'nearest');
previousValue = interp1(smallT, smallX, q, 'previous');
assert(abs(linearValue - 26) < 1e-12 && nearestValue == 30 ...
&& previousValue == 10, 'Demo:Interp', 'Interpolation contrast mismatch.');
f = figure('Visible', 'off', 'Color', 'w', 'Position', [80 80 1120 900]);
tl = tiledlayout(f, 3, 1, 'TileSpacing', 'compact', 'Padding', 'compact');
ax1 = nexttile(tl);
stairs(ax1, ts, xs, '-s', 'LineWidth', 1.5, 'DisplayName', 'Recorded samples');
hold(ax1, 'on');
plot(ax1, tq(valid), y(valid), 'o', 'MarkerSize', 8, ...
'LineWidth', 1.5, 'DisplayName', 'Accepted query value');
bad = stale & ~missing & idx > 0;
plot(ax1, tq(bad), xs(idx(bad)), 'rx', 'MarkerSize', 10, ...
'LineWidth', 1.8, 'DisplayName', 'Rejected stale hold');
xline(ax1, 7, ':k', 'NaN sample at 7 s', 'HandleVisibility', 'off');
grid(ax1, 'on'); xlim(ax1, [0 11.3]); ylim(ax1, [0 95]);
ylabel(ax1, 'Sensor value (a.u.)');
title(ax1, 'Latest historical sample is not always an acceptable value');
legend(ax1, 'Location', 'northwest');
ax2 = nexttile(tl);
stem(ax2, tq, age, 'filled', 'LineWidth', 1.2, 'DisplayName', 'Selected sample age');
hold(ax2, 'on');
yline(ax2, maxAge, '--r', 'Max age = 2 s', 'DisplayName', 'Expiry threshold');
grid(ax2, 'on'); xlim(ax2, [0 11.3]); ylim(ax2, [-0.2 3.8]);
ylabel(ax2, 'Age (s)');
title(ax2, 'Exact threshold is accepted; age is undefined without history');
ax3 = nexttile(tl);
plot(ax3, smallT, smallX, 'ks', 'MarkerFaceColor', 'k', 'DisplayName', 'Recorded samples');
hold(ax3, 'on');
plot(ax3, smallT, smallX, ':k', 'HandleVisibility', 'off');
plot(ax3, q, linearValue, 'bd', 'MarkerSize', 9, 'DisplayName', 'Linear = 26');
plot(ax3, q, nearestValue, 'r^', 'MarkerSize', 9, 'DisplayName', 'Nearest = 30');
plot(ax3, q, previousValue, 'go', 'MarkerSize', 9, 'LineWidth', 1.5, 'DisplayName', 'Previous = 10');
xline(ax3, q, '--', 'Query at 2.6 s', 'HandleVisibility', 'off');
grid(ax3, 'on'); xlim(ax3, [0.8 3.2]); ylim(ax3, [5 35]);
xlabel(ax3, 'Relative time (s)'); ylabel(ax3, 'Sensor value (a.u.)');
title(ax3, 'The sample at 3 s is future information at query time');
legend(ax3, 'Location', 'northwest');
exportgraphics(f, fullfile(outDir, 'asof_age_alignment.png'), 'Resolution', 160);
savefig(f, fullfile(outDir, 'asof_age_alignment.fig'));
close(f);
r = struct('matlabVersion', version, 'release', version('-release'), ...
'computer', computer, 'validCount', nnz(valid), ...
'noHistoryCount', nnz(idx == 0), 'staleCount', nnz(stale), ...
'missingCount', nnz(missing), 'linearValue', linearValue, ...
'nearestValue', nearestValue, 'previousValue', previousValue);
save(fullfile(outDir, 'asof_age_evidence.mat'), ...
'r', 'ts', 'xs', 'tq', 'maxAge', 'resultTable');
fprintf('MATLAB %s (%s), %s\n', r.matlabVersion, r.release, r.computer);
fprintf('Demo completed: valid=%d, no_history=%d, stale=%d, missing=%d\n', ...
r.validCount, r.noHistoryCount, r.staleCount, r.missingCount);
end
在 MATLAB 当前文件夹切换到两个 .m 文件所在目录后执行:
matlab
outDir = fullfile(pwd, 'results');
r = asof_age_demo(outDir);
也可以在系统终端中先进入该目录,再使用 MATLAB 批处理方式:
text
matlab -batch "r = asof_age_demo(fullfile(pwd,'results'));"
本次已实际生成并回读的文件类型如下。 表中使用上方复现命令的 results 目录;正式验证入口为每次运行创建独立子目录,避免覆盖或混入旧证据。
| 文件 | 内容 |
|---|---|
results/asof_hand_case.csv |
十行查询的索引、年龄、状态与输出 |
results/asof_age_alignment.png |
MATLAB 原生三联图 |
results/asof_age_alignment.fig |
可重新打开检查的 MATLAB 图形 |
results/asof_age_evidence.mat |
输入、结果表及实际运行环境信息 |
演示只有在断言及后续导出流程成功后才会打印完成信息。本次 PNG 已完成独立目视复核,FIG 已重新打开并核对三个坐标轴,CSV/MAT 已回读核对。读者复现时,仍应检查图例、坐标轴、缺失点和过期点,再把 CSV 与手算表逐行对照。MAT 文件里的 version、release 和 computer 应来自各自当次运行,不应复制其他日期的环境记录。
本文的手算演示不能替代独立技术复核。本次另行执行的独立参考算法对每个查询重新扫描源时间,并选择符合条件的最大时间,不复用核心函数的单指针扫描。它已与核心函数逐字段比较,实际检查了空输入、单点、重复或乱序输入的处理,以及年龄阈值和代表性浮点数边界。第 8 节另外讨论的绝对纳秒时间预处理和线上到达策略属于接入建议,不是已经部署实测的流水线;这些测试通过不表示可以省略真实项目自身的数据契约与误差分析。
8 接入真实日志之前检查这些边界
空输入与单点输入
空源数据配非空查询时,每个查询都没有历史:idx=0、age=NaN、y=NaN。空查询返回零行列向量。单点源数据则在该时刻之前无历史、该时刻及之后按年龄和测量值判定。
maxAge=0 也有定义:只有年龄按当前数值表示恰好为零,且样本值有效时,才允许输出。
重复时间不能用 unique 悄悄决定
本函数拒绝重复的源时间戳。两个记录时间相同但数值不同,应保留先到、后到、质量更高的一个,还是按业务聚合?这些是采集协议或数据治理规则,函数没有足够信息替使用者决定。
因此不要在调用前随手加一句 unique,就以为问题已经解决。应先确定重复记录的含义、保留规则以及质量标记,再形成严格递增的源序列。源时间乱序同样应在上游明确处理;本函数报错,不会暗中排序。查询时间允许重复,但不允许倒退。
数据年龄使用直接的 double 比较
本实现直接计算 age = tq - ts(idx),随后判断 age > maxAge。因此实际算出的 double 年龄恰好等于阈值时接受 ,大于阈值时拒绝,没有自动增加 eps 或其他容差。
十进制小数在二进制浮点数中的表示及减法舍入可能影响边界。若业务允许时钟量化误差,应先定义量化单位或明确的容差规则,而不是临时扩大阈值来让测试通过。
复核时应另外选择可解释的代表性数值,检查阈值处以及邻近可表示数。例如以 ts=0、maxAge=2 为例,查询 2-eps(2)/2、2、2+eps(2),解析预期为有效、有效、过期;本次原生独立测试已逐项核对通过,也覆盖了非零源时间的边界案例。接入真实数据时,还应按实际采样量级检查减法后的年龄,不能只测整数秒。
大整数时间戳应先减原点再转 double
如果原始时间是绝对纳秒计数,直接转换为 double 可能把相邻时间舍入成同一个数,破坏顺序或年龄精度。应在原始精确整数表示中,为两路数据选取同一个原点,确认差值范围与减法安全,再把相对差值转换为 double 并换算为秒。
不要先把巨大的绝对时间转成 double,之后才减原点,并认为丢失的低位能恢复。还应检查相对跨度与所需时间分辨率是否匹配;"相对秒"并不自动保证任何量级都足够精确。
事件时间上的历史不等于在线已经收到
本文的 ts <= tq 是事件时间条件。如果传感器在 1 秒采样,但消息到 4 秒才到达,离线日志可以把它视为 2 秒查询的历史样本,真实在线程序在 2 秒却还拿不到它。
因此本函数适合同一时钟基准下的离线或事件时间对齐。若要复现在线行为,还需要记录到达时间,并明确迟到数据、乱序缓冲和决策截止时刻的处理规则。这里没有解决通信传输延迟、迟到补发、设备时钟偏移或时钟漂移,也没有重建传感器在两个采样点之间的真实物理轨迹。
9 让无效状态继续传到下游
对齐函数返回 NaN 后,不宜立即用零替代。零可能本来就是合法测量值,这样会把"不知道"改成一个确定读数;同样,未经业务定义再次向前填充,也会绕过前面设置的过期规则。
接入差值、融合或报警时,至少同时传递 y 和 valid。需要定位问题时,再保留 idx、age、missing、stale,区分启动阶段没有历史、采集到无效值和数据长时间未更新。
如果后续要接报警状态机,相关主题可以继续阅读《MATLAB 工程实验:滞回加持续时间报警,把 UNKNOWN 留在状态机里》。本文先在数据入口保留无效状态:把测量是否可用说清楚,再让后续算法依据状态作出决定,而不是在对齐时就把未知抹平。
你的慢速通道一旦收到新的 NaN,下游应立即停止使用旧值,还是进入一个单独标记、限时保持的降级状态;这个期限具体由哪项工程约束决定?