# CV Normalization in ESPectre: A Gain-Invariant Turbulence Metric for ESP32

> Discover CV normalization in ESPectre. This gain invariant turbulence metric helps detect motion on ESP32 without hardware gain lock. Learn how it works.

- Repository: [Francesco Pace/espectre](https://github.com/francescopace/espectre)
- Tags: deep-dive
- Published: 2026-06-10

---

**CV normalization in ESPectre divides the standard deviation of sub-carrier amplitudes by their mean to create a gain-invariant turbulence metric, essential for motion detection on ESP32 devices without hardware gain lock.**

When working with Wi-Fi sensing on the ESP32 radio chip, amplitude measurements fluctuate unpredictably due to unlocked automatic gain control (AGC). The ESPectre repository provides CV (Coefficient-of-Variation) normalization as a robust solution that stabilizes turbulence calculations across varying power levels. This technique ensures reliable motion detection even when the radio chip cannot maintain a constant gain factor.

## What is CV Normalization?

CV normalization is a **statistical preprocessing technique** that makes turbulence measurements independent of signal amplitude scaling. Instead of using raw standard deviation—which scales linearly with gain changes—the method computes the ratio of standard deviation to mean amplitude.

The mathematical implementation follows this formula:

```python
CV = σ(amplitudes) / μ(amplitudes)

```

Where **σ** represents the standard deviation and **μ** represents the mean of the sub-carrier amplitude values. Because both numerator and denominator scale by the same gain factor *k*, the ratio remains constant (`σ(kA)/μ(kA) = σ(A)/μ(A)`), providing a stable turbulence value regardless of power fluctuations.

## Implementation in ESPectre Source Code

### Core Calculation in [`utils.py`](https://github.com/francescopace/espectre/blob/main/utils.py)

The primary implementation resides in [`micro-espectre/src/utils.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/utils.py) within the `calculate_spatial_turbulence` function. This utility accepts a `use_cv_normalization` boolean flag that determines whether to return the coefficient of variation or raw standard deviation.

```python

# micro-espectre/src/utils.py – calculate_spatial_turbulence

def calculate_spatial_turbulence(magnitudes, band, use_cv_normalization=True):
    ...
    std = calculate_std(band_mags)
    if use_cv_normalization:
        mean = sum(band_mags) / len(band_mags)
        return std / mean if mean > 0 else 0.0
    else:
        return std

```

The function extracts the specified sub-carrier band, calculates the standard deviation, and conditionally divides by the mean when CV normalization is enabled.

### Flag Management in [`segmentation.py`](https://github.com/francescopace/espectre/blob/main/segmentation.py)

The `Segmentation` class in [`micro-espectre/src/segmentation.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/segmentation.py) owns the `use_cv_normalization` flag and propagates it throughout the processing pipeline. By default, this value is set to `False` because most ESP chips (ESP8266, ESP32-S2) maintain hardware gain lock.

```python

# micro-espectre/src/segmentation.py – constructor

self.use_cv_normalization = False   # default for gain-locked chips

# Later, based on NPZ metadata:

self.use_cv_normalization = True    # ESP32 with no gain lock

```

The system automatically sets this flag to `True` when the dataset's NPZ files contain `"gain_locked": false`, as validated in the test suite.

### Integration with Calibrators and Detectors

The NBVI calibrator ([`micro-espectre/src/nbvi_calibrator.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/nbvi_calibrator.py)) and MVS detector ([`micro-espectre/src/mvs_detector.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/mvs_detector.py)) both expose methods to control CV mode programmatically. The calibrator provides explicit setter methods:

```python

# micro-espectre/src/nbvi_calibrator.py

def set_cv_normalization(self, enable: bool):
    self.use_cv_normalization = enable

```

However, the ML-based detector ([`micro-espectre/src/ml_detector.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/ml_detector.py)) explicitly disables CV normalization at line 184 because the underlying model was trained exclusively on raw standard deviation values, forcing `use_cv_normalization = False` during inference regardless of hardware configuration.

## When to Enable CV Normalization

**Enable CV normalization** when processing data from ESP32 devices that lack hardware gain lock. The following conditions trigger automatic enabling:

- **ESP32 default behavior**: The chip cannot lock gain, causing raw amplitude variations to dominate turbulence calculations
- **NPZ metadata detection**: When `"gain_locked": false` appears in dataset metadata
- **Cross-environment stability**: Required when operating across different power levels where gain fluctuations would cause false motion detections

**Disable CV normalization** when:
- Using ESP8266 or ESP32-S2 chips with hardware gain lock
- Running the ML-based detector ([`ml_detector.py`](https://github.com/francescopace/espectre/blob/main/ml_detector.py)), which expects raw standard deviation inputs

## Code Examples

### Enabling CV Normalization for ESP32 Recordings

```python
from micro_espectre.src.segmentation import Segmentation

# Initialize segmentation

seg = Segmentation()

# Force CV mode for ESP32 (no hardware gain lock)

seg.use_cv_normalization = True

```

### Computing Turbulence Manually

```python
from micro_espectre.src.utils import calculate_spatial_turbulence

# Sample amplitude values from sub-carriers

magnitudes = [/* ... amplitude values ... */]
band = list(range(20, 40))  # Select sub-carrier subset

# Calculate CV-normalized turbulence

turb = calculate_spatial_turbulence(
    magnitudes, 
    band, 
    use_cv_normalization=True
)

print("CV-normalized turbulence:", turb)

```

## Summary

- CV normalization divides standard deviation by mean amplitude to create a **gain-invariant turbulence metric** essential for ESP32 devices
- The core implementation lives in [`micro-espectre/src/utils.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/utils.py) within the `calculate_spatial_turbulence` function
- The `Segmentation` class manages the `use_cv_normalization` flag, defaulting to `False` for gain-locked chips but enabling it automatically for ESP32 based on NPZ metadata
- The ML detector explicitly disables CV normalization because it was trained on raw standard deviation values
- Use CV normalization whenever hardware gain is unlocked to prevent false motion detections and maintain stable measurements across varying power levels

## Frequently Asked Questions

### Why is CV normalization necessary for ESP32 devices?

ESP32 chips lack hardware gain lock, meaning their automatic gain control (AGC) fluctuates continuously. Raw amplitude standard deviation scales with these gain changes, making it impossible to distinguish between actual motion-induced turbulence and simple power level variations. CV normalization eliminates this dependency by using a ratio that remains constant regardless of gain scaling, providing a stable metric for motion detection.

### How does ESPectre automatically detect when to use CV normalization?

ESPectre checks the metadata within NPZ dataset files for the `"gain_locked"` field. When this value is `false`, as determined during the calibration step in [`nbvi_calibrator.py`](https://github.com/francescopace/espectre/blob/main/nbvi_calibrator.py) and validated in [`test_validation_real_data.py`](https://github.com/francescopace/espectre/blob/main/test_validation_real_data.py), the system automatically sets `use_cv_normalization = True` in the `Segmentation` class. This ensures the correct turbulence metric is applied without manual configuration.

### Can I use CV normalization with the ML-based detector?

No. The ML detector ([`ml_detector.py`](https://github.com/francescopace/espectre/blob/main/ml_detector.py)) explicitly forces `use_cv_normalization = False` during inference because the underlying machine learning model was trained exclusively on raw standard deviation values. Enabling CV normalization would provide inputs outside the model's training distribution, leading to inaccurate predictions. Use CV normalization only with the NBVI or MVS detection methods.

### What is the mathematical formula for CV normalization in ESPectre?

ESPectre calculates CV normalization as `σ / μ`, where **σ** is the standard deviation of the selected sub-carrier amplitudes and **μ** is their arithmetic mean. This ratio remains invariant under linear scaling because both numerator and denominator are multiplied by the same gain factor, effectively canceling out gain-induced variations while preserving motion-induced turbulence signatures.