#
# 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)