wifi-densepose-core 0.3.1

Core types, traits, and utilities for WiFi-DensePose pose estimation system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
//! Core trait definitions for the WiFi-DensePose system.
//!
//! This module defines the fundamental abstractions used throughout the system,
//! enabling a modular and testable architecture.
//!
//! # Traits
//!
//! - [`SignalProcessor`]: Process raw CSI frames into neural network-ready tensors
//! - [`NeuralInference`]: Run pose estimation inference on processed signals
//! - [`DataStore`]: Persist and retrieve CSI data and pose estimates
//!
//! # Design Philosophy
//!
//! These traits are designed with the following principles:
//!
//! 1. **Single Responsibility**: Each trait handles one concern
//! 2. **Testability**: All traits can be easily mocked for unit testing
//! 3. **Async-Ready**: Async versions available with the `async` feature
//! 4. **Error Handling**: Consistent use of `Result` types with domain errors

use crate::error::{CoreResult, InferenceError, SignalError, StorageError};
use crate::types::{CsiFrame, FrameId, PoseEstimate, ProcessedSignal, Timestamp};

/// ADR-136 §2.5 — deterministic, architecture-independent frame serialisation.
///
/// Every frame type that crosses a [`Stage`](https://example.invalid) boundary
/// or is recorded/replayed (`homecore-recorder`) implements `CanonicalFrame`.
/// The encoding is stable across architectures (little-endian per ADR-136 §2.3,
/// via [`ComplexSample::to_le_bytes`](crate::types::ComplexSample::to_le_bytes))
/// and across runs (fixed field order), so a BLAKE3 of the bytes is a witness
/// hash compatible with the ADR-028 proof chain and the ADR-119
/// `signature_hasher` precedent.
///
/// # Determinism contract
///
/// Feeding a recorded `Vec<CsiFrame>` through the stage chain twice MUST yield
/// byte-identical output streams, verified by equal [`Self::witness_hash`].
pub trait CanonicalFrame {
    /// Deterministic, architecture-independent encoding of this frame.
    ///
    /// Rules (ADR-136 §2.5): fixed-width little-endian fields in declared order;
    /// complex payload as `ComplexSample::to_le_bytes()` in stream-major order;
    /// raw IEEE-754 LE only (no text formatting of floats).
    fn to_canonical_bytes(&self) -> alloc_vec::Vec<u8>;

    /// BLAKE3-256 of [`Self::to_canonical_bytes`] — the witness hash (ADR-028).
    fn witness_hash(&self) -> [u8; 32] {
        blake3::hash(&self.to_canonical_bytes()).into()
    }
}

// `Vec` alias that works under both `std` and `no_std + alloc` (core is
// `#![cfg_attr(not(feature = "std"), no_std)]`). Keeps `CanonicalFrame` usable
// on the Xtensa/ESP32 target referenced by ADR-136 §2.3.
#[cfg(feature = "std")]
mod alloc_vec {
    pub use std::vec::Vec;
}
#[cfg(not(feature = "std"))]
mod alloc_vec {
    pub use alloc::vec::Vec;
}

/// Configuration for signal processing.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct SignalProcessorConfig {
    /// Number of frames to buffer before processing
    pub buffer_size: usize,
    /// Sampling rate in Hz
    pub sample_rate_hz: f64,
    /// Whether to apply noise filtering
    pub apply_noise_filter: bool,
    /// Noise filter cutoff frequency in Hz
    pub filter_cutoff_hz: f64,
    /// Whether to normalize amplitudes
    pub normalize_amplitude: bool,
    /// Whether to unwrap phases
    pub unwrap_phase: bool,
    /// Window function for spectral analysis
    pub window_function: WindowFunction,
}

impl Default for SignalProcessorConfig {
    fn default() -> Self {
        Self {
            buffer_size: 64,
            sample_rate_hz: 1000.0,
            apply_noise_filter: true,
            filter_cutoff_hz: 50.0,
            normalize_amplitude: true,
            unwrap_phase: true,
            window_function: WindowFunction::Hann,
        }
    }
}

/// Window functions for spectral analysis.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WindowFunction {
    /// Rectangular window (no windowing)
    Rectangular,
    /// Hann window
    #[default]
    Hann,
    /// Hamming window
    Hamming,
    /// Blackman window
    Blackman,
    /// Kaiser window
    Kaiser,
}

/// Signal processor for converting raw CSI frames into processed signals.
///
/// Implementations of this trait handle:
/// - Buffering and aggregating CSI frames
/// - Noise filtering and signal conditioning
/// - Phase unwrapping and amplitude normalization
/// - Feature extraction
///
/// # Example
///
/// ```ignore
/// use wifi_densepose_core::{SignalProcessor, CsiFrame};
///
/// fn process_frames(processor: &mut impl SignalProcessor, frames: Vec<CsiFrame>) {
///     for frame in frames {
///         if let Err(e) = processor.push_frame(frame) {
///             eprintln!("Failed to push frame: {}", e);
///         }
///     }
///
///     if let Some(signal) = processor.try_process() {
///         println!("Processed signal with {} time steps", signal.num_time_steps());
///     }
/// }
/// ```
pub trait SignalProcessor: Send + Sync {
    /// Returns the current configuration.
    fn config(&self) -> &SignalProcessorConfig;

    /// Updates the configuration.
    ///
    /// # Errors
    ///
    /// Returns an error if the configuration is invalid.
    fn set_config(&mut self, config: SignalProcessorConfig) -> Result<(), SignalError>;

    /// Pushes a new CSI frame into the processing buffer.
    ///
    /// # Errors
    ///
    /// Returns an error if the frame is invalid or the buffer is full.
    fn push_frame(&mut self, frame: CsiFrame) -> Result<(), SignalError>;

    /// Attempts to process the buffered frames.
    ///
    /// Returns `None` if insufficient frames are buffered.
    /// Returns `Some(ProcessedSignal)` on successful processing.
    ///
    /// # Errors
    ///
    /// Returns an error if processing fails.
    fn try_process(&mut self) -> Result<Option<ProcessedSignal>, SignalError>;

    /// Forces processing of whatever frames are buffered.
    ///
    /// # Errors
    ///
    /// Returns an error if no frames are buffered or processing fails.
    fn force_process(&mut self) -> Result<ProcessedSignal, SignalError>;

    /// Returns the number of frames currently buffered.
    fn buffered_frame_count(&self) -> usize;

    /// Clears the frame buffer.
    fn clear_buffer(&mut self);

    /// Resets the processor to its initial state.
    fn reset(&mut self);
}

/// Configuration for neural network inference.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct InferenceConfig {
    /// Path to the model file
    pub model_path: String,
    /// Device to run inference on
    pub device: InferenceDevice,
    /// Maximum batch size
    pub max_batch_size: usize,
    /// Number of threads for CPU inference
    pub num_threads: usize,
    /// Confidence threshold for detections
    pub confidence_threshold: f32,
    /// Non-maximum suppression threshold
    pub nms_threshold: f32,
    /// Whether to use half precision (FP16)
    pub use_fp16: bool,
}

impl Default for InferenceConfig {
    fn default() -> Self {
        Self {
            model_path: String::new(),
            device: InferenceDevice::Cpu,
            max_batch_size: 8,
            num_threads: 4,
            confidence_threshold: 0.5,
            nms_threshold: 0.45,
            use_fp16: false,
        }
    }
}

/// Device for running neural network inference.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum InferenceDevice {
    /// CPU inference
    #[default]
    Cpu,
    /// CUDA GPU inference
    Cuda {
        /// GPU device index
        device_id: usize,
    },
    /// TensorRT accelerated inference
    TensorRt {
        /// GPU device index
        device_id: usize,
    },
    /// CoreML (Apple Silicon)
    CoreMl,
    /// WebGPU for browser environments
    WebGpu,
}

/// Neural network inference engine for pose estimation.
///
/// Implementations of this trait handle:
/// - Loading and managing neural network models
/// - Running inference on processed signals
/// - Post-processing outputs into pose estimates
///
/// # Example
///
/// ```ignore
/// use wifi_densepose_core::{NeuralInference, ProcessedSignal};
///
/// async fn estimate_pose(
///     engine: &impl NeuralInference,
///     signal: ProcessedSignal,
/// ) -> Result<PoseEstimate, InferenceError> {
///     engine.infer(signal).await
/// }
/// ```
pub trait NeuralInference: Send + Sync {
    /// Returns the current configuration.
    fn config(&self) -> &InferenceConfig;

    /// Returns `true` if the model is loaded and ready.
    fn is_ready(&self) -> bool;

    /// Returns the model version string.
    fn model_version(&self) -> &str;

    /// Loads the model from the configured path.
    ///
    /// # Errors
    ///
    /// Returns an error if the model cannot be loaded.
    fn load_model(&mut self) -> Result<(), InferenceError>;

    /// Unloads the current model to free resources.
    fn unload_model(&mut self);

    /// Runs inference on a single processed signal.
    ///
    /// # Errors
    ///
    /// Returns an error if inference fails.
    fn infer(&self, signal: &ProcessedSignal) -> Result<PoseEstimate, InferenceError>;

    /// Runs inference on a batch of processed signals.
    ///
    /// # Errors
    ///
    /// Returns an error if inference fails.
    fn infer_batch(&self, signals: &[ProcessedSignal])
        -> Result<Vec<PoseEstimate>, InferenceError>;

    /// Warms up the model by running a dummy inference.
    ///
    /// # Errors
    ///
    /// Returns an error if warmup fails.
    fn warmup(&mut self) -> Result<(), InferenceError>;

    /// Returns performance statistics.
    fn stats(&self) -> InferenceStats;
}

/// Performance statistics for neural network inference.
#[derive(Debug, Clone, Default)]
pub struct InferenceStats {
    /// Total number of inferences performed
    pub total_inferences: u64,
    /// Average inference latency in milliseconds
    pub avg_latency_ms: f64,
    /// 95th percentile latency in milliseconds
    pub p95_latency_ms: f64,
    /// Maximum latency in milliseconds
    pub max_latency_ms: f64,
    /// Inferences per second throughput
    pub throughput: f64,
    /// GPU memory usage in bytes (if applicable)
    pub gpu_memory_bytes: Option<u64>,
}

/// Query options for data store operations.
#[derive(Debug, Clone, Default)]
pub struct QueryOptions {
    /// Maximum number of results to return
    pub limit: Option<usize>,
    /// Number of results to skip
    pub offset: Option<usize>,
    /// Start time filter (inclusive)
    pub start_time: Option<Timestamp>,
    /// End time filter (inclusive)
    pub end_time: Option<Timestamp>,
    /// Device ID filter
    pub device_id: Option<String>,
    /// Sort order
    pub sort_order: SortOrder,
}

/// Sort order for query results.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortOrder {
    /// Ascending order (oldest first)
    #[default]
    Ascending,
    /// Descending order (newest first)
    Descending,
}

/// Data storage trait for persisting and retrieving CSI data and pose estimates.
///
/// Implementations can use various backends:
/// - PostgreSQL/SQLite for relational storage
/// - Redis for caching
/// - Time-series databases for efficient temporal queries
///
/// # Example
///
/// ```ignore
/// use wifi_densepose_core::{DataStore, CsiFrame, PoseEstimate};
///
/// async fn save_and_query(
///     store: &impl DataStore,
///     frame: CsiFrame,
///     estimate: PoseEstimate,
/// ) {
///     store.store_csi_frame(&frame).await?;
///     store.store_pose_estimate(&estimate).await?;
///
///     let recent = store.get_recent_estimates(10).await?;
///     println!("Found {} recent estimates", recent.len());
/// }
/// ```
pub trait DataStore: Send + Sync {
    /// Returns `true` if the store is connected and ready.
    fn is_connected(&self) -> bool;

    /// Stores a CSI frame.
    ///
    /// # Errors
    ///
    /// Returns an error if the store operation fails.
    fn store_csi_frame(&self, frame: &CsiFrame) -> Result<(), StorageError>;

    /// Retrieves a CSI frame by ID.
    ///
    /// # Errors
    ///
    /// Returns an error if the frame is not found or retrieval fails.
    fn get_csi_frame(&self, id: &FrameId) -> Result<CsiFrame, StorageError>;

    /// Retrieves CSI frames matching the query options.
    ///
    /// # Errors
    ///
    /// Returns an error if the query fails.
    fn query_csi_frames(&self, options: &QueryOptions) -> Result<Vec<CsiFrame>, StorageError>;

    /// Stores a pose estimate.
    ///
    /// # Errors
    ///
    /// Returns an error if the store operation fails.
    fn store_pose_estimate(&self, estimate: &PoseEstimate) -> Result<(), StorageError>;

    /// Retrieves a pose estimate by ID.
    ///
    /// # Errors
    ///
    /// Returns an error if the estimate is not found or retrieval fails.
    fn get_pose_estimate(&self, id: &FrameId) -> Result<PoseEstimate, StorageError>;

    /// Retrieves pose estimates matching the query options.
    ///
    /// # Errors
    ///
    /// Returns an error if the query fails.
    fn query_pose_estimates(
        &self,
        options: &QueryOptions,
    ) -> Result<Vec<PoseEstimate>, StorageError>;

    /// Retrieves the N most recent pose estimates.
    ///
    /// # Errors
    ///
    /// Returns an error if the query fails.
    fn get_recent_estimates(&self, count: usize) -> Result<Vec<PoseEstimate>, StorageError>;

    /// Deletes CSI frames older than the given timestamp.
    ///
    /// # Errors
    ///
    /// Returns an error if the deletion fails.
    fn delete_csi_frames_before(&self, timestamp: &Timestamp) -> Result<u64, StorageError>;

    /// Deletes pose estimates older than the given timestamp.
    ///
    /// # Errors
    ///
    /// Returns an error if the deletion fails.
    fn delete_pose_estimates_before(&self, timestamp: &Timestamp) -> Result<u64, StorageError>;

    /// Returns storage statistics.
    fn stats(&self) -> StorageStats;
}

/// Storage statistics.
#[derive(Debug, Clone, Default)]
pub struct StorageStats {
    /// Total number of CSI frames stored
    pub csi_frame_count: u64,
    /// Total number of pose estimates stored
    pub pose_estimate_count: u64,
    /// Total storage size in bytes
    pub total_size_bytes: u64,
    /// Oldest record timestamp
    pub oldest_record: Option<Timestamp>,
    /// Newest record timestamp
    pub newest_record: Option<Timestamp>,
}

// =============================================================================
// Async Trait Definitions (with `async` feature)
// =============================================================================

#[cfg(feature = "async")]
use async_trait::async_trait;

/// Async version of [`SignalProcessor`].
#[cfg(feature = "async")]
#[async_trait]
pub trait AsyncSignalProcessor: Send + Sync {
    /// Returns the current configuration.
    fn config(&self) -> &SignalProcessorConfig;

    /// Updates the configuration.
    async fn set_config(&mut self, config: SignalProcessorConfig) -> Result<(), SignalError>;

    /// Pushes a new CSI frame into the processing buffer.
    async fn push_frame(&mut self, frame: CsiFrame) -> Result<(), SignalError>;

    /// Attempts to process the buffered frames.
    async fn try_process(&mut self) -> Result<Option<ProcessedSignal>, SignalError>;

    /// Forces processing of whatever frames are buffered.
    async fn force_process(&mut self) -> Result<ProcessedSignal, SignalError>;

    /// Returns the number of frames currently buffered.
    fn buffered_frame_count(&self) -> usize;

    /// Clears the frame buffer.
    async fn clear_buffer(&mut self);

    /// Resets the processor to its initial state.
    async fn reset(&mut self);
}

/// Async version of [`NeuralInference`].
#[cfg(feature = "async")]
#[async_trait]
pub trait AsyncNeuralInference: Send + Sync {
    /// Returns the current configuration.
    fn config(&self) -> &InferenceConfig;

    /// Returns `true` if the model is loaded and ready.
    fn is_ready(&self) -> bool;

    /// Returns the model version string.
    fn model_version(&self) -> &str;

    /// Loads the model from the configured path.
    async fn load_model(&mut self) -> Result<(), InferenceError>;

    /// Unloads the current model to free resources.
    async fn unload_model(&mut self);

    /// Runs inference on a single processed signal.
    async fn infer(&self, signal: &ProcessedSignal) -> Result<PoseEstimate, InferenceError>;

    /// Runs inference on a batch of processed signals.
    async fn infer_batch(
        &self,
        signals: &[ProcessedSignal],
    ) -> Result<Vec<PoseEstimate>, InferenceError>;

    /// Warms up the model by running a dummy inference.
    async fn warmup(&mut self) -> Result<(), InferenceError>;

    /// Returns performance statistics.
    fn stats(&self) -> InferenceStats;
}

/// Async version of [`DataStore`].
#[cfg(feature = "async")]
#[async_trait]
pub trait AsyncDataStore: Send + Sync {
    /// Returns `true` if the store is connected and ready.
    fn is_connected(&self) -> bool;

    /// Stores a CSI frame.
    async fn store_csi_frame(&self, frame: &CsiFrame) -> Result<(), StorageError>;

    /// Retrieves a CSI frame by ID.
    async fn get_csi_frame(&self, id: &FrameId) -> Result<CsiFrame, StorageError>;

    /// Retrieves CSI frames matching the query options.
    async fn query_csi_frames(&self, options: &QueryOptions)
        -> Result<Vec<CsiFrame>, StorageError>;

    /// Stores a pose estimate.
    async fn store_pose_estimate(&self, estimate: &PoseEstimate) -> Result<(), StorageError>;

    /// Retrieves a pose estimate by ID.
    async fn get_pose_estimate(&self, id: &FrameId) -> Result<PoseEstimate, StorageError>;

    /// Retrieves pose estimates matching the query options.
    async fn query_pose_estimates(
        &self,
        options: &QueryOptions,
    ) -> Result<Vec<PoseEstimate>, StorageError>;

    /// Retrieves the N most recent pose estimates.
    async fn get_recent_estimates(&self, count: usize) -> Result<Vec<PoseEstimate>, StorageError>;

    /// Deletes CSI frames older than the given timestamp.
    async fn delete_csi_frames_before(&self, timestamp: &Timestamp) -> Result<u64, StorageError>;

    /// Deletes pose estimates older than the given timestamp.
    async fn delete_pose_estimates_before(
        &self,
        timestamp: &Timestamp,
    ) -> Result<u64, StorageError>;

    /// Returns storage statistics.
    fn stats(&self) -> StorageStats;
}

// =============================================================================
// Extension Traits
// =============================================================================

/// Extension trait for pipeline composition.
pub trait Pipeline: Send + Sync {
    /// The input type for this pipeline stage.
    type Input;
    /// The output type for this pipeline stage.
    type Output;
    /// The error type for this pipeline stage.
    type Error;

    /// Processes input and produces output.
    ///
    /// # Errors
    ///
    /// Returns an error if processing fails.
    fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
}

/// Trait for types that can validate themselves.
pub trait Validate {
    /// Validates the instance.
    ///
    /// # Errors
    ///
    /// Returns an error describing validation failures.
    fn validate(&self) -> CoreResult<()>;
}

/// Trait for types that can be reset to a default state.
pub trait Resettable {
    /// Resets the instance to its initial state.
    fn reset(&mut self);
}

/// Trait for types that track health status.
pub trait HealthCheck {
    /// Health status of the component.
    type Status;

    /// Performs a health check and returns the current status.
    fn health_check(&self) -> Self::Status;

    /// Returns `true` if the component is healthy.
    fn is_healthy(&self) -> bool;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_signal_processor_config_default() {
        let config = SignalProcessorConfig::default();
        assert_eq!(config.buffer_size, 64);
        assert!(config.apply_noise_filter);
        assert!(config.sample_rate_hz > 0.0);
    }

    #[test]
    fn test_inference_config_default() {
        let config = InferenceConfig::default();
        assert_eq!(config.device, InferenceDevice::Cpu);
        assert!(config.confidence_threshold > 0.0);
        assert!(config.max_batch_size > 0);
    }

    #[test]
    fn test_query_options_default() {
        let options = QueryOptions::default();
        assert!(options.limit.is_none());
        assert!(options.offset.is_none());
        assert_eq!(options.sort_order, SortOrder::Ascending);
    }

    #[test]
    fn test_inference_device_variants() {
        let cpu = InferenceDevice::Cpu;
        let cuda = InferenceDevice::Cuda { device_id: 0 };
        let tensorrt = InferenceDevice::TensorRt { device_id: 1 };

        assert_eq!(cpu, InferenceDevice::Cpu);
        assert!(matches!(cuda, InferenceDevice::Cuda { device_id: 0 }));
        assert!(matches!(
            tensorrt,
            InferenceDevice::TensorRt { device_id: 1 }
        ));
    }
}