# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Any, Optional, Type, Union, TYPE_CHECKING
import numpy as np
from ruamel.yaml import SafeRepresenter, MappingNode
from hermespy.core import FloatingError, Serializable, SerializableEnum
from .multipath_fading_channel import AntennaCorrelation, MultipathFadingChannel
if TYPE_CHECKING:
from hermespy.simulation import SimulatedDevice # pragma: no cover
__author__ = "Tobias Kronauer"
__copyright__ = "Copyright 2023, Barkhausen Institut gGmbH"
__credits__ = ["Tobias Kronauer", "Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.2.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"
class DeviceType(SerializableEnum):
"""3GPP device type"""
BASE_STATION = 0
"""Base station"""
TERMINAL = 1
"""Mobile terminal"""
class CorrelationType(SerializableEnum):
"""3GPP correlation type"""
LOW = 0.0, 0.0
"""Low antenna correlation"""
MEDIUM = 0.3, 0.3
"""Medium antenna correlation"""
MEDIUM_A = 0.3, 0.3874
"""Medium antenna correlation"""
HIGH = 0.9, 0.9
"""High antenna correlation"""
class StandardAntennaCorrelation(Serializable, AntennaCorrelation):
"""3GPP 5G Multipath fading standardized antenna correlations"""
yaml_tag = "StandardCorrelation"
"""YAML serialization tag"""
__device_type: DeviceType # The assumed device
__correlation: CorrelationType # The assumed correlation
def __init__(
self,
device_type: DeviceType | int | str,
correlation: Union[CorrelationType, str],
**kwargs,
) -> None:
"""
Args:
device_type (Union[DeviceType, int, str]):
The assumed device.
correlation (Union[CorrelationType, str]):
The assumed correlation.
"""
self.device_type = DeviceType.from_parameters(device_type)
self.correlation = CorrelationType.from_parameters(correlation)
AntennaCorrelation.__init__(self, **kwargs)
@property
def device_type(self) -> DeviceType:
"""Assumed 3GPP device type."""
return self.__device_type
@device_type.setter
def device_type(self, value: DeviceType) -> None:
self.__device_type = value
@property
def correlation(self) -> CorrelationType:
"""Assumed 3GPP standard correlation type."""
return self.__correlation
@correlation.setter
def correlation(self, value: CorrelationType) -> None:
self.__correlation = value
@property
def covariance(self) -> np.ndarray:
if self.device is None:
raise FloatingError(
"Error trying to compute the covariance matrix of an unknown device"
)
f = self.__correlation.value[self.__device_type.value]
n = self.device.num_antennas
if n == 1:
return np.ones((1, 1), dtype=complex)
if n == 2:
return np.array([[1, f], [f, 1]], dtype=complex)
if n == 4:
return np.array(
[
[1, f ** (1 / 9), f ** (4 / 9), f],
[f ** (1 / 9), 1, f ** (1 / 9), f ** (4 / 9)],
[f ** (4 / 9), f ** (1 / 9), 1, f ** (1 / 9)],
[f, f ** (4 / 9), f ** (1 / 9), 1],
],
dtype=complex,
)
raise RuntimeError(
f"3GPP standard antenna covariance is only defined for 1, 2 and 4 antennas, device has {n} antennas"
)
[docs]
class Cost259Type(SerializableEnum):
"""Supported model types of the Cost256 channel model"""
URBAN = 0
"""Urban area"""
RURAL = 1
"""Rural area"""
HILLY = 2
"""Hilly terrain"""
[docs]
class MultipathFadingCost259(MultipathFadingChannel):
"""Cost Action 259 Multipath Fading Channel models.
Refer to :footcite:t:`2006:MolischA` and :footcite:t:`2006:MolischB` for more information.
Parametrizations can be found in the standard :footcite:t:`3GPP:TR125943`.
The following minimal example outlines how to configure the channel model
within the context of a :doc:`simulation.simulation.Simulation`:
.. literalinclude:: ../scripts/examples/channel_MultipathFadingCost259.py
:language: python
:linenos:
:lines: 11-36
"""
yaml_tag = "COST259"
__model_type: Cost259Type
def __init__(
self,
model_type: Cost259Type = Cost259Type.URBAN,
alpha_device: SimulatedDevice | None = None,
beta_device: SimulatedDevice | None = None,
gain: float = 1.0,
los_angle: Optional[float] = None,
doppler_frequency: Optional[float] = None,
los_doppler_frequency: Optional[float] = None,
**kwargs: Any,
) -> None:
"""
Args:
model_type (Cost256Type):
The model type.
alpha_device (SimulatedDevice, optional):
First device linked by the :class:`.MultipathFadingCost259` instance that generated this realization.
beta_device (SimulatedDevice, optional):
Second device linked by the :class:`.MultipathFadingCost259` instance that generated this realization.
gain (float, optional):
Linear power gain factor a signal experiences when being propagated over this realization.
:math:`1.0` by default.
los_angle (float, optional):
Angle phase of the line of sight component within the statistical distribution.
doppler_frequency (float, optional):
Doppler frequency shift of the statistical distribution.
kwargs (Any):
`MultipathFadingChannel` initialization parameters.
Raises:
ValueError:
If `model_type` is not supported.
If `los_angle` is defined in HILLY model type.
"""
if model_type == Cost259Type.URBAN:
delays = 1e-6 * np.array(
[
0,
0.217,
0.512,
0.514,
0.517,
0.674,
0.882,
1.230,
1.287,
1.311,
1.349,
1.533,
1.535,
1.622,
1.818,
1.836,
1.884,
1.943,
2.048,
2.140,
]
)
power_db = np.array(
[
-5.7,
-7.6,
-10.1,
-10.2,
-10.2,
-11.5,
-13.4,
-16.3,
-16.9,
-17.1,
-17.4,
-19.0,
-19.0,
-19.8,
-21.5,
-21.6,
-22.1,
-22.6,
-23.5,
-24.3,
]
)
rice_factors = np.zeros(delays.shape)
elif model_type == Cost259Type.RURAL:
delays = 1e-6 * np.array(
[0, 0.042, 0.101, 0.129, 0.149, 0.245, 0.312, 0.410, 0.469, 0.528]
)
power_db = np.array([-5.2, -6.4, -8.4, -9.3, -10.0, -13.1, -15.3, -18.5, -20.4, -22.4])
rice_factors = np.zeros(delays.shape)
elif model_type == Cost259Type.HILLY:
if los_angle is not None:
raise ValueError(
"Model type HILLY does not support line of sight angle configuration"
)
delays = 1e-6 * np.array(
[
0,
0.356,
0.441,
0.528,
0.546,
0.609,
0.625,
0.842,
0.916,
0.941,
15.0,
16.172,
16.492,
16.876,
16.882,
16.978,
17.615,
17.827,
17.849,
18.016,
]
)
power_db = np.array(
[
-3.6,
-8.9,
-10.2,
-11.5,
-11.8,
-12.7,
-13.0,
-16.2,
-17.3,
-17.7,
-17.6,
-22.7,
-24.1,
-25.8,
-25.8,
-26.2,
-29.0,
-29.9,
-30.0,
-30.7,
]
)
rice_factors = np.hstack([np.array([np.inf]), np.zeros(delays.size - 1)])
los_angle = np.arccos(0.7)
else:
raise ValueError("Requested model type not supported")
self.__model_type = Cost259Type(model_type)
# Convert power and normalize
power_profile = 10 ** (power_db / 10)
power_profile /= sum(power_profile)
# Init base class with pre-defined model parameters
MultipathFadingChannel.__init__(
self,
alpha_device=alpha_device,
beta_device=beta_device,
gain=gain,
delays=delays,
power_profile=power_profile,
rice_factors=rice_factors,
los_angle=los_angle,
doppler_frequency=doppler_frequency,
los_doppler_frequency=los_doppler_frequency,
**kwargs,
)
@property
def model_type(self) -> Cost259Type:
"""Access the configured model type.
Returns: The configured model type.
"""
return self.__model_type
[docs]
@classmethod
def to_yaml(
cls: Type[MultipathFadingCost259],
representer: SafeRepresenter,
node: MultipathFadingCost259,
) -> MappingNode:
"""Serialize a serializable object to YAML.
Args:
representer (SafeRepresenter):
A handle to a representer used to generate valid YAML code.
The representer gets passed down the serialization tree to each node.
node (Serializable):
The MultipathFadingCost256 instance to be serialized.
Returns: The serialized YAML node.
"""
blacklist = set()
if node.model_type == Cost259Type.HILLY:
blacklist.add("los_angle")
return node._mapping_serialization_wrapper(representer, blacklist=blacklist)
[docs]
class TDLType(SerializableEnum):
"""Supported model types of the 5G TDL channel model"""
A = 0
B = 1
C = 2
D = 4
E = 5
[docs]
class MultipathFading5GTDL(MultipathFadingChannel):
"""5G TDL Multipath Fading Channel models.
Implementation of the 3GPP standard parameterizations as stated in ETSI TR 38.900 :footcite:p:`3GPP:TR38901`.
Five scenario types A-E are defined, differing in the number of considered paths and the path's
respective delay and power.
The following minimal example outlines how to configure the channel model
within the context of a :doc:`simulation.simulation.Simulation`:
.. literalinclude:: ../scripts/examples/channel_MultipathFading5GTDL.py
:language: python
:linenos:
:lines: 11-37
"""
yaml_tag = "5GTDL"
__rms_delay: float
def __init__(
self,
model_type: TDLType = TDLType.A,
rms_delay: float = 0.0,
alpha_device: SimulatedDevice | None = None,
beta_device: SimulatedDevice | None = None,
gain: float = 1.0,
doppler_frequency: float | None = None,
los_doppler_frequency: float | None = None,
**kwargs: Any,
) -> None:
"""
Args:
model_type (TYPE):
The model type.
Initializes the :attr:`model_type` attribute.
rms_delay (float):
Root-Mean-Squared delay in seconds.
Initializes the :attr:`rms_delay` attribute.
alpha_device (SimulatedDevice, optional):
First device linked by this :class:`MultipathFading5GTDL` channel instance.
Initializes the :attr:`alpha_device` property.
If not specified the channel is considered floating,
meaning a call to :meth:`realize<Channel.realize>` will raise an exception.
beta_device (SimulatedDevice, optional):
Second device linked by this :class:`MultipathFading5GTDL` channel.
Initializes the :attr:`beta_device` property.
If not specified the channel is considered floating,
meaning a call to :meth:`realize` will raise an exception.
gain (float, otional):
Linear power gain factor a signal experiences when being propagated over this realization.
:math:`1.0` by default.
num_sinusoids (int, optional):
Number of sinusoids used to sample the statistical distribution.
doppler_frequency (float, optional)
Doppler frequency shift of the statistical distribution.
kwargs (Any):
Additional `MultipathFadingChannel` initialization parameters.
Raises:
ValueError: If `rms_delay` is smaller than zero.
ValueError: If `los_angle` is specified in combination with `model_type` D or E.
"""
if rms_delay < 0.0:
raise ValueError("Root-Mean-Squared delay must be greater or equal to zero")
self.__rms_delay = rms_delay
if model_type == TDLType.A:
normalized_delays = np.array(
[
0,
0.3819,
0.4025,
0.5868,
0.4610,
0.5375,
0.6708,
0.5750,
0.7618,
1.5375,
1.8978,
2.2242,
2.1717,
2.4942,
2.5119,
3.0582,
4.0810,
4.4579,
4.5695,
4.7966,
5.0066,
5.3043,
9.6586,
]
)
power_db = np.array(
[
-13.4,
0,
-2.2,
-4,
-6,
-8.2,
-9.9,
-10.5,
-7.5,
-15.9,
-6.6,
-16.7,
-12.4,
-15.2,
-10.8,
-11.3,
-12.7,
-16.2,
-18.3,
-18.9,
-16.6,
-19.9,
-29.7,
]
)
rice_factors = np.zeros(normalized_delays.shape)
elif model_type == TDLType.B:
normalized_delays = np.array(
[
0,
0.1072,
0.2155,
0.2095,
0.2870,
0.2986,
0.3752,
0.5055,
0.3681,
0.3697,
0.5700,
0.5283,
1.1021,
1.2756,
1.5474,
1.7842,
2.0169,
2.8294,
3.0219,
3.6187,
4.1067,
4.2790,
4.7834,
]
)
power_db = np.array(
[
0,
-2.2,
-4,
-3.2,
-9.8,
-3.2,
-3.4,
-5.2,
-7.6,
-3,
-8.9,
-9,
-4.8,
-5.7,
-7.5,
-1.9,
-7.6,
-12.2,
-9.8,
-11.4,
-14.9,
-9.2,
-11.3,
]
)
rice_factors = np.zeros(normalized_delays.shape)
elif model_type == TDLType.C:
normalized_delays = np.array(
[
0,
0.2099,
0.2219,
0.2329,
0.2176,
0.6366,
0.6448,
0.6560,
0.6584,
0.7935,
0.8213,
0.9336,
1.2285,
1.3083,
2.1704,
2.7105,
4.2589,
4.6003,
5.4902,
5.6077,
6.3065,
6.6374,
7.0427,
8.6523,
]
)
power_db = np.array(
[
-4.4,
-1.2,
-3.5,
-5.2,
-2.5,
0,
-2.2,
-3.9,
-7.4,
-7.1,
-10.7,
-11.1,
-5.1,
-6.8,
-8.7,
-13.2,
-13.9,
-13.9,
-15.8,
-17.1,
-16,
-15.7,
-21.6,
-22.8,
]
)
rice_factors = np.zeros(normalized_delays.shape)
elif model_type == TDLType.D:
if los_doppler_frequency is not None:
raise ValueError(
"Model type D does not support line of sight doppler frequency configuration"
)
normalized_delays = np.array(
[
0,
0.035,
0.612,
1.363,
1.405,
1.804,
2.596,
1.775,
4.042,
7.937,
9.424,
9.708,
12.525,
]
)
power_db = np.array(
[
-13.5,
-18.8,
-21,
-22.8,
-17.9,
-20.1,
-21.9,
-22.9,
-27.8,
-23.6,
-24.8,
-30.0,
-27.7,
]
)
rice_factors = np.zeros(normalized_delays.shape)
rice_factors[0] = 13.3
los_doppler_frequency = 0.7
elif model_type == TDLType.E:
if los_doppler_frequency is not None:
raise ValueError(
"Model type E does not support line of sight doppler frequency configuration"
)
normalized_delays = np.array(
[
0,
0.5133,
0.5440,
0.5630,
0.5440,
0.7112,
1.9092,
1.9293,
1.9589,
2.6426,
3.7136,
5.4524,
12.0034,
20.6519,
]
)
power_db = np.array(
[
-22.03,
-15.8,
-18.1,
-19.8,
-22.9,
-22.4,
-18.6,
-20.8,
-22.6,
-22.3,
-25.6,
-20.2,
-29.8,
-29.2,
]
)
rice_factors = np.zeros(normalized_delays.shape)
rice_factors[0] = 22
los_doppler_frequency = 0.7
else:
raise ValueError("Requested model type not supported")
self.__model_type = TDLType(model_type)
# Convert power and normalize
power_profile = 10 ** (power_db / 10)
power_profile /= sum(power_profile)
# Scale delays
delays = rms_delay * normalized_delays
# Init base class with pre-defined model parameters
MultipathFadingChannel.__init__(
self,
alpha_device=alpha_device,
beta_device=beta_device,
gain=gain,
delays=delays,
power_profile=power_profile,
rice_factors=rice_factors,
doppler_frequency=doppler_frequency,
los_doppler_frequency=los_doppler_frequency,
**kwargs,
)
@property
def model_type(self) -> TDLType:
"""Access the configured model type.
Returns:
MultipathFading5gTDL.TYPE: The configured model type.
"""
return self.__model_type
@property
def rms_delay(self) -> float:
"""Root mean squared channel delay.
Returns: Delay in seconds.
"""
return self.__rms_delay
[docs]
class MultipathFadingExponential(MultipathFadingChannel):
"""Exponential Multipath Fading Channel models.
The following minimal example outlines how to configure the channel model
within the context of a :doc:`simulation.simulation.Simulation`:
.. literalinclude:: ../scripts/examples/channel_MultipathFadingExponential.py
:language: python
:linenos:
:lines: 11-36
"""
yaml_tag = "Exponential"
__exponential_truncation: float = 1e-5
__tap_interval: float
__rms_delay: float
def __init__(
self,
tap_interval: float,
rms_delay: float,
alpha_device: SimulatedDevice | None = None,
beta_device: SimulatedDevice | None = None,
gain: float = 1.0,
**kwargs: Any,
) -> None:
"""
Args:
tap_interval (float):
Tap interval in seconds.
rms_delay (float):
Root-Mean-Squared delay in seconds.
alpha_device (SimulatedDevice, optional):
First device linked by the :class:`.MultipathFadingExponential` instance that generated this realization.
beta_device (SimulatedDevice, optional):
Second device linked by the :class:`.MultipathFadingExponential` instance that generated this realization.
gain (float, optional):
Linear power gain factor a signal experiences when being propagated over this realization.
:math:`1.0` by default.
kwargs (Any):
`MultipathFadingChannel` initialization parameters.
Raises:
ValueError: On invalid arguments.
"""
if tap_interval <= 0.0:
raise ValueError("Tap interval must be greater than zero")
if rms_delay <= 0.0:
raise ValueError("Root-Mean-Squared delay must be greater than zero")
self.__tap_interval = tap_interval
self.__rms_delay = rms_delay
rms_norm = rms_delay / tap_interval
# Calculate the decay exponent alpha based on an infinite power delay profile, in which case
# rms_delay = exp(-alpha/2)/(1-exp(-alpha)), cf. geometric distribution.
# Truncate the distributions for paths whose average power is very
# small (less than exponential_truncation).
alpha = -2 * np.log((-1 + np.sqrt(1 + 4 * rms_norm**2)) / (2 * rms_norm))
max_delay_in_samples = int(
-np.ceil(np.log(MultipathFadingExponential.__exponential_truncation) / alpha)
)
delays = np.arange(max_delay_in_samples + 1) * tap_interval
power_profile = np.exp(-alpha * np.arange(max_delay_in_samples + 1))
rice_factors = np.zeros(delays.shape)
# Init base class with pre-defined model parameters
MultipathFadingChannel.__init__(
self,
alpha_device=alpha_device,
beta_device=beta_device,
gain=gain,
delays=delays,
power_profile=power_profile,
rice_factors=rice_factors,
**kwargs,
)
@property
def tap_interval(self) -> float:
"""Tap interval.
Returns: Tap interval in seconds.
"""
return self.__tap_interval
@property
def rms_delay(self) -> float:
"""Root mean squared channel delay.
Returns: Delay in seconds.
"""
return self.__rms_delay