# CSI Phase Unwrapping Algorithm in WiFi-DensePose: Implementation Guide

> Implement the CSI phase unwrapping algorithm in WiFi-DensePose. Restore continuous phase values from wrapped measurements using NumPy or a custom block-wise approach. Get the guide today.

- Repository: [rUv/wifi-densepose](https://github.com/ruvnet/wifi-densepose)
- Tags: how-to-guide
- Published: 2026-02-19

---

**The CSI phase unwrapping algorithm in WiFi-DensePose restores continuous phase values from wrapped [-π, π] measurements using either vectorized NumPy operations or a custom block-wise approach that respects hardware sampling boundaries.**

The `ruvnet/wifi-densepose` repository implements a robust CSI phase unwrapping algorithm to convert raw Channel State Information into usable signals for dense human pose estimation. Before outlier removal or smoothing can occur, the system must resolve phase discontinuities caused by the inherent 2π periodicity of wireless measurements. This article examines the dual implementation strategy found in the codebase, covering both the high-performance vectorized approach and the domain-specific custom unwrapping logic.

## Core Problem – Phase Wrap-Around in CSI Measurements

CSI phase measurements are inherently wrapped to the `[-π, π]` interval. When the true physical phase changes slowly across frequency or time, the measured value may abruptly jump from `+π` to `-π` (or vice-versa), creating artificial discontinuities. The CSI phase unwrapping algorithm detects these jumps—where the absolute difference between consecutive samples exceeds π—and restores the original monotonic progression by adding or subtracting multiples of `2π`.

## Vectorised CSI Phase Unwrapping with NumPy and SciPy

The primary implementation resides in [`v1/src/core/phase_sanitizer.py`](https://github.com/ruvnet/wifi-densepose/blob/main/v1/src/core/phase_sanitizer.py) and offers a configurable interface that delegates to either NumPy or SciPy backends.

### The unwrap_phase Entry Point

The `PhaseSanitizer.unwrap_phase()` method (lines 77-98) serves as the central dispatcher. It selects an underlying implementation based on the configuration flag `unwrapping_method`, which accepts `"numpy"`, `"scipy"`, or `"custom"`.

### NumPy Implementation Details

The `_unwrap_numpy()` method (lines 102-107) leverages `np.unwrap` for high-performance vectorized processing:

```python
def _unwrap_numpy(self, phase_data: np.ndarray) -> np.ndarray:
    """Unwrap phase using numpy's unwrap function."""
    if phase_data.size == 0:
        raise ValueError("Cannot unwrap empty phase data")
    return np.unwrap(phase_data, axis=1)

```

The `axis=1` argument treats the **frequency axis** (sub-carrier index) as the dimension to unwrap, leaving the sample-time axis untouched. The algorithm computes differences between successive samples and applies `2π` corrections whenever the absolute difference exceeds the default discontinuity threshold of π.

### SciPy Wrapper

The `_unwrap_scipy()` method (lines 108-113) currently functions as a thin wrapper around the NumPy implementation. This design preserves API consistency and allows for future migration to SciPy's `signal.unwrap` without modifying external calling code.

## Custom Block-Wise CSI Phase Unwrapping Algorithm

For scenarios where hardware sampling boundaries must be respected, the repository provides a domain-specific implementation in [`references/script_5.py`](https://github.com/ruvnet/wifi-densepose/blob/main/references/script_5.py) (lines 20-41).

### Handling Hardware Sampling Boundaries

CSI acquisition in WiFi-DensePose organizes data into blocks of 30 consecutive sub-carriers, with five such blocks comprising a full capture of 150 frequency samples. The hardware may introduce independent phase offsets between these blocks, making a pure vectorized unwrap across the entire frequency axis potentially destructive.

### Group-Wise Implementation

The custom algorithm iterates over each 30-subcarrier block and each transmit-receive antenna pair, explicitly correcting jumps greater than π or less than -π:

```python
for sample_group in range(5):            # five blocks of 30 sub-carriers

    start_idx = sample_group * 30
    end_idx   = start_idx + 30
    for tx in range(3):
        for rx in range(3):
            for i in range(start_idx + 1, end_idx):
                diff = unwrapped[i, tx, rx] - unwrapped[i-1, tx, rx]
                if diff > np.pi:
                    unwrapped[i, tx, rx] = unwrapped[i-1, tx, rx] + diff - 2*np.pi
                elif diff < -np.pi:
                    unwrapped[i, tx, rx] = unwrapped[i-1, tx, rx] + diff + 2*np.pi

```

This approach preserves physical antenna path independence while preventing error propagation across hardware-defined block boundaries.

## Integration into the Sanitization Pipeline

The `PhaseSanitizer.sanitize_phase()` method (lines 66-90) orchestrates the complete preprocessing workflow. Unwrapping occurs as the **first deterministic transformation**, ensuring that subsequent modules—outlier removal, smoothing, and noise filtering—operate on physically plausible continuous phase surfaces rather than wrapped discontinuities.

## Configuration and Method Selection

The algorithm behavior is controlled through a configuration dictionary passed to `PhaseSanitizer.__init__`:

```json
{
  "unwrapping_method": "numpy",
  "outlier_threshold": 3.0,
  "smoothing_window": 5,
  "enable_noise_filtering": false
}

```

Valid options for `"unwrapping_method"` include `"numpy"`, `"scipy"`, and `"custom"`. Changing this parameter swaps the underlying implementation without requiring modifications to downstream code, facilitating rapid experimentation between vectorized speed and block-wise accuracy.

## Practical Code Examples

### Using PhaseSanitizer with NumPy

```python
import numpy as np
from v1.src.core.phase_sanitizer import PhaseSanitizer

# Configuration for vectorized NumPy unwrapping

cfg = {
    "unwrapping_method": "numpy",
    "outlier_threshold": 3.0,
    "smoothing_window": 5,
    "enable_outlier_removal": True,
    "enable_smoothing": True,
    "enable_noise_filtering": False,
}
sanitizer = PhaseSanitizer(cfg)

# Simulated wrapped CSI phase (samples × 150 frequencies × 3 tx × 3 rx)

wrapped_phase = np.random.uniform(-np.pi, np.pi, (2, 150, 3, 3))

# Execute full sanitization pipeline

clean_phase = sanitizer.sanitize_phase(wrapped_phase)

```

### Direct Block-Wise Unwrapping

```python
from references.script_5 import CSIPhaseProcessor
import numpy as np

processor = CSIPhaseProcessor(num_subcarriers=30)

# Shape: (150 frequency samples, 3 transmitters, 3 receivers)

wrapped = np.random.uniform(-np.pi, np.pi, (150, 3, 3))

# Apply custom unwrap respecting 5 blocks of 30 sub-carriers

unwrapped = processor.unwrap_phase(wrapped)

```

### Switching to Custom Implementation

```python

# Reconfigure sanitizer to use block-wise logic

cfg["unwrapping_method"] = "custom"
sanitizer = PhaseSanitizer(cfg)

# Now delegates to bespoke group-wise algorithm

clean_phase = sanitizer.unwrap_phase(wrapped_phase)

```

## Key Source Files

- **[`v1/src/core/phase_sanitizer.py`](https://github.com/ruvnet/wifi-densepose/blob/main/v1/src/core/phase_sanitizer.py)** – Central sanitization class implementing `PhaseSanitizer.unwrap_phase()`, `_unwrap_numpy()`, `_unwrap_scipy()`, and `sanitize_phase()`. Contains the vectorized unwrapping logic at lines 77-98 and 102-113.
- **[`references/script_5.py`](https://github.com/ruvnet/wifi-densepose/blob/main/references/script_5.py)** – Stand-alone demonstration script containing the custom block-wise CSI phase unwrapping algorithm (lines 20-41) that handles grouped sub-carrier blocks.
- **[`v1/tests/unit/test_phase_sanitizer.py`](https://github.com/ruvnet/wifi-densepose/blob/main/v1/tests/unit/test_phase_sanitizer.py)** – Unit tests verifying correct unwrapping behavior for both NumPy and custom methods.
- **[`v1/src/services/pose_service.py`](https://github.com/ruvnet/wifi-densepose/blob/main/v1/src/services/pose_service.py)** – Service layer consuming sanitized phase data for downstream dense pose prediction.

## Summary

- The CSI phase unwrapping algorithm resolves [-π, π] discontinuities by detecting jumps exceeding π and correcting them with 2π multiples.
- Two implementations coexist: a **vectorized NumPy approach** (`np.unwrap` on axis=1) for speed, and a **custom block-wise method** respecting 30-subcarrier hardware blocks.
- The `PhaseSanitizer` class in [`v1/src/core/phase_sanitizer.py`](https://github.com/ruvnet/wifi-densepose/blob/main/v1/src/core/phase_sanitizer.py) orchestrates unwrapping as the first step in a pipeline that includes outlier removal and smoothing.
- Configuration via `unwrapping_method` allows switching between `"numpy"`, `"scipy"`, and `"custom"` without code changes.

## Frequently Asked Questions

### What causes phase wrapping in CSI measurements?

Phase wrapping occurs because Channel State Information is measured modulo 2π, constraining values to the [-π, π] interval. When the true physical phase changes gradually across frequency or time, the measured signal appears to jump discontinuously from +π to -π (or vice versa), creating artificial discontinuities that must be corrected before further signal processing.

### How does the NumPy unwrapping method handle multi-antenna CSI data?

The `_unwrap_numpy()` method in [`v1/src/core/phase_sanitizer.py`](https://github.com/ruvnet/wifi-densepose/blob/main/v1/src/core/phase_sanitizer.py) applies `np.unwrap(phase_data, axis=1)`, treating axis 1 as the frequency dimension (sub-carrier index) while preserving the sample-time and antenna dimensions. This vectorized approach simultaneously processes all transmit-receive pairs across the time axis, making it efficient for batched CSI tensors with shape `(samples, 150, 3, 3)`.

### When should I use the custom block-wise unwrapping instead of the NumPy method?

Use the custom block-wise implementation—found in [`references/script_5.py`](https://github.com/ruvnet/wifi-densepose/blob/main/references/script_5.py)—when your CSI acquisition hardware introduces independent phase offsets between sampling blocks. The WiFi-DensePose system acquires data in five blocks of 30 sub-carriers each; the custom algorithm unwraps within each block separately to prevent error propagation across block boundaries, whereas the standard NumPy method would treat the entire 150-subcarrier range as continuous.

### Where is the unwrapping algorithm configured in the WiFi-DensePose pipeline?

Configuration occurs in the `PhaseSanitizer` class constructor via a dictionary parameter. Set `"unwrapping_method"` to `"numpy"`, `"scipy"`, or `"custom"` to select the implementation. This configuration is typically defined in the service initialization or experiment script and passed to `sanitize_phase()`, which orchestrates the full preprocessing pipeline including unwrapping, outlier removal, and smoothing.