Source code for demos.mimo_ofdm_neural_receiver.src.csi

# SPDX-License-Identifier: MIT
# Copyright (c) 2025–present Srikanth Pagadarai

"""
Generate Channel State Information (CSI).

Provides the ``CSI`` class which encapsulates all channel-related
setup and generation for a MIMO-OFDM simulation. It handles:

1. Antenna array configuration (UT and BS arrays with dual polarization)
2. 3GPP CDL channel model instantiation
3. Channel impulse response (CIR) generation
4. Conversion from CIR to frequency-domain channel response

The CSI object is shared across Tx, Channel, and Rx components to ensure
they all operate on the same channel realization within a simulation iteration.
This is critical for fair comparison between perfect-CSI and estimated-CSI
receiver paths.
"""

from __future__ import annotations

import os

# Suppress TensorFlow logging before import to avoid cluttering output
# Level 0 = all messages; used here for debugging, change to 2+ for production
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "0"

import sionna.phy  # noqa: E402
import tensorflow as tf  # noqa: E402
from sionna.phy.channel.tr38901 import CDL, AntennaArray  # noqa: E402
from sionna.phy.ofdm import RemoveNulledSubcarriers  # noqa: E402
from sionna.phy.channel import subcarrier_frequencies, cir_to_ofdm_channel  # noqa: E402
from .config import Config  # noqa: E402


[docs] class CSI: """ Channel State Information generator for MIMO-OFDM simulations. This class manages the complete channel generation pipeline from antenna array configuration through to frequency-domain channel coefficients. A single ``CSI`` instance should be shared across all pipeline components (Tx, Channel, Rx) to ensure consistent channel realizations. The channel generation follows the 3GPP TR 38.901 CDL model: 1. **Antenna Arrays**: Configured with dual cross-polarized elements following the 38.901 antenna pattern specification. 2. **CDL Channel**: Generates delay-domain channel impulse response with configurable model (A-E), delay spread, and Doppler. 3. **Frequency Response**: CIR is converted to frequency-domain via ``cir_to_ofdm_channel`` for OFDM processing. Parameters ---------- cfg : ~demos.mimo_ofdm_neural_receiver.src.config.Config Configuration object containing PHY parameters (carrier frequency, antenna counts, CDL model selection, etc.). Attributes ---------- cfg : ~demos.mimo_ofdm_neural_receiver.src.config.Config Reference to the configuration object. remove_nulled_scs : RemoveNulledSubcarriers Utility layer for extracting channel coefficients on active (non-nulled) subcarriers. Used by Rx for perfect-CSI path. Note ---- The ``build()`` method must be called once per simulation batch to generate a new channel realization. The returned ``h_freq`` tensor should be passed to both the Channel and Rx components. Example ------- >>> cfg = Config(cdl_model="C", carrier_frequency=3.5e9) >>> csi = CSI(cfg) >>> h_freq = csi.build(batch_size=32) >>> # Use h_freq with Channel and Rx """ def __init__(self, cfg: Config): """ Initialize CSI generator with antenna arrays and CDL channel model. Parameters ---------- cfg : ~demos.mimo_ofdm_neural_receiver.src.config.Config Configuration object specifying PHY-layer parameters. Post-conditions --------------- - Sionna's global RNG seed is set to ``cfg.seed`` for reproducibility. - UT and BS antenna arrays are configured with dual cross-polarization. - CDL channel model is instantiated with configured parameters. - Subcarrier frequencies are pre-computed for CIR-to-CFR conversion. - ``remove_nulled_scs`` is ready for perfect-CSI extraction. """ self.cfg = cfg # Set global seed for deterministic channel realizations across runs # This affects CDL's internal random path gains and delays sionna.phy.config.seed = int(self.cfg._seed) # ===================================================================== # Antenna Array Configuration # Using dual cross-polarized arrays per 3GPP 38.901 specification. # num_cols = num_ant/2 because dual polarization doubles effective elements. # Cross-polarization provides diversity gain in fading channels. # ===================================================================== self._ut_array = AntennaArray( num_rows=1, num_cols=int(self.cfg.num_ut_ant / 2), polarization="dual", polarization_type="cross", antenna_pattern="38.901", carrier_frequency=self.cfg.carrier_frequency, ) self._bs_array = AntennaArray( num_rows=1, num_cols=int(self.cfg.num_bs_ant / 2), polarization="dual", polarization_type="cross", antenna_pattern="38.901", carrier_frequency=self.cfg.carrier_frequency, ) # ===================================================================== # CDL Channel Model # Models A-C: NLOS with increasing delay spread # Models D-E: LOS with Rician fading # min_speed sets the minimum UE velocity for Doppler calculation # ===================================================================== self._cdl = CDL( model=self.cfg.cdl_model, delay_spread=self.cfg.delay_spread, carrier_frequency=self.cfg.carrier_frequency, ut_array=self._ut_array, bs_array=self._bs_array, direction=self.cfg.direction, min_speed=self.cfg.speed, ) # ===================================================================== # Subcarrier Frequencies # Pre-computed frequency grid for converting CIR to frequency response. # These frequencies are relative to the carrier (baseband equivalent). # ===================================================================== self._frequencies = subcarrier_frequencies( self.cfg.rg.fft_size, self.cfg.rg.subcarrier_spacing ) # ===================================================================== # Perfect CSI Extraction Utility # Removes guard bands and DC null from full channel response to get # coefficients only on data/pilot subcarriers for Rx processing. # ===================================================================== self.remove_nulled_scs = RemoveNulledSubcarriers(self.cfg.rg)
[docs] def build(self, batch_size: int | tf.Tensor): """ Generate frequency-domain channel response for a batch of samples. This method generates new channel impulse responses from the CDL model and converts them to frequency-domain coefficients. Each call produces an independent channel realization (different random path gains). Parameters ---------- batch_size : int or tf.Tensor Number of independent channel realizations to generate. Can be a Python int or a scalar TensorFlow tensor. Returns ------- h_freq : tf.Tensor, complex64 Frequency-domain channel response with shape [batch, num_rx, num_rx_ant, num_tx, num_tx_ant, num_ofdm_symbols, fft_size]. Each element ``h_freq[b,r,ra,t,ta,s,f]`` is the complex channel gain from TX antenna ``ta`` of transmitter ``t`` to RX antenna ``ra`` of receiver ``r``, on OFDM symbol ``s`` and subcarrier ``f``, for batch sample ``b``. Note ---- - The CDL model internally uses Sionna's global RNG seeded in ``__init__``. For reproducible results across runs, ensure ``cfg.seed`` is fixed and no other code modifies ``sionna.phy.config.seed`` between calls. - Channel varies across OFDM symbols if ``cfg.speed > 0`` (Doppler). - For ``cfg.speed == 0``, channel is static within each batch sample but varies across batch samples. """ # Handle both Python int and TensorFlow tensor batch_size inputs # This flexibility allows use in both eager and graph execution modes if isinstance(batch_size, tf.Tensor): bs = tf.cast(batch_size, tf.int32) else: bs = int(batch_size) # Generate channel impulse response (CIR) from CDL model # a: path coefficients [batch, num_rx, num_rx_ant, num_tx, num_tx_ant, num_paths, num_time_steps] # tau: path delays [batch, num_rx, num_tx, num_paths] a, tau = self._cdl( batch_size=bs, num_time_steps=self.cfg.rg.num_ofdm_symbols, sampling_frequency=1.0 / self.cfg.rg.ofdm_symbol_duration, ) # Convert CIR to frequency-domain channel response # normalize=True ensures unit average energy for fair SNR comparison h_freq = cir_to_ofdm_channel(self._frequencies, a, tau, normalize=True) return h_freq