Skip to main content

wifi_densepose_worldgraph/
model.rs

1//! ADR-139 §2.1 — typed node/edge model.
2//!
3//! Nodes and edges are `serde` enums (NOT boxed trait objects) for
4//! deterministic, schema-versioned, RVF-friendly persistence. Cross-ADR
5//! references (ADR-137 evidence, ADR-141 privacy decision) are carried as
6//! opaque content-address `String` handles so the WorldGraph compiles and
7//! persists independently of those crates (§2.1, §2.3).
8
9use serde::{Deserialize, Serialize};
10
11/// Stable, monotonic identity for a world entity. Distinct from petgraph's
12/// `NodeIndex` (graph-internal handle); `WorldId` survives RVF round-trips and
13/// node removal. `WorldId(0)` is the "assign me one" sentinel for `upsert_node`.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct WorldId(pub u64);
16
17impl WorldId {
18    /// The "allocate a fresh id" sentinel.
19    pub const UNASSIGNED: WorldId = WorldId(0);
20
21    /// Whether this id is the unassigned sentinel.
22    #[must_use]
23    pub fn is_unassigned(&self) -> bool {
24        self.0 == 0
25    }
26}
27
28/// Local ENU coordinate in metres relative to the installation origin (ADR-044).
29#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
30pub struct EnuPoint {
31    /// East offset (m).
32    pub east_m: f64,
33    /// North offset (m).
34    pub north_m: f64,
35    /// Up offset (m).
36    pub up_m: f64,
37}
38
39/// MAT `ZoneBounds` reprojected into the installation ENU frame.
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(tag = "shape", rename_all = "snake_case")]
42pub enum ZoneBoundsEnu {
43    /// Axis-aligned rectangle.
44    Rectangle {
45        /// Minimum east (m).
46        min_e: f64,
47        /// Minimum north (m).
48        min_n: f64,
49        /// Maximum east (m).
50        max_e: f64,
51        /// Maximum north (m).
52        max_n: f64,
53    },
54    /// Circle.
55    Circle {
56        /// Centre east (m).
57        center_e: f64,
58        /// Centre north (m).
59        center_n: f64,
60        /// Radius (m).
61        radius_m: f64,
62    },
63    /// Polygon (east, north) vertices.
64    Polygon {
65        /// (east, north) vertices.
66        vertices: Vec<(f64, f64)>,
67    },
68}
69
70impl ZoneBoundsEnu {
71    /// Whether an ENU point lies within these bounds (up ignored).
72    #[must_use]
73    pub fn contains(&self, p: &EnuPoint) -> bool {
74        match self {
75            Self::Rectangle { min_e, min_n, max_e, max_n } => {
76                p.east_m >= *min_e && p.east_m <= *max_e && p.north_m >= *min_n && p.north_m <= *max_n
77            }
78            Self::Circle { center_e, center_n, radius_m } => {
79                let de = p.east_m - center_e;
80                let dn = p.north_m - center_n;
81                (de * de + dn * dn).sqrt() <= *radius_m
82            }
83            Self::Polygon { vertices } => point_in_polygon(p.east_m, p.north_m, vertices),
84        }
85    }
86}
87
88fn point_in_polygon(px: f64, py: f64, verts: &[(f64, f64)]) -> bool {
89    if verts.len() < 3 {
90        return false;
91    }
92    // Ray-casting parity test.
93    let mut inside = false;
94    let mut j = verts.len() - 1;
95    for i in 0..verts.len() {
96        let (xi, yi) = verts[i];
97        let (xj, yj) = verts[j];
98        let intersect = ((yi > py) != (yj > py))
99            && (px < (xj - xi) * (py - yi) / (yj - yi) + xi);
100        if intersect {
101            inside = !inside;
102        }
103        j = i;
104    }
105    inside
106}
107
108/// Sensing modality of a physical device placement.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum SensorModality {
112    /// WiFi CSI sensing node (ESP32-S3/C6).
113    WifiCsi,
114    /// 60 GHz mmWave FMCW radar.
115    MmWave,
116    /// Ultra-wideband ranging beacon (ADR-144).
117    Uwb,
118    /// Coarse presence sensor.
119    Presence,
120}
121
122/// Kind of persistent static anchor.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum AnchorKind {
126    /// A persistent RF reflector (ADR-143 RF SLAM).
127    Reflector,
128    /// A piece of furniture inferred from reflector clustering.
129    Furniture,
130    /// A surveyed UWB beacon (ADR-144).
131    UwbBeacon,
132}
133
134/// Mandatory provenance for every [`WorldNode::SemanticState`] (house rule):
135/// every semantic belief traces to signal evidence + model + calibration +
136/// privacy decision.
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct SemanticProvenance {
139    /// ADR-137 `EvidenceRef` content-address handle(s).
140    pub evidence: Vec<String>,
141    /// Model version (ADR-136 `model_id`/`model_version`) that produced this.
142    pub model_version: String,
143    /// Calibration version (ADR-135 baseline id) in effect.
144    pub calibration_version: String,
145    /// Privacy decision (ADR-141 mode + action) it was derived under.
146    pub privacy_decision: String,
147}
148
149/// A typed world node (ADR-139 §2.1). Persistence-deterministic serde enum.
150#[derive(Clone, Debug, Serialize, Deserialize)]
151#[serde(tag = "kind", rename_all = "snake_case")]
152pub enum WorldNode {
153    /// A bounded interior space, linked to a HomeCore `area_id` (ADR-127).
154    Room {
155        /// Stable id (or `UNASSIGNED` to allocate).
156        id: WorldId,
157        /// HomeCore registry area_id — the entity-linkage join key.
158        area_id: Option<String>,
159        /// Human name.
160        name: String,
161        /// Room footprint in local ENU.
162        bounds_enu: ZoneBoundsEnu,
163        /// Floor index.
164        floor: i16,
165    },
166    /// A sub-region of a room targeted for sensing (MAT ScanZone analogue).
167    Zone {
168        /// Stable id.
169        id: WorldId,
170        /// Containing room.
171        parent_room: WorldId,
172        /// Human name.
173        name: String,
174        /// Zone footprint.
175        bounds_enu: ZoneBoundsEnu,
176    },
177    /// A wall segment (coarse 2D topological element in ENU).
178    Wall {
179        /// Stable id.
180        id: WorldId,
181        /// Segment start.
182        a: EnuPoint,
183        /// Segment end.
184        b: EnuPoint,
185        /// Coarse RF attenuation (dB): drywall ≈ 3, brick ≈ 12.
186        rf_attenuation_db: f32,
187    },
188    /// A passable opening between two rooms.
189    Doorway {
190        /// Stable id.
191        id: WorldId,
192        /// Centre point.
193        center: EnuPoint,
194        /// Opening width (m).
195        width_m: f32,
196    },
197    /// A physical sensing device placement (ADR-113 placement target).
198    Sensor {
199        /// Stable id.
200        id: WorldId,
201        /// Matches HomeCore `EntityEntry.device_id`.
202        device_id: String,
203        /// Placement in local ENU.
204        position: EnuPoint,
205        /// Sensing modality.
206        modality: SensorModality,
207    },
208    /// A directed RF propagation channel between two sensors (ADR-138 LinkGroup member).
209    RfLink {
210        /// Stable id.
211        id: WorldId,
212        /// Transmit sensor node.
213        tx: WorldId,
214        /// Receive sensor node.
215        rx: WorldId,
216        /// ADR-138 MLO LinkGroup id.
217        link_group_id: Option<String>,
218        /// Centre frequency (MHz).
219        center_freq_mhz: u32,
220    },
221    /// A tracked person (Kalman track id from ruvsense `pose_tracker`).
222    PersonTrack {
223        /// Stable id.
224        id: WorldId,
225        /// Tracker track id.
226        track_id: u64,
227        /// Last known ENU position.
228        last_position: EnuPoint,
229        /// AETHER re-ID embedding handle.
230        reid_embedding_ref: Option<String>,
231    },
232    /// A persistent static reflector / object (ADR-143 / ADR-144 anchor).
233    ObjectAnchor {
234        /// Stable id.
235        id: WorldId,
236        /// ENU position.
237        position: EnuPoint,
238        /// Anchor classification.
239        anchor_kind: AnchorKind,
240        /// Confidence in [0, 1].
241        confidence: f32,
242    },
243    /// A discrete detected event (fall, entry, gesture) at a point in time.
244    Event {
245        /// Stable id.
246        id: WorldId,
247        /// Event type tag.
248        event_type: String,
249        /// Wall-clock time (Unix ms).
250        at_unix_ms: i64,
251        /// Containing room/zone.
252        located_in: Option<WorldId>,
253    },
254    /// A fused semantic belief about the world (the ADR-140 record's graph anchor).
255    SemanticState {
256        /// Stable id.
257        id: WorldId,
258        /// Human-readable belief statement.
259        statement: String,
260        /// Confidence in [0, 1].
261        confidence: f32,
262        /// Mandatory provenance (house rule).
263        provenance: SemanticProvenance,
264        /// Belief validity start (Unix ms).
265        valid_from_unix_ms: i64,
266    },
267}
268
269impl WorldNode {
270    /// The embedded stable id of this node.
271    #[must_use]
272    pub fn id(&self) -> WorldId {
273        match self {
274            Self::Room { id, .. }
275            | Self::Zone { id, .. }
276            | Self::Wall { id, .. }
277            | Self::Doorway { id, .. }
278            | Self::Sensor { id, .. }
279            | Self::RfLink { id, .. }
280            | Self::PersonTrack { id, .. }
281            | Self::ObjectAnchor { id, .. }
282            | Self::Event { id, .. }
283            | Self::SemanticState { id, .. } => *id,
284        }
285    }
286
287    /// Overwrite the embedded id (used by `upsert_node` when allocating one).
288    pub(crate) fn set_id(&mut self, new: WorldId) {
289        match self {
290            Self::Room { id, .. }
291            | Self::Zone { id, .. }
292            | Self::Wall { id, .. }
293            | Self::Doorway { id, .. }
294            | Self::Sensor { id, .. }
295            | Self::RfLink { id, .. }
296            | Self::PersonTrack { id, .. }
297            | Self::ObjectAnchor { id, .. }
298            | Self::Event { id, .. }
299            | Self::SemanticState { id, .. } => *id = new,
300        }
301    }
302
303    /// Static kind tag for diagnostics/queries.
304    #[must_use]
305    pub fn kind(&self) -> &'static str {
306        match self {
307            Self::Room { .. } => "room",
308            Self::Zone { .. } => "zone",
309            Self::Wall { .. } => "wall",
310            Self::Doorway { .. } => "doorway",
311            Self::Sensor { .. } => "sensor",
312            Self::RfLink { .. } => "rf_link",
313            Self::PersonTrack { .. } => "person_track",
314            Self::ObjectAnchor { .. } => "object_anchor",
315            Self::Event { .. } => "event",
316            Self::SemanticState { .. } => "semantic_state",
317        }
318    }
319}
320
321/// A typed edge between two [`WorldNode`]s (ADR-139 §2.1). Stored as the
322/// petgraph edge weight; metadata is structurally per-relation.
323#[derive(Clone, Debug, Serialize, Deserialize)]
324#[serde(tag = "rel", rename_all = "snake_case")]
325pub enum WorldEdge {
326    /// sensor/rf_link → observable node. Weight is field-of-regard quality.
327    Observes {
328        /// Field-of-regard quality in [0, 1].
329        quality: f32,
330        /// Last observation time (Unix ms).
331        last_seen_unix_ms: i64,
332    },
333    /// person/object/event → room/zone containment.
334    LocatedIn {
335        /// Containment start (Unix ms).
336        since_unix_ms: i64,
337    },
338    /// room ↔ room through a doorway (undirected pair stored as two edges).
339    AdjacentTo {
340        /// The connecting doorway node.
341        via_doorway: WorldId,
342    },
343    /// sensor/rf_link → sensor/rf_link physical/clock support (ADR-138).
344    Supports {
345        /// Support strength in [0, 1].
346        strength: f32,
347    },
348    /// evidence/state → evidence/state: sources disagree (ADR-137).
349    Contradicts {
350        /// Disagreement magnitude.
351        magnitude: f32,
352        /// ADR-137 contradiction-flag content-address handle.
353        flag: String,
354    },
355    /// semantic_state → prior state/evidence provenance chain (ADR-137).
356    DerivedFrom {
357        /// ADR-137 evidence content-address handle.
358        evidence: String,
359    },
360    /// sensor → node: observation constrained by a privacy mode (ADR-141).
361    PrivacyLimitedBy {
362        /// Limiting privacy mode name.
363        mode: String,
364        /// Action evaluated.
365        action: String,
366        /// Whether observation is allowed under the current mode.
367        allowed: bool,
368    },
369}
370
371impl WorldEdge {
372    /// Static relation tag.
373    #[must_use]
374    pub fn rel(&self) -> &'static str {
375        match self {
376            Self::Observes { .. } => "observes",
377            Self::LocatedIn { .. } => "located_in",
378            Self::AdjacentTo { .. } => "adjacent_to",
379            Self::Supports { .. } => "supports",
380            Self::Contradicts { .. } => "contradicts",
381            Self::DerivedFrom { .. } => "derived_from",
382            Self::PrivacyLimitedBy { .. } => "privacy_limited_by",
383        }
384    }
385}