Source code for sionna.phy.nr.pusch_config

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""PUSCH configuration for the 5G NR module of Sionna PHY."""

from typing import List, Optional, Tuple, Union
import numpy as np

from sionna.phy import nr
from .config import Config
from .utils import generate_prng_seq, calculate_tb_size


__all__ = ["PUSCHConfig", "check_pusch_configs"]


[docs] class PUSCHConfig(Config): r"""Configuration for a physical uplink shared channel (PUSCH). Implements parameters as described in Sections 6.3 and 6.4 :cite:p:`3GPPTS38211`. All configurable properties can be provided as keyword arguments during initialization or changed later. :param carrier_config: If `None`, a :class:`~sionna.phy.nr.CarrierConfig` instance with default settings will be created. :param pusch_dmrs_config: If `None`, a :class:`~sionna.phy.nr.PUSCHDMRSConfig` instance with default settings will be created. :param tb_config: If `None`, a :class:`~sionna.phy.nr.TBConfig` instance with default settings will be created. .. rubric:: Examples .. code-block:: python from sionna.phy.nr import PUSCHConfig pusch_config = PUSCHConfig(mapping_type="B") pusch_config.dmrs.config_type = 2 pusch_config.carrier.subcarrier_spacing = 30 """ def __init__( self, carrier_config: Optional["nr.CarrierConfig"] = None, pusch_dmrs_config: Optional["nr.PUSCHDMRSConfig"] = None, tb_config: Optional["nr.TBConfig"] = None, **kwargs, ): super().__init__(**kwargs) self._name = "PUSCH Configuration" self.carrier = carrier_config self.dmrs = pusch_dmrs_config self.tb = tb_config self.check_config() #-----------------------------# #---Configurable parameters---# #-----------------------------# @property def carrier(self) -> "nr.CarrierConfig": """:class:`~sionna.phy.nr.CarrierConfig`: Carrier configuration.""" return self._carrier @carrier.setter def carrier(self, value: Optional["nr.CarrierConfig"]) -> None: if value is None: value = nr.CarrierConfig() elif not isinstance(value, nr.CarrierConfig): raise TypeError("carrier must be an instance of CarrierConfig") self._carrier = value @property def dmrs(self) -> "nr.PUSCHDMRSConfig": """:class:`~sionna.phy.nr.PUSCHDMRSConfig`: PUSCH DMRS configuration.""" return self._dmrs @dmrs.setter def dmrs(self, value: Optional["nr.PUSCHDMRSConfig"]) -> None: if value is None: value = nr.PUSCHDMRSConfig() elif not isinstance(value, nr.PUSCHDMRSConfig): raise TypeError( "pusch_dmrs_config must be an instance of PUSCHDMRSConfig") self._dmrs = value @property def tb(self) -> "nr.TBConfig": """:class:`~sionna.phy.nr.TBConfig`: Transport block configuration.""" return self._tb @tb.setter def tb(self, value: Optional["nr.TBConfig"]) -> None: if value is None: value = nr.TBConfig(channel_type="PUSCH") elif not isinstance(value, nr.TBConfig): raise TypeError("tb must be an instance of TBConfig") elif value.channel_type != "PUSCH": raise ValueError('TBConfig must be configured for "PUSCH"') self._tb = value @property def n_size_bwp(self) -> Optional[int]: r"""Number of resource blocks in the BWP :math:`N^{\text{size},\mu}_{\text{BWP},i}`. Defaults to `None`. Must be in [1, ..., 275]. If set to `None`, the property :attr:`~sionna.phy.nr.CarrierConfig.n_size_grid` of ``carrier`` will be used. """ self._ifndef("n_size_bwp", None) return self._n_size_bwp @n_size_bwp.setter def n_size_bwp(self, value: Optional[int]) -> None: if value is not None and value not in range(1, 276): raise ValueError("n_size_bwp must be in the range from 1 to 275") self._n_size_bwp = value @property def n_start_bwp(self) -> int: r"""Start of BWP relative to common resource block (CRB) 0 :math:`N^{\text{start},\mu}_{\text{BWP},i}`. Defaults to 0. Must be in [0, ..., 2473]. """ self._ifndef("n_start_bwp", 0) return self._n_start_bwp @n_start_bwp.setter def n_start_bwp(self, value: int) -> None: if value not in range(0, 2474): raise ValueError("n_start_bwp must be in the range from 0 to 2473") self._n_start_bwp = value @property def num_layers(self) -> int: r"""Number of transmission layers :math:`\nu`. Defaults to 1. Must be in [1, 2, 3, 4]. Must be smaller than or equal to :attr:`~sionna.phy.nr.PUSCHConfig.num_antenna_ports`. """ self._ifndef("num_layers", 1) return self._num_layers @num_layers.setter def num_layers(self, value: int) -> None: if value not in [1, 2, 3, 4]: raise ValueError("num_layers must be in [1, 2, 3, 4]") self._num_layers = value @property def num_antenna_ports(self) -> int: """Number of antenna ports. Defaults to 1. Must be in [1, 2, 4]. Must be larger than or equal to :attr:`~sionna.phy.nr.PUSCHConfig.num_layers`. """ self._ifndef("num_antenna_ports", 1) return self._num_antenna_ports @num_antenna_ports.setter def num_antenna_ports(self, value: int) -> None: if value not in [1, 2, 4]: raise ValueError("num_antenna_ports must be in [1, 2, 4]") self._num_antenna_ports = value @property def mapping_type(self) -> str: """Mapping type. Defaults to ``"A"``. Must be ``"A"`` or ``"B"``.""" self._ifndef("mapping_type", "A") return self._mapping_type @mapping_type.setter def mapping_type(self, value: str) -> None: if value not in ["A", "B"]: raise ValueError("mapping_type must be 'A' or 'B'") self._mapping_type = value @property def symbol_allocation(self) -> List[int]: """PUSCH symbol allocation. Defaults to [0, 14]. The first element denotes the start of the symbol allocation. The second denotes the positive number of allocated OFDM symbols. For ``mapping_type`` ``"A"``, the first element must be zero. For ``mapping_type`` ``"B"``, the first element must be in [0, ..., 13]. The second element must be such that the index of the last allocated OFDM symbol is not larger than 13 (for ``"normal"`` cyclic prefix) or 11 (for ``"extended"`` cyclic prefix). """ self._ifndef("symbol_allocation", [0, 14]) return self._symbol_allocation @symbol_allocation.setter def symbol_allocation(self, value: List[int]) -> None: if len(value) != 2: raise ValueError("symbol_allocation must have two elements") self._symbol_allocation = list(value) @property def n_rnti(self) -> int: r"""Radio network temporary identifier :math:`n_\text{RNTI}`. Defaults to 1. Must be in [0, ..., 65535]. """ self._ifndef("n_rnti", 1) return self._n_rnti @n_rnti.setter def n_rnti(self, value: Optional[int]) -> None: if value is not None and value not in range(65536): raise ValueError("n_rnti must be in [0, 65535]") self._n_rnti = value @property def precoding(self) -> str: """PUSCH transmission scheme. Defaults to ``"non-codebook"``. Must be ``"codebook"`` or ``"non-codebook"``. """ self._ifndef("precoding", "non-codebook") return self._precoding @precoding.setter def precoding(self, value: str) -> None: if value not in ["codebook", "non-codebook"]: raise ValueError("precoding must be 'codebook' or 'non-codebook'") self._precoding = value @property def transform_precoding(self) -> bool: """Use transform precoding. Defaults to `False`.""" self._ifndef("transform_precoding", False) return self._transform_precoding @transform_precoding.setter def transform_precoding(self, value: bool) -> None: if not isinstance(value, bool): raise TypeError("transform_precoding must be bool") self._transform_precoding = value @property def tpmi(self) -> int: """Transmit precoding matrix indicator. Defaults to 0. Must be in [0, ..., 27]. The allowed value depends on the number of layers and the number of antenna ports according to Tables 6.3.1.5-1 to 6.3.1.5-7 :cite:p:`3GPPTS38211`. """ self._ifndef("tpmi", 0) return self._tpmi @tpmi.setter def tpmi(self, value: int) -> None: if value not in range(28): raise ValueError("tpmi must be in [0, 27]") self._tpmi = value #-----------------------------# #---Read-only parameters------# #-----------------------------# @property def frequency_hopping(self) -> str: """Frequency hopping configuration. Read-only, defaults to ``"neither"``.""" return "neither" @property def l_0(self) -> int: r"""Position of the first DMRS symbol :math:`l_0` relative to the reference ``l_ref``. Read-only.""" if self.mapping_type == "A": return self.dmrs.type_a_position return 0 @property def l_d(self) -> int: r"""Length of the symbol allocation :math:`l_\text{d}`. Read-only.""" return self.symbol_allocation[1] @property def l_ref(self) -> int: """Reference OFDM symbol index used for DMRS generation. Read-only.""" if self.mapping_type == "A": return 0 return self.symbol_allocation[0] @property def l_prime(self) -> List[int]: r"""List of possible values of :math:`l'` used for DMRS generation. Read-only.""" if self.dmrs.length == 1: return [0] return [0, 1] @property def l_bar(self) -> List[int]: r"""List of possible values of :math:`\bar{l}` used for DMRS generation. Read-only. Defined in Tables 6.4.1.1.3-3 and 6.4.1.1.3-4 :cite:p:`3GPPTS38211`. """ l_0 = self.l_0 ind = 0 if self.l_d < 4 else self.l_d - 3 if self.mapping_type == "A": if self.dmrs.length == 1: l_bar = [ [[], [], [], []], [[l_0], [l_0], [l_0], [l_0]], [[l_0], [l_0], [l_0], [l_0]], [[l_0], [l_0], [l_0], [l_0]], [[l_0], [l_0], [l_0], [l_0]], [[l_0], [l_0, 7], [l_0, 7], [l_0, 7]], [[l_0], [l_0, 7], [l_0, 7], [l_0, 7]], [[l_0], [l_0, 9], [l_0, 6, 9], [l_0, 6, 9]], [[l_0], [l_0, 9], [l_0, 6, 9], [l_0, 6, 9]], [[l_0], [l_0, 9], [l_0, 6, 9], [l_0, 5, 8, 11]], [[l_0], [l_0, 11], [l_0, 7, 11], [l_0, 5, 8, 11]], [[l_0], [l_0, 11], [l_0, 7, 11], [l_0, 5, 8, 11]], ] else: l_bar = [ [[], []], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0, 8]], [[l_0], [l_0, 8]], [[l_0], [l_0, 8]], [[l_0], [l_0, 10]], [[l_0], [l_0, 10]], ] else: # mapping_type == "B" if self.dmrs.length == 1: l_bar = [ [[l_0], [l_0], [l_0], [l_0]], [[l_0], [l_0], [l_0], [l_0]], [[l_0], [l_0, 4], [l_0, 4], [l_0, 4]], [[l_0], [l_0, 4], [l_0, 4], [l_0, 4]], [[l_0], [l_0, 4], [l_0, 4], [l_0, 4]], [[l_0], [l_0, 6], [l_0, 3, 6], [l_0, 3, 6]], [[l_0], [l_0, 6], [l_0, 3, 6], [l_0, 3, 6]], [[l_0], [l_0, 8], [l_0, 4, 8], [l_0, 3, 6, 9]], [[l_0], [l_0, 8], [l_0, 4, 8], [l_0, 3, 6, 9]], [[l_0], [l_0, 10], [l_0, 5, 10], [l_0, 3, 6, 9]], [[l_0], [l_0, 10], [l_0, 5, 10], [l_0, 3, 6, 9]], [[l_0], [l_0, 10], [l_0, 5, 10], [l_0, 3, 6, 9]], ] else: l_bar = [ [[], []], [[], []], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0]], [[l_0], [l_0, 5]], [[l_0], [l_0, 5]], [[l_0], [l_0, 7]], [[l_0], [l_0, 7]], [[l_0], [l_0, 9]], [[l_0], [l_0, 9]], [[l_0], [l_0, 9]], ] return l_bar[ind][self.dmrs.additional_position] @property def l(self) -> List[int]: r"""List of possible values of the OFDM symbol indices :math:`l` carrying DMRS relative to :math:`l_0`. Read-only.""" result = [] for l_bar in self.l_bar: for l_prime in self.l_prime: result.append(l_bar + l_prime) return result @property def n(self) -> List[int]: """List of possible values of n used for DMRS generation. Read-only.""" if self.dmrs.config_type == 1: n_max = self.num_resource_blocks * 12 // 4 - 1 else: # config_type == 2 n_max = self.num_resource_blocks * 12 // 6 - 1 return list(range(n_max + 1)) @property def dmrs_symbol_indices(self) -> List[int]: """Indices of DMRS symbols within a slot. Read-only.""" return [sym + self.l_ref for sym in self.l] @property def num_resource_blocks(self) -> int: """Number of allocated resource blocks for the PUSCH transmissions. Read-only.""" if self.n_size_bwp is None: return self.carrier.n_size_grid return self.n_size_bwp @property def num_subcarriers(self) -> int: """Number of allocated subcarriers for the PUSCH transmissions. Read-only.""" return 12 * self.num_resource_blocks @property def num_res_per_prb(self) -> int: """Number of resource elements per PRB available for data. Read-only.""" num_dmrs = len(self.dmrs_symbol_indices) num_data = self.symbol_allocation[1] - num_dmrs if self.dmrs.config_type == 1: num_res_dmrs = 12 - 6 * self.dmrs.num_cdm_groups_without_data else: # config_type == 2 num_res_dmrs = 12 - 4 * self.dmrs.num_cdm_groups_without_data num_res_data = 12 return num_data * num_res_data + num_dmrs * num_res_dmrs @property def dmrs_mask(self) -> np.ndarray: """Masked resource elements in the resource grid. Read-only. Shape: [num_subcarriers, num_symbols_per_slot]. `True` corresponds to resource elements on which no data is transmitted. """ mask = np.zeros( [self.num_subcarriers, self.carrier.num_symbols_per_slot], dtype=bool) num_cdm_groups = self.dmrs.num_cdm_groups_without_data if self.dmrs.config_type == 1: cdm_ind = np.zeros([6, num_cdm_groups], np.int32) for i in range(num_cdm_groups): cdm_ind[:, i] = np.arange(i, 12, 2) else: cdm_ind = np.zeros([4, num_cdm_groups], np.int32) for i in range(num_cdm_groups): cdm_ind[:, i] = np.array([0, 1, 6, 7]) + 2 * i for i in self.dmrs_symbol_indices: for j in range(self.num_resource_blocks): for k in range(num_cdm_groups): mask[cdm_ind[:, k] + 12 * j, i] = True return mask @property def dmrs_grid(self) -> np.ndarray: """Empty resource grid for each DMRS port, filled with DMRS signals. Read-only. Shape: [num_dmrs_ports, num_subcarriers, num_symbols_per_slot]. This property returns for each configured DMRS port an empty resource grid filled with DMRS signals as defined in Section 6.4.1.1 :cite:p:`3GPPTS38211`. Not all possible options are implemented, e.g., frequency hopping and transform precoding are not available. This property provides the *unprecoded* DMRS for each configured DMRS port. Precoding might be applied to map the DMRS to the antenna ports. However, in this case, the number of DMRS ports cannot be larger than the number of layers. """ self.check_config() reset_dmrs_port_set = False if len(self.dmrs.dmrs_port_set) == 0: self.dmrs.dmrs_port_set = list(range(self.num_layers)) reset_dmrs_port_set = True a_tilde = np.zeros( [len(self.dmrs.dmrs_port_set), self.num_subcarriers, self.carrier.num_symbols_per_slot], dtype=complex) for l_bar in self.l_bar: for l_prime in self.l_prime: sym = l_bar + l_prime c_init = self.c_init(sym) c = generate_prng_seq(2 * self.num_subcarriers, c_init=c_init) r = 1 / np.sqrt(2) * ((1 - 2 * c[::2]) + 1j * (1 - 2 * c[1::2])) for j_ind, _ in enumerate(self.dmrs.dmrs_port_set): for n in self.n: for k_prime in [0, 1]: if self.dmrs.config_type == 1: k = 4 * n + 2 * k_prime + self.dmrs.deltas[j_ind] else: # config_type == 2 k = 6 * n + k_prime + self.dmrs.deltas[j_ind] a_tilde[j_ind, k, self.l_ref + sym] = ( r[2 * n + k_prime] * self.dmrs.w_f[k_prime][j_ind] * self.dmrs.w_t[l_prime][j_ind]) a = self.dmrs.beta * a_tilde if reset_dmrs_port_set: self.dmrs.dmrs_port_set = [] return a @property def dmrs_grid_precoded(self) -> Optional[np.ndarray]: """Precoded DMRS grid. Read-only. Returns `None` if :attr:`precoding` is ``"non-codebook"``. """ if self.precoding == "non-codebook": return None w = np.expand_dims(np.expand_dims(self.precoding_matrix, 0), 0) a = np.expand_dims(np.transpose(self.dmrs_grid, [1, 2, 0]), -1) a = np.squeeze(np.matmul(w, a), -1) a = np.transpose(a, [2, 0, 1]) return a @property def precoding_matrix(self) -> Optional[np.ndarray]: r"""Precoding matrix :math:`\mathbf{W}`. Read-only. Shape: [num_antenna_ports, num_layers]. Defined in Tables 6.3.1.5-1 to 6.3.1.5-7 :cite:p:`3GPPTS38211`. Only relevant if :attr:`~sionna.phy.nr.PUSCHConfig.precoding` is ``"codebook"``. """ if self.precoding == "non-codebook": return None if self.num_antenna_ports == 1: return None w = None if self.num_layers == 1: if self.num_antenna_ports == 2: w = np.zeros([6, 2, 1], complex) w[:, 0, 0] = [1, 0, 1, 1, 1, 1] w[:, 1, 0] = [0, 1, 1, -1, 1j, -1j] w /= np.sqrt(2) elif self.num_antenna_ports == 4: w = np.zeros([28, 4, 1], complex) w[:8, 0, 0] = [1, 0, 0, 0, 1, 1, 1, 1] w[:8, 1, 0] = [0, 1, 0, 0, 0, 0, 0, 0] w[:8, 2, 0] = [0, 0, 1, 0, 1, -1, 1j, -1j] w[:8, 3, 0] = [0, 0, 0, 1, 0, 0, 0, 0] w[8:16, 0, 0] = [0, 0, 0, 0, 1, 1, 1, 1] w[8:16, 1, 0] = [1, 1, 1, 1, 1, 1, 1, 1] w[8:16, 2, 0] = [0, 0, 0, 0, 1, 1j, -1, -1j] w[8:16, 3, 0] = [1, -1, 1j, -1j, 1, 1j, -1, -1j] w[16:24, 0, 0] = [1, 1, 1, 1, 1, 1, 1, 1] w[16:24, 1, 0] = [1j, 1j, 1j, 1j, -1, -1, -1, -1] w[16:24, 2, 0] = [1, 1j, -1, -1j, 1, 1j, -1, -1j] w[16:24, 3, 0] = [1j, -1, -1j, 1, -1, -1j, 1, 1j] w[24:28, 0, 0] = [1, 1, 1, 1] w[24:28, 1, 0] = [-1j, -1j, -1j, -1j] w[24:28, 2, 0] = [1, 1j, -1, -1j] w[24:28, 3, 0] = [-1j, 1, 1j, -1] w /= 2 elif self.num_layers == 2: if self.num_antenna_ports == 2: w = np.zeros([3, 2, 2], complex) w[0] = [[1, 0], [0, 1]] w[0] /= np.sqrt(2) w[1] = [[1, 1], [1, -1]] w[1] /= 2 w[2] = [[1, 1], [1j, -1j]] w[2] /= 2 elif self.num_antenna_ports == 4: w = np.zeros([22, 4, 2], complex) w[0] = [[1, 0], [0, 1], [0, 0], [0, 0]] w[0] /= 2 w[1] = [[1, 0], [0, 0], [0, 1], [0, 0]] w[1] /= 2 w[2] = [[1, 0], [0, 0], [0, 0], [0, 1]] w[2] /= 2 w[3] = [[0, 0], [1, 0], [0, 1], [0, 0]] w[3] /= 2 w[4] = [[0, 0], [1, 0], [0, 0], [0, 1]] w[4] /= 2 w[5] = [[0, 0], [0, 0], [1, 0], [0, 1]] w[5] /= 2 w[6] = [[1, 0], [0, 1], [1, 0], [0, -1j]] w[6] /= 2 w[7] = [[1, 0], [0, 1], [1, 0], [0, 1j]] w[7] /= 2 w[8] = [[1, 0], [0, 1], [-1j, 0], [0, 1]] w[8] /= 2 w[9] = [[1, 0], [0, 1], [-1j, 0], [0, -1]] w[9] /= 2 w[10] = [[1, 0], [0, 1], [-1, 0], [0, -1j]] w[10] /= 2 w[11] = [[1, 0], [0, 1], [-1, 0], [0, 1j]] w[11] /= 2 w[12] = [[1, 0], [0, 1], [1j, 0], [0, 1]] w[12] /= 2 w[13] = [[1, 0], [0, 1], [1j, 0], [0, -1]] w[13] /= 2 w[14] = [[1, 1], [1, 1], [1, -1], [1, -1]] w[14] /= 2 * np.sqrt(2) w[15] = [[1, 1], [1, 1], [1j, -1j], [1j, -1j]] w[15] /= 2 * np.sqrt(2) w[16] = [[1, 1], [1j, 1j], [1, -1], [1j, -1j]] w[16] /= 2 * np.sqrt(2) w[17] = [[1, 1], [1j, 1j], [1j, -1j], [-1, 1]] w[17] /= 2 * np.sqrt(2) w[18] = [[1, 1], [-1, -1], [1, -1], [-1, 1]] w[18] /= 2 * np.sqrt(2) w[19] = [[1, 1], [-1, -1], [1j, -1j], [-1j, 1j]] w[19] /= 2 * np.sqrt(2) w[20] = [[1, 1], [-1j, -1j], [1, -1], [-1j, 1j]] w[20] /= 2 * np.sqrt(2) w[21] = [[1, 1], [-1j, -1j], [1j, -1j], [1, -1]] w[21] /= 2 * np.sqrt(2) elif self.num_layers == 3: if self.num_antenna_ports == 4: w = np.zeros([7, 4, 3], complex) w[0] = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 0]] w[0] /= 2 w[1] = [[1, 0, 0], [0, 1, 0], [1, 0, 0], [0, 0, 1]] w[1] /= 2 w[2] = [[1, 0, 0], [0, 1, 0], [-1, 0, 0], [0, 0, 1]] w[2] /= 2 w[3] = [[1, 1, 1], [1, -1, 1], [1, 1, -1], [1, -1, -1]] w[3] /= (2 * np.sqrt(3)) w[4] = [[1, 1, 1], [1, -1, 1], [1j, 1j, -1j], [1j, -1j, -1j]] w[4] /= (2 * np.sqrt(3)) w[5] = [[1, 1, 1], [-1, 1, -1], [1, 1, -1], [-1, 1, 1]] w[5] /= (2 * np.sqrt(3)) w[6] = [[1, 1, 1], [-1, 1, -1], [1j, 1j, -1j], [-1j, 1j, 1j]] w[6] /= (2 * np.sqrt(3)) elif self.num_layers == 4: if self.num_antenna_ports == 4: w = np.zeros([5, 4, 4], complex) w[0] = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] w[0] /= 2 w[1] = [[1, 1, 0, 0], [0, 0, 1, 1], [1, -1, 0, 0], [0, 0, 1, -1]] w[1] /= 2 * np.sqrt(2) w[2] = [[1, 1, 0, 0], [0, 0, 1, 1], [1j, -1j, 0, 0], [0, 0, 1j, -1j]] w[2] /= 2 * np.sqrt(2) w[3] = [[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]] w[3] /= 4 w[4] = [[1, 1, 1, 1], [1, -1, 1, -1], [1j, 1j, -1j, -1j], [1j, -1j, -1j, 1j]] w[4] /= 4 if w is None: return w return w[self.tpmi] @property def num_ov(self) -> int: """Number of unused resource elements due to additional overhead as specified by higher layer. Read-only. Defaults to 0. """ return 0 @property def num_coded_bits(self) -> int: """Number of coded bits that fit into one PUSCH slot. Read-only.""" n_re_per_prb = self.num_res_per_prb - self.num_ov n_re = n_re_per_prb * self.num_resource_blocks num_coded_bits = int( self.tb.tb_scaling * self.tb.num_bits_per_symbol * self.num_layers * n_re) return num_coded_bits @property def tb_size(self) -> int: """Transport block size. Read-only. Number of information bits that can be encoded into a slot for the given slot configuration. """ n_re_per_prb = self.num_res_per_prb - self.num_ov n_re = min(156, n_re_per_prb) * self.num_resource_blocks target_tb_size = int( self.tb.target_coderate * self.tb.tb_scaling * n_re * self.tb.num_bits_per_symbol * self.num_layers) tb_size, *_ = calculate_tb_size( target_tb_size=target_tb_size, num_coded_bits=self.num_coded_bits, target_coderate=self.tb.target_coderate, modulation_order=self.tb.num_bits_per_symbol, verbose=False) return int(tb_size) #-------------------# #---Class methods---# #-------------------#
[docs] def c_init(self, l: int) -> int: r"""Compute RNG initialization :math:`c_\text{init}` as in Section 6.4.1.1.1.1 :cite:p:`3GPPTS38211`. :param l: OFDM symbol index relative to a reference :math:`l`. """ num_symbols_per_slot = self.carrier.num_symbols_per_slot slot_number = self.carrier.slot_number lambda_bar = 0 n_scid_bar = self.dmrs.n_scid if self.dmrs.n_id is None: n_id = self.carrier.n_cell_id else: n_id = self.dmrs.n_id[n_scid_bar] c_init = np.mod( 2**17 * (num_symbols_per_slot * slot_number + l + 1) * (2 * n_id + 1) + 2**17 * np.floor(lambda_bar / 2) + 2 * n_id + n_scid_bar, 2**31) return int(c_init)
[docs] def show(self) -> None: """Print all properties of the PUSCHConfig and children configs.""" self.carrier.show() Config.show(self) self.dmrs.show() self.tb.show()
[docs] def check_config(self) -> bool: """Test if the compound configuration is valid.""" self.carrier.check_config() self.dmrs.check_config() if self.precoding == "codebook": if len(self.dmrs.dmrs_port_set) > 0: if len(self.dmrs.dmrs_port_set) != self.num_layers: raise ValueError( "num_layers must equal the number of DMRS ports") if self.num_layers > self.num_antenna_ports: raise ValueError("num_layers must be <= num_antenna_ports") if self.num_antenna_ports < 2: raise ValueError( "precoding requires two or more antenna ports") else: if self.num_layers != self.num_antenna_ports: raise ValueError("num_layers must equal num_antenna_ports") # Check Tables 6.4.1.1.3-3/4 validity if self.dmrs.length == 1: if self.mapping_type == "A" and self.symbol_allocation[1] < 4: raise ValueError("Symbol allocation is too short") else: if self.dmrs.additional_position >= 2: raise ValueError( "dmrs.additional_position must be < 2 for this dmrs.length") if self.symbol_allocation[1] < 4: raise ValueError("Symbol allocation too short") if self.mapping_type == "B" and self.symbol_allocation[1] < 5: raise ValueError("Symbol allocation is too short") # Check type_a and additional_position if self.mapping_type == "A" and self.dmrs.additional_position == 3: if self.dmrs.type_a_position != 2: raise ValueError( "additional_position=3 only allowed for type_a_position=2") # Check TPMI validity if self.num_layers == 1: if self.num_antenna_ports == 2 and self.tpmi not in range(6): raise ValueError("tpmi must be in [0, 5]") elif self.num_antenna_ports == 4 and self.tpmi not in range(28): raise ValueError("tpmi must be in [0, 27]") elif self.num_layers == 2: if self.num_antenna_ports == 2 and self.tpmi not in range(3): raise ValueError("tpmi must be in [0, 2]") elif self.num_antenna_ports == 4 and self.tpmi not in range(22): raise ValueError("tpmi must be in [0, 21]") elif self.num_layers == 3: if self.tpmi not in range(7): raise ValueError("tpmi must be in [0, 6]") elif self.num_layers == 4: if self.tpmi not in range(5): raise ValueError("tpmi must be in [0, 4]") # Check symbol allocation max_length = 14 if self.carrier.cyclic_prefix == "normal" else 12 if self.mapping_type == "A": if self.symbol_allocation[0] != 0: raise ValueError( "symbol_allocation[0] must be 0 for mapping_type A") if not 4 <= self.symbol_allocation[1] <= max_length: raise ValueError( f"symbol_allocation[1] must be in [4, {max_length}]") if self.dmrs.length == 2 and self.symbol_allocation[1] < 4: raise ValueError( "symbol_allocation[1] must be >= 4 for dmrs.length == 2") elif self.mapping_type == "B": if not 0 <= self.symbol_allocation[0] <= 13: raise ValueError( "symbol_allocation[0] must be in [0, 13] for mapping_type B") if not 1 <= self.symbol_allocation[1] <= max_length: raise ValueError( f"symbol_allocation[1] must be in [1, {max_length}]") if self.dmrs.length == 2 and self.symbol_allocation[1] < 5: raise ValueError( "symbol_allocation[1] must be >= 5 for dmrs.length == 2") if self.symbol_allocation[0] + self.symbol_allocation[1] > max_length: raise ValueError( f"symbol_allocation[0] + [1] must be <= {max_length}") # Validate all configurable attributes attr_list = [ "n_size_bwp", "n_start_bwp", "num_layers", "mapping_type", "symbol_allocation", "n_rnti", "precoding", "transform_precoding", "tpmi" ] for attr in attr_list: value = getattr(self, attr) setattr(self, attr, value) if self.tb.channel_type != "PUSCH": raise ValueError('TB_config must be configured for "PUSCH"') if (len(self.dmrs.dmrs_port_set) > 0 and self.num_layers != len(self.dmrs.dmrs_port_set)): raise ValueError("num_layers must equal the number of DMRS ports") return True
def check_pusch_configs(pusch_configs: List[PUSCHConfig]) -> dict: """Validate a list of PUSCHConfig instances and extract common parameters. :param pusch_configs: List of :class:`~sionna.phy.nr.PUSCHConfig` instances. """ if not isinstance(pusch_configs, list): raise TypeError( "pusch_configs must be a list of PUSCHConfig instances") for pusch_config in pusch_configs: if not isinstance(pusch_config, PUSCHConfig): raise TypeError( "All elements must be instances of PUSCHConfig") pusch_config.check_config() pc = pusch_configs[0] carrier = pc.carrier params = { "num_bits_per_symbol": pc.tb.num_bits_per_symbol, "num_tx": len(pusch_configs), "num_layers": pc.num_layers, "num_subcarriers": pc.num_subcarriers, "num_ofdm_symbols": pc.symbol_allocation[1], "subcarrier_spacing": pc.carrier.subcarrier_spacing * 1e3, "num_antenna_ports": pc.num_antenna_ports, "precoding": pc.precoding, "precoding_matrices": [], "pusch_config": pc, "carrier_config": pc.carrier, "num_coded_bits": pc.num_coded_bits, "target_coderate": pc.tb.target_coderate, "n_id": [], "n_rnti": [], "tb_size": pc.tb_size, "dmrs_length": pc.dmrs.length, "dmrs_additional_position": pc.dmrs.additional_position, "num_cdm_groups_without_data": pc.dmrs.num_cdm_groups_without_data, } params["bandwidth"] = params["num_subcarriers"] * params["subcarrier_spacing"] params["cyclic_prefix_length"] = np.ceil( carrier.cyclic_prefix_length * params["bandwidth"]) for pusch_config in pusch_configs: if params["precoding"] == "codebook": params["precoding_matrices"].append(pusch_config.precoding_matrix) if pusch_config.tb.n_id is None: params["n_id"].append(pusch_config.carrier.n_cell_id) else: params["n_id"].append(pusch_config.tb.n_id) params["n_rnti"].append(pusch_config.n_rnti) return params