Skip to main content
country_code
Ctrl+K
Aerial CUDA-Accelerated RAN - Home Aerial CUDA-Accelerated RAN - Home

Aerial CUDA-Accelerated RAN

  • pdf
  • github
  • www
Aerial CUDA-Accelerated RAN - Home Aerial CUDA-Accelerated RAN - Home

Aerial CUDA-Accelerated RAN

  • pdf
  • github
  • www

Table of Contents

  • Overview
  • Features and Capabilities
    • cuPHY Features
    • cuMAC Features
  • Release Notes
    • Latest Release (26-2)
    • Previous Releases
    • Limitations
  • Installation Guide
    • Software Manifest
    • Installing Tools on MGX ARC Pro System
    • Installing Tools on Grace Hopper MGX System
    • Installing Tools on NVIDIA DGX Spark System
    • Installing Tools on Dell R750
    • Installing and Upgrading cuBB
    • Aerial System Scripts
    • Aerial CUDA-Accelerated RAN Versioning in YAML Files
    • Troubleshooting
  • Quickstart Guide
    • Quickstart Overview
    • Generating TV and Launch Pattern Files
    • Running Aerial cuPHY
    • Running cuBB End-to-End
    • Running cuBB Performance tests
    • E2E gNodeB on MIG
  • cuBB
    • cuBB Integration Guide
      • NVIPC
        • NVIPC Overview
        • NVIPC Integration
      • FAPI Support
        • Standard FAPI Support
        • Vendor-Specific Extensions
        • FAPI Message Formats and Specifications
      • Run-time Configuration and Status
      • 64 TR MU-MIMO Support with Static and Dynamic Beamforming
    • cuPHY Developer Guide
      • cuPHY Software Architecture
      • cuPHY Components
      • 5G MATLAB Models for Testing and Validation
      • AI/ML Components for PUSCH Channel Estimation
      • References
    • cuMAC
      • Overview
      • cuMAC Developer Guide
      • cuMAC-CP integration guide
        • cuMAC API Reference
        • cuMAC-CP API Procedures
        • cuMAC-CP API Messages
        • L2 integration notes
  • Data Lake
  • dApp Framework
  • Tools
    • TestMAC
    • RU Emulator
    • pyAerial
      • Overview
      • Getting Started with pyAerial
      • Examples of Using pyAerial
        • Using pyAerial to run a PUSCH link simulation
        • Using pyAerial for LDPC encoding-decoding chain
        • Using pyAerial to run 5G sounding reference signal transmission and reception
        • Using pyAerial to run CSI-RS transmission and reception
        • Using pyAerial for data generation by simulation
        • LLRNet: Dataset generation
        • LLRNet: Model training and testing
        • Using pyAerial to evaluate a PUSCH neural receiver
        • Channel Estimation for Uplink Shared Channel (PUSCH) in pyAerial
        • Using pyAerial for channel estimation on Aerial Data Lake data
        • Using pyAerial for PUSCH decoding on Aerial Data Lake data
        • Using pyAerial for decoding PUSCH transmissions from multiple cells using Aerial Data Lake data
        • Per-UE Uplink DMRS Channel Estimate Extraction from Aerial Data Lake
        • Per-UE SRS Metrics, Channel Estimates & IQ from Aerial Data Lake
      • API Reference
        • Physical layer for 5G
        • Utilities
    • RANPerf Testbench
  • OAM
    • Configuration
    • Operation
    • Logging
    • Metrics
  • Glossary
  • Acknowledgements
  • Tools
  • pyAerial
  • Examples of Using pyAerial
  • Per-UE Uplink DMRS Channel Estimate Extraction from Aerial Data Lake
Is this page helpful?

Per-UE Uplink DMRS Channel Estimate Extraction from Aerial Data Lake#

This notebook demonstrates how to extract and visualize pre-computed channel estimates (H-matrices) on a per-UE basis from the Aerial Data Lake.

Unlike the datalake_channel_estimation notebook which re-derives H-estimates from raw IQ using pyAerial/cuPHY, this notebook reads H-estimates that cuPHY already computed during live operation and stored in ClickHouse. No GPU or pyAerial is required.

The hest table stores one row per cell per slot, holding that cell’s UE groups concatenated into a single blob. The fapi table stores one row per UE per slot with the metadata needed to locate each UE inside its cell’s blob: hOffset, hSize, layerOffset, nSubcarriers, nDmrsEstimates, nBsAnts and nrOfLayers. Both tables carry CellId, so joins and per-cell UE counts must include it. Note that nUEs is the slot total across all cells, not a per-cell count.

H-estimate memory layout: [n_dmrs_estimates, n_subcarriers, n_bs_ants, n_layers_group] (row-major, complex64), with hOffset relative to the start of the cell’s blob.

Prerequisites: In cuphycontroller YAML config, enable Data Lake with at least channel estimate and pusch collections:

data_config:
  datalake_db_write_enable: 1
  datalake_data_types: [fh, pusch, hest]

The pusch type populates the fapi table (per-UE metadata), and hest populates the hest table (raw H-estimate blobs). Both are required.

Configuration, Imports, and Setup#

[1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import clickhouse_connect

plt.rcParams['figure.figsize'] = [12, 4]

# --- Configuration ---
CLICKHOUSE_HOST = 'localhost'

client = clickhouse_connect.get_client(host=CLICKHOUSE_HOST)

Per-UE H-estimate Extraction#

The get_ue_hest function extracts a single UE’s channel matrix from the concatenated H-estimate blob.

For FDM (each UE in its own group), the UE’s data is a contiguous block at hOffset with hSize elements. For MU-MIMO (multiple UEs sharing a group), layerOffset selects the UE’s layers within the group matrix.

[2]:
def get_ue_hest(hest_blob, ue_row):
    """Extract per-UE H-estimate from the concatenated per-cell blob.

    Args:
        hest_blob: Flat float32 array from the hest row of this cell and slot
            (interleaved real/imag).
        ue_row: Row from fapi table with per-UE metadata.

    Returns:
        Complex numpy array of shape (n_dmrs, n_subcarriers, n_bs_ants, n_layers_ue).
    """
    h_offset = int(ue_row['hOffset'])
    h_size = int(ue_row['hSize'])
    n_sc = int(ue_row['nSubcarriers'])
    n_dmrs = int(ue_row['nDmrsEstimates'])
    n_bs_ants = int(ue_row['nBsAnts'])
    n_layers_ue = int(ue_row['nrOfLayers'])
    layer_offset = int(ue_row['layerOffset'])

    # h_size is in float2 (complex) elements; total group layers derived from dimensions
    n_layers_group = h_size // (n_dmrs * n_sc * n_bs_ants)

    # h_offset/h_size are in float2 units; multiply by 2 for float32 array indexing
    grp_ri = hest_blob[h_offset * 2 : (h_offset + h_size) * 2]
    grp = grp_ri.reshape(-1, 2).astype(np.float32).view(np.complex64).squeeze()
    grp = grp.reshape(n_dmrs, n_sc, n_bs_ants, n_layers_group)

    # layer_offset selects this UE's layers within the group (0 for FDM, >0 for MU-MIMO)
    return grp[:, :, :, layer_offset : layer_offset + n_layers_ue]

Single-UE Channel Estimates#

Query a single-UE slot and plot the channel magnitude across subcarriers for each antenna and DMRS symbol.

[3]:
# Pick a cell and slot carrying exactly one UE, then join fapi and hest server-side.
# Both the join and the UE count are keyed on CellId: hest holds one row per cell
# per slot, and fapi.nUEs is the slot total across all cells. The count covers
# every scheduled UE, so filtering happens in HAVING rather than WHERE; filtering
# first would let a two-UE slot with one failed transport block look single-UE.
row_1ue = client.query_df("""
    WITH one_ue AS (
        SELECT CellId, SFN, Slot, TsTaiNs FROM fapi
        GROUP BY CellId, SFN, Slot, TsTaiNs
        HAVING count() = 1 AND countIf(tbCrcFail = 0 AND hSize > 0) = 1
        ORDER BY TsTaiNs
        LIMIT 1
    )
    SELECT f.CellId, f.SFN, f.Slot, f.rnti, f.nrOfLayers, f.nBsAnts,
           f.rbStart, f.rbSize, f.nSubcarriers, f.nDmrsEstimates, f.dmrsSymbPos,
           f.layerOffset, f.ueGrpIdx, f.hOffset, f.hSize, f.sinr, f.rsrp,
           h.hestData
    FROM fapi AS f
    INNER JOIN hest AS h USING (CellId, SFN, Slot, TsTaiNs)
    WHERE (f.CellId, f.SFN, f.Slot, f.TsTaiNs) IN (SELECT CellId, SFN, Slot, TsTaiNs FROM one_ue)
""")

if row_1ue.empty:
    raise RuntimeError("No single-UE slot with paired hest blob found. Check that data has been collected.")

ue = row_1ue.iloc[0]
print(f"Cell: {ue['CellId']}  SFN/Slot: {ue['SFN']}/{ue['Slot']}  RNTI: {ue['rnti']}  "
      f"RBs: {ue['rbStart']}-{ue['rbStart']+ue['rbSize']-1}  "
      f"Layers: {ue['nrOfLayers']}  Group: {ue['ueGrpIdx']}")
print(f"hOffset: {ue['hOffset']}  hSize: {ue['hSize']}  "
      f"nSubcarriers: {ue['nSubcarriers']}  nDMRS: {ue['nDmrsEstimates']}  "
      f"SINR: {ue['sinr']:.1f} dB  RSRP: {ue['rsrp']:.1f} dB")

hest_blob = np.array(ue['hestData'], dtype=np.float32)
Cell: 51  SFN/Slot: 504/7  RNTI: 54873  RBs: 0-90  Layers: 1  Group: 0
hOffset: 0  hSize: 13104  nSubcarriers: 1092  nDMRS: 3  SINR: 27.0 dB  RSRP: 5.1 dB
[4]:
h_ue = get_ue_hest(hest_blob, ue)
n_dmrs, _, n_ants, _ = h_ue.shape

fig, axes = plt.subplots(1, n_dmrs, figsize=(5 * n_dmrs, 4), squeeze=False)
fig.suptitle(f"Channel Magnitude  |  Cell {ue['CellId']}  |  RNTI {ue['rnti']}  |  SFN.Slot {ue['SFN']}.{ue['Slot']}")

for d in range(n_dmrs):
    ax = axes[0, d]
    for ant in range(n_ants):
        ax.plot(np.abs(h_ue[d, :, ant, 0]), label=f'Ant {ant}')
    ax.set_title(f'DMRS {d}')
    ax.set_xlabel('Subcarrier')
    ax.set_ylabel('|H|')
    ax.legend(fontsize=8)
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
../../_images/content_notebooks_datalake_hest_per_ue_7_0.png

Multi-UE Channel Estimates#

When multiple UEs are scheduled on the same cell in the same slot, cuPHY groups them by frequency allocation (FDM) or spatial multiplexing (MU-MIMO). The per-UE metadata in fapi provides everything needed to extract each UE’s channel independently, regardless of the number of UEs.

  • FDM: Each UE has a distinct hOffset/hSize (different frequency allocations), layerOffset = 0.

  • MU-MIMO: UEs in the same group share the same hOffset/hSize but differ in layerOffset.

[5]:
# Find the first cell and slot carrying two or more UEs and pull its fapi rows
# joined with that cell's hest blob. All returned rows share the same hestData.
# Every scheduled UE must have a channel estimate, since all of them get plotted.
fapi_multi = client.query_df("""
    WITH multi_ue AS (
        SELECT CellId, SFN, Slot, TsTaiNs FROM fapi
        GROUP BY CellId, SFN, Slot, TsTaiNs
        HAVING count() >= 2 AND countIf(hSize > 0) = count()
        ORDER BY TsTaiNs
        LIMIT 1
    )
    SELECT f.CellId, f.SFN, f.Slot, f.rnti, f.nrOfLayers, f.nBsAnts,
           f.rbStart, f.rbSize, f.nSubcarriers, f.nDmrsEstimates, f.dmrsSymbPos,
           f.layerOffset, f.ueGrpIdx, f.hOffset, f.hSize, f.sinr, f.rsrp,
           h.hestData
    FROM fapi AS f
    INNER JOIN hest AS h USING (CellId, SFN, Slot, TsTaiNs)
    WHERE (f.CellId, f.SFN, f.Slot, f.TsTaiNs) IN (SELECT CellId, SFN, Slot, TsTaiNs FROM multi_ue)
    ORDER BY f.rnti
""")

if fapi_multi.empty:
    print("No multi-UE slots with paired hest blob found.")
    hest_blob_m = None
else:
    ref = fapi_multi.iloc[0]
    print(f"Cell: {ref['CellId']}  SFN/Slot: {ref['SFN']}/{ref['Slot']}  --  {len(fapi_multi)} UEs")
    for i, ue_r in fapi_multi.iterrows():
        print(f"  UE {i}: RNTI {ue_r['rnti']}  Group {ue_r['ueGrpIdx']}  "
              f"RBs {ue_r['rbStart']}-{ue_r['rbStart']+ue_r['rbSize']-1}  "
              f"hOffset={ue_r['hOffset']}  hSize={ue_r['hSize']}  "
              f"SINR={ue_r['sinr']:.1f} dB")

    hest_blob_m = np.array(fapi_multi.iloc[0]['hestData'], dtype=np.float32)
Cell: 51  SFN/Slot: 598/9  --  2 UEs
  UE 0: RNTI 33163  Group 1  RBs 223-272  hOffset=32112  hSize=14400  SINR=30.4 dB
  UE 1: RNTI 54873  Group 0  RBs 0-222  hOffset=0  hSize=32112  SINR=30.2 dB
[6]:
if len(fapi_multi) >= 2:
    n_ues = len(fapi_multi)
    fig, axes = plt.subplots(1, n_ues, figsize=(6 * n_ues, 4), squeeze=False)
    sfn_slot = f"{ref['SFN']}.{ref['Slot']}"
    fig.suptitle(f"Multi-UE Channel Magnitude  |  Cell {ref['CellId']}  |  SFN.Slot {sfn_slot}  |  {n_ues} UEs")

    for idx in range(n_ues):
        ue_r = fapi_multi.iloc[idx]
        h = get_ue_hest(hest_blob_m, ue_r)
        ax = axes[0, idx]
        for ant in range(h.shape[2]):
            ax.plot(np.abs(h[0, :, ant, 0]), label=f'Ant {ant}')
        ax.set_title(f'RNTI {ue_r["rnti"]}  |  Group {ue_r["ueGrpIdx"]}  |  '
                     f'RBs {ue_r["rbStart"]}-{ue_r["rbStart"]+ue_r["rbSize"]-1}')
        ax.set_xlabel('Subcarrier')
        ax.set_ylabel('|H|')
        ax.legend(fontsize=8)
        ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()
../../_images/content_notebooks_datalake_hest_per_ue_10_0.png

Channel Heatmap#

Channel magnitude heatmap (antenna vs subcarrier) for each DMRS symbol.

[7]:
# Use the single-UE data from above
h_ue = get_ue_hest(hest_blob, ue)
n_dmrs, _, n_ants, _ = h_ue.shape

fig, axes = plt.subplots(1, n_dmrs, figsize=(5 * n_dmrs, 3), squeeze=False)
fig.suptitle(f"Channel Heatmap |H|  |  Cell {ue['CellId']}  |  RNTI {ue['rnti']}")

for d in range(n_dmrs):
    ax = axes[0, d]
    # Shape: (n_subcarriers, n_bs_ants) for layer 0
    img = ax.imshow(np.abs(h_ue[d, :, :, 0]).T, aspect='auto',
                    interpolation='nearest', cmap='viridis')
    ax.set_title(f'DMRS {d}')
    ax.set_xlabel('Subcarrier')
    ax.set_ylabel('Antenna')
    ax.set_yticks(range(n_ants))
    plt.colorbar(img, ax=ax, shrink=0.8)

plt.tight_layout()
plt.show()
../../_images/content_notebooks_datalake_hest_per_ue_12_0.png

Channel Phase#

Channel phase across subcarriers per antenna.

[8]:
h_ue = get_ue_hest(hest_blob, ue)

fig, ax = plt.subplots(1, 1, figsize=(12, 4))
fig.suptitle(f"Channel Phase (DMRS 0)  |  Cell {ue['CellId']}  |  RNTI {ue['rnti']}")

for ant in range(h_ue.shape[2]):
    ax.plot(np.angle(h_ue[0, :, ant, 0]), '.', markersize=2, label=f'Ant {ant}')

ax.set_xlabel('Subcarrier')
ax.set_ylabel('Phase (rad)')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
../../_images/content_notebooks_datalake_hest_per_ue_14_0.png

previous

Using pyAerial for decoding PUSCH transmissions from multiple cells using Aerial Data Lake data

next

Per-UE SRS Metrics, Channel Estimates & IQ from Aerial Data Lake

On this page
  • Configuration, Imports, and Setup
  • Per-UE H-estimate Extraction
  • Single-UE Channel Estimates
  • Multi-UE Channel Estimates
  • Channel Heatmap
  • Channel Phase
NVIDIA NVIDIA
Privacy Policy | Your Privacy Choices | Terms of Service | Accessibility | Corporate Policies | Product Security | Contact

Copyright © 2026, NVIDIA Corporation.

Last updated on Sep 16, 2026.