""" Sionna NR Resource Grid Generator - SSB (Synchronization Signal Block) - 20 MHz Bandwidth (configurable) - 15 kHz Subcarrier Spacing (for SSB Case A/B) or 30 kHz (for Case C/D/E) - SSB occupies 20 RBs (240 subcarriers) x 4 OFDM symbols - Case A: 3-6 GHz, max 8 SSB bursts in 5ms - Shows 5ms (half frame) of resource grid 3GPP TS 38.211 - SSB Structure: - Symbol 0: PSS (127 subcarriers, centered) - Symbol 1: PBCH + PBCH DMRS - Symbol 2: PBCH + SSS (127 subcarriers, centered) + PBCH DMRS - Symbol 3: PBCH + PBCH DMRS """ import numpy as np import tensorflow as tf import struct import os import matplotlib matplotlib.use('TkAgg') # Use TkAgg backend for interactive display import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from matplotlib.figure import Figure from matplotlib.patches import Rectangle, Patch from matplotlib.colors import ListedColormap import tkinter as tk from tkinter import ttk from tkinter import messagebox from tkinter import filedialog # ============================================================================= # Debug Flag # ============================================================================= DEBUG = True # Set to False to disable debug prints # ============================================================================= # SSB Configuration Parameters (Global Variables for Future Extension) # ============================================================================= # SSB Case Configuration (3GPP TS 38.213 Section 4.1) # Case A: 15 kHz SCS, FR1 (< 6 GHz) # Case B: 30 kHz SCS, FR1 (< 6 GHz) # Case C: 30 kHz SCS, FR1 (< 6 GHz) - paired spectrum # Case D: 120 kHz SCS, FR2 (> 6 GHz) # Case E: 240 kHz SCS, FR2 (> 6 GHz) SSB_CASE = 'B' # SSB case ('A', 'B', 'C', 'D', 'E') # L_max Configuration (3GPP TS 38.213 Section 4.1) # Maximum number of SS/PBCH blocks in a half frame (5ms) # FR1 (< 3 GHz): L_max = 4 # FR1 (3-6 GHz): L_max = 8 # FR2 (> 6 GHz): L_max = 64 L_MAX = 8 # Maximum SSB bursts per half frame # SSB transmission bitmap (length up to L_MAX), '1' = transmit, '0' = skip # Example for Case A (L_MAX=4): '1111' transmits all 4 bursts # Example for Case D (L_MAX=64): '11110000...' length 64 #SSB_TX_BITMAP = '1010' #SSB_TX_BITMAP = '10101011' SSB_TX_BITMAP = '11111111' #SSB_TX_BITMAP = '1010101110101011101010111010101110101011101010111010101110101011' # SSB Case Parameters (3GPP TS 38.213 Table 4.1-1) # Extended to include default L_max, SSB SCS, and bitmap for each case # Frequency range variations: # Case A: f <= 3 GHz (L_max=4), 3 GHz < f <= 6 GHz (L_max=8) # Case B: f <= 3 GHz (L_max=4), 3 GHz < f <= 6 GHz (L_max=8) # Case C: f <= 3 GHz (L_max=4 for some, L_max=8 for others), 3 GHz < f <= 6 GHz (L_max=8) # Case D: f > 6 GHz (L_max=64, specific n values) # Case E: f > 6 GHz (L_max=64, specific n values) SSB_CASE_PARAMS = { 'A': { 'scs_khz': 15, 'first_symbols': [2, 8], 'slot_pattern_period': 14, 'l_max': 8, # Default: FR1 (3-6 GHz): L_max = 8; f <= 3 GHz uses L_max = 4 'default_bitmap': '11111111', # All 8 SSBs 'default_bw_mhz': 20, 'frequency_ranges': { 'f_le_3ghz': {'l_max': 4, 'n_values': [0, 1] ,'default_bitmap': '1111', 'default_pdsch_scs_khz': 15}, # f <= 3 GHz 'f_3_to_6ghz': {'l_max': 8, 'n_values': [0, 1, 2, 3] ,'default_bitmap': '11111111', 'default_pdsch_scs_khz': 30 } # 3 GHz < f <= 6 GHz } }, 'B': { 'scs_khz': 30, 'first_symbols': [4, 8, 16, 20], 'slot_pattern_period': 28, 'l_max': 8, # Default: FR1 (3-6 GHz): L_max = 8; f <= 3 GHz uses L_max = 4 'default_bitmap': '11111111', # All 8 SSBs 'default_bw_mhz': 20, 'frequency_ranges': { 'f_le_3ghz': {'l_max': 4, 'n_values': [0] ,'default_bitmap': '1111', 'default_pdsch_scs_khz': 30 }, # f <= 3 GHz 'f_3_to_6ghz': {'l_max': 8, 'n_values': [0, 1] ,'default_bitmap': '11111111', 'default_pdsch_scs_khz': 30} # 3 GHz < f <= 6 GHz } }, 'C': { 'scs_khz': 30, 'first_symbols': [2, 8], 'slot_pattern_period': 14, 'l_max': 8, # Default: FR1 (3-6 GHz): L_max = 8; f <= 3 GHz varies by sub-band 'default_bitmap': '11111111', # All 8 SSBs 'default_bw_mhz': 20, 'frequency_ranges': { 'f_le_3ghz_tdd': {'l_max': 4, 'n_values': [0, 1] ,'default_bitmap': '1111', 'default_pdsch_scs_khz': 30 }, # TDD f < 3 GHz 'f_le_3ghz_188': {'l_max': 4, 'n_values': [0, 1] ,'default_bitmap': '1111', 'default_pdsch_scs_khz': 30}, # TDD f < 1.88 GHz 'f_le_3ghz_188_3': {'l_max': 8, 'n_values': [0, 1, 2, 3] ,'default_bitmap': '11111111', 'default_pdsch_scs_khz': 30}, # TDD 1.88 < f < 3.0 GHz 'f_3_to_6ghz': {'l_max': 8, 'n_values': [0, 1, 2, 3] ,'default_bitmap': '11111111', 'default_pdsch_scs_khz': 30} # 3 GHz < f <= 6 GHz (FDD/TDD) } }, 'D': { 'scs_khz': 120, 'first_symbols': [4, 8, 16, 20], 'slot_pattern_period': 28, 'l_max': 64, # FR2 (> 6 GHz): L_max = 64 'default_bitmap': '1' * 64, # All 64 SSBs 'default_bw_mhz': 50, 'default_pdsch_scs_khz': 120, # Case D uses specific n values (not consecutive): 0,1,2,3,5,6,7,8,10,11,12,13,15,16,17,18 'n_values': [0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 18] }, 'E': { 'scs_khz': 240, 'first_symbols': [8, 12, 16, 20, 32, 36, 40, 44], 'slot_pattern_period': 56, 'l_max': 64, # FR2 (> 6 GHz): L_max = 64 'default_bitmap': '1' * 64, # All 64 SSBs 'default_bw_mhz': 100, 'default_pdsch_scs_khz': 240, # Case E uses specific n values (not consecutive): 0,1,2,3,5,6,7,8 'n_values': [0, 1, 2, 3, 5, 6, 7, 8] }, } # Get SSB parameters based on case SSB_SCS_KHZ = SSB_CASE_PARAMS[SSB_CASE]['scs_khz'] SSB_FIRST_SYMBOLS = SSB_CASE_PARAMS[SSB_CASE]['first_symbols'] SSB_SUBCARRIER_SPACING = SSB_SCS_KHZ * 1e3 # Convert to Hz # ============================================================================= # Channel Bandwidth Configuration # ============================================================================= CHANNEL_BW_MHZ = 20 # Channel bandwidth in MHz PDSCH_SCS_KHZ = 30 # PDSCH subcarrier spacing in kHz (grid SCS) SUBCARRIER_SPACING = SSB_SUBCARRIER_SPACING # Use SSB SCS for the grid # Maximum RBs per channel bandwidth and SCS (3GPP TS 38.101) MAX_RBS_TABLE = { 5: {15: 25, 30: 11, 60: None, 120: None, 240: None}, 10: {15: 52, 30: 24, 60: 11, 120: None, 240: None}, 15: {15: 79, 30: 38, 60: 18, 120: None, 240: None}, 20: {15: 106, 30: 51, 60: 24, 120: None, 240: None}, 25: {15: 133, 30: 65, 60: 31, 120: None, 240: None}, 30: {15: 160, 30: 78, 60: 38, 120: None, 240: None}, 40: {15: 216, 30: 106, 60: 51, 120: None, 240: None}, 50: {15: 270, 30: 133, 60: 65, 120: 32, 240: None}, 60: {15: None, 30: 162, 60: 79, 120: 39, 240: None}, 80: {15: None, 30: 217, 60: 107, 120: 52, 240: None}, 100: {15: None, 30: 273, 60: 135, 120: 66, 240: 32}, } # FFT sizes for different bandwidths FFT_SIZE_TABLE = { 5: 512, 10: 1024, 15: 1536, 20: 2048, 25: 2048, 30: 2048, 40: 4096, 50: 4096, 60: 4096, 80: 4096, 100: 4096, } # Get maximum RBs for the configured bandwidth and SCS MAX_RBS = MAX_RBS_TABLE.get(CHANNEL_BW_MHZ, {}).get(SSB_SCS_KHZ, None) if MAX_RBS is None: raise ValueError(f"Invalid combination: {CHANNEL_BW_MHZ} MHz with {SSB_SCS_KHZ} kHz SCS") # ============================================================================= # SSB Physical Parameters (3GPP TS 38.211 Section 7.4.3) # ============================================================================= SSB_NUM_RBS = 20 # SSB always occupies 20 RBs (240 subcarriers) SSB_NUM_SUBCARRIERS = SSB_NUM_RBS * 12 # 240 subcarriers SSB_NUM_SYMBOLS = 4 # SSB always occupies 4 OFDM symbols # PSS/SSS Parameters (3GPP TS 38.211 Section 7.4.3.1) PSS_SSS_NUM_SUBCARRIERS = 127 # PSS and SSS each occupy 127 subcarriers PSS_SSS_START_SC = 56 # Starting subcarrier within SSB (centered) PSS_SSS_END_SC = 183 # Ending subcarrier within SSB (exclusive, so 56-182) # PBCH Parameters (3GPP TS 38.211 Section 7.4.3.1) # In Symbol 2, PBCH is only on the sides around SSS with gaps PBCH_LEFT_END_SC = 48 # Left PBCH ends at SC 47 (SC 0-47) PBCH_RIGHT_START_SC = 192 # Right PBCH starts at SC 192 (SC 192-239) # Unused REs in Symbol 2: SC 48-55 (between left PBCH and SSS) # SC 183-191 (between SSS and right PBCH) PBCH_NUM_RES = 432 # PBCH occupies 432 REs per SSB PBCH_DMRS_DENSITY = 3 # DMRS on every 4th subcarrier (v=0,1,2,3) # ============================================================================= # Time Domain Configuration for 5ms Display # ============================================================================= # Calculate number of slots in 5ms (half frame) # 1 slot = 14 OFDM symbols with normal CP # Slot duration = 14 symbols * (1/SCS) = 14 * (1/15000) = 0.933ms for 15kHz # Number of slots in 5ms = 5ms / slot_duration SLOT_DURATION_MS = 1.0 / (SSB_SCS_KHZ / 15) # ms per slot (15kHz=1ms, 30kHz=0.5ms) HALF_FRAME_MS = 5.0 # 5ms half frame NUM_SLOTS_IN_HALF_FRAME = int(HALF_FRAME_MS / SLOT_DURATION_MS) NUM_SYMBOLS_PER_SLOT = 14 # Normal CP NUM_OFDM_SYMBOLS = NUM_SLOTS_IN_HALF_FRAME * NUM_SYMBOLS_PER_SLOT # FFT size based on channel bandwidth FFT_SIZE = FFT_SIZE_TABLE.get(CHANNEL_BW_MHZ, 2048) # Calculate guard carriers MAX_SUBCARRIERS = MAX_RBS * 12 BASE_GUARD_TOTAL = FFT_SIZE - MAX_SUBCARRIERS BASE_GUARD_LEFT = BASE_GUARD_TOTAL // 2 BASE_GUARD_RIGHT = BASE_GUARD_TOTAL - BASE_GUARD_LEFT # ============================================================================= # SSB Position in Frequency Domain (3GPP offsetToPointA / k_SSB) # ============================================================================= # offsetToPointA: in RB units at 15 kHz common raster # k_SSB: subcarrier offset (0..23) at 15 kHz common raster # COMMON_SCS_KHZ: common raster (15 kHz for FR1) COMMON_SCS_KHZ = 15 # Center the SSB in the configured channel bandwidth using 3GPP raster rules. # Target start RB at the SSB SCS: mid = floor((MAX_RBS - SSB_NUM_RBS)/2). _center_start_rb = max(0, (MAX_RBS - SSB_NUM_RBS) // 2) # Convert that RB start (at SSB SCS) to common 15 kHz raster subcarriers. _center_start_sc_common = int(round(_center_start_rb * 12 * (SSB_SCS_KHZ / COMMON_SCS_KHZ))) # Derive offsetToPointA (RB) and k_SSB (SC) on 15 kHz raster. OFFSET_TO_POINTA_RB = _center_start_sc_common // 12 - 20 K_SSB = _center_start_sc_common % 12 # Compute actual SSB start subcarrier at the SSB SCS from those offsets. SSB_START_SC = int((OFFSET_TO_POINTA_RB * 12 + K_SSB) * (COMMON_SCS_KHZ / SSB_SCS_KHZ)) SSB_OFFSET_RB = SSB_START_SC // 12 # Clamp to channel if needed (rare) if SSB_START_SC + SSB_NUM_SUBCARRIERS > MAX_SUBCARRIERS: SSB_START_SC = max(0, MAX_SUBCARRIERS - SSB_NUM_SUBCARRIERS) SSB_OFFSET_RB = SSB_START_SC // 12 print(f"WARNING: SSB shifted to fit in channel. New start SC: {SSB_START_SC}") # ============================================================================= # Calculate SSB Burst Positions (3GPP TS 38.213 Section 4.1) # ============================================================================= def get_ssb_symbol_positions(ssb_case, num_ssb, l_max): """ Get SSB starting symbol positions for a given case. Parameters: ----------- ssb_case : str SSB case ('A', 'B', 'C', 'D', 'E') num_ssb : int Number of SSBs to transmit l_max : int Maximum number of SSBs (4, 8, or 64) Returns: -------- list: Starting symbol indices for each SSB burst """ params = SSB_CASE_PARAMS[ssb_case] first_symbols = params['first_symbols'] period = params['slot_pattern_period'] ssb_positions = [] if ssb_case == 'A': # Case A: Symbols {2, 8} + 14*n, n = 0, 1, 2, 3 for L_max = 8 # For L_max = 4: n = 0, 1 max_n = l_max // 2 for n in range(max_n): for first_sym in first_symbols: sym = first_sym + period * n ssb_positions.append(sym) if len(ssb_positions) >= num_ssb: return ssb_positions[:num_ssb] elif ssb_case == 'B': # Case B: Symbols {4, 8, 16, 20} + 28*n max_n = l_max // 4 for n in range(max_n): for first_sym in first_symbols: sym = first_sym + period * n ssb_positions.append(sym) if len(ssb_positions) >= num_ssb: return ssb_positions[:num_ssb] elif ssb_case == 'C': # Case C: Symbols {2, 8} + 14*n max_n = l_max // 2 for n in range(max_n): for first_sym in first_symbols: sym = first_sym + period * n ssb_positions.append(sym) if len(ssb_positions) >= num_ssb: return ssb_positions[:num_ssb] elif ssb_case in ['D', 'E']: # Case D/E: More complex patterns for FR2 with specific n values (not consecutive) case_params = SSB_CASE_PARAMS[ssb_case] if 'n_values' in case_params: # Use specific n values from the table (e.g., Case D: [0,1,2,3,5,6,7,8,10,11,12,13,15,16,17,18]) n_values_to_use = case_params['n_values'] else: # Fallback: use consecutive n values max_n = l_max // len(first_symbols) n_values_to_use = list(range(max_n)) for n in n_values_to_use: for first_sym in first_symbols: sym = first_sym + period * n ssb_positions.append(sym) if len(ssb_positions) >= num_ssb: return ssb_positions[:num_ssb] return ssb_positions[:num_ssb] # Normalize SSB transmission bitmap def normalize_ssb_bitmap(bitmap, l_max): """Keep only 0/1, pad or trim to l_max.""" b = ''.join(ch for ch in bitmap if ch in ['0', '1']) if len(b) < l_max: b = b.ljust(l_max, '0') elif len(b) > l_max: b = b[:l_max] return b def apply_ssb_bitmap(ssb_positions_all, bitmap): """Select SSB positions based on bitmap bits (1=transmit, 0=skip).""" return [pos for pos, bit in zip(ssb_positions_all, bitmap) if bit == '1'] # Get all possible SSB positions for this case (up to L_MAX) SSB_TX_BITMAP_NORM = normalize_ssb_bitmap(SSB_TX_BITMAP, L_MAX) SSB_POSITIONS_ALL = get_ssb_symbol_positions(SSB_CASE, L_MAX, L_MAX) SSB_ACTIVE = [(idx, pos) for idx, (bit, pos) in enumerate(zip(SSB_TX_BITMAP_NORM, SSB_POSITIONS_ALL)) if bit == '1'] SSB_SYMBOL_POSITIONS = [pos for _, pos in SSB_ACTIVE] SSB_SYMBOL_INDICES = [idx for idx, _ in SSB_ACTIVE] NUM_SSB_TRANSMITTED = len(SSB_SYMBOL_POSITIONS) # ============================================================================= # Create SSB Resource Element Masks # ============================================================================= def create_ssb_masks(num_symbols, num_subcarriers, ssb_start_sc, ssb_symbol_positions, ssb_num_subcarriers=None, ssb_scs_ratio=1.0, n_cell_id=0): """ Create masks for different SSB components. SSB Structure (within the 4 symbols x 240 subcarriers at SSB SCS): - Symbol 0: PSS (SC 56-182, 127 subcarriers) - Symbol 1: PBCH (all 240 SC) + PBCH DMRS (every 4th SC, offset by v) - Symbol 2: PBCH (SC 0-47 & 192-239) + SSS (SC 56-182) + PBCH DMRS - Symbol 3: PBCH (all 240 SC) + PBCH DMRS Parameters: ----------- num_symbols : int Total number of OFDM symbols in the grid num_subcarriers : int Total number of subcarriers in the channel ssb_start_sc : int Starting subcarrier of SSB in the channel (at grid SCS) ssb_symbol_positions : list Starting symbol indices for each SSB burst (at grid SCS) ssb_num_subcarriers : int, optional SSB size in grid subcarriers (default: SSB_NUM_SUBCARRIERS) ssb_scs_ratio : float, optional Ratio SSB_SCS / PDSCH_SCS for scaling internal offsets (default: 1.0) n_cell_id : int, optional Physical Cell ID (PCI) for DMRS offset calculation (default: 0) According to 3GPP TS 38.211 Section 7.4.1.4.1: v-bar = N_ID_cell mod 4 Returns: -------- dict: Masks for PSS, SSS, PBCH, PBCH_DMRS, and combined SSB """ # Use provided SSB size or default if ssb_num_subcarriers is None: ssb_num_subcarriers = SSB_NUM_SUBCARRIERS # Calculate PBCH DMRS offset v-bar = N_ID_cell mod 4 (3GPP TS 38.211 Section 7.4.1.4.1) v_bar = n_cell_id % 4 # Offset within each group of 4 subcarriers (0, 1, 2, or 3) if DEBUG: print(f"[DEBUG] create_ssb_masks: n_cell_id={n_cell_id}, v_bar={v_bar}") # Scale internal SSB offsets based on SCS ratio # These offsets are defined at SSB SCS and need to be converted to grid SCS pss_sss_start_sc_offset = int(round(PSS_SSS_START_SC * ssb_scs_ratio)) pss_sss_end_sc_offset = int(round(PSS_SSS_END_SC * ssb_scs_ratio)) pbch_left_end_sc_offset = int(round(PBCH_LEFT_END_SC * ssb_scs_ratio)) pbch_right_start_sc_offset = int(round(PBCH_RIGHT_START_SC * ssb_scs_ratio)) pss_sss_num_subcarriers = int(round(PSS_SSS_NUM_SUBCARRIERS * ssb_scs_ratio)) # Scale DMRS spacing and offset to grid SCS # Note: v-bar offset is applied at SSB SCS, so we need to scale it proportionally dmrs_spacing = max(1, int(round(4 * ssb_scs_ratio))) # Every 4th subcarrier at SSB SCS # The offset v-bar should be scaled, but we need to ensure it stays within [0, dmrs_spacing) v_bar_scaled = int(round(v_bar * ssb_scs_ratio)) % dmrs_spacing if dmrs_spacing > 0 else 0 if DEBUG: print(f"[DEBUG] create_ssb_masks: ssb_scs_ratio={ssb_scs_ratio}, dmrs_spacing={dmrs_spacing}, v_bar_scaled={v_bar_scaled}") # Initialize masks pss_mask = np.zeros((num_symbols, num_subcarriers), dtype=bool) sss_mask = np.zeros((num_symbols, num_subcarriers), dtype=bool) pbch_mask = np.zeros((num_symbols, num_subcarriers), dtype=bool) pbch_dmrs_mask = np.zeros((num_symbols, num_subcarriers), dtype=bool) ssb_combined_mask = np.zeros((num_symbols, num_subcarriers), dtype=bool) # SSB subcarrier boundaries in channel ssb_end_sc = ssb_start_sc + ssb_num_subcarriers # PSS/SSS position within SSB (centered, scaled) pss_sss_start = ssb_start_sc + pss_sss_start_sc_offset pss_sss_end = ssb_start_sc + pss_sss_end_sc_offset for ssb_idx, ssb_start_sym in enumerate(ssb_symbol_positions): if ssb_start_sym + SSB_NUM_SYMBOLS > num_symbols: continue # Skip if SSB doesn't fit in the grid # SSB Symbol 0: PSS sym = ssb_start_sym + 0 for sc in range(pss_sss_start, pss_sss_end): if sc < num_subcarriers: pss_mask[sym, sc] = True ssb_combined_mask[sym, sc] = True # SSB Symbol 1: PBCH + PBCH DMRS sym = ssb_start_sym + 1 for sc_offset in range(ssb_num_subcarriers): sc = ssb_start_sc + sc_offset if sc >= num_subcarriers: continue # PBCH DMRS on every 4th subcarrier with offset v-bar (3GPP TS 38.211 Section 7.4.1.4.1) # DMRS positions: v-bar + 4*k where k = 0, 1, 2, ... # Check: (sc_offset - v_bar_scaled) % dmrs_spacing == 0 # This is equivalent to: sc_offset % dmrs_spacing == v_bar_scaled (when v_bar_scaled < dmrs_spacing) if sc_offset >= v_bar_scaled and (sc_offset - v_bar_scaled) % dmrs_spacing == 0: pbch_dmrs_mask[sym, sc] = True else: pbch_mask[sym, sc] = True ssb_combined_mask[sym, sc] = True # SSB Symbol 2: SSS (center) + PBCH (sides) + PBCH DMRS # Structure: PBCH(0-47) | unused(48-55) | SSS(56-182) | unused(183-191) | PBCH(192-239) (scaled) sym = ssb_start_sym + 2 for sc_offset in range(ssb_num_subcarriers): sc = ssb_start_sc + sc_offset if sc >= num_subcarriers: continue # SSS is in the center (scaled) if pss_sss_start_sc_offset <= sc_offset < pss_sss_end_sc_offset: sss_mask[sym, sc] = True ssb_combined_mask[sym, sc] = True # Left PBCH (scaled) elif sc_offset < pbch_left_end_sc_offset: # PBCH DMRS on every 4th subcarrier with offset v-bar if sc_offset >= v_bar_scaled and (sc_offset - v_bar_scaled) % dmrs_spacing == 0: pbch_dmrs_mask[sym, sc] = True else: pbch_mask[sym, sc] = True ssb_combined_mask[sym, sc] = True # Right PBCH (scaled) elif sc_offset >= pbch_right_start_sc_offset: # PBCH DMRS on every 4th subcarrier with offset v-bar if sc_offset >= v_bar_scaled and (sc_offset - v_bar_scaled) % dmrs_spacing == 0: pbch_dmrs_mask[sym, sc] = True else: pbch_mask[sym, sc] = True ssb_combined_mask[sym, sc] = True # Unused REs (scaled) - leave as zeros # SSB Symbol 3: PBCH + PBCH DMRS sym = ssb_start_sym + 3 for sc_offset in range(ssb_num_subcarriers): sc = ssb_start_sc + sc_offset if sc >= num_subcarriers: continue # PBCH DMRS on every 4th subcarrier with offset v-bar (3GPP TS 38.211 Section 7.4.1.4.1) # DMRS positions: v-bar + 4*k where k = 0, 1, 2, ... if sc_offset >= v_bar_scaled and (sc_offset - v_bar_scaled) % dmrs_spacing == 0: pbch_dmrs_mask[sym, sc] = True else: pbch_mask[sym, sc] = True ssb_combined_mask[sym, sc] = True return { 'pss': pss_mask, 'sss': sss_mask, 'pbch': pbch_mask, 'pbch_dmrs': pbch_dmrs_mask, 'ssb_combined': ssb_combined_mask } # ============================================================================= # Generate SSB Sequences # ============================================================================= def generate_pss_sequence(n_id_2): """ Generate PSS sequence (3GPP TS 38.211 Section 7.4.2.2.1). Parameters: ----------- n_id_2 : int Physical layer cell identity group (0, 1, or 2) Returns: -------- np.array: Complex PSS sequence of length 127 """ # Polynomial: x^7 + x^4 + 1 # Recursion: x(i+7) = (x(i+4) + x(i)) mod 2 # Initial conditions x(0)...x(6) # 3GPP: x(6)=1, x(5)=1, x(4)=1, x(3)=0, x(2)=1, x(1)=1, x(0)=0 x = [0, 1, 1, 0, 1, 1, 1] # Generate m-sequence length 127 # Recursion: x(i+7) = (x(i+4) + x(i)) mod 2 seq_len = 127 x_full = list(x) for i in range(seq_len - 7): # x(i+7) = (x(i+4) + x(i)) % 2 next_bit = (x_full[i+4] + x_full[i]) % 2 x_full.append(next_bit) # Generate d_PSS with cyclic shift d_pss = np.zeros(127, dtype=complex) for n in range(127): m = (n + 43 * n_id_2) % 127 d_pss[n] = 1 - 2 * x_full[m] return d_pss def generate_sss_sequence(n_id_1, n_id_2): """ Generate SSS sequence (3GPP TS 38.211 Section 7.4.2.3.1). Parameters: ----------- n_id_1 : int Physical layer cell identity (0-335) n_id_2 : int Physical layer cell identity group (0, 1, or 2) Returns: -------- np.array: Complex SSS sequence of length 127 """ seq_len = 127 # --- Generate x0 --- # Polynomial: x^7 + x^4 + 1 (Same as PSS) # Recursion: x0(i+7) = (x0(i+4) + x0(i)) mod 2 # Init: x0(0)~x0(5)=0, x0(6)=1 <-- CORRECTED per 3GPP TS 38.211 Section 7.4.2.3.1 x0 = [0, 0, 0, 0, 0, 0, 1] for i in range(seq_len - 7): next_bit = (x0[i+4] + x0[i]) % 2 x0.append(next_bit) # --- Generate x1 --- # Polynomial: x^7 + x + 1 # Recursion: x1(i+7) = (x1(i+1) + x1(i)) mod 2 # Init: x1(0)~x1(5)=0, x1(6)=1 <-- CORRECTED per 3GPP TS 38.211 Section 7.4.2.3.1 x1 = [0, 0, 0, 0, 0, 0, 1] for i in range(seq_len - 7): next_bit = (x1[i+1] + x1[i]) % 2 x1.append(next_bit) # --- Calculate cyclic shifts --- m0 = 15 * (n_id_1 // 112) + 5 * n_id_2 m1 = n_id_1 % 112 # --- Generate d_SSS --- # 3GPP SSS equation: # d_sss(n) = [1 - 2x0((n+m0)%127)] * [1 - 2x1((n+m1)%127)] d_sss = np.zeros(seq_len, dtype=complex) for n in range(seq_len): val_x0 = x0[(n + m0) % 127] val_x1 = x1[(n + m1) % 127] d_sss[n] = (1 - 2 * val_x0) * (1 - 2 * val_x1) return d_sss def generate_gold_sequence(length, c_init): """ Generate Gold Sequence c(n) as defined in 3GPP TS 38.211 Sec 5.2.1. This is the standard 3GPP-compliant Gold sequence generator implementation. Parameters: ----------- length : int Length of the sequence to generate c_init : int Initialization value for the sequence Returns: -------- np.array: Gold sequence bits (0 or 1) """ x1 = np.zeros(1600 + length, dtype=int) x2 = np.zeros(1600 + length, dtype=int) # Init x1: x1(0) = 1, x1(n) = 0 for n = 1, 2, ..., 30 x1[0] = 1 # Init x2 based on c_init: x2(n) = (c_init >> n) & 1 for n = 0, 1, ..., 30 for i in range(31): x2[i] = (c_init >> i) & 1 # Generate x1: x1(n+31) = (x1(n+3) + x1(n)) mod 2 for n in range(1600 + length - 31): x1[n+31] = (x1[n+3] + x1[n]) % 2 # Generate x2: x2(n+31) = (x2(n+3) + x2(n+2) + x2(n+1) + x2(n)) mod 2 for n in range(1600 + length - 31): x2[n+31] = (x2[n+3] + x2[n+2] + x2[n+1] + x2[n]) % 2 # Output c(n) = (x1(n+Nc) + x2(n+Nc)) mod 2, Nc = 1600 c = np.zeros(length, dtype=int) for n in range(length): c[n] = (x1[n+1600] + x2[n+1600]) % 2 return c def generate_pbch_dmrs_sequence(n_cell_id, ssb_index, l_ssb_max): """ Generate PBCH DMRS sequence r(m) (QPSK) per 3GPP TS 38.211 Sec 7.4.1.4.1. Parameters: ----------- n_cell_id : int Physical Cell ID (PCI) (0-1007) ssb_index : int SSB index (0 to L_max-1) - determines the sequence for SSB identification l_ssb_max : int Maximum number of SSBs (not used in generation, kept for compatibility) Returns: -------- np.array: PBCH DMRS sequence (QPSK symbols, complex) """ # c_init calculation per 3GPP TS 38.211 Sec 7.4.1.4.1 # c_init = 2^11 * (i_bar_SSB + 1) * floor(N_ID_cell/4 + 1) + 2^6 * (i_bar_SSB + 1) + (N_ID_cell mod 4) # We use ssb_index as i_bar_SSB i_bar_ssb = ssb_index term1 = (i_bar_ssb + 1) * ((n_cell_id // 4) + 1) term2 = (i_bar_ssb + 1) term3 = n_cell_id % 4 c_init = (2**11 * term1) + (2**6 * term2) + term3 if DEBUG: print(f"[DEBUG] generate_pbch_dmrs_sequence:") print(f" n_cell_id={n_cell_id}, ssb_index={ssb_index}") print(f" term1 = ({i_bar_ssb}+1) * (({n_cell_id}//4)+1) = {term1}") print(f" term2 = ({i_bar_ssb}+1) = {term2}") print(f" term3 = {n_cell_id} % 4 = {term3}") print(f" c_init = 2^11*{term1} + 2^6*{term2} + {term3} = {c_init}") # Generate Gold sequence: need 2 bits per QPSK symbol # Total DMRS REs: 60 (Sym 1) + 60 (Sym 3) + 24 (Sym 2) = 144 REs max seq_len_bits = 144 * 2 # Use the standard 3GPP generate_gold_sequence() function gold = generate_gold_sequence(seq_len_bits, c_init) if DEBUG: print(f" Generated Gold sequence: length={len(gold)} bits") print(f" Gold sequence statistics: sum={np.sum(gold)}, mean={np.mean(gold):.3f}") # QPSK Modulation: r(m) = 1/sqrt(2) * (1 - 2*c(2m) + j*(1 - 2*c(2m+1))) dmrs_seq = np.zeros(seq_len_bits // 2, dtype=complex) factor = 1.0 / np.sqrt(2) for m in range(len(dmrs_seq)): real = 1 - 2 * gold[2*m] imag = 1 - 2 * gold[2*m+1] dmrs_seq[m] = complex(real, imag) * factor if DEBUG: print(f" Generated DMRS sequence: length={len(dmrs_seq)} QPSK symbols") print(f" DMRS magnitude stats: min={np.min(np.abs(dmrs_seq)):.6f}, max={np.max(np.abs(dmrs_seq)):.6f}, mean={np.mean(np.abs(dmrs_seq)):.6f}") print(f" DMRS magnitude should be ~1.0 (normalized QPSK): mean={np.mean(np.abs(dmrs_seq)):.6f}") return dmrs_seq # ============================================================================= # PBCH Encoding Constants (3GPP TS 38.211, 38.212) # ============================================================================= PBCH_NR_M = 432 # Number of QPSK symbols (432 REs for PBCH data) PBCH_NR_E = 864 # Number of encoded bits after rate matching (432 symbols * 2 bits) PBCH_NR_N = 512 # Polar code block size (N=512) PBCH_NR_A = 32 # Payload size (24 MIB bits + 8 overhead bits: SFN LSB, HRF, SSB index/k_SSB MSB) PBCH_NR_K = 56 # Information block size (32 payload + 24 CRC) # PBCH message interleaving pattern G (Table 7.1.1-1 in 3GPP TS 38.212) # This pattern is used to pack/unpack the 32-bit PBCH message PBCH_G_PATTERN = [16, 23, 18, 17, 8, 30, 10, 6, 24, 7, 0, 5, 3, 2, 1, 4, 9, 11, 12, 13, 14, 15, 19, 20, 21, 22, 25, 26, 27, 28, 29, 31] # Block interleaver pattern for n=9 (sub-block interleaving) BLK_INTERLEAVER_9 = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511 ] # Mother code pattern for n=9 (from polar_code.h) MOTHER_CODE_9 = [ 0, 1, 2, 4, 8, 16, 32, 3, 5, 64, 9, 6, 17, 10, 18, 128, 12, 33, 65, 20, 256, 34, 24, 36, 7, 129, 66, 11, 40, 68, 130, 19, 13, 48, 14, 72, 257, 21, 132, 35, 258, 26, 80, 37, 25, 22, 136, 260, 264, 38, 96, 67, 41, 144, 28, 69, 42, 49, 74, 272, 160, 288, 192, 70, 44, 131, 81, 50, 73, 15, 320, 133, 52, 23, 134, 384, 76, 137, 82, 56, 27, 97, 39, 259, 84, 138, 145, 261, 29, 43, 98, 88, 140, 30, 146, 71, 262, 265, 161, 45, 100, 51, 148, 46, 75, 266, 273, 104, 162, 53, 193, 152, 77, 164, 268, 274, 54, 83, 57, 112, 135, 78, 289, 194, 85, 276, 58, 168, 139, 99, 86, 60, 280, 89, 290, 196, 141, 101, 147, 176, 142, 321, 31, 200, 90, 292, 322, 263, 149, 102, 105, 304, 296, 163, 92, 47, 267, 385, 324, 208, 386, 150, 153, 165, 106, 55, 328, 113, 154, 79, 269, 108, 224, 166, 195, 270, 275, 291, 59, 169, 114, 277, 156, 87, 197, 116, 170, 61, 281, 278, 177, 293, 388, 91, 198, 172, 120, 201, 336, 62, 282, 143, 103, 178, 294, 93, 202, 323, 392, 297, 107, 180, 151, 209, 284, 94, 204, 298, 400, 352, 325, 155, 210, 305, 300, 109, 184, 115, 167, 225, 326, 306, 157, 329, 110, 117, 212, 171, 330, 226, 387, 308, 216, 416, 271, 279, 158, 337, 118, 332, 389, 173, 121, 199, 179, 228, 338, 312, 390, 174, 393, 283, 122, 448, 353, 203, 63, 340, 394, 181, 295, 285, 232, 124, 205, 182, 286, 299, 354, 211, 401, 185, 396, 344, 240, 206, 95, 327, 402, 356, 307, 301, 417, 213, 186, 404, 227, 418, 302, 360, 111, 331, 214, 309, 188, 449, 217, 408, 229, 159, 420, 310, 333, 119, 339, 218, 368, 230, 391, 313, 450, 334, 233, 175, 123, 341, 220, 314, 424, 395, 355, 287, 183, 234, 125, 342, 316, 241, 345, 452, 397, 403, 207, 432, 357, 187, 236, 126, 242, 398, 346, 456, 358, 405, 303, 244, 189, 361, 215, 348, 419, 406, 464, 362, 409, 219, 311, 421, 410, 231, 248, 369, 190, 364, 335, 480, 315, 221, 370, 422, 425, 451, 235, 412, 343, 372, 317, 222, 426, 453, 237, 433, 347, 243, 454, 318, 376, 428, 238, 359, 457, 399, 434, 349, 245, 458, 363, 127, 191, 407, 436, 465, 246, 350, 460, 249, 411, 365, 440, 374, 423, 466, 250, 371, 481, 413, 366, 468, 429, 252, 373, 482, 427, 414, 223, 472, 455, 377, 435, 319, 484, 430, 488, 239, 378, 459, 437, 380, 461, 496, 351, 467, 438, 251, 462, 442, 441, 469, 247, 367, 253, 375, 444, 470, 483, 415, 485, 473, 474, 254, 379, 431, 489, 486, 476, 439, 490, 463, 381, 497, 492, 443, 382, 498, 445, 471, 500, 446, 475, 487, 504, 255, 477, 491, 478, 383, 493, 499, 502, 494, 501, 447, 505, 506, 479, 508, 495, 503, 507, 509, 510, 511 ] # Polar interleaver pattern (from polar_interleaver.c) # Updated to use the full 164-element pattern for PBCH (K=56) POLAR_INTERLEAVER_PATTERN = [ 0, 2, 4, 7, 9, 14, 19, 20, 24, 25, 26, 28, 31, 34, 42, 45, 49, 50, 51, 53, 54, 56, 58, 59, 61, 62, 65, 66, 67, 69, 70, 71, 72, 76, 77, 81, 82, 83, 87, 88, 89, 91, 93, 95, 98, 101, 104, 106, 108, 110, 111, 113, 115, 118, 119, 120, 122, 123, 126, 127, 129, 132, 134, 138, 139, 140, 1, 3, 5, 8, 10, 15, 21, 27, 29, 32, 35, 43, 46, 52, 55, 57, 60, 63, 68, 73, 78, 84, 90, 92, 94, 96, 99, 102, 105, 107, 109, 112, 114, 116, 121, 124, 128, 130, 133, 135, 141, 6, 11, 16, 22, 30, 33, 36, 44, 47, 64, 74, 79, 85, 97, 100, 103, 117, 125, 131, 136, 142, 12, 17, 23, 37, 48, 75, 80, 86, 137, 143, 13, 18, 38, 144, 39, 145, 40, 146, 41, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163 ] POLAR_INTERLEAVER_K_MAX_IL = 164 # ============================================================================= # PBCH Encoding Helper Functions (3GPP TS 38.211, 38.212) # ============================================================================= def compute_crc24c(bits): """ Compute CRC-24C checksum (24 bits) matching srsRAN implementation. Polynomial: 0x1B2B117 (CRC-24C) This implementation matches srsRAN's byte-based CRC computation. """ poly = 0x1B2B117 order = 24 crcmask = (((1 << (order - 1)) - 1) << 1) | 1 crchighbit = 1 << (order - 1) # Generate lookup table (same as srsRAN) pad = 0 # order >= 8, so pad = 0 ord = order - 8 polynom = poly << pad crchighbit_shifted = crchighbit << pad table = [0] * 256 for i in range(256): crc = i << ord for j in range(8): bit = crc & crchighbit_shifted crc <<= 1 if bit: crc ^= polynom table[i] = (crc >> pad) & crcmask # Initialize CRC register crcinit = 0 # Pack bits into bytes (MSB first, matching srsRAN bit_pack) len_bits = len(bits) len8 = len_bits // 8 res8 = len_bits % 8 # Process full bytes for i in range(len8): byte = 0 for k in range(8): byte |= (int(bits[i * 8 + k]) << (7 - k)) # Update CRC (matching srsran_crc_checksum_put_byte) idx = ((crcinit >> (order - 8)) & 0xff) ^ byte crcinit = ((crcinit << 8) ^ table[idx]) & ((1 << 32) - 1) # Process remaining bits if res8 > 0: byte = 0 for k in range(res8): byte |= (int(bits[len8 * 8 + k]) << (7 - k)) idx = ((crcinit >> (order - 8)) & 0xff) ^ byte crcinit = ((crcinit << 8) ^ table[idx]) & ((1 << 32) - 1) # Reverse CRC for remaining bits (matching reversecrcbit) crc = crcinit & crcmask for m in range(8 - res8): if (crc & 1) == 0: crc = crc >> 1 else: crc = ((crc ^ poly) >> 1) | (1 << (order - 1)) crcinit = crc # Get final CRC value crc_value = crcinit & crcmask # Convert to bit array (MSB first, matching srsran_bit_unpack) crc_bits = [] for i in range(order - 1, -1, -1): crc_bits.append((crc_value >> i) & 1) return np.array(crc_bits, dtype=np.uint8) def compute_frozen_set_pbch(K=PBCH_NR_K, E=PBCH_NR_E, nMax=9): """ Compute frozen set, K_set, and PC_set for PBCH polar code. This is needed for channel allocation during encoding. Returns K_set (sorted information bit positions). """ # Determine N and n e = 1 while (1 << e) < E: e += 1 tmpE = 1 << e n1 = e if (8 * E <= 9 * (1 << (e - 1))) and (16 * K < 9 * E): n1 = e - 1 k = 0 while (1 << k) < K: k += 1 n2 = k + 3 n = min(n1, n2, nMax) if n < 5: n = 5 N = 1 << n nPC = 0 # For PBCH: K=56, so nPC=0 # Helper function to find set difference def setdiff_stable(mother_code, punct_short, tmp_K_set, T, len1, len2): o = 0 for i in range(len1): flag = 0 if T >= 0 and mother_code[i] <= T: flag = 1 else: for j in range(len2): if mother_code[i] == punct_short[j]: flag = 1 break if flag == 0: tmp_K_set[o] = mother_code[i] o += 1 return o blk_interleaver = np.array(BLK_INTERLEAVER_9, dtype=np.uint16) mother_code = np.array(MOTHER_CODE_9, dtype=np.uint16) # Frozen bits due to puncturing/shortening T = -1 tmp_F_set_size = N - E F_set_punct_short = np.zeros(N, dtype=np.uint16) if tmp_F_set_size > 0: N_th = 3 * N // 4 if 16 * K <= 7 * E: # Puncturing if E >= N_th: T = N_th - (E >> 1) - 1 else: T = 9 * N // 16 - (E >> 2) F_set_punct_short[:tmp_F_set_size] = blk_interleaver[:tmp_F_set_size] else: # Shortening F_set_punct_short[:tmp_F_set_size] = blk_interleaver[E:E+tmp_F_set_size] else: tmp_F_set_size = 0 # Compute K_set (information + parity positions) tmp_K_set = np.zeros(N, dtype=np.uint16) tmp_K = setdiff_stable(mother_code, F_set_punct_short, tmp_K_set, T, N, tmp_F_set_size) # Select only the most reliable (message and parity) K_set = tmp_K_set[tmp_K - K - nPC:tmp_K].copy() K_set_sorted = np.sort(K_set) return K_set_sorted def polar_chanalloc_tx(message: np.ndarray, K: int, K_set: np.ndarray) -> np.ndarray: """ Channel allocation (transmitter side). Maps K information bits to K_set positions in the codeword. Frozen bits (positions not in K_set) are set to 0. """ N = len(K_set) + (512 - len(K_set)) # N=512 for PBCH codeword = np.zeros(512, dtype=np.uint8) for i in range(K): codeword[K_set[i]] = message[i] return codeword def polar_interleaver_tx(c: np.ndarray, K: int) -> np.ndarray: """ Polar interleaver (transmitter side, interleaving direction). This matches the decoder's deinterleaving logic (dir=False): - Decoder: c[pi_k] = c_prime[k] (deinterleaving) - Generator: c_prime[k] = c[pi_k] (interleaving, inverse of decoder) """ c_prime = np.zeros(K, dtype=np.uint8) k = 0 for m in range(POLAR_INTERLEAVER_K_MAX_IL): if POLAR_INTERLEAVER_PATTERN[m] >= POLAR_INTERLEAVER_K_MAX_IL - K: pi_k = POLAR_INTERLEAVER_PATTERN[m] - (POLAR_INTERLEAVER_K_MAX_IL - K) c_prime[k] = c[pi_k] # Interleaving: output[k] = input[pi_k] (inverse of decoder's c[pi_k] = c_prime[k]) k += 1 return c_prime def polar_encode(bits: np.ndarray, n: int) -> np.ndarray: """ Full polar encoder for N=2^n bits. Implements the polar code encoding transformation. """ if n == 0: return bits.copy() N = 1 << n output = bits.copy().astype(np.uint8) # Polar encoding: recursive XOR structure for stage in range(n): block_size = 1 << (n - stage - 1) num_blocks = 1 << stage for block in range(num_blocks): offset = block * (block_size << 1) for i in range(block_size): output[offset + i] = (output[offset + i] ^ output[offset + block_size + i]) & 1 return output def subblock_interleave(bits: np.ndarray) -> np.ndarray: """ Sub-block interleaving for polar rate matching (TX side). According to srsRAN reference implementation (polar_rm.c line 76-81): output[j] = input[indices[j]] This means: natural position indices[j] → interleaved position j So: interleaved[j] = bits[blk_interleaver_9[j]] The decoder's deinterleaver does: output[indices[j]] = input[j] which reverses this: interleaved position j → natural position indices[j] """ N = len(bits) blk_interleaver = np.array(BLK_INTERLEAVER_9, dtype=np.uint16) interleaved = np.zeros(N, dtype=np.uint8) for j in range(N): interleaved[j] = bits[blk_interleaver[j]] return interleaved def rate_matching_pbch_tx(bits: np.ndarray, E: int = PBCH_NR_E) -> np.ndarray: """ Rate matching for PBCH (TX side): bit selection with repetition. For E > N (repetition case), selects E bits from N interleaved bits. """ N = len(bits) output = np.zeros(E, dtype=np.uint8) for k in range(E): output[k] = bits[k % N] return output def pbch_scramble_tx(bits: np.ndarray, n_cell_id: int, ssb_idx: int, n_hf: int, Lmax: int) -> np.ndarray: """ PBCH scrambling (TX side) using Gold sequence. Scrambles bits using Gold sequence initialized with N_id. Note: For scrambling, we need c_init = N_id (masked), and advance by v * E. However, generate_gold_sequence doesn't support advance, so we'll generate the full sequence and take the appropriate portion. Actually, for proper implementation we'd need to generate from position (advance), but for now we'll generate the full sequence from the start and use the portion. """ seed = (n_cell_id & 0x7FFFFFFF) if Lmax == 4: v = ssb_idx & 0x3 else: v = ssb_idx & 0x7 advance = v * PBCH_NR_E # Generate Gold sequence for the required length # Note: This is a simplification - proper implementation would advance the state # For now, generate enough sequence and take the portion we need total_needed = advance + len(bits) gold_seq = generate_gold_sequence(total_needed, seed) # Take the portion starting from 'advance' scramble_seq = gold_seq[advance:advance+len(bits)] # Apply scrambling (XOR) scrambled = (bits ^ scramble_seq) & 1 return scrambled def qpsk_modulate(bits: np.ndarray) -> np.ndarray: """ QPSK modulation: maps 2 bits to 1 complex symbol. Gray mapping: 00 -> (1+1j)/sqrt(2), 01 -> (1-1j)/sqrt(2), 10 -> (-1+1j)/sqrt(2), 11 -> (-1-1j)/sqrt(2) """ num_symbols = len(bits) // 2 symbols = np.zeros(num_symbols, dtype=complex) scale = 1.0 / np.sqrt(2.0) for i in range(num_symbols): b0 = bits[2*i] b1 = bits[2*i + 1] # Gray mapping real = 1 - 2*b0 imag = 1 - 2*b1 symbols[i] = complex(real, imag) * scale return symbols def encode_pbch(mib_payload_bits: np.ndarray, n_cell_id: int, sfn: int, ssb_idx: int, k_ssb_msb: int, Lmax: int, n_hf: int = 0): """ Full PBCH encoding chain (3GPP TS 38.211, 38.212). Parameters: ----------- mib_payload_bits : np.ndarray 24-bit MIB payload (from encode_mib_payload) n_cell_id : int Physical Cell ID (PCI) sfn : int System Frame Number (10 bits, 0-1023) ssb_idx : int SSB index within the SSB burst set k_ssb_msb : int k_SSB MSB (1 bit for Lmax <= 8, not used for Lmax=64) Lmax : int Maximum number of SSBs (4, 8, or 64) n_hf : int Half-frame indicator (0 or 1) Returns: -------- np.ndarray: PBCH QPSK symbols (432 symbols) """ # Step 1: Pack PBCH message (32 bits) from MIB payload + overhead a = pbch_msg_pack(mib_payload_bits, sfn, n_hf, ssb_idx, k_ssb_msb, Lmax) # Step 2: Payload scrambling (before CRC) b = pbch_payload_scramble_tx(a, n_cell_id, sfn, Lmax) # Step 3: Add CRC-24C (24 bits) -> total 56 bits crc_bits = compute_crc24c(b) c = np.concatenate([b, crc_bits]).astype(np.uint8) # 32 + 24 = 56 bits # Step 4: Polar interleaving (interleaving direction) c_prime = polar_interleaver_tx(c, PBCH_NR_K) # Step 5: Channel allocation (map to K_set positions) K_set = compute_frozen_set_pbch(PBCH_NR_K, PBCH_NR_E, 9) codeword_input = np.zeros(512, dtype=np.uint8) for i in range(PBCH_NR_K): codeword_input[K_set[i]] = c_prime[i] # Frozen bits are already 0 # Step 6: Polar encoding (N=512) encoded = polar_encode(codeword_input, 9) # n=9 for N=512 # Step 7: Sub-block interleaving interleaved = subblock_interleave(encoded) # Step 8: Rate matching (bit selection with repetition: 512 -> 864) rate_matched = rate_matching_pbch_tx(interleaved, PBCH_NR_E) # Step 9: PBCH scrambling (using Gold sequence based on N_id, SSB index, half-frame) scrambled = pbch_scramble_tx(rate_matched, n_cell_id, ssb_idx, n_hf, Lmax) # Step 10: QPSK modulation (864 bits -> 432 symbols) symbols = qpsk_modulate(scrambled) return symbols def pbch_msg_pack(mib_payload_bits: np.ndarray, sfn: int, hrf: int, ssb_idx: int, k_ssb_msb: int, Lmax: int) -> np.ndarray: """ Pack PBCH message (32 bits) from MIB payload (24 bits) + overhead bits. Uses G pattern interleaving as per 3GPP TS 38.212 Table 7.1.1-1. This reverses the unpacking logic from pbch_msg_unpack. """ G = PBCH_G_PATTERN a = np.zeros(PBCH_NR_A, dtype=np.uint8) # Extract SFN 4 LSB bits (bits 0-3 of 10-bit SFN) sfn_4lsb = sfn & 0x0F # Unpacking logic (from detection script): # - Payload bits [1:7) (6 bits, SFN MSB) -> G[j_sfn] positions, j_sfn = 0..5 # - Other payload bits -> G[j_other] positions, j_other starts at 14 # - SFN 4 LSB -> G[j_sfn] positions, j_sfn continues from 6 # - HRF -> G[10] # - SSB index/k_SSB MSB -> G[11:14] or G[11] depending on Lmax # Packing (reverse of unpacking): j_sfn = 0 j_other = 14 # Pack SFN MSB (payload bits 1-6, which are indices 1-6 in mib_payload_bits) PBCH_SFN_PAYLOAD_BEGIN = 1 PBCH_SFN_PAYLOAD_LENGTH = 6 for i in range(PBCH_SFN_PAYLOAD_BEGIN, PBCH_SFN_PAYLOAD_BEGIN + PBCH_SFN_PAYLOAD_LENGTH): if i < len(mib_payload_bits): a[G[j_sfn]] = mib_payload_bits[i] j_sfn += 1 # Pack other payload bits (all except bits 1-6) A_hat = 24 # Total payload bits for i in range(A_hat): if i < PBCH_SFN_PAYLOAD_BEGIN or i >= (PBCH_SFN_PAYLOAD_BEGIN + PBCH_SFN_PAYLOAD_LENGTH): if i < len(mib_payload_bits) and j_other < len(G): a[G[j_other]] = mib_payload_bits[i] j_other += 1 # Pack SFN 4 LSB (4 bits, MSB first) for i in range(4): a[G[j_sfn]] = (sfn_4lsb >> (3-i)) & 1 j_sfn += 1 # Pack HRF (1 bit) a[G[10]] = hrf & 1 # Pack SSB index bits or k_SSB MSB if Lmax == 64: # Pack 3 bits of SSB index MSB (bits 5, 4, 3) ssb_idx_msb = (ssb_idx >> 3) & 0x7 a[G[11]] = (ssb_idx_msb >> 2) & 1 a[G[12]] = (ssb_idx_msb >> 1) & 1 a[G[13]] = (ssb_idx_msb >> 0) & 1 else: # k_SSB MSB (1 bit) a[G[11]] = k_ssb_msb & 1 return a def pbch_payload_scramble_tx(a: np.ndarray, n_cell_id: int, sfn: int, Lmax: int) -> np.ndarray: """ PBCH payload scrambling (TX side, before CRC). Uses Gold sequence initialized with N_id and advanced by M * v, where v is derived from SFN 2nd and 3rd LSB bits. """ G = PBCH_G_PATTERN PBCH_SFN_2ND_LSB_G = G[8] PBCH_SFN_3RD_LSB_G = G[7] # Compute v from SFN sfn_2nd_lsb = (sfn >> 1) & 1 sfn_3rd_lsb = (sfn >> 2) & 1 v = 2 * sfn_3rd_lsb + sfn_2nd_lsb c_init = n_cell_id & 0x7FFFFFFF M = PBCH_NR_A - 3 if Lmax == 64: M = PBCH_NR_A - 6 advance = M * v # Generate scrambling sequence (need enough bits for positions that are scrambled) # We need at most PBCH_NR_A bits, but some positions are skipped # Generate sequence starting from 'advance' total_needed = advance + PBCH_NR_A gold_seq_full = generate_gold_sequence(total_needed, c_init) scramble_seq = gold_seq_full[advance:advance+PBCH_NR_A] # Scramble (skip certain positions) b = np.zeros(PBCH_NR_A, dtype=np.uint8) j = 0 for i in range(PBCH_NR_A): is_ssb_idx = (i == G[11] or i == G[12] or i == G[13]) and Lmax == 64 if is_ssb_idx or i == G[10] or i == PBCH_SFN_2ND_LSB_G or i == PBCH_SFN_3RD_LSB_G: s_i = 0 # Don't scramble these positions else: s_i = scramble_seq[j] j += 1 b[i] = (a[i] ^ s_i) & 1 return b # ============================================================================= # Cell ID Configuration # ============================================================================= N_CELL_ID = 500 # Physical Cell ID (0-1007) N_ID_1 = N_CELL_ID // 3 # N_ID^(1) = floor(N_cell_ID / 3) N_ID_2 = N_CELL_ID % 3 # N_ID^(2) = N_cell_ID mod 3 # Generate PSS and SSS sequences PSS_SEQUENCE = generate_pss_sequence(N_ID_2) SSS_SEQUENCE = generate_sss_sequence(N_ID_1, N_ID_2) # ============================================================================= # Create SSB Masks # ============================================================================= print("=" * 70) print("SSB Resource Grid Configuration") print("=" * 70) print(f"SSB Case: {SSB_CASE}") print(f"SSB Subcarrier Spacing: {SSB_SCS_KHZ} kHz") print(f"L_max: {L_MAX}") print(f"SSB TX Bitmap: {SSB_TX_BITMAP_NORM}") print(f"SSBs Transmitted: {NUM_SSB_TRANSMITTED}") print(f"SSB Symbol Positions: {SSB_SYMBOL_POSITIONS}") print(f"SSB Indices (bitmap): {SSB_SYMBOL_INDICES}") print(f"offsetToPointA (RB@15k): {OFFSET_TO_POINTA_RB}") print(f"k_SSB (SC@15k): {K_SSB}") print("-" * 70) print(f"Channel Bandwidth: {CHANNEL_BW_MHZ} MHz") print(f"Max RBs in Channel: {MAX_RBS}") print(f"FFT Size: {FFT_SIZE}") print(f"Guard Carriers: {BASE_GUARD_LEFT} (left), {BASE_GUARD_RIGHT} (right)") print("-" * 70) print(f"Half Frame Duration: {HALF_FRAME_MS} ms") print(f"Slot Duration: {SLOT_DURATION_MS} ms") print(f"Slots in Half Frame: {NUM_SLOTS_IN_HALF_FRAME}") print(f"Total OFDM Symbols: {NUM_OFDM_SYMBOLS}") print("-" * 70) print(f"SSB Size: {SSB_NUM_RBS} RBs x {SSB_NUM_SYMBOLS} symbols") print(f"SSB Offset (RB): {SSB_OFFSET_RB}") print(f"SSB Start Subcarrier: {SSB_START_SC}") print("-" * 70) print(f"Cell ID: {N_CELL_ID}") print(f"N_ID^(1): {N_ID_1}") print(f"N_ID^(2): {N_ID_2}") print("=" * 70) # Create SSB masks SSB_MASKS = create_ssb_masks( NUM_OFDM_SYMBOLS, MAX_SUBCARRIERS, SSB_START_SC, SSB_SYMBOL_POSITIONS, n_cell_id=N_CELL_ID ) # Calculate RE statistics pss_res = np.sum(SSB_MASKS['pss']) sss_res = np.sum(SSB_MASKS['sss']) pbch_res = np.sum(SSB_MASKS['pbch']) pbch_dmrs_res = np.sum(SSB_MASKS['pbch_dmrs']) ssb_total_res = np.sum(SSB_MASKS['ssb_combined']) print(f"\nSSB Resource Element Statistics:") print(f" PSS REs: {pss_res}") print(f" SSS REs: {sss_res}") print(f" PBCH REs: {pbch_res}") print(f" PBCH DMRS REs: {pbch_dmrs_res}") print(f" Total SSB REs: {ssb_total_res}") print(f" REs per SSB: {SSB_NUM_SUBCARRIERS * SSB_NUM_SYMBOLS}") # ============================================================================= # Create Resource Grid Display Data # ============================================================================= def create_resource_grid_display(num_symbols, num_subcarriers, ssb_masks): """ Create a resource grid array for visualization. Values: 0 = Empty/Unused 1 = PSS 2 = SSS 3 = PBCH 4 = PBCH DMRS Parameters: ----------- num_symbols : int Total OFDM symbols num_subcarriers : int Total subcarriers ssb_masks : dict Dictionary of SSB component masks Returns: -------- np.array: Resource grid with values indicating RE type """ grid = np.zeros((num_symbols, num_subcarriers), dtype=float) # Fill in order of priority (later overwrites earlier) # 1 = PSS (red) grid[ssb_masks['pss']] = 1 # 2 = SSS (blue) grid[ssb_masks['sss']] = 2 # 3 = PBCH (green) grid[ssb_masks['pbch']] = 3 # 4 = PBCH DMRS (yellow) grid[ssb_masks['pbch_dmrs']] = 4 return grid # Create display grid RESOURCE_GRID_DISPLAY = create_resource_grid_display( NUM_OFDM_SYMBOLS, MAX_SUBCARRIERS, SSB_MASKS ) # ============================================================================= # Tabbed GUI Visualization # ============================================================================= class SSBResourceGridViewer: """ Tabbed GUI for viewing SSB Resource Grid visualizations with interactive parameter configuration """ def __init__(self, initial_params=None): # Initialize with default or provided parameters if initial_params is None: initial_params = { 'ssb_case': SSB_CASE, 'ssb_scs_khz': 30, # Default SSB SCS: 30 kHz 'pdsch_scs_khz': PDSCH_SCS_KHZ, # Default PDSCH SCS: 30 kHz 'l_max': L_MAX, 'ssb_tx_bitmap': SSB_TX_BITMAP, 'channel_bw_mhz': CHANNEL_BW_MHZ, 'n_cell_id': N_CELL_ID, } self.params = initial_params.copy() # Store visualization references for updates self.figures = {} self.canvases = {} self.tabs = {} self.axes = {} self.toolbars = {} # Create main window self.root = tk.Tk() self.root.title("NR SSB Resource Grid Viewer - Interactive") self.root.geometry("1500x1000") # Create main container with vertical layout main_container = ttk.Frame(self.root) main_container.pack(fill='both', expand=True, padx=5, pady=5) # Create status bar first (will be packed at bottom) self.create_status_bar(main_container) # Create two separate notebooks: one for parameters, one for visualizations # Parameters notebook (top, smaller height) self.param_notebook = ttk.Notebook(main_container) self.param_notebook.pack(fill='x', padx=5, pady=(5, 2)) # Visualizations notebook (bottom, takes remaining space) self.notebook = ttk.Notebook(main_container) self.notebook.pack(fill='both', expand=True, padx=5, pady=(2, 5)) # Generate initial resource grid self.generate_resource_grid() # Create parameter tabs self.create_tab_parameters() self.create_tab_mib() self.create_tab_export() # Create visualization tabs self.create_tab0_summary() self.create_tab1_full_grid() self.create_tab2_ssb_structure() self.create_tab3_time_domain() self.create_tab4_constellation() def generate_resource_grid(self): """Generate resource grid based on current parameters""" # Extract parameters ssb_case = self.params['ssb_case'] l_max = self.params['l_max'] ssb_tx_bitmap = self.params['ssb_tx_bitmap'] channel_bw_mhz = self.params['channel_bw_mhz'] n_cell_id = self.params['n_cell_id'] # Get SSB parameters # SSB Case determines the pattern (first_symbols, period) - always from SSB_CASE_PARAMS ssb_case_params = SSB_CASE_PARAMS[ssb_case] ssb_first_symbols = ssb_case_params['first_symbols'] # Always from SSB Case # Use SSB SCS from params if available, otherwise derive from SSB Case if 'ssb_scs_khz' in self.params: ssb_scs_khz = self.params['ssb_scs_khz'] else: ssb_scs_khz = ssb_case_params['scs_khz'] ssb_subcarrier_spacing = ssb_scs_khz * 1e3 # Get PDSCH SCS (grid SCS) - use for resource grid calculation if 'pdsch_scs_khz' in self.params: pdsch_scs_khz = self.params['pdsch_scs_khz'] else: pdsch_scs_khz = PDSCH_SCS_KHZ # Get max RBs for bandwidth and PDSCH SCS (grid SCS) max_rbs = MAX_RBS_TABLE.get(channel_bw_mhz, {}).get(pdsch_scs_khz, None) if max_rbs is None: raise ValueError(f"Invalid combination: {channel_bw_mhz} MHz with {pdsch_scs_khz} kHz PDSCH SCS") # Calculate time domain parameters - use PDSCH SCS for grid (always 5ms) slot_duration_ms = 1.0 / (pdsch_scs_khz / 15) # Slot duration at PDSCH SCS half_frame_ms = 5.0 num_slots_in_half_frame = int(half_frame_ms / slot_duration_ms) num_symbols_per_slot = 14 num_ofdm_symbols = num_slots_in_half_frame * num_symbols_per_slot # FFT size fft_size = FFT_SIZE_TABLE.get(channel_bw_mhz, 2048) # Calculate guard carriers max_subcarriers = max_rbs * 12 base_guard_total = fft_size - max_subcarriers base_guard_left = base_guard_total // 2 base_guard_right = base_guard_total - base_guard_left # SSB position calculation auto_calculate = self.var_auto_ssb_position.get() if hasattr(self, 'var_auto_ssb_position') else False if auto_calculate: # Auto-calculate: center SSB in channel center_start_rb = max(0, (max_rbs - SSB_NUM_RBS) // 2) center_start_sc_common = int(round(center_start_rb * 12 * (ssb_scs_khz / COMMON_SCS_KHZ))) offset_to_pointa_rb = center_start_sc_common // 12 - 20 k_ssb = center_start_sc_common % 12 else: # Use manual values from GUI (if available, otherwise use defaults) if hasattr(self, 'var_offset_to_pointa'): offset_to_pointa_rb = self.var_offset_to_pointa.get() else: offset_to_pointa_rb = 20 # Default value if hasattr(self, 'var_k_ssb'): k_ssb = self.var_k_ssb.get() else: k_ssb = 0 # Default value # Validate k_SSB range (0-23 per 3GPP) if k_ssb < 0 or k_ssb > 23: if hasattr(self, 'update_status'): self.update_status(f"Warning: k_SSB must be 0-23. Clamping to valid range.", 'warning') k_ssb = max(0, min(23, k_ssb)) self.var_k_ssb.set(k_ssb) # Calculate SSB start subcarrier at SSB SCS from offsetToPointA and k_SSB # Formula: ssb_start_sc_ssb = (offsetToPointA * 12 + k_SSB) * (COMMON_SCS / SSB_SCS) ssb_start_sc_ssb = int((offset_to_pointa_rb * 12 + k_ssb) * (COMMON_SCS_KHZ / ssb_scs_khz)) # Convert SSB position to grid subcarriers (at PDSCH SCS) # SSB subcarriers at SSB SCS need to be converted to grid subcarriers at PDSCH SCS # Formula: grid_sc = ssb_sc_ssb * (SSB_SCS / PDSCH_SCS) ssb_start_sc = int(round(ssb_start_sc_ssb * (ssb_scs_khz / pdsch_scs_khz))) ssb_offset_rb = ssb_start_sc // 12 # SSB size in grid subcarriers (at PDSCH SCS) ssb_num_subcarriers_grid = int(round(SSB_NUM_SUBCARRIERS * (ssb_scs_khz / pdsch_scs_khz))) # Update GUI variables if auto-calculated (so user can see the values) if auto_calculate and hasattr(self, 'var_offset_to_pointa'): self.var_offset_to_pointa.set(offset_to_pointa_rb) self.var_k_ssb.set(k_ssb) # Validate SSB position fits within channel if ssb_start_sc < 0: if hasattr(self, 'update_status'): self.update_status(f"Warning: SSB start subcarrier is negative ({ssb_start_sc}). Adjusting to 0.", 'warning') ssb_start_sc = 0 ssb_offset_rb = 0 # Recalculate offsetToPointA and k_SSB from adjusted position ssb_start_sc_common = int(round(ssb_start_sc * (ssb_scs_khz / COMMON_SCS_KHZ))) offset_to_pointa_rb = ssb_start_sc_common // 12 - 20 k_ssb = ssb_start_sc_common % 12 if hasattr(self, 'var_offset_to_pointa'): self.var_offset_to_pointa.set(offset_to_pointa_rb) self.var_k_ssb.set(k_ssb) elif ssb_start_sc + ssb_num_subcarriers_grid > max_subcarriers: if hasattr(self, 'update_status'): self.update_status(f"Warning: SSB extends beyond channel ({ssb_start_sc + ssb_num_subcarriers_grid} > {max_subcarriers}). Adjusting position.", 'warning') ssb_start_sc = max(0, max_subcarriers - ssb_num_subcarriers_grid) ssb_offset_rb = ssb_start_sc // 12 # Recalculate offsetToPointA and k_SSB from adjusted position ssb_start_sc_common = int(round(ssb_start_sc * (ssb_scs_khz / COMMON_SCS_KHZ))) offset_to_pointa_rb = ssb_start_sc_common // 12 - 20 k_ssb = ssb_start_sc_common % 12 if hasattr(self, 'var_offset_to_pointa'): self.var_offset_to_pointa.set(offset_to_pointa_rb) self.var_k_ssb.set(k_ssb) # Get SSB positions - need to calculate at grid SCS (PDSCH SCS) to avoid overlaps # SSB pattern is defined in terms of slots and symbol positions within slots # When SSB SCS != PDSCH SCS, we need to recalculate positions at grid SCS ssb_tx_bitmap_norm = normalize_ssb_bitmap(ssb_tx_bitmap, l_max) # Calculate SSB positions directly at grid SCS (PDSCH SCS) # The SSB pattern parameters (first_symbols, period) are defined at SSB SCS # We need to convert these to grid SCS coordinates params = SSB_CASE_PARAMS[ssb_case] first_symbols_ssb_scs = params['first_symbols'] period_ssb_scs = params['slot_pattern_period'] # Convert SSB positions from SSB SCS to grid SCS # The period is in symbols (one slot = 14 symbols), which is constant regardless of SCS # However, when SSB SCS != grid SCS, we need to calculate positions at SSB SCS first, # then convert each position individually to grid coordinates # OR: Calculate all positions at SSB SCS, then convert each one # Get n_values from frequency range if available, otherwise calculate from l_max n_values_to_use = None frequency_range_key = self.params.get('frequency_range_key') # Get stored key if available if DEBUG: print(f"[DEBUG] SSB Position Calculation:") print(f" SSB Case: {ssb_case}") print(f" SSB SCS: {ssb_scs_khz} kHz") print(f" PDSCH SCS: {pdsch_scs_khz} kHz") print(f" L_max: {l_max}") print(f" first_symbols_ssb_scs: {first_symbols_ssb_scs}") print(f" period_ssb_scs: {period_ssb_scs}") print(f" frequency_range_key from params: {frequency_range_key}") print(f" frequency_range label from params: {self.params.get('frequency_range', 'NOT FOUND')}") # Check if frequency range is specified and get n_values from it if 'frequency_range' in self.params and 'frequency_ranges' in params: freq_range_label = self.params['frequency_range'] freq_ranges = params['frequency_ranges'] if DEBUG: print(f" Looking up frequency range: '{freq_range_label}'") print(f" Available frequency ranges: {list(freq_ranges.keys())}") # First try to use stored frequency_range_key if available freq_range_key = frequency_range_key if frequency_range_key else None if DEBUG: print(f" Using stored freq_range_key: {freq_range_key}") # If no stored key, map label to key if freq_range_key is None: # Map labels (display names) to keys (internal names) label_to_key_map = { 'f <= 3 GHz': 'f_le_3ghz', '3 GHz < f <= 6 GHz': 'f_3_to_6ghz', 'f > 6 GHz': 'f_gt_6ghz', 'f <= 3 GHz (TDD)': 'f_le_3ghz_tdd', 'f <= 3 GHz (TDD f < 1.88 GHz)': 'f_le_3ghz_188', 'f <= 3 GHz (TDD 1.88 < f < 3.0 GHz)': 'f_le_3ghz_188_3', } if DEBUG: print(f" Label to key map: {label_to_key_map}") freq_range_key = label_to_key_map.get(freq_range_label) if DEBUG: print(f" Mapped '{freq_range_label}' -> '{freq_range_key}'") # If not found in label map, try direct match (key might be stored as-is) if freq_range_key is None and freq_range_label in freq_ranges: freq_range_key = freq_range_label if DEBUG: print(f" Direct match found: '{freq_range_key}'") if freq_range_key and freq_range_key in freq_ranges: freq_range_params = freq_ranges[freq_range_key] if DEBUG: print(f" Found frequency range params: {freq_range_params}") if 'n_values' in freq_range_params: n_values_to_use = freq_range_params['n_values'] if DEBUG: print(f" Using n_values from frequency range: {n_values_to_use}") else: if DEBUG: print(f" WARNING: frequency range params missing 'n_values'") else: if DEBUG: print(f" WARNING: freq_range_key '{freq_range_key}' not found in freq_ranges") # For Cases D/E, use n_values from case params directly if n_values_to_use is None and ssb_case in ['D', 'E']: if 'n_values' in params: n_values_to_use = params['n_values'] # Calculate positions at SSB SCS first using n_values or fallback calculation ssb_positions_ssb_scs = [] if n_values_to_use is not None: # Use n_values from frequency range or case params if DEBUG: print(f" Calculating positions using n_values: {n_values_to_use}") for n in n_values_to_use: for first_sym in first_symbols_ssb_scs: sym = first_sym + period_ssb_scs * n ssb_positions_ssb_scs.append(sym) if DEBUG: print(f" n={n}, first_sym={first_sym} -> sym={first_sym} + {period_ssb_scs}*{n} = {sym}") else: # Fallback: calculate n_values from l_max (for backward compatibility) if DEBUG: print(f" WARNING: Using fallback calculation (n_values not found)") if ssb_case == 'A': max_n = l_max // 2 n_values_to_use = list(range(max_n)) elif ssb_case == 'B': max_n = l_max // 4 n_values_to_use = list(range(max_n)) elif ssb_case == 'C': max_n = l_max // 2 n_values_to_use = list(range(max_n)) else: max_n = l_max // len(first_symbols_ssb_scs) n_values_to_use = list(range(max_n)) if DEBUG: print(f" Fallback n_values: {n_values_to_use}") for n in n_values_to_use: for first_sym in first_symbols_ssb_scs: sym = first_sym + period_ssb_scs * n ssb_positions_ssb_scs.append(sym) if DEBUG: print(f" n={n}, first_sym={first_sym} -> sym={first_sym} + {period_ssb_scs}*{n} = {sym}") if DEBUG: print(f" SSB positions at SSB SCS ({ssb_scs_khz} kHz): {ssb_positions_ssb_scs[:l_max]}") # SSB positions remain in SSB SCS units - DO NOT scale by PDSCH SCS # According to 3GPP TS 38.211, SSB symbol positions are defined at the SSB's own SCS # and should NOT be affected by PDSCH SCS ssb_positions_all_grid = ssb_positions_ssb_scs[:l_max] if DEBUG: print(f" SSB positions (using native SSB SCS, not converted to PDSCH SCS): {ssb_positions_all_grid}") print(f" Note: SSB positions are defined at SSB SCS ({ssb_scs_khz} kHz) and remain unchanged") # Apply bitmap to get active SSB positions ssb_active = [(idx, pos) for idx, (bit, pos) in enumerate(zip(ssb_tx_bitmap_norm, ssb_positions_all_grid[:l_max])) if bit == '1'] ssb_symbol_positions = [pos for _, pos in ssb_active] ssb_symbol_indices = [idx for idx, _ in ssb_active] num_ssb_transmitted = len(ssb_symbol_positions) # Ensure SSBs don't overlap (each SSB is 4 symbols long) # Sort positions and check for overlaps, then adjust if needed if len(ssb_symbol_positions) > 1: # Create list of (index, position) pairs to maintain original order pos_pairs = list(zip(ssb_symbol_indices, ssb_symbol_positions)) # Sort by position pos_pairs_sorted = sorted(pos_pairs, key=lambda x: x[1]) # Check and fix overlaps adjusted_positions = [pos_pairs_sorted[0][1]] # First position unchanged overlaps_found = False for i in range(1, len(pos_pairs_sorted)): prev_pos = adjusted_positions[-1] curr_pos = pos_pairs_sorted[i][1] # If current SSB would overlap with previous, adjust it if curr_pos < prev_pos + SSB_NUM_SYMBOLS: overlaps_found = True curr_pos = prev_pos + SSB_NUM_SYMBOLS adjusted_positions.append(curr_pos) # Warn user if overlaps were detected and fixed if overlaps_found and hasattr(self, 'update_status'): self.update_status( f"Warning: SSB positions overlapped after SCS conversion. Positions adjusted to prevent overlap.", 'warning' ) # Restore original order pos_dict = {idx: pos for (idx, _), pos in zip(pos_pairs_sorted, adjusted_positions)} ssb_symbol_positions = [pos_dict[idx] for idx in ssb_symbol_indices] ssb_active = [(idx, pos) for idx, pos in zip(ssb_symbol_indices, ssb_symbol_positions)] # Generate PSS/SSS sequences n_id_1 = n_cell_id // 3 n_id_2 = n_cell_id % 3 pss_sequence = generate_pss_sequence(n_id_2) sss_sequence = generate_sss_sequence(n_id_1, n_id_2) # Generate DMRS sequences for all transmitted SSB indices if DEBUG: print(f"[DEBUG] Generating DMRS sequences for {num_ssb_transmitted} transmitted SSBs") for ssb_idx in ssb_symbol_indices: _ = generate_pbch_dmrs_sequence(n_cell_id, ssb_idx, l_max) # Debug: Print PBCH data summary for each SSB if DEBUG: print("\n" + "=" * 80) print("[DEBUG] PBCH Data Summary for All SSBs") print("=" * 80) # Get MIB parameters (use defaults if not available) if 'mib' in self.params: mib_params = self.params['mib'] sfn = mib_params.get('sfn', 270) k_ssb_msb = (mib_params.get('ssb_subcarrier_offset', 0) >> 4) & 0x1 else: # Use default values if MIB not yet initialized sfn = 270 k_ssb_msb = 0 # Get MIB payload bits (same for all SSBs) if hasattr(self, 'var_sfn'): try: mib_payload_bits_list, _, _ = self.encode_mib_payload() mib_payload_bits = np.array(mib_payload_bits_list, dtype=np.uint8) except: mib_payload_bits = np.zeros(24, dtype=np.uint8) else: mib_payload_bits = np.zeros(24, dtype=np.uint8) # Calculate half-frame indicator (0 for first 5ms, 1 for second 5ms) n_hf = 0 # Could be calculated based on actual timing # Per-SSB Details for i, ssb_idx in enumerate(ssb_symbol_indices): print(f"\nSSB Index {ssb_idx}:") print("-" * 80) # Pack PBCH message (32 bits) a = pbch_msg_pack(mib_payload_bits, sfn, n_hf, ssb_idx, k_ssb_msb, l_max) # Extract overhead bits for display G = PBCH_G_PATTERN sfn_4lsb = sfn & 0x0F hrf_bit = a[G[10]] # Convert 32-bit message to hex and binary # Pack bits into bytes (MSB first per byte, so reverse each byte) a_reshaped = a.reshape(4, 8) # 4 bytes a_reversed = np.flip(a_reshaped, axis=1) # Reverse bits within each byte (MSB first) packed = np.packbits(a_reversed, axis=1).flatten() msg_hex = ''.join(f'{byte:02x}' for byte in packed) msg_binary = ''.join(str(bit) for bit in a) print(f" PBCH Message (32 bits):") print(f" Hex: 0x{msg_hex.upper()}") print(f" Binary: {msg_binary}") print(f" Overhead Bits Breakdown:") print(f" SFN 4 LSB (4 bits): {sfn_4lsb:04b} ({sfn_4lsb})") print(f" HRF (1 bit): {hrf_bit}") if l_max == 64: ssb_idx_msb = (a[G[11]] << 2) | (a[G[12]] << 1) | a[G[13]] print(f" SSB Index MSB (3 bits): {ssb_idx_msb:03b} ({ssb_idx_msb})") else: k_ssb_msb_bit = a[G[11]] print(f" k_SSB MSB (1 bit): {k_ssb_msb_bit}") # Scrambling parameters # Payload scrambling (before CRC) sfn_2nd_lsb = (sfn >> 1) & 1 sfn_3rd_lsb = (sfn >> 2) & 1 v_payload = 2 * sfn_3rd_lsb + sfn_2nd_lsb c_init_payload = n_cell_id & 0x7FFFFFFF M = PBCH_NR_A - 3 if l_max != 64 else PBCH_NR_A - 6 advance_payload = M * v_payload # PBCH scrambling (after rate matching) if l_max == 4: v_pbch = ssb_idx & 0x3 else: v_pbch = ssb_idx & 0x7 c_init_pbch = n_cell_id & 0x7FFFFFFF advance_pbch = v_pbch * PBCH_NR_E print(f" Scrambling Parameters:") print(f" Payload Scrambling (before CRC):") print(f" c_init: 0x{c_init_payload:08X}") print(f" v (from SFN): {v_payload}") print(f" advance: {advance_payload} bits") print(f" PBCH Scrambling (after rate matching):") print(f" c_init: 0x{c_init_pbch:08X}") print(f" v (from SSB index): {v_pbch}") print(f" advance: {advance_pbch} bits") print("\n" + "=" * 80 + "\n") # Create SSB masks with scaled size ssb_scs_ratio = ssb_scs_khz / pdsch_scs_khz ssb_masks = create_ssb_masks( num_ofdm_symbols, max_subcarriers, ssb_start_sc, ssb_symbol_positions, ssb_num_subcarriers=ssb_num_subcarriers_grid, ssb_scs_ratio=ssb_scs_ratio, n_cell_id=n_cell_id ) # Create resource grid display resource_grid_display = create_resource_grid_display( num_ofdm_symbols, max_subcarriers, ssb_masks ) # Store computed values self.rg_display = resource_grid_display self.ssb_masks = ssb_masks self.ssb_info = ssb_active self.ssb_indices = ssb_symbol_indices self.ssb_positions = ssb_symbol_positions # Store computed parameters self.computed_params = { 'ssb_scs_khz': ssb_scs_khz, 'pdsch_scs_khz': pdsch_scs_khz, 'max_rbs': max_rbs, 'fft_size': fft_size, 'num_ofdm_symbols': num_ofdm_symbols, 'num_slots_in_half_frame': num_slots_in_half_frame, 'slot_duration_ms': slot_duration_ms, 'ssb_start_sc': ssb_start_sc, 'ssb_start_sc_ssb': ssb_start_sc_ssb, # SSB position at SSB SCS 'ssb_num_subcarriers_grid': ssb_num_subcarriers_grid, # SSB size in grid subcarriers 'ssb_offset_rb': ssb_offset_rb, 'offset_to_pointa_rb': offset_to_pointa_rb, 'k_ssb': k_ssb, 'num_ssb_transmitted': num_ssb_transmitted, 'n_id_1': n_id_1, 'n_id_2': n_id_2, 'pss_sequence': pss_sequence, 'sss_sequence': sss_sequence, } # Update window title self.root.title(f"NR SSB Resource Grid Viewer - Case {ssb_case}, {channel_bw_mhz} MHz, {ssb_scs_khz} kHz SCS") def create_tab_parameters(self): """Parameters Tab: Parameter Configuration""" tab = ttk.Frame(self.param_notebook) self.param_notebook.add(tab, text="Parameters") self.tabs['parameters'] = tab # Create container for two side-by-side panels panels_container = ttk.Frame(tab) panels_container.pack(fill='both', expand=True, padx=5, pady=5) # Left Panel: Basic SSB Parameters left_panel = ttk.LabelFrame(panels_container, text="SSB Basic Parameters", padding=10) left_panel.pack(side='left', fill='both', expand=True, padx=(0, 5)) # Right Panel: SSB Frequency Position right_panel = ttk.LabelFrame(panels_container, text="SSB Frequency Position (3GPP)", padding=10) right_panel.pack(side='right', fill='both', expand=True, padx=(5, 0)) # Parameter variables self.var_ssb_case = tk.StringVar(value=self.params['ssb_case']) # SSB SCS - default to 30 kHz if not in params if 'ssb_scs_khz' not in self.params: self.params['ssb_scs_khz'] = 30 self.var_ssb_scs_khz = tk.IntVar(value=self.params['ssb_scs_khz']) # PDSCH SCS - default to 30 kHz if not in params if 'pdsch_scs_khz' not in self.params: self.params['pdsch_scs_khz'] = PDSCH_SCS_KHZ self.var_pdsch_scs_khz = tk.IntVar(value=self.params['pdsch_scs_khz']) self.var_l_max = tk.IntVar(value=self.params['l_max']) # Frequency range - initialize with default based on case if 'frequency_range' not in self.params: # Set default based on case (f_3_to_6ghz for A/B/C, f_gt_6ghz for D/E) ssb_case = self.params.get('ssb_case', 'A') if ssb_case in ['D', 'E']: self.params['frequency_range'] = 'f > 6 GHz' else: self.params['frequency_range'] = '3 GHz < f <= 6 GHz' self.var_frequency_range = tk.StringVar(value=self.params['frequency_range']) self.var_ssb_tx_bitmap = tk.StringVar(value=self.params['ssb_tx_bitmap']) self.var_channel_bw_mhz = tk.IntVar(value=self.params['channel_bw_mhz']) self.var_n_cell_id = tk.IntVar(value=self.params['n_cell_id']) # SSB frequency position parameters (3GPP) # Initialize with auto-calculated values (will be set after first generation) self.var_auto_ssb_position = tk.BooleanVar(value=False) # Auto-calculate disabled by default self.var_offset_to_pointa = tk.IntVar(value=20) # RB units at 15 kHz self.var_k_ssb = tk.IntVar(value=0) # Subcarrier offset (0-23) at 15 kHz # Helper function to update TX bitmap based on L_max def update_tx_bitmap(): l_max = self.var_l_max.get() # Set bitmap to all '1's matching L_max new_bitmap = '1' * l_max self.var_ssb_tx_bitmap.set(new_bitmap) # Helper function to get frequency range labels for display def get_frequency_range_labels(case_params): """Get user-friendly labels for frequency ranges""" labels = { 'f_le_3ghz': 'f <= 3 GHz', 'f_3_to_6ghz': '3 GHz < f <= 6 GHz', 'f_gt_6ghz': 'f > 6 GHz', 'f_le_3ghz_tdd': 'f <= 3 GHz (TDD)', 'f_le_3ghz_188': 'f <= 3 GHz (TDD f < 1.88 GHz)', 'f_le_3ghz_188_3': 'f <= 3 GHz (TDD 1.88 < f < 3.0 GHz)', } if 'frequency_ranges' in case_params: freq_ranges = case_params['frequency_ranges'] return [(key, labels.get(key, key)) for key in freq_ranges.keys()] else: # Cases D and E only have f > 6 GHz return [('f_gt_6ghz', 'f > 6 GHz')] # Helper function to update frequency range dropdown based on SSB Case def update_frequency_range_dropdown(): """Update frequency range dropdown options based on selected SSB Case""" ssb_case = self.var_ssb_case.get() if ssb_case in SSB_CASE_PARAMS: case_params = SSB_CASE_PARAMS[ssb_case] freq_range_options = get_frequency_range_labels(case_params) # Store (key, label) pairs self.freq_range_options = freq_range_options # Extract labels for display labels = [label for _, label in freq_range_options] # Update dropdown values if combo exists if hasattr(self, 'freq_range_combo'): self.freq_range_combo['values'] = labels # Set to first option if current selection is not valid current_label = self.var_frequency_range.get() if current_label not in labels: if labels: self.var_frequency_range.set(labels[0]) # Find the corresponding key self.freq_range_key = freq_range_options[0][0] else: # No options available, clear selection self.var_frequency_range.set('') else: # Current selection is valid, find corresponding key for key, label in freq_range_options: if label == current_label: self.freq_range_key = key break # Helper function to update L_max, SSB SCS, and bitmap based on SSB Case and frequency range def update_l_max_from_case(event=None): """Update L_max, SSB SCS, and bitmap when SSB Case changes""" ssb_case = self.var_ssb_case.get() if ssb_case in SSB_CASE_PARAMS: case_params = SSB_CASE_PARAMS[ssb_case] # Update SSB SCS from case params self.var_ssb_scs_khz.set(case_params['scs_khz']) # Update PDSCH SCS from case params if available if 'default_pdsch_scs_khz' in case_params: self.var_pdsch_scs_khz.set(case_params['default_pdsch_scs_khz']) # Update Channel BW from case params if available if 'default_bw_mhz' in case_params: self.var_channel_bw_mhz.set(case_params['default_bw_mhz']) # Update frequency range dropdown update_frequency_range_dropdown() # Get current frequency range selection freq_range_label = self.var_frequency_range.get() if hasattr(self, 'var_frequency_range') else None if freq_range_label and hasattr(self, 'freq_range_options'): # Find the key for this label freq_range_key = None for key, label in self.freq_range_options: if label == freq_range_label: freq_range_key = key if hasattr(self, 'freq_range_key'): self.freq_range_key = key break # Get L_max, bitmap, and PDSCH SCS from frequency range if available if freq_range_key and 'frequency_ranges' in case_params: freq_range_params = case_params['frequency_ranges'].get(freq_range_key) if freq_range_params: self.var_l_max.set(freq_range_params['l_max']) # Use default_bitmap from frequency range if available if 'default_bitmap' in freq_range_params: self.var_ssb_tx_bitmap.set(freq_range_params['default_bitmap']) else: # Fallback to case default self.var_ssb_tx_bitmap.set(case_params['default_bitmap']) # Use default_pdsch_scs_khz from frequency range if available if 'default_pdsch_scs_khz' in freq_range_params: self.var_pdsch_scs_khz.set(freq_range_params['default_pdsch_scs_khz']) elif 'default_pdsch_scs_khz' in case_params: # Fallback to case default self.var_pdsch_scs_khz.set(case_params['default_pdsch_scs_khz']) else: self.var_l_max.set(case_params['l_max']) self.var_ssb_tx_bitmap.set(case_params['default_bitmap']) else: # Cases D/E don't have frequency_ranges dict, use default self.var_l_max.set(case_params['l_max']) self.var_ssb_tx_bitmap.set(case_params['default_bitmap']) else: # Use default L_max and bitmap self.var_l_max.set(case_params['l_max']) self.var_ssb_tx_bitmap.set(case_params['default_bitmap']) # Update status if hasattr(self, 'update_status'): status_msg = f"SSB Case changed to {ssb_case}. Updated SSB SCS={case_params['scs_khz']} kHz, L_max={self.var_l_max.get()}, Bitmap={self.var_ssb_tx_bitmap.get()}" if 'default_pdsch_scs_khz' in case_params or (freq_range_label and hasattr(self, 'freq_range_options')): # Show actual PDSCH SCS value that was set status_msg += f", PDSCH SCS={self.var_pdsch_scs_khz.get()} kHz" if 'default_bw_mhz' in case_params: status_msg += f", Channel BW={case_params['default_bw_mhz']} MHz" self.update_status(status_msg, 'info') # Helper function to update L_max when frequency range changes def update_l_max_from_freq_range(event=None): """Update L_max and bitmap when frequency range changes""" ssb_case = self.var_ssb_case.get() freq_range_label = self.var_frequency_range.get() if ssb_case in SSB_CASE_PARAMS and hasattr(self, 'freq_range_options'): case_params = SSB_CASE_PARAMS[ssb_case] # Find the key for this label freq_range_key = None for key, label in self.freq_range_options: if label == freq_range_label: freq_range_key = key if hasattr(self, 'freq_range_key'): self.freq_range_key = key break # Get L_max and bitmap from frequency range if available if freq_range_key and 'frequency_ranges' in case_params: freq_range_params = case_params['frequency_ranges'].get(freq_range_key) if freq_range_params: self.var_l_max.set(freq_range_params['l_max']) # Use default_bitmap from frequency range if available, otherwise generate from l_max if 'default_bitmap' in freq_range_params: new_bitmap = freq_range_params['default_bitmap'] else: new_bitmap = '1' * freq_range_params['l_max'] self.var_ssb_tx_bitmap.set(new_bitmap) # Use default_pdsch_scs_khz from frequency range if available if 'default_pdsch_scs_khz' in freq_range_params: self.var_pdsch_scs_khz.set(freq_range_params['default_pdsch_scs_khz']) elif 'default_pdsch_scs_khz' in case_params: # Fallback to case default self.var_pdsch_scs_khz.set(case_params['default_pdsch_scs_khz']) status_msg = f"Frequency range changed to {freq_range_label}. Updated L_max={freq_range_params['l_max']}, Bitmap={new_bitmap}" if 'default_pdsch_scs_khz' in freq_range_params: status_msg += f", PDSCH SCS={freq_range_params['default_pdsch_scs_khz']} kHz" if hasattr(self, 'update_status'): self.update_status(status_msg, 'info') # Helper function to validate L_max based on SSB Case def validate_l_max(event=None): ssb_case = self.var_ssb_case.get() l_max = self.var_l_max.get() # Validate L_max based on SSB Case if ssb_case in ['D', 'E']: # FR2 cases must have L_max = 64 if l_max != 64: if hasattr(self, 'update_status'): self.update_status(f"Warning: SSB Case {ssb_case} (FR2) requires L_max = 64. Setting to 64.", 'warning') self.var_l_max.set(64) update_tx_bitmap() else: # Case A, B, C # FR1 cases can have L_max = 4 or 8 if l_max not in [4, 8]: if hasattr(self, 'update_status'): self.update_status(f"Warning: SSB Case {ssb_case} (FR1) requires L_max = 4 or 8. Setting to 8.", 'warning') self.var_l_max.set(8) update_tx_bitmap() update_tx_bitmap() # ===================================================================== # Left Panel: Basic SSB Parameters # ===================================================================== left_row = 0 # SSB Case ttk.Label(left_panel, text="SSB Case:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) ssb_case_combo = ttk.Combobox(left_panel, textvariable=self.var_ssb_case, values=['A', 'B', 'C', 'D', 'E'], state='readonly', width=15) ssb_case_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) ssb_case_combo.bind('<>', update_l_max_from_case) left_row += 1 # Frequency Range (populated based on SSB Case) ttk.Label(left_panel, text="Frequency Range:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) self.freq_range_combo = ttk.Combobox(left_panel, textvariable=self.var_frequency_range, state='readonly', width=25) self.freq_range_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) self.freq_range_combo.bind('<>', update_l_max_from_freq_range) # Initialize frequency range dropdown for current case update_frequency_range_dropdown() left_row += 1 # SSB SCS ttk.Label(left_panel, text="SSB SCS:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) ssb_scs_combo = ttk.Combobox(left_panel, textvariable=self.var_ssb_scs_khz, values=[15, 30, 60, 120, 240], state='readonly', width=15) ssb_scs_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # PDSCH SCS ttk.Label(left_panel, text="PDSCH SCS:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) pdsch_scs_combo = ttk.Combobox(left_panel, textvariable=self.var_pdsch_scs_khz, values=[15, 30, 60, 120, 240], state='readonly', width=15) pdsch_scs_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # L_max ttk.Label(left_panel, text="L_max:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) l_max_combo = ttk.Combobox(left_panel, textvariable=self.var_l_max, values=[4, 8, 64], state='readonly', width=15) l_max_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) l_max_combo.bind('<>', validate_l_max) left_row += 1 # SSB TX Bitmap ttk.Label(left_panel, text="SSB TX Bitmap:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) ssb_bitmap_entry = ttk.Entry(left_panel, textvariable=self.var_ssb_tx_bitmap, width=67) ssb_bitmap_entry.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # Channel Bandwidth ttk.Label(left_panel, text="Channel BW (MHz):").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) channel_bw_combo = ttk.Combobox(left_panel, textvariable=self.var_channel_bw_mhz, values=[5, 10, 15, 20, 25, 30, 40, 50, 60, 80, 100], state='readonly', width=15) channel_bw_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # Cell ID ttk.Label(left_panel, text="Cell ID (PCI):").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) cell_id_spinbox = ttk.Spinbox(left_panel, from_=0, to=1007, textvariable=self.var_n_cell_id, width=15) cell_id_spinbox.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # ===================================================================== # Right Panel: SSB Frequency Position # ===================================================================== right_row = 0 # Auto-calculate SSB position checkbox auto_pos_check = ttk.Checkbutton(right_panel, text="Auto-calculate SSB position (center in channel)", variable=self.var_auto_ssb_position) auto_pos_check.grid(row=right_row, column=0, columnspan=2, sticky='w', padx=5, pady=5) right_row += 1 # offsetToPointA (RB units at 15 kHz) ttk.Label(right_panel, text="offsetToPointA (RB@15kHz):").grid(row=right_row, column=0, sticky='w', padx=5, pady=5) offset_to_pointa_spinbox = ttk.Spinbox(right_panel, from_=-100, to=1000, textvariable=self.var_offset_to_pointa, width=15) offset_to_pointa_spinbox.grid(row=right_row, column=1, sticky='w', padx=5, pady=5) right_row += 1 ttk.Label(right_panel, text="Offset from Point A to SSB start, in RBs at 15 kHz", font=('TkDefaultFont', 8)).grid(row=right_row, column=0, columnspan=2, sticky='w', padx=5, pady=2) right_row += 1 # k_SSB (subcarrier offset 0-23 at 15 kHz) ttk.Label(right_panel, text="k_SSB (SC@15kHz):").grid(row=right_row, column=0, sticky='w', padx=5, pady=5) k_ssb_spinbox = ttk.Spinbox(right_panel, from_=0, to=23, textvariable=self.var_k_ssb, width=15) k_ssb_spinbox.grid(row=right_row, column=1, sticky='w', padx=5, pady=5) right_row += 1 ttk.Label(right_panel, text="Subcarrier offset within RB, 0-23 at 15 kHz", font=('TkDefaultFont', 8)).grid(row=right_row, column=0, columnspan=2, sticky='w', padx=5, pady=2) right_row += 1 # Enable/disable manual fields based on auto checkbox def toggle_manual_fields(): state = 'disabled' if self.var_auto_ssb_position.get() else 'normal' offset_to_pointa_spinbox.config(state=state) k_ssb_spinbox.config(state=state) auto_pos_check.config(command=toggle_manual_fields) toggle_manual_fields() # Initial state # Add button panel at the bottom of the parameters tab button_panel = ttk.Frame(tab) button_panel.pack(fill='x', padx=5, pady=10) # Create separator above button separator_top = ttk.Separator(button_panel, orient='horizontal') separator_top.pack(fill='x', pady=(0, 10)) # Button container (centered) button_container = ttk.Frame(button_panel) button_container.pack(expand=True) # Generate/Update button self.generate_btn = ttk.Button(button_container, text="Generate/Update Resource Grid", command=self.update_resource_grid, width=30) self.generate_btn.pack() # Create separator below button separator_bottom = ttk.Separator(button_panel, orient='horizontal') separator_bottom.pack(fill='x', pady=(10, 0)) def create_status_bar(self, parent): """Create status bar at the bottom of the window""" # Create separator above status bar separator = ttk.Separator(parent, orient='horizontal') separator.pack(fill='x', pady=(5, 0), side='bottom') # Create status bar frame (pack at bottom) self.status_frame = ttk.Frame(parent) self.status_frame.pack(fill='x', padx=5, pady=5, side='bottom') # Status label self.status_label = ttk.Label(self.status_frame, text="Ready", relief=tk.SUNKEN, anchor='w', padding=5) self.status_label.pack(fill='x', side='left', expand=True) def update_status(self, message, status_type='info'): """Update status bar message Parameters: ----------- message : str Status message to display status_type : str Type of status: 'info', 'error', 'success', 'warning' """ if hasattr(self, 'status_label'): self.status_label.config(text=message) # Optionally change color based on status type if status_type == 'error': self.status_label.config(foreground='red') elif status_type == 'success': self.status_label.config(foreground='green') elif status_type == 'warning': self.status_label.config(foreground='orange') else: # info self.status_label.config(foreground='black') def create_tab_mib(self): """MIB Tab: MIB Information Elements Configuration""" tab = ttk.Frame(self.param_notebook) self.param_notebook.add(tab, text="MIB") self.tabs['mib'] = tab # Create container for two columns container = ttk.Frame(tab) container.pack(fill='both', expand=True, padx=10, pady=10) # Left column left_col = ttk.Frame(container) left_col.pack(side='left', fill='both', expand=True, padx=(0, 5)) # Right column right_col = ttk.Frame(container) right_col.pack(side='right', fill='both', expand=True, padx=(5, 0)) # Initialize MIB parameters if not exists (default values) if 'mib' not in self.params: self.params['mib'] = { 'sfn': 270, # System Frame Number (0-1023) 'scs_common': 30, # Subcarrier Spacing Common (15 or 30 kHz) 'ssb_subcarrier_offset': 0, # SSB Subcarrier Offset (0-15, 4 MSB bits in MIB) 'dmrs_typea_position': 'pos2', # DMRS Type A Position (pos2 or pos3) 'coreset0_idx': 10, # ControlResourceSetZero index (0-15) 'ss0_idx': 0, # SearchSpaceZero index (0-15) 'cell_barred': False, # Cell Barred (False=notBarred, True=barred) 'intra_freq_reselection': True, # Intra-Freq Reselection (True=allowed, False=notAllowed) } # MIB parameter variables self.var_sfn = tk.IntVar(value=self.params['mib']['sfn']) self.var_scs_common = tk.IntVar(value=self.params['mib']['scs_common']) self.var_ssb_subcarrier_offset = tk.IntVar(value=self.params['mib']['ssb_subcarrier_offset']) self.var_dmrs_typea_position = tk.StringVar(value=self.params['mib']['dmrs_typea_position']) self.var_coreset0_idx = tk.IntVar(value=self.params['mib']['coreset0_idx']) self.var_ss0_idx = tk.IntVar(value=self.params['mib']['ss0_idx']) self.var_cell_barred = tk.BooleanVar(value=self.params['mib']['cell_barred']) self.var_intra_freq_reselection = tk.BooleanVar(value=self.params['mib']['intra_freq_reselection']) # Left Column left_row = 0 # System Frame Number (SFN) - 0-1023 ttk.Label(left_col, text="System Frame Number (SFN):").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) sfn_spinbox = ttk.Spinbox(left_col, from_=0, to=1023, textvariable=self.var_sfn, width=15) sfn_spinbox.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # Subcarrier Spacing Common - 15 or 30 kHz ttk.Label(left_col, text="Subcarrier Spacing Common:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) scs_combo = ttk.Combobox(left_col, textvariable=self.var_scs_common, values=[15, 30], state='readonly', width=12) scs_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # SSB Subcarrier Offset (k_SSB LSB) - 0-15 (4 bits) ttk.Label(left_col, text="SSB Subcarrier Offset (k_SSB LSB):").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) ssb_offset_spinbox = ttk.Spinbox(left_col, from_=0, to=15, textvariable=self.var_ssb_subcarrier_offset, width=15) ssb_offset_spinbox.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # DMRS Type A Position ttk.Label(left_col, text="DMRS Type A Position:").grid(row=left_row, column=0, sticky='w', padx=5, pady=5) dmrs_combo = ttk.Combobox(left_col, textvariable=self.var_dmrs_typea_position, values=['pos2', 'pos3'], state='readonly', width=12) dmrs_combo.grid(row=left_row, column=1, sticky='w', padx=5, pady=5) left_row += 1 # Cell Barred cell_barred_check = ttk.Checkbutton(left_col, text="Cell Barred", variable=self.var_cell_barred) cell_barred_check.grid(row=left_row, column=0, columnspan=2, sticky='w', padx=5, pady=5) left_row += 1 # Right Column right_row = 0 # PDCCH Config SIB1 - ControlResourceSetZero ttk.Label(right_col, text="CORESET0 Index:").grid(row=right_row, column=0, sticky='w', padx=5, pady=5) coreset0_spinbox = ttk.Spinbox(right_col, from_=0, to=15, textvariable=self.var_coreset0_idx, width=15) coreset0_spinbox.grid(row=right_row, column=1, sticky='w', padx=5, pady=5) right_row += 1 # PDCCH Config SIB1 - SearchSpaceZero ttk.Label(right_col, text="SS0 Index:").grid(row=right_row, column=0, sticky='w', padx=5, pady=5) ss0_spinbox = ttk.Spinbox(right_col, from_=0, to=15, textvariable=self.var_ss0_idx, width=15) ss0_spinbox.grid(row=right_row, column=1, sticky='w', padx=5, pady=5) right_row += 1 # Intra-Freq Reselection intra_freq_check = ttk.Checkbutton(right_col, text="Intra-Freq Reselection Allowed", variable=self.var_intra_freq_reselection) intra_freq_check.grid(row=right_row, column=0, columnspan=2, sticky='w', padx=5, pady=5) right_row += 1 # Add separator and encoded payload display payload_frame = ttk.LabelFrame(tab, text="Encoded MIB Payload (24 bits)", padding=10) payload_frame.pack(fill='x', padx=10, pady=10) # Payload display (read-only text) self.mib_payload_text = tk.Text(payload_frame, height=4, width=80, wrap=tk.WORD, font=('Courier', 9), state='disabled') self.mib_payload_text.pack(fill='x', padx=5, pady=5) # Update payload display self.update_mib_payload_display() # Bind update events to all MIB parameter widgets for widget in [sfn_spinbox, scs_combo, ssb_offset_spinbox, dmrs_combo, coreset0_spinbox, ss0_spinbox, cell_barred_check, intra_freq_check]: if isinstance(widget, ttk.Spinbox): widget.configure(command=self.update_mib_payload_display) elif isinstance(widget, ttk.Combobox): widget.bind('<>', lambda e: self.update_mib_payload_display()) elif isinstance(widget, ttk.Checkbutton): widget.configure(command=self.update_mib_payload_display) # Also bind to variable changes (using trace for compatibility) self.var_sfn.trace('w', lambda *args: self.update_mib_payload_display()) self.var_scs_common.trace('w', lambda *args: self.update_mib_payload_display()) self.var_ssb_subcarrier_offset.trace('w', lambda *args: self.update_mib_payload_display()) self.var_dmrs_typea_position.trace('w', lambda *args: self.update_mib_payload_display()) self.var_coreset0_idx.trace('w', lambda *args: self.update_mib_payload_display()) self.var_ss0_idx.trace('w', lambda *args: self.update_mib_payload_display()) self.var_cell_barred.trace('w', lambda *args: self.update_mib_payload_display()) self.var_intra_freq_reselection.trace('w', lambda *args: self.update_mib_payload_display()) def encode_mib_payload(self): """ Encode MIB information elements into 24-bit payload (3GPP TS 38.331). Returns: -------- tuple: (payload_bits, payload_hex, payload_binary) - payload_bits: list of 24 bits (0 or 1) - payload_hex: hex string (6 hex digits) - payload_binary: binary string (24 bits) """ if not hasattr(self, 'var_sfn'): return ([0]*24, "000000", "0"*24) # Get MIB parameter values sfn = self.var_sfn.get() scs_common = self.var_scs_common.get() ssb_offset = self.var_ssb_subcarrier_offset.get() dmrs_pos = self.var_dmrs_typea_position.get() coreset0_idx = self.var_coreset0_idx.get() ss0_idx = self.var_ss0_idx.get() cell_barred = self.var_cell_barred.get() intra_freq_reselection = self.var_intra_freq_reselection.get() # Initialize 24-bit payload (all zeros) payload_bits = [0] * 24 bit_ptr = 0 # Bit 0: MIB indicator (always 0) payload_bits[bit_ptr] = 0 bit_ptr += 1 # Bits 1-6: System Frame Number MSB (6 bits) # SFN is 10 bits total, extract 6 MSB bits (bits 9-4) sfn_msb = (sfn >> 4) & 0x3F # 6 bits for i in range(6): payload_bits[bit_ptr + i] = (sfn_msb >> (5 - i)) & 1 bit_ptr += 6 # Bit 7: Subcarrier Spacing Common # Encoding: 0 for 15kHz or 60kHz, 1 for 30kHz or 120kHz (matches srsRAN) payload_bits[bit_ptr] = 0 if scs_common in [15, 60] else 1 bit_ptr += 1 # Bits 8-11: SSB Subcarrier Offset LSB (4 bits) # Note: Only 4 LSB bits of k_SSB are in MIB payload # The 4 MSB bits are in PBCH message context ssb_offset_lsb = ssb_offset & 0x0F # 4 LSB bits for i in range(4): payload_bits[bit_ptr + i] = (ssb_offset_lsb >> (3 - i)) & 1 bit_ptr += 4 # Bit 12: DMRS Type A Position (0=pos2, 1=pos3) payload_bits[bit_ptr] = 1 if dmrs_pos == 'pos3' else 0 bit_ptr += 1 # Bits 13-16: ControlResourceSetZero (4 bits) for i in range(4): payload_bits[bit_ptr + i] = (coreset0_idx >> (3 - i)) & 1 bit_ptr += 4 # Bits 17-20: SearchSpaceZero (4 bits) for i in range(4): payload_bits[bit_ptr + i] = (ss0_idx >> (3 - i)) & 1 bit_ptr += 4 # Bit 21: Cell Barred (0=barred, 1=notBarred) payload_bits[bit_ptr] = 0 if cell_barred else 1 bit_ptr += 1 # Bit 22: Intra-Freq Reselection (0=allowed, 1=notAllowed) payload_bits[bit_ptr] = 0 if intra_freq_reselection else 1 bit_ptr += 1 # Bit 23: Reserved (typically 0) payload_bits[bit_ptr] = 0 # Convert to hex string (3 bytes, MSB first) payload_bytes = bytearray(3) for i in range(24): byte_idx = i // 8 bit_pos = 7 - (i % 8) # MSB first if payload_bits[i]: payload_bytes[byte_idx] |= (1 << bit_pos) payload_hex = ''.join(f'{b:02x}' for b in payload_bytes) payload_binary = ''.join(str(b) for b in payload_bits) return payload_bits, payload_hex, payload_binary def update_mib_payload_display(self): """Update the MIB payload display with encoded payload""" if not hasattr(self, 'mib_payload_text'): return try: payload_bits, payload_hex, payload_binary = self.encode_mib_payload() # Format display display_text = f"Hex: 0x{payload_hex.upper()}\n" display_text += f"Binary: {payload_binary}\n" display_text += f"Bytes: {payload_hex[0:2]} {payload_hex[2:4]} {payload_hex[4:6]}" self.mib_payload_text.config(state='normal') self.mib_payload_text.delete(1.0, tk.END) self.mib_payload_text.insert(1.0, display_text) self.mib_payload_text.config(state='disabled') except Exception as e: if hasattr(self, 'mib_payload_text'): self.mib_payload_text.config(state='normal') self.mib_payload_text.delete(1.0, tk.END) self.mib_payload_text.insert(1.0, f"Error encoding MIB payload: {str(e)}") self.mib_payload_text.config(state='disabled') def create_tab_export(self): """Export Tab: Export configuration and file save""" tab = ttk.Frame(self.param_notebook) self.param_notebook.add(tab, text="Export") self.tabs['export'] = tab # Initialize export parameters if not exists if 'export' not in self.params: self.params['export'] = { 'sample_rate_msps': 23.04, 'duration_ms': 20, 'ssb_period_ms': 20, 'filename': 'ssb_output.bin', 'normalize': False } # Export parameter variables self.var_sample_rate_msps = tk.DoubleVar(value=self.params['export']['sample_rate_msps']) self.var_duration_ms = tk.IntVar(value=self.params['export']['duration_ms']) self.var_ssb_period_ms = tk.IntVar(value=self.params['export']['ssb_period_ms']) self.var_export_filename = tk.StringVar(value=self.params['export']['filename']) self.var_export_normalize = tk.BooleanVar(value=self.params['export'].get('normalize', False)) # Create container with padding container = ttk.Frame(tab) container.pack(fill='both', expand=True, padx=10, pady=10) row = 0 # Sample Rate (Ms/s) ttk.Label(container, text="Sample Rate (Ms/s):").grid(row=row, column=0, sticky='w', padx=5, pady=5) sample_rate_spinbox = ttk.Spinbox(container, from_=1.0, to=200.0, increment=0.01, textvariable=self.var_sample_rate_msps, width=15) sample_rate_spinbox.grid(row=row, column=1, sticky='w', padx=5, pady=5) row += 1 # Duration (ms) ttk.Label(container, text="Duration (ms):").grid(row=row, column=0, sticky='w', padx=5, pady=5) duration_spinbox = ttk.Spinbox(container, from_=1, to=1000, textvariable=self.var_duration_ms, width=15) duration_spinbox.grid(row=row, column=1, sticky='w', padx=5, pady=5) row += 1 # SSB Period (ms) ttk.Label(container, text="SSB Period (ms):").grid(row=row, column=0, sticky='w', padx=5, pady=5) ssb_period_spinbox = ttk.Spinbox(container, from_=1, to=1000, textvariable=self.var_ssb_period_ms, width=15) ssb_period_spinbox.grid(row=row, column=1, sticky='w', padx=5, pady=5) row += 1 # Normalize option normalize_checkbox = ttk.Checkbutton(container, text="Normalize (amplitude to 1.0)", variable=self.var_export_normalize) normalize_checkbox.grid(row=row, column=0, columnspan=2, sticky='w', padx=5, pady=5) row += 1 # File name ttk.Label(container, text="File Name:").grid(row=row, column=0, sticky='w', padx=5, pady=5) filename_frame = ttk.Frame(container) filename_frame.grid(row=row, column=1, sticky='ew', padx=5, pady=5) filename_entry = ttk.Entry(filename_frame, textvariable=self.var_export_filename, width=40) filename_entry.pack(side='left', fill='x', expand=True) browse_button = ttk.Button(filename_frame, text="Browse...", command=self.browse_export_file) browse_button.pack(side='right', padx=(5, 0)) row += 1 # Export button export_button_frame = ttk.Frame(container) export_button_frame.grid(row=row, column=0, columnspan=2, pady=15) export_button = ttk.Button(export_button_frame, text="Export", command=self.export_ssb_data, width=20) export_button.pack() row += 1 # Configure column weights for proper layout container.columnconfigure(1, weight=1) filename_frame.columnconfigure(0, weight=1) def browse_export_file(self): """Open file save dialog for export file""" initial_filename = self.var_export_filename.get() if self.var_export_filename.get() else "ssb_output.bin" filename = filedialog.asksaveasfilename( title="Save SSB Export File", defaultextension=".bin", filetypes=[("Binary files", "*.bin"), ("All files", "*.*")], initialfile=initial_filename ) if filename: self.var_export_filename.set(filename) def export_ssb_data(self): """Export SSB data to file (fc32 format: interleaved float32 I/Q)""" # Get export parameters sample_rate_msps = self.var_sample_rate_msps.get() duration_ms = self.var_duration_ms.get() ssb_period_ms = self.var_ssb_period_ms.get() filename = self.var_export_filename.get() if not filename: messagebox.showerror("Error", "Please specify a file name.") return try: # Update status if hasattr(self, 'update_status'): self.update_status(f"Export: Generating SSB IQ data...", 'info') # Generate resource grid and convert to time domain sample_rate_hz = sample_rate_msps * 1e6 total_samples = int(sample_rate_hz * duration_ms / 1000.0) # Get current SSB parameters ssb_case = self.params['ssb_case'] l_max = self.params['l_max'] ssb_tx_bitmap = self.params['ssb_tx_bitmap'] channel_bw_mhz = self.params['channel_bw_mhz'] n_cell_id = self.params['n_cell_id'] # Get SSB SCS and PDSCH SCS if 'ssb_scs_khz' in self.params: ssb_scs_khz = self.params['ssb_scs_khz'] else: ssb_scs_khz = SSB_CASE_PARAMS[ssb_case]['scs_khz'] if 'pdsch_scs_khz' in self.params: pdsch_scs_khz = self.params['pdsch_scs_khz'] else: pdsch_scs_khz = 30 # Calculate FFT size based on sample rate and SSB SCS ssb_scs_hz = ssb_scs_khz * 1e3 fft_size = int(round(sample_rate_hz / ssb_scs_hz)) # CP length calculation (3GPP TS 38.211) cp_length = int(fft_size * 144 / 2048) # Normal CP length # Calculate number of OFDM symbols needed symbol_duration_samples = fft_size + cp_length num_symbols = int(np.ceil(total_samples / symbol_duration_samples)) # Get max RBs for bandwidth max_rbs = MAX_RBS_TABLE.get(channel_bw_mhz, {}).get(pdsch_scs_khz, 106) max_subcarriers = max_rbs * 12 # Generate time domain IQ samples iq_samples = self.generate_ssb_time_domain( sample_rate_hz=sample_rate_hz, num_symbols=num_symbols, fft_size=fft_size, cp_length=cp_length, max_subcarriers=max_subcarriers, ssb_case=ssb_case, l_max=l_max, ssb_tx_bitmap=ssb_tx_bitmap, ssb_scs_khz=ssb_scs_khz, pdsch_scs_khz=pdsch_scs_khz, n_cell_id=n_cell_id, ssb_period_ms=ssb_period_ms ) # Truncate to exact duration iq_samples = iq_samples[:total_samples] # Normalize if requested normalize_enabled = self.var_export_normalize.get() if normalize_enabled: max_magnitude = np.max(np.abs(iq_samples)) if max_magnitude > 0: iq_samples = iq_samples / max_magnitude if hasattr(self, 'update_status'): self.update_status(f"Normalized IQ samples: max magnitude was {max_magnitude:.6f}", 'info') # Generate metadata filename (same name, .meta extension) base_name = os.path.splitext(filename)[0] metadata_filename = base_name + '.meta' # Write binary file (fc32 format: interleaved float32 I/Q) with open(filename, 'wb') as f: for sample in iq_samples: # Write as interleaved I/Q (float32 each) f.write(struct.pack('ff', np.real(sample), np.imag(sample))) # Write metadata file (plain text) with open(metadata_filename, 'w') as f: f.write(f"sample_rate_msps={sample_rate_msps}\n") f.write(f"duration_ms={duration_ms}\n") f.write(f"ssb_period_ms={ssb_period_ms}\n") f.write(f"channel_bw_mhz={channel_bw_mhz}\n") f.write(f"ssb_case={ssb_case}\n") f.write(f"l_max={l_max}\n") f.write(f"ssb_tx_bitmap={ssb_tx_bitmap}\n") f.write(f"n_cell_id={n_cell_id}\n") f.write(f"ssb_scs_khz={ssb_scs_khz}\n") f.write(f"pdsch_scs_khz={pdsch_scs_khz}\n") f.write(f"fft_size={fft_size}\n") f.write(f"cp_length={cp_length}\n") f.write(f"normalized={normalize_enabled}\n") if hasattr(self, 'var_offset_to_pointa'): f.write(f"offset_to_pointa={self.var_offset_to_pointa.get()}\n") if hasattr(self, 'var_k_ssb'): f.write(f"k_ssb={self.var_k_ssb.get()}\n") # Add MIB parameters if available if 'mib' in self.params: mib_params = self.params['mib'] f.write(f"mib_sfn={mib_params.get('sfn', 0)}\n") f.write(f"mib_scs_common={mib_params.get('scs_common', 30)}\n") f.write(f"mib_ssb_subcarrier_offset={mib_params.get('ssb_subcarrier_offset', 0)}\n") # Update status if hasattr(self, 'update_status'): self.update_status(f"Export successful: {filename} and {metadata_filename} saved ({len(iq_samples)} samples)", 'success') messagebox.showinfo("Export Complete", f"Files saved successfully:\n\n" f"Data: {filename}\n" f"Metadata: {metadata_filename}\n\n" f"Samples: {len(iq_samples)}\n" f"Duration: {len(iq_samples) / sample_rate_hz * 1000:.2f} ms") except Exception as e: import traceback error_msg = f"Error during export: {str(e)}\n\n{traceback.format_exc()}" messagebox.showerror("Export Error", error_msg) if hasattr(self, 'update_status'): self.update_status(f"Export failed: {str(e)}", 'error') def generate_ssb_time_domain(self, sample_rate_hz, num_symbols, fft_size, cp_length, max_subcarriers, ssb_case, l_max, ssb_tx_bitmap, ssb_scs_khz, pdsch_scs_khz, n_cell_id, ssb_period_ms): """ Generate time domain IQ samples with SSB symbols. Returns: -------- np.ndarray: Time domain IQ samples (complex) """ # Get SSB positions at SSB SCS ssb_tx_bitmap_norm = normalize_ssb_bitmap(ssb_tx_bitmap, l_max) ssb_positions_all = get_ssb_symbol_positions(ssb_case, l_max, l_max) ssb_active = [(idx, pos) for idx, (bit, pos) in enumerate(zip(ssb_tx_bitmap_norm, ssb_positions_all)) if bit == '1'] # Calculate slot duration at SSB SCS slot_duration_ms = 1.0 / (ssb_scs_khz / 15) num_symbols_per_slot = 14 # Calculate SSB start subcarrier at SSB SCS if hasattr(self, 'var_offset_to_pointa') and hasattr(self, 'var_k_ssb'): offset_to_pointa_rb = self.var_offset_to_pointa.get() k_ssb = self.var_k_ssb.get() else: offset_to_pointa_rb = 20 k_ssb = 0 # SSB start subcarrier at 15 kHz reference, convert to SSB SCS ssb_start_sc = int((offset_to_pointa_rb * 12 + k_ssb) * (15.0 / ssb_scs_khz)) # Generate PSS/SSS sequences n_id_1 = n_cell_id // 3 n_id_2 = n_cell_id % 3 pss_sequence = generate_pss_sequence(n_id_2) sss_sequence = generate_sss_sequence(n_id_1, n_id_2) # Initialize resource grid at SSB SCS (frequency domain) resource_grid = np.zeros((num_symbols, fft_size), dtype=complex) # Calculate guard carriers (center the active subcarriers) guard_left = (fft_size - max_subcarriers) // 2 # Calculate SSB period in symbols ssb_period_symbols = int(round(ssb_period_ms / slot_duration_ms * num_symbols_per_slot)) # Place SSBs at their symbol positions (repeat every ssb_period_ms) for period_offset in range(0, num_symbols, ssb_period_symbols): for ssb_idx, ssb_start_sym in ssb_active: symbol_idx = period_offset + ssb_start_sym if symbol_idx + 3 >= num_symbols: continue # SSB doesn't fit # Generate PBCH symbols for this SSB if 'mib' in self.params: try: mib_params = self.params['mib'] sfn = mib_params.get('sfn', 0) k_ssb_msb = (mib_params.get('ssb_subcarrier_offset', 0) >> 4) & 0x1 mib_payload_bits_list, _, _ = self.encode_mib_payload() mib_payload_bits = np.array(mib_payload_bits_list, dtype=np.uint8) n_hf = 0 # Half-frame indicator pbch_symbols = encode_pbch( mib_payload_bits=mib_payload_bits, n_cell_id=n_cell_id, sfn=sfn, ssb_idx=ssb_idx, k_ssb_msb=k_ssb_msb, Lmax=l_max, n_hf=n_hf ) except: # Fallback: generate random PBCH if encoding fails qpsk_constellation = np.array([1+1j, 1-1j, -1+1j, -1-1j]) / np.sqrt(2) pbch_symbols = qpsk_constellation[np.random.randint(0, 4, 432)] else: # Fallback: generate random PBCH qpsk_constellation = np.array([1+1j, 1-1j, -1+1j, -1-1j]) / np.sqrt(2) pbch_symbols = qpsk_constellation[np.random.randint(0, 4, 432)] # Generate DMRS sequence dmrs_sequence = generate_pbch_dmrs_sequence(n_cell_id, ssb_idx, l_max) # Calculate v_bar for DMRS positions (at SSB SCS, spacing = 4) v_bar = n_cell_id % 4 # SSB structure: 240 subcarriers at SSB SCS ssb_num_subcarriers = 240 ssb_start_sc_in_grid = guard_left + ssb_start_sc ssb_end_sc_in_grid = ssb_start_sc_in_grid + ssb_num_subcarriers # Ensure SSB fits within FFT size if ssb_end_sc_in_grid > fft_size: ssb_end_sc_in_grid = fft_size ssb_start_sc_in_grid = ssb_end_sc_in_grid - ssb_num_subcarriers # SSB structure (all offsets at SSB SCS): # Symbol 0: PSS (SC 56-182, 127 subcarriers) # Symbol 1: PBCH + DMRS (all 240 SC, DMRS every 4th SC starting at v_bar) # Symbol 2: SSS (SC 56-182) + PBCH (SC 0-55, 183-239) + DMRS # Symbol 3: PBCH + DMRS (all 240 SC) PSS_SSS_START_SC = 56 PSS_SSS_END_SC = 183 pbch_idx = 0 dmrs_idx = 0 # Symbol 0: PSS (SC 56-182) for sc_offset in range(PSS_SSS_START_SC, PSS_SSS_END_SC): sc_grid = ssb_start_sc_in_grid + sc_offset if 0 <= sc_grid < fft_size: resource_grid[symbol_idx, sc_grid] = pss_sequence[sc_offset - PSS_SSS_START_SC] # Symbol 1: PBCH + DMRS (all 240 SC) for sc_offset in range(ssb_num_subcarriers): sc_grid = ssb_start_sc_in_grid + sc_offset if 0 <= sc_grid < fft_size: if sc_offset >= v_bar and (sc_offset - v_bar) % 4 == 0: # DMRS position if dmrs_idx < len(dmrs_sequence): resource_grid[symbol_idx + 1, sc_grid] = dmrs_sequence[dmrs_idx] dmrs_idx += 1 else: # PBCH position if pbch_idx < len(pbch_symbols): resource_grid[symbol_idx + 1, sc_grid] = pbch_symbols[pbch_idx] pbch_idx += 1 # Symbol 2: SSS (center) + PBCH (sides) + DMRS pbch_idx = 0 dmrs_idx = 0 # Left PBCH (SC 0-55) for sc_offset in range(PSS_SSS_START_SC): sc_grid = ssb_start_sc_in_grid + sc_offset if 0 <= sc_grid < fft_size: if sc_offset >= v_bar and (sc_offset - v_bar) % 4 == 0: if dmrs_idx < len(dmrs_sequence): resource_grid[symbol_idx + 2, sc_grid] = dmrs_sequence[dmrs_idx] dmrs_idx += 1 else: if pbch_idx < len(pbch_symbols): resource_grid[symbol_idx + 2, sc_grid] = pbch_symbols[pbch_idx] pbch_idx += 1 # SSS (SC 56-182) for sc_offset in range(PSS_SSS_START_SC, PSS_SSS_END_SC): sc_grid = ssb_start_sc_in_grid + sc_offset if 0 <= sc_grid < fft_size: resource_grid[symbol_idx + 2, sc_grid] = sss_sequence[sc_offset - PSS_SSS_START_SC] # Right PBCH (SC 183-239) for sc_offset in range(PSS_SSS_END_SC, ssb_num_subcarriers): sc_grid = ssb_start_sc_in_grid + sc_offset if 0 <= sc_grid < fft_size: if sc_offset >= v_bar and (sc_offset - v_bar) % 4 == 0: if dmrs_idx < len(dmrs_sequence): resource_grid[symbol_idx + 2, sc_grid] = dmrs_sequence[dmrs_idx] dmrs_idx += 1 else: if pbch_idx < len(pbch_symbols): resource_grid[symbol_idx + 2, sc_grid] = pbch_symbols[pbch_idx] pbch_idx += 1 # Symbol 3: PBCH + DMRS (all 240 SC) pbch_idx = 0 dmrs_idx = 0 for sc_offset in range(ssb_num_subcarriers): sc_grid = ssb_start_sc_in_grid + sc_offset if 0 <= sc_grid < fft_size: if sc_offset >= v_bar and (sc_offset - v_bar) % 4 == 0: if dmrs_idx < len(dmrs_sequence): resource_grid[symbol_idx + 3, sc_grid] = dmrs_sequence[dmrs_idx] dmrs_idx += 1 else: if pbch_idx < len(pbch_symbols): resource_grid[symbol_idx + 3, sc_grid] = pbch_symbols[pbch_idx] pbch_idx += 1 # Convert resource grid to time domain time_domain_samples = [] for sym_idx in range(num_symbols): # Get frequency domain symbol freq_symbol = resource_grid[sym_idx, :].copy() # Apply ifftshift to move DC to index 0 freq_symbol_shifted = np.fft.ifftshift(freq_symbol) # IFFT to get time domain symbol (normalize by sqrt(fft_size)) time_symbol = np.fft.ifft(freq_symbol_shifted, n=fft_size) * np.sqrt(fft_size) # Add cyclic prefix (copy last cp_length samples to the beginning) time_symbol_with_cp = np.concatenate([time_symbol[-cp_length:], time_symbol]) time_domain_samples.append(time_symbol_with_cp) # Concatenate all symbols iq_samples = np.concatenate(time_domain_samples) return iq_samples def create_tab0_summary(self): """Tab 0: Summary/Status Information""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="Summary") self.tabs['summary'] = tab # Create scrollable text area scrollbar = ttk.Scrollbar(tab) scrollbar.pack(side="right", fill="y") self.status_text = tk.Text(tab, height=30, width=100, wrap=tk.WORD, font=('Courier', 9), yscrollcommand=scrollbar.set) self.status_text.pack(side="left", fill="both", expand=True, padx=10, pady=10) scrollbar.config(command=self.status_text.yview) # Update status with current parameters self.update_status_text() def update_tab0_summary(self): """Update Summary tab""" self.update_status_text() def update_status_text(self): """Update status text with current configuration""" if hasattr(self, 'status_text'): self.status_text.delete(1.0, tk.END) try: # Get current parameter values ssb_case = self.var_ssb_case.get() l_max = self.var_l_max.get() ssb_tx_bitmap = self.var_ssb_tx_bitmap.get() channel_bw_mhz = self.var_channel_bw_mhz.get() n_cell_id = self.var_n_cell_id.get() # Calculate derived parameters # Use SSB SCS from params if available, otherwise derive from SSB Case if 'ssb_scs_khz' in self.params: ssb_scs_khz = self.params['ssb_scs_khz'] else: ssb_scs_khz = SSB_CASE_PARAMS[ssb_case]['scs_khz'] ssb_first_symbols = SSB_CASE_PARAMS[ssb_case]['first_symbols'] max_rbs = MAX_RBS_TABLE.get(channel_bw_mhz, {}).get(ssb_scs_khz, None) if max_rbs is None: status = f"ERROR: Invalid combination: {channel_bw_mhz} MHz with {ssb_scs_khz} kHz SCS\n" status += "Please select a valid bandwidth/SCS combination." else: slot_duration_ms = 1.0 / (ssb_scs_khz / 15) num_slots_in_half_frame = int(5.0 / slot_duration_ms) num_ofdm_symbols = num_slots_in_half_frame * 14 ssb_tx_bitmap_norm = normalize_ssb_bitmap(ssb_tx_bitmap, l_max) ssb_positions_all = get_ssb_symbol_positions(ssb_case, l_max, l_max) ssb_active = [(idx, pos) for idx, (bit, pos) in enumerate(zip(ssb_tx_bitmap_norm, ssb_positions_all)) if bit == '1'] num_ssb_transmitted = len(ssb_active) n_id_1 = n_cell_id // 3 n_id_2 = n_cell_id % 3 status = "=" * 70 + "\n" status += "Current Configuration\n" status += "=" * 70 + "\n" status += f"SSB Case: {ssb_case}\n" status += f"SSB Subcarrier Spacing: {ssb_scs_khz} kHz\n" status += f"SSB Frequency Span: {SSB_NUM_SUBCARRIERS * ssb_scs_khz / 1000.0:.2f} MHz ({SSB_NUM_SUBCARRIERS} SCs @ {ssb_scs_khz} kHz)\n" status += f"L_max: {l_max}\n" status += f"SSB TX Bitmap: {ssb_tx_bitmap_norm}\n" status += f"SSBs Transmitted: {num_ssb_transmitted}\n" status += f"Channel Bandwidth: {channel_bw_mhz} MHz\n" status += f"Max RBs in Channel: {max_rbs}\n" status += f"FFT Size: {FFT_SIZE_TABLE.get(channel_bw_mhz, 2048)}\n" status += "-" * 70 + "\n" status += f"Half Frame Duration: {5.0} ms\n" status += f"Slot Duration: {slot_duration_ms:.3f} ms\n" status += f"Slots in Half Frame: {num_slots_in_half_frame}\n" status += f"Total OFDM Symbols: {num_ofdm_symbols}\n" status += "-" * 70 + "\n" status += f"Cell ID (PCI): {n_cell_id}\n" status += f"N_ID^(1): {n_id_1}\n" status += f"N_ID^(2): {n_id_2}\n" status += "=" * 70 + "\n" except Exception as e: status = f"Error calculating parameters: {str(e)}\n" if hasattr(self, 'status_text'): self.status_text.insert(1.0, status) def update_resource_grid(self): """Update resource grid with new parameters""" try: # Get parameter values self.params['ssb_case'] = self.var_ssb_case.get() self.params['ssb_scs_khz'] = self.var_ssb_scs_khz.get() self.params['pdsch_scs_khz'] = self.var_pdsch_scs_khz.get() self.params['l_max'] = self.var_l_max.get() if hasattr(self, 'var_frequency_range'): self.params['frequency_range'] = self.var_frequency_range.get() # Also save the frequency range key if available if hasattr(self, 'freq_range_key'): self.params['frequency_range_key'] = self.freq_range_key self.params['ssb_tx_bitmap'] = self.var_ssb_tx_bitmap.get() self.params['channel_bw_mhz'] = self.var_channel_bw_mhz.get() self.params['n_cell_id'] = self.var_n_cell_id.get() # Update MIB parameters if MIB tab exists if hasattr(self, 'var_sfn'): if 'mib' not in self.params: self.params['mib'] = {} self.params['mib']['sfn'] = self.var_sfn.get() self.params['mib']['scs_common'] = self.var_scs_common.get() self.params['mib']['ssb_subcarrier_offset'] = self.var_ssb_subcarrier_offset.get() self.params['mib']['dmrs_typea_position'] = self.var_dmrs_typea_position.get() self.params['mib']['coreset0_idx'] = self.var_coreset0_idx.get() self.params['mib']['ss0_idx'] = self.var_ss0_idx.get() self.params['mib']['cell_barred'] = self.var_cell_barred.get() self.params['mib']['intra_freq_reselection'] = self.var_intra_freq_reselection.get() # Validate parameters ssb_case = self.params['ssb_case'] channel_bw_mhz = self.params['channel_bw_mhz'] # Use SSB SCS from params if available, otherwise derive from SSB Case if 'ssb_scs_khz' in self.params: ssb_scs_khz = self.params['ssb_scs_khz'] else: ssb_scs_khz = SSB_CASE_PARAMS[ssb_case]['scs_khz'] # Get PDSCH SCS if 'pdsch_scs_khz' in self.params: pdsch_scs_khz = self.params['pdsch_scs_khz'] else: pdsch_scs_khz = PDSCH_SCS_KHZ max_rbs = MAX_RBS_TABLE.get(channel_bw_mhz, {}).get(pdsch_scs_khz, None) if max_rbs is None: self.update_status( f"Error: Invalid combination: {channel_bw_mhz} MHz with {pdsch_scs_khz} kHz PDSCH SCS. Please select a valid bandwidth/SCS combination.", 'error' ) return # Validate SSB position parameters if manual mode if hasattr(self, 'var_auto_ssb_position') and not self.var_auto_ssb_position.get(): k_ssb = self.var_k_ssb.get() if k_ssb < 0 or k_ssb > 23: self.update_status(f"Error: k_SSB must be 0-23. Current value: {k_ssb}", 'error') self.var_k_ssb.set(max(0, min(23, k_ssb))) return # Generate new resource grid self.generate_resource_grid() # Update all visualization tabs self.update_tab0_summary() self.update_tab1_full_grid() self.update_tab2_ssb_structure() self.update_tab3_time_domain() self.update_tab4_constellation() # Update status bar self.update_status("Resource grid updated successfully!", 'success') except Exception as e: # Update status bar with error self.update_status(f"Error: {str(e)}", 'error') import traceback traceback.print_exc() def create_tab1_full_grid(self): """Tab 1: Full Resource Grid showing 5ms with all SSBs""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="Full Resource Grid (5ms)") self.tabs['full_grid'] = tab # Create figure fig = Figure(figsize=(14, 8)) self.figures['full_grid'] = fig ax = fig.add_subplot(1, 1, 1) self.axes['full_grid'] = ax # Get computed parameters cp = self.computed_params num_ofdm_symbols = cp['num_ofdm_symbols'] num_slots = cp['num_slots_in_half_frame'] max_rbs = cp['max_rbs'] ssb_start_sc = cp['ssb_start_sc'] ssb_scs_khz = cp['ssb_scs_khz'] channel_bw_mhz = self.params['channel_bw_mhz'] ssb_case = self.params['ssb_case'] num_ssb = cp['num_ssb_transmitted'] offset_to_pointa = cp['offset_to_pointa_rb'] k_ssb = cp['k_ssb'] # Custom colormap for SSB components # 0=Empty (gray), 1=PSS (red), 2=SSS (blue), 3=PBCH (green), 4=PBCH_DMRS (yellow) colors = ['#404040', # 0: Empty - Dark gray '#ff4444', # 1: PSS - Red '#4444ff', # 2: SSS - Blue '#44ff44', # 3: PBCH - Green '#ffff00'] # 4: PBCH DMRS - Yellow cmap = ListedColormap(colors) # Plot the full resource grid im = ax.imshow(self.rg_display.T, aspect='auto', origin='lower', cmap=cmap, interpolation='nearest', vmin=0, vmax=4) # Add slot boundary lines for slot in range(num_slots + 1): sym = slot * NUM_SYMBOLS_PER_SLOT ax.axvline(x=sym - 0.5, color='white', linewidth=0.5, alpha=0.5) # Add RB boundary lines (every 10 RBs for visibility) for rb in range(0, max_rbs + 1, 10): ax.axhline(y=rb * 12 - 0.5, color='white', linewidth=0.3, alpha=0.3) # Highlight SSB region ssb_rect = Rectangle( (-0.5, ssb_start_sc - 0.5), num_ofdm_symbols, SSB_NUM_SUBCARRIERS, linewidth=2, edgecolor='cyan', facecolor='none', linestyle='--', label=f'SSB Region ({SSB_NUM_RBS} RBs)' ) ax.add_patch(ssb_rect) # Annotate offsetToPointA / k_SSB (15 kHz raster), derived start/end SC/RB offset_info = ( f"offsetToPointA (RB@15k): {offset_to_pointa}\n" f"k_SSB (SC@15k): {k_ssb}\n" f"SSB start SC: {ssb_start_sc}\n" f"SSB end SC: {ssb_start_sc + SSB_NUM_SUBCARRIERS - 1}\n" f"SSB start RB: {cp['ssb_offset_rb']}" ) ax.text(0.01, 0.99, offset_info, transform=ax.transAxes, fontsize=8, va='top', ha='left', bbox=dict(boxstyle='round', facecolor='white', alpha=0.75, edgecolor='magenta')) # Mark each SSB burst (use short labels when many SSBs) ssb_label_step = max(1, len(self.ssb_indices) // 16) # Show ~16 labels max for idx, ssb_sym in self.ssb_info: ssb_burst_rect = Rectangle( (ssb_sym - 0.5, ssb_start_sc - 0.5), SSB_NUM_SYMBOLS, SSB_NUM_SUBCARRIERS, linewidth=1.5, edgecolor='magenta', facecolor='none', alpha=0.8 ) ax.add_patch(ssb_burst_rect) # Label (only show every Nth label to avoid overlap) if idx % ssb_label_step == 0: ax.text(ssb_sym + SSB_NUM_SYMBOLS/2, ssb_start_sc + SSB_NUM_SUBCARRIERS + 5, f'{idx}', ha='center', va='bottom', fontsize=7, color='magenta') ax.set_xlabel('OFDM Symbol Index', fontsize=10) ax.set_ylabel('Subcarrier Index', fontsize=10) ax.set_title(f'SSB Resource Grid - Full 5ms Half Frame - Case {ssb_case}, {num_ssb} SSBs, {channel_bw_mhz} MHz Channel, {ssb_scs_khz} kHz SCS', fontsize=11) # Slot labels on top (use short format to avoid overlap) ax_top = ax.twiny() ax_top.set_xlim(ax.get_xlim()) # Show every Nth slot label based on total number of slots slot_label_step = max(1, num_slots // 20) # Show ~20 labels max slot_positions = [(i + 0.5) * NUM_SYMBOLS_PER_SLOT for i in range(0, num_slots, slot_label_step)] ax_top.set_xticks(slot_positions) ax_top.set_xticklabels([f's{i}' for i in range(0, num_slots, slot_label_step)], fontsize=7) ax_top.set_xlabel('Slot Index', fontsize=10) # RB labels on right ax_right = ax.twinx() ax_right.set_ylim(ax.get_ylim()) rb_step = 10 rb_positions = [i * 12 for i in range(0, max_rbs, rb_step)] ax_right.set_yticks(rb_positions) ax_right.set_yticklabels([f'RB{i}' for i in range(0, max_rbs, rb_step)], fontsize=8) ax_right.set_ylabel('Resource Block', fontsize=10) # Frequency axis (MHz) - shows actual frequency span based on PDSCH SCS (grid SCS) # Create another twin axis for frequency in MHz # Get PDSCH SCS for frequency calculation pdsch_scs_khz = cp.get('pdsch_scs_khz', 30) # Frequency (MHz) = Subcarrier Index * PDSCH_SCS (kHz) / 1000 ax_freq = ax.twinx() ax_freq.set_ylim(ax.get_ylim()) # Position frequency axis on the right side, offset from RB axis ax_freq.spines['right'].set_position(('outward', 60)) # Calculate frequency positions in MHz # Show frequency labels at major subcarrier positions freq_label_step = max(1, max_rbs * 12 // 20) # ~20 labels across the grid freq_subcarrier_positions = list(range(0, max_rbs * 12, freq_label_step)) freq_mhz_values = [sc * pdsch_scs_khz / 1000.0 for sc in freq_subcarrier_positions] ax_freq.set_yticks(freq_subcarrier_positions) ax_freq.set_yticklabels([f'{freq:.2f}' for freq in freq_mhz_values], fontsize=8) ax_freq.set_ylabel(f'Frequency (MHz) @ {pdsch_scs_khz} kHz SCS', fontsize=10) # Annotate SSB frequency span # Use PDSCH SCS for frequency calculation since grid is at PDSCH SCS ssb_num_subcarriers_grid = cp.get('ssb_num_subcarriers_grid', SSB_NUM_SUBCARRIERS) ssb_freq_start_mhz = ssb_start_sc * pdsch_scs_khz / 1000.0 ssb_freq_end_mhz = (ssb_start_sc + ssb_num_subcarriers_grid) * pdsch_scs_khz / 1000.0 ssb_freq_span_mhz = ssb_num_subcarriers_grid * pdsch_scs_khz / 1000.0 freq_info = ( f"SSB Frequency Span: {ssb_freq_span_mhz:.2f} MHz\n" f"SSB Start: {ssb_freq_start_mhz:.2f} MHz\n" f"SSB End: {ssb_freq_end_mhz:.2f} MHz" ) ax.text(0.99, 0.01, freq_info, transform=ax.transAxes, fontsize=8, va='bottom', ha='right', bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.75, edgecolor='orange')) # Legend legend_elements = [ Patch(facecolor='#404040', edgecolor='black', label='Empty'), Patch(facecolor='#ff4444', edgecolor='black', label='PSS'), Patch(facecolor='#4444ff', edgecolor='black', label='SSS'), Patch(facecolor='#44ff44', edgecolor='black', label='PBCH'), Patch(facecolor='#ffff00', edgecolor='black', label='PBCH DMRS'), ] ax.legend(handles=legend_elements, loc='upper right', fontsize=8) fig.tight_layout() # Embed in tkinter canvas = FigureCanvasTkAgg(fig, master=tab) self.canvases['full_grid'] = canvas # Add toolbar first (NavigationToolbar2Tk packs itself at bottom) toolbar = NavigationToolbar2Tk(canvas, tab) toolbar.update() self.toolbars['full_grid'] = toolbar # Pack canvas after toolbar (fills remaining space above toolbar) canvas.draw() canvas.get_tk_widget().pack(side=tk.TOP, fill='both', expand=True) def update_tab1_full_grid(self): """Update Tab 1: Full Resource Grid""" ax = self.axes['full_grid'] fig = self.figures['full_grid'] # Remove all twin axes before clearing to prevent overlay # Get list of all axes in the figure all_axes = fig.get_axes() for a in all_axes: if a != ax: # Keep only the main axis fig.delaxes(a) # Now clear the main axis ax.clear() # Get computed parameters cp = self.computed_params num_ofdm_symbols = cp['num_ofdm_symbols'] num_slots = cp['num_slots_in_half_frame'] max_rbs = cp['max_rbs'] ssb_start_sc = cp['ssb_start_sc'] ssb_scs_khz = cp['ssb_scs_khz'] channel_bw_mhz = self.params['channel_bw_mhz'] ssb_case = self.params['ssb_case'] num_ssb = cp['num_ssb_transmitted'] offset_to_pointa = cp['offset_to_pointa_rb'] k_ssb = cp['k_ssb'] # Replot with new data colors = ['#404040', '#ff4444', '#4444ff', '#44ff44', '#ffff00'] cmap = ListedColormap(colors) im = ax.imshow(self.rg_display.T, aspect='auto', origin='lower', cmap=cmap, interpolation='nearest', vmin=0, vmax=4) # Add slot boundary lines for slot in range(num_slots + 1): sym = slot * NUM_SYMBOLS_PER_SLOT ax.axvline(x=sym - 0.5, color='white', linewidth=0.5, alpha=0.5) # Add RB boundary lines for rb in range(0, max_rbs + 1, 10): ax.axhline(y=rb * 12 - 0.5, color='white', linewidth=0.3, alpha=0.3) # Get SSB size in grid subcarriers ssb_num_subcarriers_grid = cp.get('ssb_num_subcarriers_grid', SSB_NUM_SUBCARRIERS) ssb_num_rbs_grid = ssb_num_subcarriers_grid // 12 # Highlight SSB region ssb_rect = Rectangle( (-0.5, ssb_start_sc - 0.5), num_ofdm_symbols, ssb_num_subcarriers_grid, linewidth=2, edgecolor='cyan', facecolor='none', linestyle='--', label=f'SSB Region ({ssb_num_rbs_grid} RBs @ {cp.get("pdsch_scs_khz", 30)} kHz)' ) ax.add_patch(ssb_rect) # Annotate offsetToPointA / k_SSB offset_info = ( f"offsetToPointA (RB@15k): {offset_to_pointa}\n" f"k_SSB (SC@15k): {k_ssb}\n" f"SSB start SC (grid): {ssb_start_sc}\n" f"SSB end SC (grid): {ssb_start_sc + ssb_num_subcarriers_grid - 1}\n" f"SSB start RB (grid): {cp['ssb_offset_rb']}\n" f"SSB size: {ssb_num_rbs_grid} RBs @ {cp.get('pdsch_scs_khz', 30)} kHz" ) ax.text(0.01, 0.99, offset_info, transform=ax.transAxes, fontsize=8, va='top', ha='left', bbox=dict(boxstyle='round', facecolor='white', alpha=0.75, edgecolor='magenta')) # Mark each SSB burst ssb_label_step = max(1, len(self.ssb_indices) // 16) for idx, ssb_sym in self.ssb_info: ssb_burst_rect = Rectangle( (ssb_sym - 0.5, ssb_start_sc - 0.5), SSB_NUM_SYMBOLS, ssb_num_subcarriers_grid, linewidth=1.5, edgecolor='magenta', facecolor='none', alpha=0.8 ) ax.add_patch(ssb_burst_rect) if idx % ssb_label_step == 0: ax.text(ssb_sym + SSB_NUM_SYMBOLS/2, ssb_start_sc + ssb_num_subcarriers_grid + 5, f'{idx}', ha='center', va='bottom', fontsize=7, color='magenta') ax.set_xlabel('OFDM Symbol Index', fontsize=10) ax.set_ylabel('Subcarrier Index', fontsize=10) ax.set_title(f'SSB Resource Grid - Full 5ms Half Frame - Case {ssb_case}, {num_ssb} SSBs, {channel_bw_mhz} MHz Channel, {ssb_scs_khz} kHz SCS', fontsize=11) # Slot labels ax_top = ax.twiny() ax_top.set_xlim(ax.get_xlim()) slot_label_step = max(1, num_slots // 20) slot_positions = [(i + 0.5) * NUM_SYMBOLS_PER_SLOT for i in range(0, num_slots, slot_label_step)] ax_top.set_xticks(slot_positions) ax_top.set_xticklabels([f's{i}' for i in range(0, num_slots, slot_label_step)], fontsize=7) ax_top.set_xlabel('Slot Index', fontsize=10) # RB labels (right axis) ax_right = ax.twinx() ax_right.set_ylim(ax.get_ylim()) rb_step = 10 rb_positions = [i * 12 for i in range(0, max_rbs, rb_step)] ax_right.set_yticks(rb_positions) ax_right.set_yticklabels([f'RB{i}' for i in range(0, max_rbs, rb_step)], fontsize=8) ax_right.set_ylabel('Resource Block', fontsize=10) # Frequency axis (MHz) - shows actual frequency span based on PDSCH SCS (grid SCS) # Create another twin axis for frequency in MHz # Get PDSCH SCS for frequency calculation pdsch_scs_khz = cp.get('pdsch_scs_khz', 30) # Frequency (MHz) = Subcarrier Index * PDSCH_SCS (kHz) / 1000 ax_freq = ax.twinx() ax_freq.set_ylim(ax.get_ylim()) # Position frequency axis on the right side, offset from RB axis ax_freq.spines['right'].set_position(('outward', 60)) # Calculate frequency positions in MHz # Show frequency labels at major subcarrier positions freq_label_step = max(1, max_rbs * 12 // 20) # ~20 labels across the grid freq_subcarrier_positions = list(range(0, max_rbs * 12, freq_label_step)) freq_mhz_values = [sc * pdsch_scs_khz / 1000.0 for sc in freq_subcarrier_positions] ax_freq.set_yticks(freq_subcarrier_positions) ax_freq.set_yticklabels([f'{freq:.2f}' for freq in freq_mhz_values], fontsize=8) ax_freq.set_ylabel(f'Frequency (MHz) @ {pdsch_scs_khz} kHz SCS', fontsize=10) # Annotate SSB frequency span # Use PDSCH SCS for frequency calculation since grid is at PDSCH SCS ssb_num_subcarriers_grid = cp.get('ssb_num_subcarriers_grid', SSB_NUM_SUBCARRIERS) ssb_freq_start_mhz = ssb_start_sc * pdsch_scs_khz / 1000.0 ssb_freq_end_mhz = (ssb_start_sc + ssb_num_subcarriers_grid) * pdsch_scs_khz / 1000.0 ssb_freq_span_mhz = ssb_num_subcarriers_grid * pdsch_scs_khz / 1000.0 freq_info = ( f"SSB Frequency Span: {ssb_freq_span_mhz:.2f} MHz\n" f"SSB Start: {ssb_freq_start_mhz:.2f} MHz\n" f"SSB End: {ssb_freq_end_mhz:.2f} MHz" ) ax.text(0.99, 0.01, freq_info, transform=ax.transAxes, fontsize=8, va='bottom', ha='right', bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.75, edgecolor='orange')) # Legend legend_elements = [ Patch(facecolor='#404040', edgecolor='black', label='Empty'), Patch(facecolor='#ff4444', edgecolor='black', label='PSS'), Patch(facecolor='#4444ff', edgecolor='black', label='SSS'), Patch(facecolor='#44ff44', edgecolor='black', label='PBCH'), Patch(facecolor='#ffff00', edgecolor='black', label='PBCH DMRS'), ] ax.legend(handles=legend_elements, loc='upper right', fontsize=8) self.figures['full_grid'].tight_layout() self.canvases['full_grid'].draw() def create_tab2_ssb_structure(self): """Tab 2: SSB Structure Diagram""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="SSB Structure Diagram") self.tabs['ssb_structure'] = tab fig = Figure(figsize=(14, 8)) self.figures['ssb_structure'] = fig ax = fig.add_subplot(1, 1, 1) self.axes['ssb_structure'] = ax # Create a schematic view of SSB structure # X-axis: 4 symbols, Y-axis: 240 subcarriers # Draw background grid ssb_grid = np.zeros((SSB_NUM_SYMBOLS, SSB_NUM_SUBCARRIERS)) # Fill with component colors # Symbol 0: PSS ssb_grid[0, PSS_SSS_START_SC:PSS_SSS_END_SC] = 1 # Symbol 1: PBCH + DMRS # Calculate PBCH DMRS offset v-bar = N_ID_cell mod 4 (3GPP TS 38.211 Section 7.4.1.4.1) n_cell_id = self.params.get('n_cell_id', 0) v_bar = n_cell_id % 4 for sc in range(SSB_NUM_SUBCARRIERS): if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[1, sc] = 4 # DMRS else: ssb_grid[1, sc] = 3 # PBCH # Symbol 2: SSS (center) + PBCH (sides) + DMRS + unused gaps # Structure: PBCH(0-47) | unused(48-55) | SSS(56-182) | unused(183-191) | PBCH(192-239) for sc in range(SSB_NUM_SUBCARRIERS): if PSS_SSS_START_SC <= sc < PSS_SSS_END_SC: ssb_grid[2, sc] = 2 # SSS elif sc < PBCH_LEFT_END_SC: # Left PBCH (SC 0-47) if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[2, sc] = 4 # DMRS else: ssb_grid[2, sc] = 3 # PBCH elif sc >= PBCH_RIGHT_START_SC: # Right PBCH (SC 192-239) if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[2, sc] = 4 # DMRS else: ssb_grid[2, sc] = 3 # PBCH # else: SC 48-55 and SC 183-191 remain 0 (unused) # Symbol 3: PBCH + DMRS for sc in range(SSB_NUM_SUBCARRIERS): if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[3, sc] = 4 # DMRS else: ssb_grid[3, sc] = 3 # PBCH colors = ['#404040', '#ff4444', '#4444ff', '#44ff44', '#ffff00'] cmap = ListedColormap(colors) im = ax.imshow(ssb_grid.T, aspect='auto', origin='lower', cmap=cmap, interpolation='nearest', vmin=0, vmax=4) # Add grid lines for sym in range(SSB_NUM_SYMBOLS + 1): ax.axvline(x=sym - 0.5, color='black', linewidth=2) # Add RB boundaries for rb in range(SSB_NUM_RBS + 1): ax.axhline(y=rb * 12 - 0.5, color='black', linewidth=0.3, alpha=0.5) # Add annotations ax.text(0, SSB_NUM_SUBCARRIERS/2, 'PSS\n(127 SC)', ha='center', va='center', fontsize=12, fontweight='bold', color='white') ax.text(1, SSB_NUM_SUBCARRIERS/2, 'PBCH\n+ DMRS', ha='center', va='center', fontsize=12, fontweight='bold', color='black') ax.text(2, 120, 'SSS (127 SC)\n+ PBCH sides', ha='center', va='center', fontsize=10, fontweight='bold', color='white') ax.text(3, SSB_NUM_SUBCARRIERS/2, 'PBCH\n+ DMRS', ha='center', va='center', fontsize=12, fontweight='bold', color='black') # Mark PSS/SSS region ax.axhline(y=PSS_SSS_START_SC, color='white', linewidth=2, linestyle='--') ax.axhline(y=PSS_SSS_END_SC - 1, color='white', linewidth=2, linestyle='--') ax.set_xlabel('SSB Symbol Index', fontsize=12) ax.set_ylabel('Subcarrier Index (within SSB)', fontsize=12) ax.set_title('SSB (Synchronization Signal Block) Structure - 3GPP TS 38.211 Section 7.4.3', fontsize=14) ax.set_xticks(range(SSB_NUM_SYMBOLS)) ax.set_xticklabels(['Symbol 0\n(PSS)', 'Symbol 1\n(PBCH)', 'Symbol 2\n(SSS+PBCH)', 'Symbol 3\n(PBCH)']) # Legend legend_elements = [ Patch(facecolor='#ff4444', edgecolor='black', label='PSS (Primary Sync Signal)'), Patch(facecolor='#4444ff', edgecolor='black', label='SSS (Secondary Sync Signal)'), Patch(facecolor='#44ff44', edgecolor='black', label='PBCH (Broadcast Channel)'), Patch(facecolor='#ffff00', edgecolor='black', label='PBCH DMRS'), Patch(facecolor='#404040', edgecolor='black', label='Unused'), ] ax.legend(handles=legend_elements, loc='upper right', fontsize=10) # Add text box with SSB parameters cp = self.computed_params n_cell_id = self.params['n_cell_id'] ssb_case = self.params['ssb_case'] ssb_scs_khz = cp['ssb_scs_khz'] v_bar = n_cell_id % 4 info_text = (f"SSB Parameters:\n" f"• Case: {ssb_case}\n" f"• SCS: {ssb_scs_khz} kHz\n" f"• Size: {SSB_NUM_RBS} RBs × {SSB_NUM_SYMBOLS} symbols\n" f"• PSS/SSS: 127 subcarriers (centered)\n" f"• PBCH DMRS: Every 4th subcarrier (offset v={v_bar})\n" f"• Cell ID: {n_cell_id}") ax.text(0.02, 0.98, info_text, transform=ax.transAxes, fontsize=9, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) fig.tight_layout() # Embed in tkinter canvas = FigureCanvasTkAgg(fig, master=tab) self.canvases['ssb_structure'] = canvas # Add toolbar first (NavigationToolbar2Tk packs itself at bottom) toolbar = NavigationToolbar2Tk(canvas, tab) toolbar.update() self.toolbars['ssb_structure'] = toolbar # Pack canvas after toolbar (fills remaining space above toolbar) canvas.draw() canvas.get_tk_widget().pack(side=tk.TOP, fill='both', expand=True) def update_tab2_ssb_structure(self): """Update Tab 2: SSB Structure Diagram""" ax = self.axes['ssb_structure'] ax.clear() cp = self.computed_params n_cell_id = self.params['n_cell_id'] ssb_case = self.params['ssb_case'] ssb_scs_khz = cp['ssb_scs_khz'] # Redraw SSB structure (same as create, but using current data) ssb_grid = np.zeros((SSB_NUM_SYMBOLS, SSB_NUM_SUBCARRIERS)) # Symbol 0: PSS ssb_grid[0, PSS_SSS_START_SC:PSS_SSS_END_SC] = 1 # Symbol 1: PBCH + DMRS # Calculate PBCH DMRS offset v-bar = N_ID_cell mod 4 (3GPP TS 38.211 Section 7.4.1.4.1) v_bar = n_cell_id % 4 for sc in range(SSB_NUM_SUBCARRIERS): if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[1, sc] = 4 # DMRS else: ssb_grid[1, sc] = 3 # PBCH # Symbol 2: SSS + PBCH + DMRS for sc in range(SSB_NUM_SUBCARRIERS): if PSS_SSS_START_SC <= sc < PSS_SSS_END_SC: ssb_grid[2, sc] = 2 elif sc < PBCH_LEFT_END_SC: if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[2, sc] = 4 # DMRS else: ssb_grid[2, sc] = 3 # PBCH elif sc >= PBCH_RIGHT_START_SC: if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[2, sc] = 4 # DMRS else: ssb_grid[2, sc] = 3 # PBCH # Symbol 3: PBCH + DMRS for sc in range(SSB_NUM_SUBCARRIERS): if sc >= v_bar and (sc - v_bar) % 4 == 0: ssb_grid[3, sc] = 4 # DMRS else: ssb_grid[3, sc] = 3 # PBCH colors = ['#404040', '#ff4444', '#4444ff', '#44ff44', '#ffff00'] cmap = ListedColormap(colors) im = ax.imshow(ssb_grid.T, aspect='auto', origin='lower', cmap=cmap, interpolation='nearest', vmin=0, vmax=4) # Add grid lines for sym in range(SSB_NUM_SYMBOLS + 1): ax.axvline(x=sym - 0.5, color='black', linewidth=2) # Add RB boundaries for rb in range(SSB_NUM_RBS + 1): ax.axhline(y=rb * 12 - 0.5, color='black', linewidth=0.3, alpha=0.5) # Add annotations ax.text(0, SSB_NUM_SUBCARRIERS/2, 'PSS\n(127 SC)', ha='center', va='center', fontsize=12, fontweight='bold', color='white') ax.text(1, SSB_NUM_SUBCARRIERS/2, 'PBCH\n+ DMRS', ha='center', va='center', fontsize=12, fontweight='bold', color='black') ax.text(2, 120, 'SSS (127 SC)\n+ PBCH sides', ha='center', va='center', fontsize=10, fontweight='bold', color='white') ax.text(3, SSB_NUM_SUBCARRIERS/2, 'PBCH\n+ DMRS', ha='center', va='center', fontsize=12, fontweight='bold', color='black') # Mark PSS/SSS region ax.axhline(y=PSS_SSS_START_SC, color='white', linewidth=2, linestyle='--') ax.axhline(y=PSS_SSS_END_SC - 1, color='white', linewidth=2, linestyle='--') ax.set_xlabel('SSB Symbol Index', fontsize=12) ax.set_ylabel('Subcarrier Index (within SSB)', fontsize=12) ax.set_title('SSB (Synchronization Signal Block) Structure - 3GPP TS 38.211 Section 7.4.3', fontsize=14) ax.set_xticks(range(SSB_NUM_SYMBOLS)) ax.set_xticklabels(['Symbol 0\n(PSS)', 'Symbol 1\n(PBCH)', 'Symbol 2\n(SSS+PBCH)', 'Symbol 3\n(PBCH)']) # Legend legend_elements = [ Patch(facecolor='#ff4444', edgecolor='black', label='PSS (Primary Sync Signal)'), Patch(facecolor='#4444ff', edgecolor='black', label='SSS (Secondary Sync Signal)'), Patch(facecolor='#44ff44', edgecolor='black', label='PBCH (Broadcast Channel)'), Patch(facecolor='#ffff00', edgecolor='black', label='PBCH DMRS'), Patch(facecolor='#404040', edgecolor='black', label='Unused'), ] ax.legend(handles=legend_elements, loc='upper right', fontsize=10) # Info text v_bar = n_cell_id % 4 info_text = (f"SSB Parameters:\n" f"• Case: {ssb_case}\n" f"• SCS: {ssb_scs_khz} kHz\n" f"• Size: {SSB_NUM_RBS} RBs × {SSB_NUM_SYMBOLS} symbols\n" f"• PSS/SSS: 127 subcarriers (centered)\n" f"• PBCH DMRS: Every 4th subcarrier (offset v={v_bar})\n" f"• Cell ID: {n_cell_id}") ax.text(0.02, 0.98, info_text, transform=ax.transAxes, fontsize=9, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) self.figures['ssb_structure'].tight_layout() self.canvases['ssb_structure'].draw() def create_tab3_time_domain(self): """Tab 3: Time domain view showing SSB bursts over 5ms""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="SSB Time Pattern") self.tabs['time_domain'] = tab fig = Figure(figsize=(14, 8)) self.figures['time_domain'] = fig # Get computed parameters cp = self.computed_params num_ofdm_symbols = cp['num_ofdm_symbols'] num_slots = cp['num_slots_in_half_frame'] slot_duration_ms = cp['slot_duration_ms'] ssb_case = self.params['ssb_case'] num_ssb = cp['num_ssb_transmitted'] ssb_first_symbols = SSB_CASE_PARAMS[ssb_case]['first_symbols'] slot_pattern_period = SSB_CASE_PARAMS[ssb_case]['slot_pattern_period'] # Plot 1: SSB presence per OFDM symbol ax1 = fig.add_subplot(2, 1, 1) # Create binary SSB presence indicator ssb_presence = np.zeros(num_ofdm_symbols) for _, ssb_sym in self.ssb_info: for offset in range(SSB_NUM_SYMBOLS): if ssb_sym + offset < num_ofdm_symbols: ssb_presence[ssb_sym + offset] = 1 # Color-coded bar for each symbol colors_per_symbol = [] for sym in range(num_ofdm_symbols): if ssb_presence[sym]: colors_per_symbol.append('#ff6600') # Orange for SSB else: colors_per_symbol.append('#404040') # Gray for empty bars = ax1.bar(range(num_ofdm_symbols), np.ones(num_ofdm_symbols), color=colors_per_symbol, edgecolor='black', linewidth=0.5) # Add slot boundaries for slot in range(num_slots + 1): ax1.axvline(x=slot * NUM_SYMBOLS_PER_SLOT - 0.5, color='blue', linewidth=1.5, linestyle='-') # Add labels for each SSB (use short labels when many SSBs) ssb_label_step = max(1, len(self.ssb_indices) // 16) # Show ~16 labels max for idx, ssb_sym in self.ssb_info: if idx % ssb_label_step == 0: ax1.annotate(f'{idx}', xy=(ssb_sym + 1.5, 1.05), ha='center', fontsize=7, fontweight='bold', color='#ff6600') ax1.set_xlabel('OFDM Symbol Index', fontsize=10) ax1.set_ylabel('SSB Presence', fontsize=10) # Truncate symbol positions display if too many if len(self.ssb_info) <= 8: pos_str = str([pos for _, pos in self.ssb_info]) else: pos_str = f'[{self.ssb_info[0][1]}, {self.ssb_info[1][1]}, ..., {self.ssb_info[-1][1]}]' ax1.set_title(f'SSB Burst Pattern in 5ms Half Frame - Case {ssb_case}: {num_ssb} SSBs at symbols {pos_str}', fontsize=11) ax1.set_xlim(-0.5, num_ofdm_symbols - 0.5) ax1.set_ylim(0, 1.2) ax1.set_yticks([]) # Slot labels (use short format to avoid overlap) ax1_top = ax1.twiny() ax1_top.set_xlim(ax1.get_xlim()) slot_label_step = max(1, num_slots // 20) # Show ~20 labels max slot_centers = [(i + 0.5) * NUM_SYMBOLS_PER_SLOT for i in range(0, num_slots, slot_label_step)] ax1_top.set_xticks(slot_centers) ax1_top.set_xticklabels([f's{i}' for i in range(0, num_slots, slot_label_step)], fontsize=7) legend_elements = [ Patch(facecolor='#ff6600', edgecolor='black', label='SSB'), Patch(facecolor='#404040', edgecolor='black', label='Empty'), ] ax1.legend(handles=legend_elements, loc='upper right', fontsize=9) # Plot 2: SSB pattern diagram ax2 = fig.add_subplot(2, 1, 2) # Draw time axis time_axis_length = 5.0 ax2.axhline(y=0.5, color='black', linewidth=2) # Calculate time position for each symbol symbol_duration_ms = slot_duration_ms / NUM_SYMBOLS_PER_SLOT # Draw each SSB as a block (use short labels when many SSBs) ssb_label_step = max(1, len(self.ssb_indices) // 16) # Show ~16 labels max for idx, ssb_sym in self.ssb_info: ssb_start_time = ssb_sym * symbol_duration_ms ssb_duration = SSB_NUM_SYMBOLS * symbol_duration_ms rect = Rectangle((ssb_start_time, 0.2), ssb_duration, 0.6, facecolor='#ff6600', edgecolor='black', linewidth=1) ax2.add_patch(rect) # Label (only show every Nth label to avoid overlap) if idx % ssb_label_step == 0: ax2.text(ssb_start_time + ssb_duration/2, 0.5, f'{idx}', ha='center', va='center', fontsize=8, fontweight='bold', color='white') # Draw slot boundaries (use short labels to avoid overlap) slot_label_step = max(1, num_slots // 20) # Show ~20 labels max for slot in range(num_slots + 1): slot_time = slot * slot_duration_ms ax2.axvline(x=slot_time, color='blue', linewidth=1, linestyle='--', alpha=0.7) if slot < num_slots and slot % slot_label_step == 0: ax2.text(slot_time + slot_duration_ms/2, 0.9, f's{slot}', ha='center', va='center', fontsize=7, color='blue') ax2.set_xlim(-0.1, time_axis_length + 0.1) ax2.set_ylim(0, 1.1) ax2.set_xlabel('Time (ms)', fontsize=10) ax2.set_title(f'SSB Timing in 5ms Half Frame - Slot Duration: {slot_duration_ms:.3f} ms, Symbol Duration: {symbol_duration_ms*1000:.1f} µs', fontsize=11) ax2.set_yticks([]) # Add info text info_text = (f"SSB Case {ssb_case} Pattern:\n" f"• First symbols: {ssb_first_symbols}\n" f"• Slot pattern period: {slot_pattern_period} symbols\n" f"• L_max: {self.params['l_max']}\n" f"• SSBs transmitted: {num_ssb}") ax2.text(0.98, 0.85, info_text, transform=ax2.transAxes, fontsize=9, verticalalignment='top', horizontalalignment='right', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9)) fig.tight_layout() # Embed in tkinter canvas = FigureCanvasTkAgg(fig, master=tab) self.canvases['time_domain'] = canvas # Add toolbar first (NavigationToolbar2Tk packs itself at bottom) toolbar = NavigationToolbar2Tk(canvas, tab) toolbar.update() self.toolbars['time_domain'] = toolbar # Pack canvas after toolbar (fills remaining space above toolbar) canvas.draw() canvas.get_tk_widget().pack(side=tk.TOP, fill='both', expand=True) def update_tab3_time_domain(self): """Update Tab 3: Time domain view""" fig = self.figures['time_domain'] fig.clear() cp = self.computed_params num_ofdm_symbols = cp['num_ofdm_symbols'] num_slots = cp['num_slots_in_half_frame'] slot_duration_ms = cp['slot_duration_ms'] ssb_case = self.params['ssb_case'] num_ssb = cp['num_ssb_transmitted'] ssb_first_symbols = SSB_CASE_PARAMS[ssb_case]['first_symbols'] slot_pattern_period = SSB_CASE_PARAMS[ssb_case]['slot_pattern_period'] # Plot 1: SSB presence per OFDM symbol ax1 = fig.add_subplot(2, 1, 1) ssb_presence = np.zeros(num_ofdm_symbols) for _, ssb_sym in self.ssb_info: for offset in range(SSB_NUM_SYMBOLS): if ssb_sym + offset < num_ofdm_symbols: ssb_presence[ssb_sym + offset] = 1 colors_per_symbol = [] for sym in range(num_ofdm_symbols): if ssb_presence[sym]: colors_per_symbol.append('#ff6600') else: colors_per_symbol.append('#404040') bars = ax1.bar(range(num_ofdm_symbols), np.ones(num_ofdm_symbols), color=colors_per_symbol, edgecolor='black', linewidth=0.5) # Add slot boundaries for slot in range(num_slots + 1): ax1.axvline(x=slot * NUM_SYMBOLS_PER_SLOT - 0.5, color='blue', linewidth=1.5, linestyle='-') # Add labels ssb_label_step = max(1, len(self.ssb_indices) // 16) for idx, ssb_sym in self.ssb_info: if idx % ssb_label_step == 0: ax1.annotate(f'{idx}', xy=(ssb_sym + 1.5, 1.05), ha='center', fontsize=7, fontweight='bold', color='#ff6600') ax1.set_xlabel('OFDM Symbol Index', fontsize=10) ax1.set_ylabel('SSB Presence', fontsize=10) if len(self.ssb_info) <= 8: pos_str = str([pos for _, pos in self.ssb_info]) else: pos_str = f'[{self.ssb_info[0][1]}, {self.ssb_info[1][1]}, ..., {self.ssb_info[-1][1]}]' ax1.set_title(f'SSB Burst Pattern in 5ms Half Frame - Case {ssb_case}: {num_ssb} SSBs at symbols {pos_str}', fontsize=11) ax1.set_xlim(-0.5, num_ofdm_symbols - 0.5) ax1.set_ylim(0, 1.2) ax1.set_yticks([]) # Slot labels ax1_top = ax1.twiny() ax1_top.set_xlim(ax1.get_xlim()) slot_label_step = max(1, num_slots // 20) slot_centers = [(i + 0.5) * NUM_SYMBOLS_PER_SLOT for i in range(0, num_slots, slot_label_step)] ax1_top.set_xticks(slot_centers) ax1_top.set_xticklabels([f's{i}' for i in range(0, num_slots, slot_label_step)], fontsize=7) legend_elements = [ Patch(facecolor='#ff6600', edgecolor='black', label='SSB'), Patch(facecolor='#404040', edgecolor='black', label='Empty'), ] ax1.legend(handles=legend_elements, loc='upper right', fontsize=9) # Plot 2: SSB pattern diagram ax2 = fig.add_subplot(2, 1, 2) time_axis_length = 5.0 ax2.axhline(y=0.5, color='black', linewidth=2) symbol_duration_ms = slot_duration_ms / NUM_SYMBOLS_PER_SLOT ssb_label_step = max(1, len(self.ssb_indices) // 16) for idx, ssb_sym in self.ssb_info: ssb_start_time = ssb_sym * symbol_duration_ms ssb_duration = SSB_NUM_SYMBOLS * symbol_duration_ms rect = Rectangle((ssb_start_time, 0.2), ssb_duration, 0.6, facecolor='#ff6600', edgecolor='black', linewidth=1) ax2.add_patch(rect) if idx % ssb_label_step == 0: ax2.text(ssb_start_time + ssb_duration/2, 0.5, f'{idx}', ha='center', va='center', fontsize=8, fontweight='bold', color='white') # Draw slot boundaries slot_label_step = max(1, num_slots // 20) for slot in range(num_slots + 1): slot_time = slot * slot_duration_ms ax2.axvline(x=slot_time, color='blue', linewidth=1, linestyle='--', alpha=0.7) if slot < num_slots and slot % slot_label_step == 0: ax2.text(slot_time + slot_duration_ms/2, 0.9, f's{slot}', ha='center', va='center', fontsize=7, color='blue') ax2.set_xlim(-0.1, time_axis_length + 0.1) ax2.set_ylim(0, 1.1) ax2.set_xlabel('Time (ms)', fontsize=10) ax2.set_title(f'SSB Timing in 5ms Half Frame - Slot Duration: {slot_duration_ms:.3f} ms, Symbol Duration: {symbol_duration_ms*1000:.1f} µs', fontsize=11) ax2.set_yticks([]) # Info text info_text = (f"SSB Case {ssb_case} Pattern:\n" f"• First symbols: {ssb_first_symbols}\n" f"• Slot pattern period: {slot_pattern_period} symbols\n" f"• L_max: {self.params['l_max']}\n" f"• SSBs transmitted: {num_ssb}") ax2.text(0.98, 0.85, info_text, transform=ax2.transAxes, fontsize=9, verticalalignment='top', horizontalalignment='right', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9)) fig.tight_layout() self.canvases['time_domain'].draw() def create_tab4_constellation(self): """Tab 4: Constellation diagrams for SSB components""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="Constellation") self.tabs['constellation'] = tab # Create figure with 2x3 subplot grid fig = Figure(figsize=(14, 9)) self.figures['constellation'] = fig self._draw_constellation() # Embed in tkinter canvas = FigureCanvasTkAgg(fig, master=tab) self.canvases['constellation'] = canvas # Add toolbar first (NavigationToolbar2Tk packs itself at bottom) toolbar = NavigationToolbar2Tk(canvas, tab) toolbar.update() self.toolbars['constellation'] = toolbar # Pack canvas after toolbar (fills remaining space above toolbar) canvas.draw() canvas.get_tk_widget().pack(side=tk.TOP, fill='both', expand=True) def _draw_constellation(self): """Draw constellation diagrams (used by both create and update)""" fig = self.figures['constellation'] fig.clear() cp = self.computed_params n_cell_id = self.params['n_cell_id'] # Get sequences from computed params pss_symbols = cp['pss_sequence'] sss_symbols = cp['sss_sequence'] # Generate PBCH symbols using 3GPP-compliant encoding from MIB # Get MIB parameters if 'mib' in self.params: mib_params = self.params['mib'] sfn = mib_params.get('sfn', 0) # Get MIB payload bits (24 bits) if hasattr(self, 'var_sfn'): mib_payload_bits_list, _, _ = self.encode_mib_payload() mib_payload_bits = np.array(mib_payload_bits_list, dtype=np.uint8) else: mib_payload_bits = np.zeros(24, dtype=np.uint8) # Get SSB index (use 0 for constellation plot, or could iterate through all) ssb_idx = 0 # Get k_SSB MSB from MIB (ssb_subcarrier_offset >> 4) k_ssb_msb = (mib_params.get('ssb_subcarrier_offset', 0) >> 4) & 0x1 # Get L_max l_max = self.params.get('l_max', 8) # Half-frame indicator (0 for first 5ms, 1 for second 5ms) n_hf = 0 # Encode PBCH pbch_symbols = encode_pbch( mib_payload_bits=mib_payload_bits, n_cell_id=n_cell_id, sfn=sfn, ssb_idx=ssb_idx, k_ssb_msb=k_ssb_msb, Lmax=l_max, n_hf=n_hf ) else: # Fallback to random if MIB not available qpsk_constellation = np.array([1+1j, 1-1j, -1+1j, -1-1j]) / np.sqrt(2) pbch_symbols = qpsk_constellation[np.random.randint(0, 4, 432)] # PBCH DMRS: QPSK dmrs_symbols = generate_pbch_dmrs_sequence(n_cell_id, 0, self.params['l_max']) # ===================================================================== # Plot 1: PSS Constellation (BPSK) # ===================================================================== ax1 = fig.add_subplot(2, 3, 1) ax1.scatter(np.real(pss_symbols), np.imag(pss_symbols), c='#ff4444', s=50, alpha=0.7, edgecolors='black', linewidth=0.5) ax1.axhline(y=0, color='gray', linewidth=0.5, linestyle='--') ax1.axvline(x=0, color='gray', linewidth=0.5, linestyle='--') ax1.set_xlim([-1.5, 1.5]) ax1.set_ylim([-1.5, 1.5]) ax1.set_aspect('equal') ax1.set_title(f'PSS Constellation (BPSK) - {len(pss_symbols)} symbols', fontsize=8) ax1.tick_params(labelsize=6) ax1.grid(True, alpha=0.3) # ===================================================================== # Plot 2: SSS Constellation (BPSK) # ===================================================================== ax2 = fig.add_subplot(2, 3, 2) ax2.scatter(np.real(sss_symbols), np.imag(sss_symbols), c='#4444ff', s=50, alpha=0.7, edgecolors='black', linewidth=0.5) ax2.axhline(y=0, color='gray', linewidth=0.5, linestyle='--') ax2.axvline(x=0, color='gray', linewidth=0.5, linestyle='--') ax2.set_xlim([-1.5, 1.5]) ax2.set_ylim([-1.5, 1.5]) ax2.set_aspect('equal') ax2.set_title(f'SSS Constellation (BPSK) - {len(sss_symbols)} symbols', fontsize=8) ax2.tick_params(labelsize=6) ax2.grid(True, alpha=0.3) # ===================================================================== # Plot 3: PBCH Constellation (QPSK) # ===================================================================== ax3 = fig.add_subplot(2, 3, 3) ax3.scatter(np.real(pbch_symbols), np.imag(pbch_symbols), c='#44ff44', s=30, alpha=0.6, edgecolors='black', linewidth=0.3) ax3.axhline(y=0, color='gray', linewidth=0.5, linestyle='--') ax3.axvline(x=0, color='gray', linewidth=0.5, linestyle='--') ax3.set_xlim([-1.2, 1.2]) ax3.set_ylim([-1.2, 1.2]) ax3.set_aspect('equal') ax3.set_title(f'PBCH Constellation (QPSK) - {len(pbch_symbols)} symbols', fontsize=8) ax3.tick_params(labelsize=6) ax3.grid(True, alpha=0.3) # ===================================================================== # Plot 4: PBCH DMRS Constellation (QPSK) # ===================================================================== ax4 = fig.add_subplot(2, 3, 4) ax4.scatter(np.real(dmrs_symbols), np.imag(dmrs_symbols), c='#ffff00', s=30, alpha=0.7, edgecolors='black', linewidth=0.3) ax4.axhline(y=0, color='gray', linewidth=0.5, linestyle='--') ax4.axvline(x=0, color='gray', linewidth=0.5, linestyle='--') ax4.set_xlim([-1.2, 1.2]) ax4.set_ylim([-1.2, 1.2]) ax4.set_aspect('equal') ax4.set_title(f'PBCH DMRS Constellation (QPSK) - {len(dmrs_symbols)} symbols', fontsize=8) ax4.tick_params(labelsize=6) ax4.grid(True, alpha=0.3) # ===================================================================== # Plot 5: Combined Constellation (All SSB components) # ===================================================================== ax5 = fig.add_subplot(2, 3, 5) # Plot each component with different colors ax5.scatter(np.real(pss_symbols), np.imag(pss_symbols), c='#ff4444', s=40, alpha=0.7, label='PSS', edgecolors='black', linewidth=0.3) ax5.scatter(np.real(sss_symbols), np.imag(sss_symbols), c='#4444ff', s=40, alpha=0.7, label='SSS', edgecolors='black', linewidth=0.3) ax5.scatter(np.real(pbch_symbols), np.imag(pbch_symbols), c='#44ff44', s=20, alpha=0.5, label='PBCH', edgecolors='none') ax5.scatter(np.real(dmrs_symbols), np.imag(dmrs_symbols), c='#ffff00', s=20, alpha=0.6, label='DMRS', edgecolors='black', linewidth=0.2) ax5.axhline(y=0, color='gray', linewidth=0.5, linestyle='--') ax5.axvline(x=0, color='gray', linewidth=0.5, linestyle='--') ax5.set_xlim([-1.5, 1.5]) ax5.set_ylim([-1.5, 1.5]) ax5.set_aspect('equal') ax5.set_title(f'Combined SSB Constellation - All components', fontsize=8) ax5.legend(loc='upper right', fontsize=7) ax5.tick_params(labelsize=6) ax5.grid(True, alpha=0.3) # ===================================================================== # Plot 6: Info text box (styled to match plot size) # ===================================================================== ax6 = fig.add_subplot(2, 3, 6) ax6.set_xlim([0, 1]) ax6.set_ylim([0, 1]) ax6.set_xticks([]) ax6.set_yticks([]) ax6.set_facecolor('#f5f5dc') # Beige background # Add border for spine in ax6.spines.values(): spine.set_edgecolor('#8b7355') spine.set_linewidth(2) ax6.set_title('SSB Signal Characteristics', fontsize=9, fontweight='bold', pad=8) # Info text content info_lines = [ ('PSS (Primary Sync Signal)', '#ff4444'), (' Modulation: BPSK (±1)', 'black'), (' Length: 127 symbols', 'black'), (' m-sequence based', 'black'), (f' N_ID^(2) = {cp["n_id_2"]}', 'black'), ('', 'black'), ('SSS (Secondary Sync Signal)', '#4444ff'), (' Modulation: BPSK (±1)', 'black'), (' Length: 127 symbols', 'black'), (' Gold sequence based', 'black'), (f' N_ID^(1) = {cp["n_id_1"]}', 'black'), ('', 'black'), ('PBCH (Broadcast Channel)', '#228b22'), (' Modulation: QPSK', 'black'), (' REs per SSB: 432', 'black'), (' Carries MIB', 'black'), ('', 'black'), ('PBCH DMRS', '#b8860b'), (' Modulation: QPSK', 'black'), (' Every 4th subcarrier', 'black'), (' Gold sequence based', 'black'), ('', 'black'), (f'Cell ID: {n_cell_id}', '#800080'), ] y_pos = 0.92 for text, color in info_lines: if text: fontweight = 'bold' if not text.startswith(' ') else 'normal' ax6.text(0.08, y_pos, text, transform=ax6.transAxes, fontsize=7, color=color, fontweight=fontweight, fontfamily='monospace') y_pos -= 0.035 fig.tight_layout() def update_tab4_constellation(self): """Update Tab 4: Constellation diagrams""" self._draw_constellation() self.canvases['constellation'].draw() def run(self): """Start the GUI""" self.root.mainloop() # ============================================================================= # Print Configuration Summary # ============================================================================= print("\n" + "=" * 70) print("SSB Configuration Summary") print("=" * 70) print(f"SSB Case: {SSB_CASE}") print(f"Subcarrier Spacing: {SSB_SCS_KHZ} kHz") print(f"L_max: {L_MAX}") print(f"SSB TX Bitmap: {SSB_TX_BITMAP_NORM}") print(f"SSBs Transmitted: {NUM_SSB_TRANSMITTED}") print(f"SSB Starting Symbols: {SSB_SYMBOL_POSITIONS}") print(f"SSB Indices (bitmap order): {SSB_SYMBOL_INDICES}") print(f"offsetToPointA (RB@15k): {OFFSET_TO_POINTA_RB}") print(f"k_SSB (SC@15k): {K_SSB}") print("-" * 70) print(f"Time Domain:") print(f" Half Frame Duration: {HALF_FRAME_MS} ms") print(f" Slots in Half Frame: {NUM_SLOTS_IN_HALF_FRAME}") print(f" Symbols per Slot: {NUM_SYMBOLS_PER_SLOT}") print(f" Total Symbols (5ms): {NUM_OFDM_SYMBOLS}") print("-" * 70) print(f"Frequency Domain:") print(f" Channel Bandwidth: {CHANNEL_BW_MHZ} MHz") print(f" Max RBs in Channel: {MAX_RBS}") print(f" SSB Size: {SSB_NUM_RBS} RBs ({SSB_NUM_SUBCARRIERS} subcarriers)") print(f" SSB Start RB: {SSB_OFFSET_RB}") print(f" SSB Start Subcarrier: {SSB_START_SC}") print("-" * 70) print(f"SSB Components (per burst):") print(f" PSS: {PSS_SSS_NUM_SUBCARRIERS} subcarriers (Symbol 0)") print(f" SSS: {PSS_SSS_NUM_SUBCARRIERS} subcarriers (Symbol 2)") print(f" PBCH: 3 symbols with DMRS (every 4th SC)") print("=" * 70) # ============================================================================= # Launch Viewer # ============================================================================= print("\n" + "=" * 70) print("Launching SSB Resource Grid Viewer...") print("=" * 70) # Initialize with default parameters initial_params = { 'ssb_case': SSB_CASE, 'l_max': L_MAX, 'ssb_tx_bitmap': SSB_TX_BITMAP, 'channel_bw_mhz': CHANNEL_BW_MHZ, 'n_cell_id': N_CELL_ID, } viewer = SSBResourceGridViewer(initial_params) viewer.run() print("\n" + "=" * 70) print("SSB Resource Grid Visualization Complete!") print("=" * 70)