Source code for hermespy.channel.cdl.urban_macrocells

# -*- coding: utf-8 -*-

from __future__ import annotations
from math import log10
from typing import Mapping, Set, Tuple, Type

from h5py import Group
import numpy as np

from hermespy.core.factory import Serializable
from .cluster_delay_lines import (
    ClusterDelayLineBase,
    ClusterDelayLineRealization,
    ClusterDelayLineRealizationParameters,
    ClusterDelayLineSample,
    ClusterDelayLineSampleParameters,
    O2IState,
)
from ..channel import ChannelSampleHook
from ..consistent import ConsistentGenerator, ConsistentRealization

__author__ = "Jan Adler"
__copyright__ = "Copyright 2024, Barkhausen Institut gGmbH"
__credits__ = ["Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.3.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"


[docs] class UrbanMacrocellsRealization(ClusterDelayLineRealization[O2IState]): """Realization of an urban street canyon cluster delay line model.""" def __init__( self, expected_state: O2IState | None, state_realization: ConsistentRealization, los_realization: ConsistentRealization, nlos_realization: ConsistentRealization, o2i_realization: ConsistentRealization, parameters: ClusterDelayLineRealizationParameters, sample_hooks: Set[ChannelSampleHook[ClusterDelayLineSample]], gain=1.0, ) -> None: """ Args: expected_state (O2IState | None): Expected large-scale state of the channel. If not specified, the large-scale state is randomly generated. state_realization (ConsistentRealization): Realization of a spatially consistent random number generator for the large-scale state. los_realization (ConsistentRealization): Realization of a spatially consistent random number generator for small-scale parameters in the LOS state. nlos_realization (ConsistentRealization): Realization of a spatially consistent random number generator for small-scale parameters in the NLOS state. o2i_realization (ConsistentRealization): Realization of a spatially consistent random number generator for small-scale parameters in the O2I state. parameters (ClusterDelayLineRealizationParameters): General parameters of the cluster delay line realization. sample_hooks (Set[ChannelSampleHook[ClusterDelayLineSample]]): Hooks to be called when a channel sample is generated. gain (float, optional): Linear amplitude scaling factor if signals propagated over the channel. """ # Initialize base class ClusterDelayLineRealization.__init__( self, expected_state, state_realization, parameters, sample_hooks, gain ) # Initialize class attributes self.__los_realization = los_realization self.__nlos_realization = nlos_realization self.__o2i_realization = o2i_realization # Table 7.4.4-1 in TR 138.901 v17.0.0 @staticmethod def _pathloss_dB(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: if state == O2IState.O2I: return 0.0 h_BS = 25.0 # Height of the base station in meters h_UT = max(1.5, min(22.5, parameters.terminal_height)) # Height of the terminal in meters # Note 1 in Table 7.4.4-1 of TR 138.901 v17.0.0 breakpoint_distance = ( 4 * (h_BS - 1) * (h_UT - 1) * parameters.carrier_frequency * 1e-8 / 3.0 ) if parameters.distance_2d < breakpoint_distance: PL_LOS = ( 28.0 + 22.0 * log10(parameters.distance_3d) + 20.0 * log10(parameters.carrier_frequency * 1e-9) ) else: PL_LOS = ( 28.0 + 40.0 * log10(parameters.distance_3d) + 20.0 * log10(parameters.carrier_frequency * 1e-9) - 9.0 * log10((breakpoint_distance) ** 2 + (h_BS - h_UT) ** 2) ) if state == O2IState.LOS: return PL_LOS PL_NLOS = ( 13.54 + 39.08 * log10(parameters.distance_3d) + 20.0 * log10(parameters.carrier_frequency * 1e-9) - 0.6 * (h_UT - 1.5) ) return max(PL_LOS, PL_NLOS) def _small_scale_realization(self, state: O2IState) -> ConsistentRealization: if state == O2IState.LOS: return self.__los_realization elif state == O2IState.NLOS: return self.__nlos_realization else: return self.__o2i_realization def _sample_large_scale_state( self, state_variable_sample: float, parameters: ClusterDelayLineSampleParameters ) -> O2IState: # pragma: no cover los_probability = 18 / parameters.distance_3d + np.exp(-parameters.distance_3d / 36.0) * ( 1 - 18 / parameters.distance_3d ) if state_variable_sample < los_probability: return O2IState.LOS else: return O2IState.NLOS @staticmethod def __parameter_dependency(carrier_frequency: float, summand: float, factor: float) -> float: """An implementation of the frequently used equation .. math:: y = a + b * log_{10}(f) Args: carrier_frequency (float): Carrier frequency summand (float): Added constant. factor (float): Factor scaling the logarithmic frequency dependency. Returns: The result. """ fc = ( max(6e9, carrier_frequency) * 1e-9 ) # Frequency is lower-bounded by 2 GHz, according to Note 7 in table 7.5-6 of TR 138.901 v17.0.0 return summand + factor * log10(fc) # Parameters for computing the mean delay spread # TR 138.901 v17.0.0 Table 7.5-6 __delay_spread_mean: Mapping[O2IState, Tuple[float, float]] = { O2IState.LOS: (-6.955, -0.0963), O2IState.NLOS: (-6.28, -0.204), O2IState.O2I: (-6.62, 0), } @staticmethod def _delay_spread_mean(state: O2IState, carrier_frequency: float) -> float: mean_parameters = UrbanMacrocellsRealization.__delay_spread_mean[state] return UrbanMacrocellsRealization.__parameter_dependency( carrier_frequency, *mean_parameters ) # Parameters for computing the standard deviation of the delay spread # TR 138.901 v17.0.0 Table 7.5-6 __delay_spread_std: Mapping[O2IState, float] = { O2IState.LOS: 0.66, O2IState.NLOS: 0.39, O2IState.O2I: 0.32, } @staticmethod def _delay_spread_std(state: O2IState, carrier_frequency: float) -> float: return UrbanMacrocellsRealization.__delay_spread_std[state] # Parameters for computing the mean angle of departure spread # TR 138.901 v17.0.0 Table 7.5-6 __aod_spread_mean: Mapping[O2IState, Tuple[float, float]] = { O2IState.LOS: (1.06, 0.1114), O2IState.NLOS: (1.5, -0.1114), O2IState.O2I: (1.25, 0), } @staticmethod def _aod_spread_mean(state: O2IState, carrier_frequency: float) -> float: mean_parameters = UrbanMacrocellsRealization.__aod_spread_mean[state] return UrbanMacrocellsRealization.__parameter_dependency( carrier_frequency, *mean_parameters ) # Parameters for computing the standard deviation of the angle of departure spread # TR 138.901 v17.0.0 Table 7.5-6 __aod_spread_std: Mapping[O2IState, float] = { O2IState.LOS: 0.28, O2IState.NLOS: 0.28, O2IState.O2I: 0.42, } @staticmethod def _aod_spread_std(state: O2IState, carrier_frequency: float) -> float: return UrbanMacrocellsRealization.__aod_spread_std[state] # Parameters for computing the mean angle of arrival spread # TR 138.901 v17.0.0 Table 7.5-6 __aoa_spread_mean: Mapping[O2IState, Tuple[float, float]] = { O2IState.LOS: (1.81, 0.0), O2IState.NLOS: (2.08, -0.27), O2IState.O2I: (1.76, 0.0), } @staticmethod def _aoa_spread_mean(state: O2IState, carrier_frequency: float) -> float: mean_parameters = UrbanMacrocellsRealization.__aoa_spread_mean[state] return UrbanMacrocellsRealization.__parameter_dependency( carrier_frequency, *mean_parameters ) # Parameters for computing the standard deviation of the angle of arrival spread # TR 138.901 v17.0.0 Table 7.5-6 __aoa_spread_std: Mapping[O2IState, float] = { O2IState.LOS: 0.20, O2IState.NLOS: 0.11, O2IState.O2I: 0.16, } @staticmethod def _aoa_spread_std(state: O2IState, carrier_frequency: float) -> float: return UrbanMacrocellsRealization.__aoa_spread_std[state] # Parameters for computing the mean zenith of arrival spread # TR 138.901 v17.0.0 Table 7.5-6 __zoa_spread_mean: Mapping[O2IState, Tuple[float, float]] = { O2IState.LOS: (0.95, 0.0), O2IState.NLOS: (1.1512, -0.3236), O2IState.O2I: (1.76, 0.0), } @staticmethod def _zoa_spread_mean(state: O2IState, carrier_frequency: float) -> float: mean_parameters = UrbanMacrocellsRealization.__zoa_spread_mean[state] return UrbanMacrocellsRealization.__parameter_dependency( carrier_frequency, *mean_parameters ) # Parameters for computing the standard deviation of the zenith of arrival spread # TR 138.901 v17.0.0 Table 7.5-6 __zoa_spread_std: Mapping[O2IState, float] = { O2IState.LOS: 0.16, O2IState.NLOS: 0.16, O2IState.O2I: 0.43, } @staticmethod def _zoa_spread_std(state: O2IState, carrier_frequency: float) -> float: return UrbanMacrocellsRealization.__zoa_spread_std[state] @staticmethod def _rice_factor_mean() -> float: return 9.0 @staticmethod def _rice_factor_std() -> float: return 3.5 # Delay scaling factors for different LOS states # TR 138.901 v17.0.0 Table 7.5-6 __delay_scaling: Mapping[O2IState, float] = { O2IState.LOS: 2.5, O2IState.NLOS: 2.3, O2IState.O2I: 2.2, } @staticmethod def _delay_scaling(state: O2IState) -> float: return UrbanMacrocellsRealization.__delay_scaling[state] # Mean cross-polarization power ratio for different LOS states # TR 138.901 v17.0.0 Table 7.5-6 __cross_polarization_power_mean: Mapping[O2IState, float] = { O2IState.LOS: 8.0, O2IState.NLOS: 7.0, O2IState.O2I: 9.0, } @staticmethod def _cross_polarization_power_mean(state: O2IState) -> float: return UrbanMacrocellsRealization.__cross_polarization_power_mean[state] # Standard deviation of the cross-polarization power ratio for different LOS states # TR 138.901 v17.0.0 Table 7.5-6 __cross_polarization_power_std: Mapping[O2IState, float] = { O2IState.LOS: 4.0, O2IState.NLOS: 3.0, O2IState.O2I: 3.0, } @staticmethod def _cross_polarization_power_std(state: O2IState) -> float: return UrbanMacrocellsRealization.__cross_polarization_power_std[state] # Number of clusters for different LOS states # TR 138.901 v17.0.0 Table 7.5-6 __num_clusters: Mapping[O2IState, int] = {O2IState.LOS: 12, O2IState.NLOS: 20, O2IState.O2I: 12} @staticmethod def _num_clusters(state: O2IState) -> int: return UrbanMacrocellsRealization.__num_clusters[state] @staticmethod def _cluster_delay_spread( state: O2IState, carrier_frequency: float ) -> float: # pragma: no cover # Implementation of TR 138.901 v17.0.0 Table 7.5-6 if state == O2IState.O2I: return 11.0 else: return max(0.25, 6.5622 - 3.4084 * log10(carrier_frequency * 1e-9)) # RMS cluster azimuth of departure spread for different LOS states in degrees # TR 138.901 v17.0.0 Table 7.5-6 __cluster_aod_spread: Mapping[O2IState, float] = { O2IState.LOS: 5.0, O2IState.NLOS: 2.0, O2IState.O2I: 5.0, } @staticmethod def _cluster_aod_spread(state: O2IState) -> float: return UrbanMacrocellsRealization.__cluster_aod_spread[state] # RMS cluster azimuth of arrival spread for different LOS states in degrees # TR 138.901 v17.0.0 Table 7.5-6 __cluster_aoa_spread: Mapping[O2IState, float] = { O2IState.LOS: 11.0, O2IState.NLOS: 15.0, O2IState.O2I: 8.0, } @staticmethod def _cluster_aoa_spread(state: O2IState) -> float: return UrbanMacrocellsRealization.__cluster_aoa_spread[state] # RMS cluster zenith of arrival spread for different LOS states in degrees # TR 138.901 v17.0.0 Table 7.5-6 __cluster_zoa_spread: Mapping[O2IState, float] = { O2IState.LOS: 3.0, O2IState.NLOS: 3.0, O2IState.O2I: 4.0, } @staticmethod def _cluster_zoa_spread(state: O2IState) -> float: return UrbanMacrocellsRealization.__cluster_zoa_spread[state] # Standard deviation of the shadowing for different LOS states in dB # TR 138.901 v17.0.0 Table 7.5-6 __cluster_shadowing_std: Mapping[O2IState, float] = { O2IState.LOS: 3.0, O2IState.NLOS: 3.0, O2IState.O2I: 4.0, } @staticmethod def _cluster_shadowing_std(state: O2IState) -> float: return UrbanMacrocellsRealization.__cluster_shadowing_std[state] @staticmethod def _zod_spread_mean(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: # Implementation of TR 138.901 v17.0.0 Table 7.5-7 if state == O2IState.LOS: return ( max(-0.5, -2.1 * parameters.distance_2d / 1000) - 0.01 * (parameters.terminal_height - 1.5) + 0.75 ) else: return ( max(-0.5, -2.1 * parameters.distance_2d / 1000) - 0.01 * (parameters.terminal_height - 1.5) + 0.9 ) # Standard deviation of the zenith of departure spread for different LOS states # TR 138.901 v17.0.0 Table 7.5-7 __zod_spread_std: Mapping[O2IState, float] = { O2IState.LOS: 0.4, O2IState.NLOS: 0.49, O2IState.O2I: 0.49, } @staticmethod def _zod_spread_std(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: return UrbanMacrocellsRealization.__zod_spread_std[state] @staticmethod def _zod_offset(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: # Implementation of TR 138.901 v17.0.0 Table 7.5-7 if state == O2IState.LOS: return 0.0 else: fc = log10(max(6.0, parameters.carrier_frequency * 1e-9)) a = 0.208 * fc - 0.782 b = 25 c = -0.13 * fc + 2.03 e = 7.66 * fc - 5.96 return e - 10 ** ( a * log10(max(b, parameters.distance_2d)) + c - 0.07 * (parameters.terminal_height - 1.5) )
[docs] def to_HDF(self, group: Group) -> None: ClusterDelayLineRealization.to_HDF(self, group) self.__los_realization.to_HDF(group.create_group("los_realization")) self.__nlos_realization.to_HDF(group.create_group("nlos_realization")) self.__o2i_realization.to_HDF(group.create_group("o2i_realization")) if self.expected_state is not None: group.attrs["expected_state"] = self.expected_state.value
[docs] @classmethod def From_HDF( cls: Type[UrbanMacrocellsRealization], group: Group, parameters: ClusterDelayLineRealizationParameters, sample_hooks: Set[ChannelSampleHook[ClusterDelayLineSample]], ) -> UrbanMacrocellsRealization: state_realization = ConsistentRealization.from_HDF(group["state_realization"]) los_realization = ConsistentRealization.from_HDF(group["los_realization"]) nlos_realization = ConsistentRealization.from_HDF(group["nlos_realization"]) o2i_realization = ConsistentRealization.from_HDF(group["o2i_realization"]) gain = group.attrs["gain"] if "gain" in group.attrs else 1.0 if "expected_state" in group.attrs: expected_state = O2IState(group.attrs["expected_state"]) else: expected_state = None return UrbanMacrocellsRealization( expected_state, state_realization, los_realization, nlos_realization, o2i_realization, parameters, sample_hooks, gain, )
[docs] class UrbanMacrocells(ClusterDelayLineBase[UrbanMacrocellsRealization, O2IState], Serializable): """3GPP cluster delay line preset modeling an urban macrocell scenario.""" yaml_tag = "UMa" """YAML serialization tag.""" @property def _large_scale_correlations(self) -> np.ndarray: # Large scale cross correlations # TR 138.901 v17.0.0 Table 7.5-6 return np.array( [ # LOS NLOS O2I [+0.4, +0.4, +0.4], # 0: ASD vs DS [+0.8, +0.6, +0.4], # 1: ASA vs DS [-0.5, +0.0, +0.0], # 2: ASA VS SF [-0.5, -0.6, +0.2], # 3: ASD vs SF [-0.4, -0.4, -0.5], # 4: DS vs SF [+0.0, +0.4, +0.0], # 5: ASD vs ASA [+0.0, +0.0, +0.0], # 6: ASD vs K [-0.2, +0.0, +0.0], # 7: ASA vs K [-0.4, +0.0, +0.0], # 8: DS vs K [+0.0, +0.0, +0.0], # 9: SF vs K [+0.0, +0.0, +0.0], # 10: ZSD vs SF [-0.8, -0.4, +0.0], # 11: ZSA vs SF [+0.0, +0.0, +0.0], # 12: ZSD vs K [+0.0, +0.0, +0.0], # 13: ZSA vs K [-0.2, -0.5, -0.6], # 14: ZSD vs DS [+0.0, +0.0, -0.2], # 15: ZSA vs DS [+0.5, +0.5, -0.2], # 16: ZSD vs ASD [+0.0, -0.1, +0.0], # 17: ZSA vs ASD [-0.3, +0.0, +0.0], # 18: ZSD vs ASA [+0.4, +0.0, +0.5], # 19: ZSA vs ASA [+0.0, +0.0, +0.5], # 20: ZSD vs ZSA ], dtype=np.float_, ).T @property def max_num_clusters(self) -> int: return 19 @property def max_num_rays(self) -> int: return 20 def _initialize_realization( self, state_generator: ConsistentGenerator, parameter_generator: ConsistentGenerator, parameters: ClusterDelayLineRealizationParameters, ) -> UrbanMacrocellsRealization: # Generate realizations for each large scale state # TR 138.901 v17.0.0 Table 7.6.3.1-2 state_realization = state_generator.realize(50.0) los_realization = parameter_generator.realize(12.0) nlos_realization = parameter_generator.realize(15.0) o2i_realization = parameter_generator.realize(15.0) return UrbanMacrocellsRealization( self.expected_state, state_realization, los_realization, nlos_realization, o2i_realization, parameters, self.sample_hooks, self.gain, ) def _recall_specific_realization( self, group: Group, parameters: ClusterDelayLineRealizationParameters ) -> UrbanMacrocellsRealization: return UrbanMacrocellsRealization.From_HDF(group, parameters, self.sample_hooks)