# -*- 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 (
ClusterDelayLineRealization,
ClusterDelayLineBase,
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.4.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"
[docs]
class UrbanMicrocellsRealization(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: 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]]):
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: # pragma: no cover
if state == O2IState.O2I:
return 0.0
h_BS = 10.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 = (
32.4
+ 21 * log10(parameters.distance_3d)
+ 20 * log10(parameters.carrier_frequency * 1e-9)
)
else:
PL_LOS = (
32.4
+ 40 * log10(parameters.distance_3d)
+ 20 * log10(parameters.carrier_frequency * 1e-9)
- 9.5 * log10(breakpoint_distance**2 + (h_BS - h_UT) ** 2)
)
if state == O2IState.LOS:
return PL_LOS
PL_NLOS = (
35.3 * log10(parameters.distance_3d)
+ 22.4
+ 21.3 * log10(parameters.carrier_frequency * 1e-9)
- 0.3 * (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:
los_probability = 18 / parameters.distance_3d + np.exp(-parameters.distance_3d / 36.0) * (
1 - 18 / parameters.distance_3d
)
return O2IState.LOS if state_variable_sample < los_probability else O2IState.NLOS
@staticmethod
def __parameter_dependency(carrier_frequency: float, factor: float, summand: float) -> float:
"""An implementation of the frequently used equation
.. math::
y = a \\log_{10}(1 + f_c) + b
Args:
carrier_frequency (float): Carrier frequency
factor (float): Factor scaling the logarithmic frequency dependency.
summand (float): Added constant.
Returns: The result.
"""
fc = (
max(2e9, 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 factor * log10(1 + fc) + summand
# 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: (-0.24, -7.14),
O2IState.NLOS: (-0.24, -6.83),
O2IState.O2I: (0.0, -6.62),
}
@staticmethod
def _delay_spread_mean(state: O2IState, carrier_frequency: float) -> float:
mean_parameters = UrbanMicrocellsRealization.__delay_spread_mean[state]
return UrbanMicrocellsRealization.__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, Tuple[float, float]] = {
O2IState.LOS: (0.0, 0.38),
O2IState.NLOS: (0.16, 0.28),
O2IState.O2I: (0.0, 0.32),
}
@staticmethod
def _delay_spread_std(state: O2IState, carrier_frequency: float) -> float:
std_parameters = UrbanMicrocellsRealization.__delay_spread_std[state]
return UrbanMicrocellsRealization.__parameter_dependency(carrier_frequency, *std_parameters)
# 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: (-0.05, 1.21),
O2IState.NLOS: (-0.23, 1.53),
O2IState.O2I: (0.0, 1.25),
}
@staticmethod
def _aod_spread_mean(state: O2IState, carrier_frequency: float) -> float:
mean_parameters = UrbanMicrocellsRealization.__aod_spread_mean[state]
return UrbanMicrocellsRealization.__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, Tuple[float, float]] = {
O2IState.LOS: (0.0, 0.41),
O2IState.NLOS: (0.11, 0.33),
O2IState.O2I: (0.0, 0.42),
}
@staticmethod
def _aod_spread_std(state: O2IState, carrier_frequency: float) -> float:
std_parameters = UrbanMicrocellsRealization.__aod_spread_std[state]
return UrbanMicrocellsRealization.__parameter_dependency(carrier_frequency, *std_parameters)
# 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: (-0.08, 1.73),
O2IState.NLOS: (-0.08, 1.81),
O2IState.O2I: (0.0, 1.76),
}
@staticmethod
def _aoa_spread_mean(state: O2IState, carrier_frequency: float) -> float:
mean_parameters = UrbanMicrocellsRealization.__aoa_spread_mean[state]
return UrbanMicrocellsRealization.__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, Tuple[float, float]] = {
O2IState.LOS: (0.014, 0.28),
O2IState.NLOS: (0.05, 0.3),
O2IState.O2I: (0.0, 0.16),
}
@staticmethod
def _aoa_spread_std(state: O2IState, carrier_frequency: float) -> float:
std_parameters = UrbanMicrocellsRealization.__aoa_spread_std[state]
return UrbanMicrocellsRealization.__parameter_dependency(carrier_frequency, *std_parameters)
# 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.1, 0.73),
O2IState.NLOS: (-0.04, 0.92),
O2IState.O2I: (0.0, 1.01),
}
@staticmethod
def _zoa_spread_mean(state: O2IState, carrier_frequency: float) -> float:
mean_parameters = UrbanMicrocellsRealization.__zoa_spread_mean[state]
return UrbanMicrocellsRealization.__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, Tuple[float, float]] = {
O2IState.LOS: (-0.04, 0.34),
O2IState.NLOS: (-0.07, 0.41),
O2IState.O2I: (0.0, 0.43),
}
@staticmethod
def _zoa_spread_std(state: O2IState, carrier_frequency: float) -> float:
std_parameters = UrbanMicrocellsRealization.__zoa_spread_std[state]
return UrbanMicrocellsRealization.__parameter_dependency(carrier_frequency, *std_parameters)
@staticmethod
def _rice_factor_mean() -> float:
return 9.0
@staticmethod
def _rice_factor_std() -> float:
return 5.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.0,
O2IState.NLOS: 2.1,
O2IState.O2I: 2.2,
}
@staticmethod
def _delay_scaling(state: O2IState) -> float:
return UrbanMicrocellsRealization.__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: 9.0,
O2IState.NLOS: 8.0,
O2IState.O2I: 9.0,
}
@staticmethod
def _cross_polarization_power_mean(state: O2IState) -> float:
return UrbanMicrocellsRealization.__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: 3.0,
O2IState.NLOS: 3.0,
O2IState.O2I: 5.0,
}
@staticmethod
def _cross_polarization_power_std(state: O2IState) -> float:
return UrbanMicrocellsRealization.__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: 19, O2IState.O2I: 12}
@staticmethod
def _num_clusters(state: O2IState) -> int:
return UrbanMicrocellsRealization.__num_clusters[state]
# RMS cluster delay spread for different LOS states
# TR 138.901 v17.0.0 Table 7.5-6
# pragma: no cover
__cluster_delay_spread: Mapping[O2IState, float] = {
O2IState.LOS: 5.0 * 1e-9,
O2IState.NLOS: 11.0 * 1e-9,
O2IState.O2I: 11.0 * 1e-9,
}
@staticmethod
def _cluster_delay_spread(state: O2IState, carrier_frequency: float) -> float:
return UrbanMicrocellsRealization.__cluster_delay_spread[state] # pragma: no cover
# 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: 3.0,
O2IState.NLOS: 10.0,
O2IState.O2I: 5.0,
}
@staticmethod
def _cluster_aod_spread(state: O2IState) -> float:
return UrbanMicrocellsRealization.__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: 17.0,
O2IState.NLOS: 22.0,
O2IState.O2I: 8.0,
}
@staticmethod
def _cluster_aoa_spread(state: O2IState) -> float:
return UrbanMicrocellsRealization.__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: 7.0,
O2IState.NLOS: 7.0,
O2IState.O2I: 3.0,
}
@staticmethod
def _cluster_zoa_spread(state: O2IState) -> float:
return UrbanMicrocellsRealization.__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 UrbanMicrocellsRealization.__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-8
if state == O2IState.LOS:
return (
max(-0.21, -14.8 * parameters.distance_2d / 1000)
+ 0.01 * abs(parameters.terminal_height - parameters.base_height)
+ 0.83
)
else:
return (
max(-0.5, -3.1 * parameters.distance_2d / 1000)
+ 0.01 * max(parameters.terminal_height - parameters.base_height, 0.0)
+ 0.2
)
@staticmethod
def _zod_spread_std(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float:
# TR 138.901 v17.0.0 Table 7.5-8
return 0.35
@staticmethod
def _zod_offset(state: O2IState, parameters: ClusterDelayLineSampleParameters) -> float:
if state == O2IState.LOS:
return 0.0
else:
return -(10 ** (-1.5 * log10(max(10, parameters.terminal_height)) + 3.3))
[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[UrbanMicrocellsRealization],
group: Group,
parameters: ClusterDelayLineRealizationParameters,
sample_hooks: Set[ChannelSampleHook[ClusterDelayLineSample]],
) -> UrbanMicrocellsRealization:
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 UrbanMicrocellsRealization(
expected_state,
state_realization,
los_realization,
nlos_realization,
o2i_realization,
parameters,
sample_hooks,
gain,
)
[docs]
class UrbanMicrocells(ClusterDelayLineBase[UrbanMicrocellsRealization, O2IState], Serializable):
"""3GPP cluster delay line preset modeling an urban street canyon."""
yaml_tag = "UMi"
"""YAML serialization tag."""
@property
def max_num_clusters(self) -> int:
return 19
@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.5, +0.0, +0.4], # 0: ASD vs DS
[+0.8, +0.4, +0.4], # 1: ASA vs DS
[-0.4, -0.4, +0.0], # 2: ASA VS SF
[-0.5, +0.0, +0.2], # 3: ASD vs SF
[-0.4, -0.7, -0.5], # 4: DS vs SF
[+0.4, +0.0, +0.0], # 5: ASD vs ASA
[-0.2, +0.0, +0.0], # 6: ASD vs K
[-0.3, +0.0, +0.0], # 7: ASA vs K
[-0.7, +0.0, +0.0], # 8: DS vs K
[+0.5, +0.0, +0.0], # 9: SF vs K
[+0.0, +0.0, +0.0], # 10: ZSD vs SF
[+0.0, +0.0, +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.0, -0.5, -0.6], # 14: ZSD vs DS
[+0.2, +0.0, -0.2], # 15: ZSA vs DS
[+0.5, +0.5, -0.2], # 16: ZSD vs ASD
[+0.3, +0.5, +0.0], # 17: ZSA vs ASD
[+0.0, +0.0, +0.0], # 18: ZSD vs ASA
[+0.0, +0.2, +0.5], # 19: ZSA vs ASA
[+0.0, +0.0, +0.5], # 20: ZSD vs ZSA
],
dtype=np.float64,
).T
def _initialize_realization(
self,
state_generator: ConsistentGenerator,
parameter_generator: ConsistentGenerator,
parameters: ClusterDelayLineRealizationParameters,
) -> UrbanMicrocellsRealization:
# 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 UrbanMicrocellsRealization(
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
) -> UrbanMicrocellsRealization:
return UrbanMicrocellsRealization.From_HDF(group, parameters, self.sample_hooks)