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}