RuView 时间演化感知解析:EvolutionTracker 跨链路变化检测与 VoxelMap 时间体素证据聚合(ADR-142)
本文以《ADR-142 Evolution Tracker and Temporal VoxelMap Evidence Aggregation》为骨架,结合仓库内 wifi-densepose-signal 源码 深入讲解:如何为 RuvSense 多径网格补上"跨链路协调器 + 时间体素占用记忆"两层缺失,实现环境变化的共识判定、体素级贝叶斯证据累积,以及经 BFLD 隐私门控(VoxelGate)后才可离开节点的输出路径。读完本文,你将掌握该 ADR 的完整数据结构、判定规则、隐私降级模型、测试验收方法与当前仓库的真实实现边界。
1. 背景与缺口:五个孤立模块之间没有"第二个时间导数"
RuvSense 的传感栈(ADR-029、ADR-030)为"演化追踪器(Evolution Tracker)"所需的所有单个零件其实都已就位,但它们散落在五个互不通信的模块中,没有任何一个编排器把它们按时间、跨链路地串起来。对 v2/crates/wifi-densepose-signal/src/ruvsense/ 检索 EvolutionTracker、change_point、VoxelMap 或跨模块驱动逻辑,得到的都是空结果。
| 模块 | 文件 | 它已经做了什么 | 它做不到什么 |
|---|---|---|---|
| 场模型 | field_model.rs | 每条链路的 Welford 基线(LinkBaselineStats、WelfordStats)、SVD 本征结构分解(finalize_calibration())、estimate_occupancy(&[Vec<f64>])、校准新鲜度 check_freshness |
每个 FieldModel 都是"单房间单条链路"实例,无法把新鲜度跨链路聚合——它看不见兄弟实例 |
| 空房间基线 | calibration.rs(ADR-135) | BaselineCalibration、带 W 帧过期窗口的 CalibrationRecorder、逐帧 CalibrationDeviationScore(携带 drift_score) |
漂移信号只被单条链路自己的 recorder 消费;没有"3 条链路同时漂移 ⇒ 房间变了"这类跨链路规则 |
| 纵向漂移 | longitudinal.rs | 逐人 PersonalBaseline(5 个 Welford 指标)、EmbeddingHistory FIFO、输出每人的 DriftReport/MonitoringLevel |
逐人独立,永不回填到产生该 embedding 的逐链路 RF 证据上 |
| 吸引子漂移 | attractor_drift.rs | 相空间状态分类 AttractorDriftAnalyzer,analyze() 返回 AttractorDriftReport { regime_changed, .. } |
逐人逐指标,从不把 regime 变化升级到场/校准层 |
| 断层成像 | tomography.rs | 粗粒度 RF 断层 RfTomographer::reconstruct()(ISTA L1 求解)、无状态 OccupancyVolume |
OccupancyVolume 完全无记忆——每次 reconstruct() 都由单张衰减快照产生全新体积;占据 200 帧的体素与闪烁 1 帧的噪声体素无法区分,也没有逐体素置信度、last_update_ns、证据计数与多普勒 |
而在隐私侧,privacy_gate.rs 实现了单调的 PrivacyGate::demote(BfldFrame, PrivacyClass)(ADR-120),按 Raw(0) → Derived(1) → Anonymous(2) → Restricted(3) 方向清零 payload 区段,并对任何"升档"尝试返回 BfldError::InvalidDemote。但它操作的是 BfldFrame 的 payload 区段(compressed_angle_matrix、csi_delta、amplitude_proxy、phase_proxy)——对"体素网格"毫无概念。一旦断层成像产出 OccupancyVolume,它将处于"无人把门"的状态直接流出节点。
因此缺口是双重的:
- 缺少编排器。每条链路各自维护基线、漂移分、吸引子状态与占用估计;物理环境变化(挪家具、开墙)表现为多条链路的相关联漂移,但没有任何模块同时读取超过一条链路。跨链路变点检测——正是区分"世界变了"与"这条链路自己很吵"的信号——目前不存在。
- 缺少时间占用记忆。
RfTomographer::reconstruct()无记忆,占用无法累积证据、无法赋置信度、无法在 20 Hz 重建节奏上做贝叶斯更新;而且产出物没有经过隐私门控。
ADR-030 定义了单房间场模型与 Tier-2 断层,却未提及多房间/多链路编排与时间体素状态。ADR-142 正是给 ADR-030 补上编排层与时间体素层,并把两条路径都接进 BFLD 隐私门(ADR-120/ADR-141)。
"演化(Evolution)"在这里指什么
"演化"是二阶信号:不是场的瞬时状态,而是场的统计描述如何随时间变化、且这种变化在链路上是否一致。ADR-142 让下列三个此前没有模块能回答的问题变得可答:
- 各链路基线作为"一个集合"还成立吗?(全网格新鲜度,而非单链路)
- 环境刚刚变了,还是某条链路在闹脾气?(跨链路变点)
- 模型的占用估计与原始 RF 人体扰动能量一致吗?(占用一致性——一种喂给 ADR-137 的内部矛盾自检)
它不是新断层求解器(只是包住现有 RfTomographer),不是新校准算法(读 ADR-135 的 BaselineCalibration 与 ADR-030 的 FieldModel),也不是新隐私模型(复用 BFLD PrivacyGate::demote 模式)。它只新增两样东西:一个协调器 EvolutionTracker,和一个有状态、经门控的占用记忆 VoxelMap + VoxelGate。
在流水线中的位置
Per-link CSI frame (baseline-subtracted, ADR-135)
→ CalibrationRecorder::record() (ruvsense/calibration.rs) → drift_score[link]
→ FieldModel::extract_perturbation() (ruvsense/field_model.rs) → body_energy[link]
→ RfTomographer::reconstruct() (ruvsense/tomography.rs) → OccupancyVolume (snapshot)
│ │ │
└────────────────┴───────────────────────┴──► EvolutionTracker::tick() ← NEW
├─ baseline freshness across mesh
├─ cross-link change-point
├─ occupancy-consistency check
└─ VoxelMap::ingest(volume) ← NEW (temporal)
│
VoxelGate::demote(map, mode) ← NEW (BFLD-gated)
│
┌─────────────────────────────────────┴───────────────────┐
ADR-137 contradiction flags ADR-139 WorldGraph nodes
EvolutionTracker::tick() 每个重建周期(20 Hz)运行一次:读取各链路漂移分与人体扰动能量、场模型占用估计、最新 OccupancyVolume,把体积折入持久的 VoxelMap;节点输出只能经 VoxelGate 离开。
2. 决策一:EvolutionTracker trait 与默认 MeshEvolutionTracker
EvolutionTracker 设计为 trait(生产聚合器与测试 harness 可注入不同的链路状态提供者),外加默认实现 MeshEvolutionTracker。它持有对既有模块已维护的逐链路状态的引用,而不复制它们的累加器。
use wifi_densepose_signal::ruvsense::calibration::{BaselineCalibration, CalibrationDeviationScore};
use wifi_densepose_signal::ruvsense::field_model::CalibrationStatus;
use wifi_densepose_signal::ruvsense::tomography::OccupancyVolume;
/// Stable identifier for one TX→RX link in the mesh.
pub type LinkId = usize;
/// Per-link evidence handed to the tracker each tick.
#[derive(Debug, Clone)]
pub struct LinkObservation {
pub link_id: LinkId,
/// ADR-135 per-frame deviation (carries drift_score + rms_amplitude_z).
pub deviation: CalibrationDeviationScore,
/// ADR-030 field-model freshness for this link's room.
pub freshness: CalibrationStatus,
/// Body-perturbation energy from FieldModel::extract_perturbation(),
/// the residual after environmental modes are projected out.
pub body_energy: f32,
/// Capture timestamp, nanoseconds since the 802.15.4 epoch (ADR-110).
pub timestamp_ns: u64,
}
/// Aggregate result of one evolution tick.
#[derive(Debug, Clone)]
pub struct EvolutionReport {
/// Worst freshness observed across all links this tick.
pub mesh_freshness: CalibrationStatus,
/// Links currently Stale or Expired (drives CoherenceAlert).
pub stale_links: Vec<LinkId>,
/// True if a cross-link change-point fired this tick (§2.2).
pub change_point: bool,
/// Links that participated in the change-point (≥2σ this window).
pub change_point_links: Vec<LinkId>,
/// Occupancy as the field model sees it.
pub model_occupancy: usize,
/// Occupancy implied by summed per-link body-perturbation energy.
pub perturbation_occupancy: usize,
/// True when |model − perturbation| > 1 (drives AnomalyWarn, §2.3).
pub occupancy_disagreement: bool,
/// Alerts emitted this tick (typed, for the streaming engine ADR-136).
pub alerts: Vec<EvolutionAlert>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EvolutionAlert {
/// One or more baselines are no longer fresh across the mesh.
CoherenceAlert { stale_links: Vec<LinkId> },
/// Cross-link change-point: the environment likely changed.
ChangePoint { links: Vec<LinkId> },
/// Model occupancy and RF-energy occupancy disagree by >1 person.
AnomalyWarn { model: usize, perturbation: usize },
}
pub trait EvolutionTracker {
/// Fold one tick of per-link observations + the latest occupancy
/// snapshot into the tracker's persistent state. Updates the VoxelMap.
fn tick(
&mut self,
observations: &[LinkObservation],
volume: &OccupancyVolume,
now_ns: u64,
) -> EvolutionReport;
/// Borrow the temporal voxel map for gated output (§2.5).
fn voxel_map(&self) -> &VoxelMap;
/// Configuration knobs.
fn config(&self) -> &EvolutionConfig;
}
MeshEvolutionTracker 持有既有模块所需的滚动窗口但不重新实现它们——只存分数(而非原始 CSI)的小型环形缓冲:
- 每条链路的
VecDeque<f32>,保存最近W = 300个drift_score(与 ADR-135CalibrationConfig.drift_window_frames相同窗口); - 每条链路的
VecDeque<f32>,保存用于变点检验的rms_amplitude_z; longitudinal.rs的EmbeddingHistoryFIFO 与attractor_drift.rs的相空间缓冲按句柄引用而非复制——tracker 按需调用它们既有的analyze()/novelty()。
#[derive(Debug, Clone)]
pub struct EvolutionConfig {
/// Change-point window length in frames. Default: 30 (1.5 s @ 20 Hz).
pub change_point_window: usize,
/// Per-link z threshold counting toward a change-point. Default: 2.0σ.
pub change_point_sigma: f32,
/// Minimum links exceeding threshold to declare a change-point. Default: 3.
pub change_point_min_links: usize,
/// Occupancy disagreement tolerance, in persons. Default: 1.
pub occupancy_tolerance: usize,
/// Per-voxel minimum evidence count before a voxel is "confident". Default: 5.
pub min_evidence_frames: u32,
}
impl Default for EvolutionConfig {
fn default() -> Self {
Self {
change_point_window: 30,
change_point_sigma: 2.0,
change_point_min_links: 3,
occupancy_tolerance: 1,
min_evidence_frames: 5,
}
}
}
2.1 跨链路变点检测:把"单链路噪声"与"环境突变"分开
一条链路漂移只是噪声;整个环境改变会表现为相关联的漂移。每个 tick 评估的规则是:
在滚动
change_point_window(默认 30 帧 / 1.5 s)内,若 3 条及以上链路各自在rms_amplitude_z上超过change_point_sigma(默认 2.0σ),则发出ChangePoint事件并点名这些链路。
fn detect_change_point(&self) -> Option<Vec<LinkId>> {
let mut hot = Vec::new();
for (link_id, window) in self.z_windows.iter() {
// Count frames in the window above the sigma threshold.
let n_hot = window.iter().filter(|&&z| z >= self.config.change_point_sigma).count();
// A link "participates" if it was hot for a majority of the window.
if n_hot * 2 > window.len() {
hot.push(*link_id);
}
}
(hot.len() >= self.config.change_point_min_links).then_some(hot)
}
- 3 链路下限刻意与 ADR-135 的
drift_confirm_frames确认逻辑同量级,但作用在空间而非时间上:ADR-135 用 45 s 确认单条链路陈旧;本规则用 1.5 s × 3 条链路确认环境变化。两者互补——ADR-135 回答"这条链路的基线是不是老了?",本规则回答"世界是不是刚刚动了?"。 - 一个
ChangePoint是让运维方(或启用 ADR-135 §2.6 的recalibrate_on_drift时由系统)对整个网格而非单条链路做重校准的上游触发信号。 - 2.0σ 阈值复用了 ADR-135 的解读:单帧
rms_amplitude_z > 3.0判为"可能有人占用",因此在 1.5 s 窗口内多条链路持续 2.0σ 属于结构性位移,而非一个人经过某条链路。 - 防抖设计:单条链路要"占窗口多数帧"才算参与,杜绝单个热点帧误报。
2.2 网格级新鲜度聚合
独立于变点,tracker 把每条链路的 CalibrationStatus 按最坏情况序 Fresh < Stale < Expired(Uncalibrated/Collecting 视为比 Fresh 更差)规约成一个 mesh_freshness。任何落在 Stale 或 Expired 的链路进入 stale_links 并产生 CoherenceAlert。这正是单靠 field_model.rs 的 check_freshness 做不到的跨网格新鲜度检查——它只认识一个房间。
在真实源码中,CalibrationStatus 枚举五个档位(field_model.rs)与这里的规约完全对应:
pub enum CalibrationStatus {
Uncalibrated, // No calibration data yet.
Collecting, // Collecting calibration frames.
Fresh, // Calibration complete and fresh.
Stale, // Calibration older than half expiry.
Expired, // Calibration has expired.
}
新鲜度判定由 elapsed 时间与 baseline_expiry_s 的关系决定(field_model.rs):超过一半寿命即 Stale,超过全额寿命即 Expired。ADR-142 把这份逐房间状态提升为网格级告警。
2.3 占用一致性检查:两个独立估计互相对账
系统里有两个本应一致的独立占用估计:
- 模型占用:
FieldModel::estimate_occupancy(recent_frames)(field_model.rs)——由"非环境子空间"中的本征结构能量导出(真实实现里结合 Marcenko-Pastur 噪声估计、空房间残差能量边界与校准噪声地板,见 field_model.rs); - 扰动占用:对逐链路
body_energy(extract_perturbation()投影掉环境模式后的残差)求和,再用装机标定的每人大致能量刻度换算成人数。
fn occupancy_consistency(&self, model_occ: usize, body_energy_total: f32) -> (usize, bool) {
let perturbation_occ = (body_energy_total / self.energy_per_person).round() as usize;
let disagree = model_occ.abs_diff(perturbation_occ) > self.config.occupancy_tolerance;
(perturbation_occ, disagree)
}
当两者相差超过 occupancy_tolerance(默认 1 人)时发出 AnomalyWarn { model, perturbation }。这正是 ADR-137 融合质量评分所消费的内部矛盾类型:下游产生的语义状态记录会把它作为矛盾标记,并携带两侧证据来源的引用。按项目规则,每条语义状态都要可回溯到信号证据(LinkObservation 集合)、模型版本(FieldModel SVD 代数)、校准版本(ADR-135 的 BaselineCalibration.captured_at_unix_s)与隐私决策(VoxelGate 模式)。
3. 决策二:带贝叶斯证据累积的时间 VoxelMap
这是核心新状态。现有 OccupancyVolume(tomography.rs)是无记忆快照;VoxelMap 是它的持久伴侣,跨 reconstruct() 调用累积证据。
/// One voxel of persistent, evidence-accumulating occupancy state.
#[derive(Debug, Clone)]
pub struct Voxel {
/// Center position (metres), copied from OccupancyVolume::voxel_center().
pub center_xyz: [f32; 3],
/// Bayesian occupancy probability ∈ [0, 1].
pub occupancy: f32,
/// Confidence ∈ [0, 1]; rises with evidence_count, falls with staleness.
pub confidence: f32,
/// Nanoseconds (802.15.4 epoch) of the last frame that updated this voxel.
pub last_update_ns: u64,
/// Number of frames that have contributed evidence to this voxel.
pub evidence_count: u32,
/// Welford mean/variance of the density observations (variance flags noise).
pub density_mean: f32,
pub density_m2: f32,
/// Radial Doppler velocity estimate (m/s), when CIR phase rate is available.
pub doppler_velocity: f32,
}
/// Persistent occupancy grid shared across all reconstruct() calls.
#[derive(Debug, Clone)]
pub struct VoxelMap {
pub voxels: Vec<Voxel>,
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub bounds: [f64; 6],
/// Half-life (frames) of the confidence decay for un-updated voxels.
decay_half_life: f32,
}
impl VoxelMap {
/// Allocate a VoxelMap matching an OccupancyVolume's geometry.
pub fn from_geometry(volume: &OccupancyVolume) -> Self;
/// Fold one fresh OccupancyVolume into the persistent map.
///
/// For each voxel:
/// 1. Bayesian log-odds update of `occupancy` from the new density
/// (density treated as a measurement likelihood via a logistic link).
/// 2. Welford update of (density_mean, density_m2).
/// 3. evidence_count += 1; last_update_ns = now_ns.
/// 4. confidence ← logistic(evidence_count) × (1 − normalised_variance).
/// Voxels NOT touched this frame decay confidence toward 0 with
/// `decay_half_life`, but retain their last occupancy estimate.
pub fn ingest(&mut self, volume: &OccupancyVolume, now_ns: u64, min_evidence: u32);
/// Per-voxel Welford sample variance.
pub fn density_variance(&self, idx: usize) -> f32;
/// Voxels with evidence_count < min_evidence are LOW CONFIDENCE.
pub fn low_confidence_indices(&self, min_evidence: u32) -> Vec<usize>;
/// Occupancy histogram (counts per occupancy bucket) for Restricted mode.
pub fn occupancy_histogram(&self, n_buckets: usize) -> Vec<u32>;
}
贝叶斯更新:每个体素的 occupancy 以 log-odds 形式维护,用逻辑斯蒂测量模型 p(occupied | density) = σ(k·(density − d₀)) 结合新密度观测更新。log-odds 累积是占用网格的标准做法(Moravec & Elfes, 1985;Thrun et al., 2005):可交换、数值稳定,让反复观测为占据的体素逼近 1.0,而单帧闪烁几乎不动估计。这直接解决无记忆快照问题:占据 200 帧与 1 帧尖峰现在可通过 evidence_count 与收敛的 log-odds 区分开。
置信度与低置信度标记:confidence = logistic(evidence_count / min_evidence) × (1 − clamp(normalised_density_variance))。evidence_count < min_evidence_frames(默认 5)的体素由 low_confidence_indices() 返回,下游被打标,使融合引擎(ADR-137)绝不会把 4 帧的体素当置信检测。这与 tomography.rs 用 density > 0.01 计 occupied_count 的做法互补——后者只是快照级的二进制门限,前者补上了时间限定词。
逐体素 Welford 方差:复用 field_model.rs WelfordStats 完全相同的 (mean, m2) 更新形式,因此"密度高但很吵(高方差)"的体素会被正确地怀疑,而不如"平稳、安静地占据"的体素可信。
4. 决策三:ADR-134 CIR 加权的可选距离先验
当 ADR-134 CIR 可用时,链路 Cir 的 dominant_delay_sec() / dominant_tap_tof_s()(cir.rs)给出主反射体的飞行时间,即距离。RfTomographer 的权重矩阵(tomography.rs,weight_matrix: Vec<Vec<(usize, f64)>>)目前仅按菲涅尔半径邻近度加权整条链路路径上的每个体素(1.0 − dist/fresnel_radius)。有了 CIR 时延,tracker 提供一个距离先验:距离 TX 与 CIR 隐含距离吻合的体素权重被提升,使证据聚焦在反射体附近,而不是沿整条射线抹开。
/// Optional per-link CIR-derived distance prior, applied to the existing
/// Fresnel weights as a multiplicative Gaussian bump centred at the CIR range.
pub struct CirDistancePrior {
pub link_id: LinkId,
/// Reflector distance from TX (m), from Cir::dominant_distance_m().
pub range_m: f64,
/// Std-dev of the range bump (m), from tap_spacing → distance resolution.
pub sigma_m: f64,
}
该先验是可选的:无 CIR 时(单天线回退,或 eigenvalue/CIR 特性关闭),tomographer 行为与今天完全一致,保持改动增量式,既有 tomography.rs 测试不受影响。每个 Voxel 的 doppler_velocity 也仅在 CIR 相位速率可用时才填充,否则保持 0.0。
5. 决策四:VoxelGate——BFLD 门控的体素输出
原始 VoxelMap 是身份泄漏的:高分辨率占用网格 + 逐体素多普勒可以重建一个人的轨迹与步态,绝不能未经门控离开节点。VoxelGate::demote 复用 privacy_gate.rs PrivacyGate::demote 的单调降级模式——接受 PrivacyClass(bfld/src/lib.rs,Raw(0) → Derived(1) → Anonymous(2) → Restricted(3)),拒绝任何"升档"(BfldError::InvalidDemote),产出逐级变粗的视图。与 BFLD 门一样,降级不可逆:字段一旦清零,字节就不在了。
use wifi_densepose_bfld::{BfldError, PrivacyClass};
/// Monotonic voxel-grid demotion, mirroring PrivacyGate::demote (ADR-120).
pub struct VoxelGate;
/// What actually leaves the node after gating.
#[derive(Debug, Clone)]
pub enum GatedVoxelOutput {
/// Raw(0)/Derived(1): full VoxelMap (local-only by invariant; Raw never
/// crosses a network sink — same structural rule as BFLD class 0).
Full(VoxelMap),
/// Anonymous(2): per-voxel doppler_velocity and confidence cleared to 0;
/// occupancy retained but quantised. No trajectory reconstruction possible.
Anonymous(VoxelMap),
/// Restricted(3): NO voxel grid leaves the node — only an occupancy
/// histogram (count of voxels per occupancy bucket).
OccupancyHistogram(Vec<u32>),
}
impl VoxelGate {
/// Demote the VoxelMap to the target class. Returns InvalidDemote if the
/// target is a *lower* class number than `current` (i.e. would add info).
pub fn demote(
map: &VoxelMap,
current: PrivacyClass,
target: PrivacyClass,
) -> Result<GatedVoxelOutput, BfldError> {
if target.as_u8() < current.as_u8() {
return Err(BfldError::InvalidDemote {
from: current.as_u8(),
to: target.as_u8(),
});
}
Ok(match target {
PrivacyClass::Raw | PrivacyClass::Derived => GatedVoxelOutput::Full(map.clone()),
PrivacyClass::Anonymous => {
let mut m = map.clone();
for v in m.voxels.iter_mut() {
v.doppler_velocity = 0.0; // strip kinematic identity surface
v.confidence = 0.0;
v.occupancy = quantise(v.occupancy);
}
GatedVoxelOutput::Anonymous(m)
}
PrivacyClass::Restricted => {
// The raw VoxelMap never leaves the node at Restricted.
GatedVoxelOutput::OccupancyHistogram(map.occupancy_histogram(8))
}
})
}
}
这与 privacy_gate.rs 的字段级降级一一对应:BFLD 在 Anonymous 清零 compressed_angle_matrix/csi_delta、在 Restricted 清零 amplitude_proxy/phase_proxy;VoxelGate 在 Anonymous 清 doppler_velocity/confidence,在 Restricted 只放行直方图。哪种 class 适用由 ADR-141 的命名隐私模式与运行时证明控制平面决定,不由本 ADR 决定——VoxelGate 是机制,ADR-141 是策略。
异常路由:EvolutionReport.alerts(CoherenceAlert / ChangePoint / AnomalyWarn)不是体素数据、不参与体素降级——它们是类型化事件:
- 到 ADR-137 融合矛盾标记:
AnomalyWarn变为语义状态记录上的矛盾引用(模型占用 vs 扰动占用),附带各自产生的模型版本与校准版本; - 到 ADR-139 WorldGraph 节点:
ChangePoint更新环境数字孪生(如"家具被移动"边),CoherenceAlert把受影响房间节点标记为待重校准。
接口边界总览
| Boundary | Direction | Type | Note |
|---|---|---|---|
calibration.rs → tracker |
in | CalibrationDeviationScore (per link) |
drift_score + rms_amplitude_z; no CSI crosses the boundary |
field_model.rs → tracker |
in | CalibrationStatus, body_energy: f32, estimate_occupancy |
mesh freshness + model occupancy |
tomography.rs → tracker |
in | &OccupancyVolume (snapshot) |
folded into VoxelMap::ingest |
cir.rs → tracker |
in (optional) | CirDistancePrior |
distance-weighted evidence; absent ⇒ unchanged behaviour |
| tracker → ADR-137 | out | EvolutionAlert (typed) |
contradiction flags, evidence references |
| tracker → ADR-139 | out | EvolutionAlert (typed) |
WorldGraph mutations |
| tracker → network sink | out | GatedVoxelOutput only |
never the raw VoxelMap; gated by VoxelGate |
tracker 不持有原始 CSI、不持有 payload 字节——只有分数、占用估计与体素网格。通往网络的唯一路径是 VoxelGate::demote。
6. 仓库实现现状:ADR 文档 vs 已落地的 building block
值得强调的是,ADR-142 的标题状态是 Accepted — partial:截至 2026-05-29,已构建并测试的构建块(commit 1f8e180d6,issue #846)包括 EvolutionTracker(跨链路变点)、TemporalVoxel(贝叶斯 log-odds 占用 + 置信度地板)与 VoxelGate(隐私降级到直方图);但把 field_model.estimate_occupancy() 一致性检查与 CIR 峰值时延距离加权接到实时信号上、把异常路由到 ADR-137 矛盾标记,属于尚未接上实时路径的集成胶水(详见 ADR-136 实现状态 系列框架)。
真实代码落在 evolution.rs,并通过 ruvsense/mod.rs 的 pub mod evolution; 与 re-export(ChangePoint, EvolutionTracker, TemporalVoxel, TemporalVoxelMap, VoxelGate, VoxelPrivacy)进入 crate 公开面。实现相对 ADR 文档有几处可对照的工程化取舍:
- 体素级别(
TemporalVoxel) 的observe()用钳制的 log-odds 累积(上下限 ±20,防止单帧坏数据饱和后验)、confidence = 1 − exp(−count/5)(等效 ADR 的 5 帧规则,evidence_count < 5即is_low_confidence()),并在更新时可选地写入doppler_velocity(evolution.rs):
pub fn observe(&mut self, p: f64, doppler: Option<f64>, ns: u64) {
let p = p.clamp(1e-4, 1.0 - 1e-4);
let evidence_logit = (p / (1.0 - p)).ln();
// Clamp the running log-odds so a single bad frame cannot saturate.
self.log_odds = (self.log_odds + evidence_logit).clamp(-20.0, 20.0);
self.occupancy = 1.0 / (1.0 + (-self.log_odds).exp());
self.welford.update(p);
self.evidence_count += 1;
self.confidence = 1.0 - (-(self.evidence_count as f64) / 5.0).exp();
if doppler.is_some() {
self.doppler_velocity = doppler;
}
self.last_update_ns = ns;
}
-
隐私降级没有引入对 BFLD crate 的依赖:实现定义了本地的
VoxelPrivacy { Full, Anonymous, Restricted }枚举(注释明确"mirrors the BFLD demotion ladder of ADR-120/141 without taking a crate dependency"),VoxelGate::demote(&mut TemporalVoxelMap, posture, bins)原地修改网格并只在 Restricted 返回聚合直方图、同时清空每个体素的 occupancy/doppler/confidence(evolution.rs)。从接口看,这比 ADR 文档中"clone 后逐字段清零"的GatedVoxelOutput更省、更贴合 Rust 的所有权风格,且同样满足"限制级下原始网格不出节点"的不变量。 -
变点检测器实现为
EvolutionTrackerstruct(非 trait):每条链路一个 WelfordStats,observe_window(&[f64])先对本轮样本折入前的基线判定发散(要求该链路 ≥2 次先验观测且标准差 > 1e-9),统计同时发散链路数,达到min_links即返回ChangePoint { diverging_links, sigma_threshold };with_defaults(n_links)即 2.0σ + ≥3 链路。实现用"瞬时窗口采样中 ≥3 链路同时超基线 2σ"来近似 ADR 文档"30 帧窗口多数帧发热"的规则。
evolution.rs 内测试 覆盖了贝叶斯更新收敛、5 帧置信地板、低置信度索引、Anonymous 清多普勒保留占用、Restricted 出直方图并清空、单链路发散不触发变点、三链路同时漂移触发变点、以及一条贯通 ADR-142 验收路径的端到端测试(acceptance_drift_to_histogram_with_contradiction:quiet baseline → 三链路同步漂移触发 ChangePoint → VoxelMap 累积证据 → 低置信体素被抑制 → Restricted 只出直方图 → 记录 ADR-137 ContradictionFlag::DriftProfileConflict)。
从代码结构看,还可以推断:ADR-142 文档中 tick() 一次性折叠多路 LinkObservation + OccupancyVolume 的"完整编排器",在实现中更偏向分件使用——调用方按需用 EvolutionTracker 做逐链路滚动统计、用 TemporalVoxelMap::observe() 按体素喂证据、最后用 VoxelGate 决定对外口径;把三者组织成单一 tick() 的胶水属于上文所述的"集成胶水 pending"部分。ADR 中"tracker 引用而非复制 EmbeddingHistory/相空间缓冲"的编排约定,需要等实时路径接线后才能真正兑现。
7. 后果、成本与风险取舍
正面收益
- 单一编排点:五个此前孤立的模块(
calibration、field_model、longitudinal、attractor_drift、tomography)有了协同读取的协调器;跨链路变点检测首次成为可能。 - 时间占用记忆:200 帧的持续占用与单帧噪声尖峰可借
evidence_count与收敛贝叶斯 log-odds 区分;融合引擎(ADR-137)获得逐体素置信度,而非二值快照门限。 - 网格级新鲜度:
FieldModel::check_freshness只认识一个房间;EvolutionTracker把逐链路新鲜度规约为网格CoherenceAlert,补上了 ADR-135 逐链路漂移分留下的运维缺口。 - 内部矛盾检测:占用一致性检查把两个独立估计(本征结构 vs 体扰动能量)变成 ADR-137 可评分的
AnomalyWarn——一条流水线从未有过的内置自检。 - 结构性隐私:除
VoxelGate::demote外没有任何体素网格能到达网络 sink;多普勒(体素网格中最强的步态身份面)在 Anonymous 即被清除;网格本身在 Restricted 下绝不外发。 - 增量式 CIR 集成:
CirDistancePrior可选;缺 CIR 时 tomography.rs 行为不变、既有测试不受影响。
负面成本
- 新增持久状态:
VoxelMap长寿命(每监测体积一份)。8×8×4 网格 = 256 体素 × ~40 字节 ≈ 10 KB,可忽略;但更细的 16×16×8 网格约 2,048 体素,且衰减循环每 tick 遍历全部体素。有限且廉价,但它是 20 Hz 下新增的常开工作。 - 每人大致能量是装机常量:一致性检查的
energy_per_person与环境相关,必须在校准时设定;数值错误会引发伪AnomalyWarn。它由与 ADR-135 基线相同的空房会话导出。 - 变点窗口需调参:30 帧 / 3 链路 / 2σ 默认值是从 ADR-135 阈值推得的,尚未在真实多房间硬件上验证;嘈杂网格可能过触发
ChangePoint。缓解:每条链路需占窗口多数帧"热"(§2.1),而非单个热帧。 - 多普勒过早被门掉:有用的运动学信息在 Anonymous 即被清除。这是有意的(它是身份面),但意味着轨迹分析必须在门之前、可信节点边界之内完成,不能依赖门控输出。
风险登记
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
噪声网格(HVAC、日照)上 ChangePoint 过触发 |
Medium | 伪网格重校准提示 | 逐链路多数窗口热 + 3 链路下限;ADR-135 漂移确认仍门控自动重校准 |
| 人离开后贝叶斯体素收敛到陈旧占用 | Medium | 空置体素数秒内仍显示占用 | 未更新体素的 decay_half_life 置信衰减;后续低密度观测把 log-odds 拉回"空" |
| Anonymous 量化仍泄漏粗粒度轨迹 | Low | 随时间从粗网格重识别 | 不可信 sink 用 Restricted(仅直方图);ADR-141 控制平面按 sink 选 class |
| CIR 距离先验在主径(而非人体)时错置证据 | Medium | 证据聚集在墙壁而非人身上 | 先验是对既有菲涅尔权重的乘性增强(射线不经过处不会凭空产生证据);体扰动能量仍门控体素是否"被占" |
错误的 energy_per_person 造成伪 AnomalyWarn |
Medium | 污染 ADR-137 矛盾流 | 默认容忍 1 人;空房会话标定该值并在 ChangePoint 时重新推导 |
8. 备选方案与被否决策
- 就地给
OccupancyVolume加状态(否):把confidence/last_update_ns/evidence_count直接加进 tomography.rs 的OccupancyVolume,让reconstruct()原地修改保留实例。被否:OccupancyVolume目前是reconstruct()的纯输出,测试(如test_zero_attenuation_empty_room断言新体积occupied_count == 0)假设它是快照;把快照与持久状态混在一起会破坏该契约,并把时间策略缠进求解器。VoxelMap让求解器保持纯净、时间状态独立。 - 每条链路一个 tracker(否):这恰恰是现状,正是它让跨链路变点与网格新鲜度无法实现。演化追踪器的全部价值就在于跨链路视角。
- 逐体素 Kalman/粒子滤波(暂缓):能联合建模占用与速度,但对当前分辨率下的粗 8×8×4 网格属于过度设计;log-odds 占用网格是标准、廉价、可交换的选择(Thrun et al., 2005),且与既有 ISTA 输出无缝集成。带运动模型的滤波器属于姿态追踪器(
pose_tracker.rs已有 17 关键点 Kalman),而非粗占用网格。若体素分辨率显著提升可重新考虑。 - 裸发 VoxelMap、下游再门控(否):与 BFLD class 0(
Raw依不变量 I1 仅限本地,见 bfld/src/lib.rs)同样的结构性理由——身份泄漏的原始体素数据一旦跨过网络边界就无法"反泄漏"。门控必须在 sink 之前、节点内部完成,正是VoxelGate::demote强制的事。 - 为体素自造新隐私机制(否):
privacy_gate.rs的单调降级不变量已被证明且过审计(ADR-120),ADR-141 已定义命名模式控制平面。复用PrivacyClass与demote模式意味着全系统只有一个隐私模型、一套证明测试、无需第二个待审计机制。
9. 测试与验收标准
单元测试
- T1 — 网格新鲜度聚合:喂入混合
CalibrationStatus(Fresh/Stale/Expired)的LinkObservation,断言mesh_freshness取最坏情况、stale_links恰好列出非 Fresh 链路,且当且仅当有链路 Stale/Expired 时发出CoherenceAlert。 - T2 — 3 链路触发跨链路变点:推入 30 帧 z 窗口,恰好 2 条链路超过 2.0σ 且占多数帧:断言无
ChangePoint;加入第 3 条:断言ChangePoint { links }触发并点名全部三条。 - T3 — 单条持续链路不触发变点:一条链路整窗口发热、其余安静:断言无
ChangePoint(这是 ADR-135 单链路陈旧域,不是环境变化)。 - T4 — 占用一致性:
model_occupancy = 1,喂入暗示 1 人的体能量:断言无AnomalyWarn;喂入暗示 3 人的体能量:断言AnomalyWarn { model: 1, perturbation: 3 }且occupancy_disagreement == true。 - T5 — VoxelMap 证据累积:对一个体素 ingest 200 个相同的占据体积、对另一个只 ingest 1 个。断言 200 帧体素
evidence_count == 200、occupancy > 0.95、不在low_confidence_indices(5);1 帧体素在low_confidence_indices(5)中且occupancy远离 1.0。 - T6 — 门限处低置信标记:某体素恰好 ingest 4 帧:断言低置信;ingest 第 5 帧:断言它离开
low_confidence_indices(5)。 - T7 — 置信衰减:把体素 ingest 到高置信,然后 ingest
decay_half_life个该体素未被触及的 tick:断言confidence减半而occupancy(最后估计)被保留。 - T8 — 逐体素 Welford 方差:喂入
[0.9, 0.1, 0.9, 0.1, ...](吵)与[0.5, 0.5, ...](稳)两组等均值密度:断言噪声体素density_variance()更高、confidence相应更低。 - T9 — VoxelGate 单调性:
demote(map, Anonymous, Derived)返回BfldError::InvalidDemote { from: 2, to: 1 };demote(map, Derived, Anonymous)成功且返回的VoxelMap每个doppler_velocity == 0.0、confidence == 0.0。 - T10 — Restricted 不出网格:
demote(map, Anonymous, Restricted)返回GatedVoxelOutput::OccupancyHistogram而绝非VoxelMap——断言变体是直方图且长度等于请求桶数。 - T11 — CIR 先验增量式:带与不带
CirDistancePrior各跑一次RfTomographer::reconstruct();断言无先验路径与当前 tomography.rs 输出逐位一致(既有测试不变),带先验路径密度更集中于 CIR 距离附近。
集成测试(门控,#[cfg(feature = "hardware-test")])
- T12 — 真实多基地网格(COM9 + cognitum-seed-1):空房跑 30 s,断言无
ChangePoint、mesh_freshness == Fresh、VoxelMap所有体素occupancy < 0.2。有人走过:沿路径占据体素升至 0.8 以上、evidence_count增长、人走开后置信衰减。搬走一把椅子离开:断言 1.5 s 内触发ChangePoint且点名受影响链路。
确定性 / Witness(CI 兼容,扩展 ADR-028)
- T13 — 确定性 VoxelMap 哈希:以 seed=42 构建固定 600-tick 合成占用流,ingest 进
VoxelMap,对序列化体素状态做 SHA-256,记为voxelmap_evidence_v1;由verify.py重新生成并断言哈希一致。方法学镜像 ADR-135 的calibration_nvs_baseline_v1证明(archive/v1/data/proof 目录是 ADR-028 证明链的既有载体)。
验收标准
EvolutionTracker::tick()对 8×8×4 网格与 12 条链路运行 < 1 ms(20 Hz 预算 50 ms,余量充足)。- 变点当且仅当 ≥
change_point_min_links条链路在窗口多数时间内超过change_point_sigma时触发(T2、T3)。 - 低于
min_evidence_frames的体素始终报低置信(T5、T6)。 - 没有任何代码路径绕过
VoxelGate::demote向网络 sink 裸发VoxelMap(由 §2.7 接口边界强制;VoxelGate是GatedVoxelOutput的唯一公开构造者)。 VoxelGate::demote单调:升档尝试必返回BfldError::InvalidDemote(T9)。- 每条发出的语义状态(占用 + 告警)携带信号证据(
LinkObservation集)、模型版本(FieldModel SVD 代数)、校准版本(BaselineCalibration.captured_at_unix_s)与隐私决策(VoxelGate目标 class)的引用。 - CIR 距离先验可证增量式——无先验重建不变(T11)。
10. 相关 ADR 关系网
| ADR | Relationship |
|---|---|
| ADR-030 | 被扩展:补上它未定义的跨链路编排器与时间体素层;消费 FieldModel::estimate_occupancy 与 CalibrationStatus |
| ADR-134 | 被集成(可选):Cir::dominant_distance_m() 经 CirDistancePrior 进入断层权重矩阵做距离加权 |
| ADR-135 | 前置/消费者:读取 CalibrationDeviationScore.drift_score;跨链路变点是其单链路陈旧判断的空间互补;共享 W=300 窗口与重校准触发 |
| ADR-120 | 被复用:VoxelGate::demote 是 PrivacyGate::demote 单调不变量与 PrivacyClass 枚举的直接应用 |
| ADR-141 | 策略提供者:决定每个 sink 适用哪个 PrivacyClass 并运行时证明;本 ADR 提供体素机制 |
| ADR-137 | 消费者:AnomalyWarn(占用不一致)成为带证据引用的矛盾标记进入语义状态记录 |
| ADR-139 | 消费者:ChangePoint 与 CoherenceAlert 修改环境数字孪生(移动家具边、房间重校准标记) |
| ADR-136 | 基座:EvolutionReport/EvolutionAlert 是流经流式引擎帧契约的类型化阶段输出 |
| ADR-084 / ADR-118 | 相关:tracker 引用的逐人基线的纵向漂移与持久化上下文 |
11. 核心参考源码清单
- evolution.rs — ADR-142 的落地实现:
TemporalVoxel/TemporalVoxelMap/VoxelGate/VoxelPrivacy/EvolutionTracker及其测试 - ruvsense/mod.rs —
evolution模块声明与类型 re-export(ChangePoint、EvolutionTracker、TemporalVoxel、TemporalVoxelMap、VoxelGate、VoxelPrivacy) - field_model.rs —
WelfordStats(逐体素方差复用)、CalibrationStatus、estimate_occupancy、extract_perturbation、check_freshness - calibration.rs — 逐链路
CalibrationDeviationScore.drift_score消费方(ADR-135) - tomography.rs —
RfTomographer/OccupancyVolume/weight_matrix,VoxelMap的时间伴侣 - longitudinal.rs —
PersonalBaseline、EmbeddingHistory(按句柄引用,不复制) - attractor_drift.rs —
AttractorDriftAnalyzer::analyzeregime 变化折入演化状态 - privacy_gate.rs 与 bfld/src/lib.rs —
PrivacyGate::demote单调降级模式与PrivacyClass(Raw/Derived/Anonymous/Restricted)、BfldError::InvalidDemote - archive/v1/data/proof — ADR-028 确定性证明链载体,
voxelmap_evidence_v1哈希将落于此处
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00