# When to Enable CV Normalization for Gain-Invariant Processing in ESPectre

> Enable CV normalization for gain-invariant processing in ESPectre when CSI amplitudes vary due to hardware AGC or FFT gain. Disable it for fixed gain locks to maximize turbulence sensitivity.

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

---

**Enable CV normalization when CSI amplitudes are not gain-locked—such as when hardware AGC/FFT gain varies between packets—and disable it when the device maintains a fixed gain lock to maximize turbulence sensitivity.**

In the ESPectre Wi-Fi sensing framework, **CV normalization** (coefficient-of-variation) ensures turbulence metrics remain valid when hardware gain fluctuates. Knowing when to enable CV normalization for gain-invariant processing is critical for accurate channel state information (CSI) analysis, as it determines whether the system uses `std/mean` or raw standard deviation for spatial turbulence calculations. The decision relies on the hardware's ability to lock AGC/FFT gain, with automatic detection implemented in the `run_gain_lock` routine.

## Gain-Lock Detection Logic

The decision to enable CV normalization starts in [`micro-espectre/src/main.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/main.py) within the `run_gain_lock` function. This routine returns `needs_cv_normalization=True` under three specific conditions: when the platform lacks hardware gain-lock support, when `config.GAIN_LOCK_MODE` is explicitly `"disabled"`, or when the mode is `"auto"` and the measured median AGC is lower than `config.GAIN_LOCK_MIN_SAFE_AGC` (indicating a signal too strong for reliable locking). In all other scenarios—specifically when gain-lock succeeds—the function returns `False`, signaling that raw standard deviation provides maximum sensitivity.

## Propagation to the Detector and Calibrator

Once determined, the boolean flag flows through the system architecture. The `detector.set_cv_normalization` method in [`micro-espectre/src/mvs_detector.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/mvs_detector.py) receives this value and stores it in `self._context.use_cv_normalization`. All subsequent turbulence calculations reference this context flag. Similarly, the `NBVICalibrator` class in [`micro-espectre/src/nbvi_calibrator.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/nbvi_calibrator.py) mirrors this setting to ensure calibration statistics use the identical normalization mode as the detector, maintaining consistency across the processing pipeline.

## Turbulence Calculation Implementation

The actual mathematical switch occurs in [`micro-espectre/src/segmentation.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/segmentation.py) and [`micro-espectre/src/utils.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/utils.py). The `SegmentationContext.compute_spatial_turbulence` method and the standalone `calculate_spatial_turbulence` function accept a `use_cv_normalization` parameter. When this flag is `True`, the functions return `std / mean`, which cancels multiplicative gain factors. When `False`, they return plain `std`, preserving amplitude sensitivity for gain-locked scenarios. The default value is `True`, but the detector overwrites this based on the gain-lock outcome.

## Practical Code Examples

### Forcing CV Normalization Manually

When you know the hardware gain is unlocked, explicitly enable normalization:

```python
from src.mvs_detector import MVSDetector

detector = MVSDetector()

# Force gain-invariant mode when gain is not locked

detector.set_cv_normalization(True)

# Compute turbulence on a CSI packet

turbulence = detector.calculate_spatial_turbulence(csi_packet)
print("CV-normalised turbulence:", turbulence)

```

### Automatic Configuration via Gain-Lock

Let the system decide based on hardware conditions:

```python
from src.main import run_gain_lock, MainLoopState
from src.detectord_interface import DetectorInterface

# Assume wlan is a connected Wi-Fi object with CSI enabled

agc, fft, needs_cv = run_gain_lock(wlan)

# Store flag for the processing loop

state = MainLoopState()
state.needs_cv_normalization = needs_cv

detector = DetectorInterface()
detector.set_cv_normalization(needs_cv)

```

### Querying the Current Mode

Verify the active normalization strategy at runtime:

```python
print("CV normalisation active?", detector.use_cv_normalization)

# Output: True (gain not locked) or False (gain locked)

```

## Summary

- **Enable CV normalization** when hardware gain is not locked (varying AGC/FFT) to ensure gain-invariant turbulence metrics via `std/mean` calculation.
- **Disable CV normalization** when gain-lock is active to maximize sensitivity using raw standard deviation.
- The `run_gain_lock` function in [`micro-espectre/src/main.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/main.py) automates this decision based on hardware capabilities, `config.GAIN_LOCK_MODE`, and signal strength thresholds.
- The flag propagates through `detector.set_cv_normalization` in [`micro-espectre/src/mvs_detector.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/mvs_detector.py) and aligns with `NBVICalibrator` to maintain consistent processing across the ESPectre pipeline.

## Frequently Asked Questions

### When should I manually enable CV normalization in ESPectre?

Manually enable CV normalization when working with hardware that does not support automatic gain control (AGC) locking, or when analyzing CSI data where gain values vary between packets. This ensures the turbulence metric remains invariant to multiplicative gain changes by computing the coefficient of variation (`std/mean`) rather than raw standard deviation.

### What happens if I leave CV normalization enabled during gain-lock?

If CV normalization remains enabled while the hardware maintains a fixed gain lock, the system calculates `std/mean` instead of using raw standard deviation. This reduces sensitivity to small amplitude fluctuations because the division by mean introduces unnecessary normalization when the gain factor is already constant, potentially masking subtle environmental changes in the CSI data.

### How does the NBVI calibrator use the CV normalization setting?

The `NBVICalibrator` class in [`micro-espectre/src/nbvi_calibrator.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/nbvi_calibrator.py) reads the same `use_cv_normalization` flag set on the detector to ensure calibration statistics are computed with identical normalization. This alignment guarantees that calibration values and real-time turbulence measurements use the same mathematical basis, preventing mismatches between training and inference data.

### Where is the turbulence calculation actually performed?

The core turbulence calculation resides in [`micro-espectre/src/segmentation.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/segmentation.py) within the `SegmentationContext.compute_spatial_turbulence` method, with a helper implementation in [`micro-espectre/src/utils.py`](https://github.com/francescopace/espectre/blob/main/micro-espectre/src/utils.py) called `calculate_spatial_turbulence`. Both locations check the `use_cv_normalization` boolean to decide whether to return the coefficient of variation or the raw standard deviation of the CSI amplitudes.