Source code for sionna.phy.nr.pusch_dmrs_config

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

from collections.abc import Sequence
from typing import List, Optional, Tuple, Union
import numpy as np

from .config import Config


__all__ = ["PUSCHDMRSConfig"]


[docs] class PUSCHDMRSConfig(Config): """Sets parameters related to the generation of demodulation reference signals (DMRS) for a physical uplink shared channel (PUSCH), as described in Section 6.4.1.1 :cite:p:`3GPPTS38211`. All configurable properties can be provided as keyword arguments during initialization or changed later. .. rubric:: Examples .. code-block:: python from sionna.phy.nr import PUSCHDMRSConfig dmrs_config = PUSCHDMRSConfig(config_type=2) dmrs_config.additional_position = 1 """ def __init__(self, **kwargs): self._name = "PUSCH DMRS Configuration" super().__init__(**kwargs) self.check_config() # ----------------------------- # Configurable parameters # ----------------------------- @property def config_type(self) -> int: """`int`, 1 (default) | 2 : DMRS configuration type. The configuration type determines the frequency density of DMRS signals. With configuration type 1, six subcarriers per PRB are used for each antenna port, with configuration type 2, four subcarriers are used. """ self._ifndef("config_type", 1) return self._config_type @config_type.setter def config_type(self, value: int) -> None: if value not in [1, 2]: raise ValueError("config_type must be in [1,2]") self._config_type = value @property def type_a_position(self) -> int: """`int`, 2 (default) | 3 : Position of first DMRS OFDM symbol. Defines the position of the first DMRS symbol within a slot. This parameter only applies if the property :attr:`~sionna.phy.nr.PUSCHConfig.mapping_type` of :class:`~sionna.phy.nr.PUSCHConfig` is equal to "A". """ self._ifndef("type_a_position", 2) return self._type_a_position @type_a_position.setter def type_a_position(self, value: int) -> None: if value not in [2, 3]: raise ValueError("type_a_position must be in [2,3]") self._type_a_position = value @property def additional_position(self) -> int: """`int`, 0 (default) | 1 | 2 | 3 : Maximum number of additional DMRS positions. The actual number of used DMRS positions depends on the length of the PUSCH symbol allocation. """ self._ifndef("additional_position", 0) return self._additional_position @additional_position.setter def additional_position(self, value: int) -> None: if value not in [0, 1, 2, 3]: raise ValueError("additional_position must be in [0,1,2,3]") self._additional_position = value @property def length(self) -> int: """`int`, 1 (default) | 2 : Number of front-loaded DMRS symbols. A value of 1 corresponds to "single-symbol" DMRS, a value of 2 corresponds to "double-symbol" DMRS. """ self._ifndef("length", 1) return self._length @length.setter def length(self, value: int) -> None: if value not in [1, 2]: raise ValueError("Invalid DMRS length") self._length = value @property def dmrs_port_set(self) -> List[int]: """`list`, [] (default) | [0,...,11] : List of used DMRS antenna ports. The elements in this list must all be from the list of `allowed_dmrs_ports` which depends on the `config_type` as well as the `length`. If set to `[]`, the port set will be equal to [0,...,num_layers-1], where :attr:`~sionna.phy.nr.PUSCHConfig.num_layers` is a property of the parent :class:`~sionna.phy.nr.PUSCHConfig` instance. """ self._ifndef("dmrs_port_set", []) return self._dmrs_port_set @dmrs_port_set.setter def dmrs_port_set(self, value: Union[int, Sequence[int]]) -> None: if isinstance(value, int): value = [value] elif isinstance(value, Sequence): value = list(value) else: raise ValueError("dmrs_port_set must be an integer or list") self._dmrs_port_set = value @property def n_id(self) -> Optional[Tuple[int, int]]: r"""2-tuple, `None` (default), [[0,...,65535], [0,...,65535]]: Scrambling identities. Defines the scrambling identities :math:`N_\text{ID}^0` and :math:`N_\text{ID}^1` as a 2-tuple of integers. If `None`, the property :attr:`~sionna.phy.nr.CarrierConfig.n_cell_id` of the :class:`~sionna.phy.nr.CarrierConfig` is used. """ self._ifndef("n_id", None) return self._n_id @n_id.setter def n_id(self, value: Optional[Union[int, Tuple[int, int]]]) -> None: if value is None: self._n_id = None elif isinstance(value, int): if value not in list(range(65536)): raise ValueError("n_id must be in [0, 65535]") self._n_id = [value, value] else: if len(value) != 2: raise ValueError("n_id must be either None or a two-tuple") for e in value: if e not in list(range(65536)): raise ValueError("Each element of n_id must be in [0, 65535]") self._n_id = list(value) @property def n_scid(self) -> int: r"""`int`, 0 (default) | 1 : DMRS scrambling initialization :math:`n_\text{SCID}`.""" self._ifndef("n_scid", 0) return self._n_scid @n_scid.setter def n_scid(self, value: int) -> None: if value not in [0, 1]: raise ValueError("n_scid must be 0 or 1") self._n_scid = value @property def num_cdm_groups_without_data(self) -> int: """`int`, 2 (default) | 1 | 3 : Number of CDM groups without data. This parameter controls how many REs are available for data transmission in a DMRS symbol. It should be greater or equal to the maximum configured number of CDM groups. A value of 1 corresponds to CDM group 0, a value of 2 corresponds to CDM groups 0 and 1, and a value of 3 corresponds to CDM groups 0, 1, and 2. """ self._ifndef("num_cdm_groups_without_data", 2) return self._num_cdm_groups_without_data @num_cdm_groups_without_data.setter def num_cdm_groups_without_data(self, value: int) -> None: if value not in [1, 2, 3]: raise ValueError("num_cdm_groups_without_data must be in [1,2,3]") self._num_cdm_groups_without_data = value # ----------------------------- # Read-only parameters # ----------------------------- @property def allowed_dmrs_ports(self) -> List[int]: """`list`, [0,...,max_num_dmrs_ports-1], read-only : List of nominal antenna ports. The maximum number of allowed antenna ports `max_num_dmrs_ports` depends on the DMRS `config_type` and `length`. It can be equal to 4, 6, 8, or 12. """ if self.length == 1: if self.config_type == 1: if self.num_cdm_groups_without_data == 1: return [0, 1] else: return [0, 1, 2, 3] elif self.config_type == 2: if self.num_cdm_groups_without_data == 1: return [0, 1] elif self.num_cdm_groups_without_data == 2: return [0, 1, 2, 3] else: return [0, 1, 2, 3, 4, 5] elif self.length == 2: if self.config_type == 1: if self.num_cdm_groups_without_data == 1: return [0, 1, 4, 5] else: return [0, 1, 2, 3, 4, 5, 6, 7] elif self.config_type == 2: if self.num_cdm_groups_without_data == 1: return [0, 1, 6, 7] elif self.num_cdm_groups_without_data == 2: return [0, 1, 2, 3, 6, 7, 8, 9] else: return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] return [] @property def cdm_groups(self) -> List[int]: r"""`list`, elements in [0,1,2], read-only : List of CDM groups :math:`\lambda` for all ports in the `dmrs_port_set` as defined in Table 6.4.1.1.3-1 or 6.4.1.1.3-2 :cite:p:`3GPPTS38211`. Depends on the `config_type`. """ if self.config_type == 1: cdm_groups = [0, 0, 1, 1, 0, 0, 1, 1] else: cdm_groups = [0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2] return [cdm_groups[port] for port in self.dmrs_port_set] @property def deltas(self) -> List[int]: r"""`list`, elements in [0,1,2,4], read-only : List of delta (frequency) shifts :math:`\Delta` for all ports in the `port_set` as defined in Table 6.4.1.1.3-1 or 6.4.1.1.3-2 :cite:p:`3GPPTS38211`. Depends on the `config_type`. """ if self.config_type == 1: deltas = [0, 0, 1, 1, 0, 0, 1, 1] else: deltas = [0, 0, 2, 2, 4, 4, 0, 0, 2, 2, 4, 4] return [deltas[port] for port in self.dmrs_port_set] @property def w_f(self) -> np.ndarray: r"""`matrix`, elements in [-1,1], read-only : Frequency weight vectors :math:`w_f(k')` for all ports in the port set as defined in Table 6.4.1.1.3-1 or 6.4.1.1.3-2 :cite:p:`3GPPTS38211`.""" if self.config_type == 1: w_f = np.array( [[1, 1, 1, 1, 1, 1, 1, 1], [1, -1, 1, -1, 1, -1, 1, -1]] ) else: # config_type == 2 w_f = np.array( [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1], ] ) return w_f[:, self.dmrs_port_set] @property def w_t(self) -> np.ndarray: r"""`matrix`, elements in [-1,1], read-only : Time weight vectors :math:`w_t(l')` for all ports in the port set as defined in Table 6.4.1.1.3-1 or 6.4.1.1.3-2 :cite:p:`3GPPTS38211`.""" if self.config_type == 1: w_t = np.array([[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, -1, -1, -1, -1]]) else: # config_type == 2 w_t = np.array( [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], ] ) return w_t[:, self.dmrs_port_set] @property def beta(self) -> float: r"""`float`, read-only : Ratio of PUSCH energy per resource element (EPRE) to DMRS EPRE :math:`\beta^{\text{DMRS}}_\text{PUSCH}`. Table 6.2.2-1 :cite:p:`3GPPTS38214`.""" if self.num_cdm_groups_without_data == 1: return 1.0 elif self.num_cdm_groups_without_data == 2: return np.sqrt(2) elif self.num_cdm_groups_without_data == 3: if self.config_type == 2: return np.sqrt(3) return 1.0 # ------------------- # Class methods # -------------------
[docs] def check_config(self) -> None: """Test if configuration is valid.""" if self.length == 2: if self.additional_position not in [0, 1]: raise ValueError("additional_position must be in [0, 1] for length==2") for p in self.dmrs_port_set: if p not in self.allowed_dmrs_ports: raise ValueError( f"Unallowed DMRS port {p}. Not in {self.allowed_dmrs_ports}." ) if self.config_type == 1: if self.num_cdm_groups_without_data not in [1, 2]: raise ValueError( "num_cdm_groups_without_data must be in [1,2] for config_type 1" ) attr_list = [ "config_type", "type_a_position", "additional_position", "length", "dmrs_port_set", "n_id", "n_scid", "num_cdm_groups_without_data", ] for attr in attr_list: value = getattr(self, attr) setattr(self, attr, value)