Source code for hermespy.channel.cdl.rural_macrocells

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

from __future__ import annotations
from math import atan
from typing import Mapping, Set, Type

import numpy as np
from h5py import Group

from hermespy.core.factory import Serializable
from .cluster_delay_lines import (
    ClusterDelayLineBase,
    ClusterDelayLineRealizationParameters,
    ClusterDelayLineSample,
    ClusterDelayLineSampleParameters,
    ClusterDelayLineRealization,
    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.4.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"


[docs] class RuralMacrocellsRealization(ClusterDelayLineRealization[O2IState]): """Realization of a rural cluster delay line channel 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: float = 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]]): Set of 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 = min(150, max(5, parameters.base_height)) h_UT = min(10, max(1, parameters.terminal_height)) h = 5 # Average building height in meters PL1 = ( 20 * np.log10( 40 * np.pi * parameters.distance_3d * parameters.carrier_frequency * 1e-9 / 3 ) + min(0.03 * h**1.72, 10) * np.log10(parameters.distance_3d) - min(0.044 * (h**1.72), 14.77) + 0.002 * np.log10(h) * parameters.distance_3d ) # Compute breakpoint distance according to Note 5 in TR 138.901 v17.0.0 # Note that the carrier frequency is in Hz here breakpoint_distance = 2 * np.pi * h_BS * h_UT * parameters.carrier_frequency * 1e-8 / 3 if parameters.distance_2d <= breakpoint_distance: PL_LOS = PL1 else: PL_LOS = PL1 + 40 * np.log10(parameters.distance_3d / breakpoint_distance) if state == O2IState.LOS: return PL_LOS W = 20 P_NLOS = ( 161.04 - 7.1 * np.log10(W) + 7.5 * np.log10(h) - (24.37 - 3.7 * (h / h_BS) ** 2) * np.log10(h_BS) + (43.42 - 3.1 * np.log10(h_BS)) * (np.log10(parameters.distance_3d) - 3) + 20 * np.log10(parameters.carrier_frequency * 1e-9) - (3.2 * (np.log10(11.75 * h_UT)) ** 2 - 4.97) ) return max(PL_LOS, P_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: # Implementation of the state probabilities according to TR 138.901 v17.0.0 Table 7.4.2-1 los_probability = np.exp(-(parameters.distance_2d - 10.0) / 1000.0) if state_variable_sample < los_probability: return O2IState.LOS else: return O2IState.NLOS # Parameters for computing the mean delay spread # TR 138.901 v17.0.0 Table 7.5-6 __delay_spread_mean: Mapping[O2IState, float] = { O2IState.LOS: -7.49, O2IState.NLOS: -7.43, O2IState.O2I: -7.47, } @staticmethod def _delay_spread_mean(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__delay_spread_mean[state] # 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.55, O2IState.NLOS: 0.48, O2IState.O2I: 0.24, } @staticmethod def _delay_spread_std(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__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, float] = { O2IState.LOS: 0.9, O2IState.NLOS: 0.95, O2IState.O2I: 0.67, } @staticmethod def _aod_spread_mean(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__aod_spread_mean[state] # 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.38, O2IState.NLOS: 0.45, O2IState.O2I: 0.18, } @staticmethod def _aod_spread_std(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__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, float] = { O2IState.LOS: 1.52, O2IState.NLOS: 1.52, O2IState.O2I: 1.66, } @staticmethod def _aoa_spread_mean(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__aoa_spread_mean[state] # 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.24, O2IState.NLOS: 0.13, O2IState.O2I: 0.21, } @staticmethod def _aoa_spread_std(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__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, float] = { O2IState.LOS: 0.47, O2IState.NLOS: 0.58, O2IState.O2I: 0.93, } @staticmethod def _zoa_spread_mean(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__zoa_spread_mean[state] # 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.40, O2IState.NLOS: 0.37, O2IState.O2I: 0.22, } @staticmethod def _zoa_spread_std(state: O2IState, carrier_frequency: float) -> float: return RuralMacrocellsRealization.__zoa_spread_std[state] @staticmethod def _rice_factor_mean() -> float: return 7.0 @staticmethod def _rice_factor_std() -> float: return 4.0 # Delay scaling factors for different LOS states # TR 138.901 v17.0.0 Table 7.5-6 __delay_scaling: Mapping[O2IState, float] = { O2IState.LOS: 3.8, O2IState.NLOS: 1.7, O2IState.O2I: 1.7, } @staticmethod def _delay_scaling(state: O2IState) -> float: return RuralMacrocellsRealization.__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: 12.0, O2IState.NLOS: 7.0, O2IState.O2I: 7.0, } @staticmethod def _cross_polarization_power_mean(state: O2IState) -> float: return RuralMacrocellsRealization.__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 RuralMacrocellsRealization.__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: 11, O2IState.NLOS: 10, O2IState.O2I: 10} @staticmethod def _num_clusters(state: O2IState) -> int: return RuralMacrocellsRealization.__num_clusters[state] @staticmethod def _cluster_delay_spread(state: O2IState, carrier_frequency: float) -> float: return 0.0 # pragma: no cover @staticmethod def _cluster_aod_spread(state: O2IState) -> float: return 2.0 # TR 138.901 v17.0.0 Table 7.5-6 @staticmethod def _cluster_aoa_spread(state: O2IState) -> float: return 3.0 # TR 138.901 v17.0.0 Table 7.5-6 @staticmethod def _cluster_zoa_spread(state: O2IState) -> float: return 3.0 # TR 138.901 v17.0.0 Table 7.5-6 @staticmethod def _cluster_shadowing_std(state: O2IState) -> float: return 3.0 # TR 138.901 v17.0.0 Table 7.5-6 @staticmethod def _zod_spread_mean(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: # Implementation of TR 138.901 v17.0.0 Table 7.5-9 if state == O2IState.LOS: return max( -1, -0.17 * parameters.distance_2d / 1000 - 0.01 * (parameters.terminal_height - 1.5) + 0.22, ) else: return max( -1, -0.19 * parameters.distance_2d / 1000 - 0.01 * (parameters.terminal_height - 1.5) + 0.28, ) # Standard deviation of the zenith of departure spread # TR 138.901 v17.0.0 Table 7.5-9 __zod_spread_std: Mapping[O2IState, float] = { O2IState.LOS: 0.34, O2IState.NLOS: 0.30, O2IState.O2I: 0.30, } @staticmethod def _zod_spread_std(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: return RuralMacrocellsRealization.__zod_spread_std[state] @staticmethod def _zod_offset(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float: # Implementation of TR 138.901 v17.0.0 Table 7.5-9 if state == O2IState.LOS: return 0.0 else: return atan((35 - 3.5) / parameters.distance_2d) - atan( (35 - 1.5) / parameters.distance_2d )
[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[RuralMacrocellsRealization], group: Group, parameters: ClusterDelayLineRealizationParameters, sample_hooks: Set[ChannelSampleHook[ClusterDelayLineSample]], ) -> RuralMacrocellsRealization: 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 RuralMacrocellsRealization( expected_state, state_realization, los_realization, nlos_realization, o2i_realization, parameters, sample_hooks, gain, )
[docs] class RuralMacrocells(ClusterDelayLineBase[RuralMacrocellsRealization, O2IState], Serializable): """3GPP cluster delay line preset modeling a rural scenario.""" yaml_tag = "RuralMacrocells" """YAML serialization tag.""" @property def max_num_clusters(self) -> int: return 11 @property def max_num_rays(self) -> int: return 20 @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.00, -0.40, +0.00], # 0: ASD vs DS [+0.00, +0.00, +0.00], # 1: ASA vs DS [+0.00, +0.00, +0.00], # 2: ASA VS SF [+0.00, +0.60, +0.00], # 3: ASD vs SF [-0.50, -0.50, +0.00], # 4: DS vs SF [+0.00, +0.00, -+0.70], # 5: ASD vs ASA [+0.00, +0.00, +0.00], # 6: ASD vs K [+0.00, +0.00, +0.00], # 7: ASA vs K [+0.00, +0.00, +0.00], # 8: DS vs K [+0.00, +0.00, +0.00], # 9: SF vs K [+0.01, -0.04, +0.00], # 10: ZSD vs SF [-0.17, -0.25, +0.00], # 11: ZSA vs SF [+0.00, +0.00, +0.00], # 12: ZSD vs K [-0.02, +0.00, +0.00], # 13: ZSA vs K [-0.05, -0.10, -0.60], # 14: ZSD vs DS [+0.27, -0.40, +0.00], # 15: ZSA vs DS [+0.73, +0.42, +0.66], # 16: ZSD vs ASD [-0.14, -0.27, +0.47], # 17: ZSA vs ASD [-0.20, -0.18, -0.55], # 18: ZSD vs ASA [+0.24, +0.26, -0.22], # 19: ZSA vs ASA [-0.07, -0.27, +0.00], # 20: ZSD vs ZSA ], dtype=np.float64, ).T def _initialize_realization( self, state_generator: ConsistentGenerator, parameter_generator: ConsistentGenerator, parameters: ClusterDelayLineRealizationParameters, ) -> RuralMacrocellsRealization: # 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(50.0) nlos_realization = parameter_generator.realize(60.0) o2i_realization = parameter_generator.realize(15.0) return RuralMacrocellsRealization( 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 ) -> RuralMacrocellsRealization: return RuralMacrocellsRealization.From_HDF(group, parameters, self.sample_hooks)