Source code for hermespy.channel.cdl.cdl

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

from __future__ import annotations
from typing import Mapping, Set
from typing_extensions import override

import numpy as np

from hermespy.core import DeserializationProcess, SerializableEnum, SerializationProcess
from ..channel import Channel, ChannelRealization, ChannelSampleHook, LinkState
from ..consistent import ConsistentGenerator, ConsistentUniform, ConsistentRealization
from .cluster_delay_lines import ClusterDelayLineSample, ClusterDelayLineRealization

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


[docs] class CDLType(SerializableEnum): """Type of the static cluster delay line model.""" A = 0 B = 1 C = 2 D = 3 E = 4
# ETSI TR 138.901 v17.0.0 Table 7.7.1-1 CDL_Cluster_Parameters: Mapping[CDLType, np.ndarray] = { # Delay Power AOD AOA ZOD ZOA # [ns] [dB] [deg] [deg] [deg] [deg] # Table 7.7.1-1: CDL-A CDLType.A: np.array( [ [0.0000, -13.4, -178.10, +51.30, +050.2, 125.4], [0.3819, +0.00, -4.2000, -152.7, +093.2, 091.3], [0.4025, -2.20, -4.2000, -152.7, +093.2, 091.3], [0.5868, -4.00, -4.2000, -152.7, +093.2, 091.3], [0.4610, -6.00, +90.200, +76.60, +122.0, 094.0], [0.5375, -8.20, +90.200, +76.60, +122.0, 094.0], [0.6708, -9.90, +90.200, +76.60, +122.0, 094.0], [0.5750, -10.5, +121.50, -1.800, +150.2, 047.1], [0.7618, -7.50, -81.700, -41.90, +055.2, 056.0], [1.5375, -15.9, +158.40, +94.20, +026.4, 030.1], [1.8978, -6.60, -83.000, +51.90, +126.4, 058.8], [2.2242, -16.7, +134.80, -115.9, +171.6, 026.0], [2.1718, -12.4, -153.00, +26.60, +151.4, 049.2], [2.4942, -15.2, -172.00, +76.60, +157.2, 143.1], [2.5119, -10.8, -129.90, -07.00, +047.2, 117.4], [3.0582, -11.3, -136.00, -23.00, +040.4, 122.7], [4.0810, -12.7, +165.40, -47.20, +043.3, 123.2], [4.4579, -16.2, +148.40, +110.4, +161.8, 032.6], [4.5695, -18.3, +132.70, +144.5, +010.8, 027.2], [4.7966, -18.9, -118.60, +155.3, +016.7, 015.2], [5.0066, -16.6, -154.10, +102.0, +171.7, 146.0], [5.3043, -19.9, +126.50, -151.8, +022.7, 150.7], [9.6586, -29.7, -56.200, +55.20, +144.9, 156.1], ], dtype=np.float64, ), # Table 7.7.1-2: CDL-B CDLType.B: np.array( [ [0.0000, +0.00, +009.30, -173.3, +105.8, +78.9], [0.1072, -02.2, +009.30, -173.3, +105.8, +78.9], [0.2155, -04.0, +009.30, -173.3, +105.8, +78.9], [0.2095, -03.2, -034.10, +125.5, +115.3, +63.3], [0.2870, -09.8, -065.40, -088.0, +119.3, +59.9], [0.2986, -01.2, -011.40, +155.1, +103.2, +67.5], [0.3752, -03.4, -011.40, +155.1, +103.2, +67.5], [0.5055, -05.2, -011.40, +155.1, +103.2, +67.5], [0.3681, -07.6, -067.20, -089.8, +118.2, +82.6], [0.3697, -03.0, +052.50, +132.1, +102.0, +66.3], [0.5700, -08.9, -072.00, -083.6, +100.4, +61.6], [0.5283, -09.0, +074.30, +095.3, +098.3, +58.0], [1.1021, -4.80, -052.20, +103.7, +103.4, +78.2], [1.2756, -5.70, -050.50, -087.8, +102.5, +82.0], [1.5474, -7.50, +061.40, -092.5, +101.4, +62.4], [1.7842, -1.90, +030.60, -139.1, +103.0, +78.0], [2.0169, -7.60, -072.50, -090.6, +100.0, +60.9], [2.8294, -12.2, -090.60, +058.6, +115.2, +82.9], [3.0219, -9.80, -077.60, -079.0, +100.5, +60.8], [3.6187, -11.4, -082.60, +065.8, +119.6, +57.3], [4.1067, -14.9, -103.60, +052.7, +118.7, +59.9], [4.2790, -9.20, +075.60, +088.7, +117.8, +60.1], [4.7834, -11.3, -077.60, -060.4, +115.7, +62.3], ], dtype=np.float64, ), # Table 7.7.1-3: CDL-C CDLType.C: np.array( [ [0.0000, -04.4, -046.6, -101.0, +097.2, +087.6], [0.2099, -01.2, -022.8, +120.0, +098.6, +072.1], [0.2219, -03.5, -022.8, +120.0, +098.6, +072.1], [0.2329, -05.2, -022.8, +120.0, +098.6, +072.1], [0.2176, -02.5, -040.7, -127.5, +100.6, +070.1], [0.6366, +00.0, +000.3, +170.4, +099.2, +075.3], [0.6448, -02.2, +000.3, +170.4, +099.2, +075.3], [0.6560, -03.9, +000.3, +170.4, +099.2, +075.3], [0.6584, -07.4, +073.1, +055.4, +105.2, +067.4], [0.7935, -07.1, -064.5, +066.5, +095.3, +063.8], [0.8213, -10.7, +080.2, -048.1, +106.1, +071.4], [0.9336, -11.1, -097.1, +046.9, +093.5, +060.5], [1.2285, -05.1, -055.3, +068.1, +103.7, +090.6], [1.3083, -06.8, -064.3, -068.7, +104.2, +060.1], [2.1704, -08.7, -078.5, +081.5, +093.0, +061.0], [2.7105, -13.2, +102.7, +030.7, +104.2, +100.7], [4.2589, -13.9, +099.2, -016.4, +094.9, +062.3], [4.6003, -13.9, +088.8, +003.8, +093.1, +066.7], [5.4902, -15.8, -101.9, -013.7, +092.2, +052.9], [5.6077, -17.1, +092.2, +009.7, +106.7, +061.8], [6.3065, -16.0, +093.3, +005.6, +093.0, +051.9], [6.6374, -15.7, +106.6, +000.7, +092.9, +061.7], [7.0427, -21.6, +119.5, -021.9, +105.2, +058.0], [8.6523, -22.8, -123.8, +033.6, +107.8, +057.0], ], dtype=np.float64, ), # Table 7.7.1-4: CDL-D CDLType.D: np.array( [ [00.000, -00.2, +000.0, -180.0, +098.5, +81.5], [00.000, -13.5, +000.0, -180.0, +098.5, +81.5], [00.035, -18.8, +089.2, +089.2, +085.5, +86.9], [00.612, -21.0, +089.2, +089.2, +085.5, +86.9], [01.363, -22.8, +089.2, +089.2, +085.5, +86.9], [01.405, -17.9, +013.0, +163.0, +097.5, +79.4], [01.804, -20.1, +013.0, +163.0, +097.5, +79.4], [02.596, -21.9, +013.0, +163.0, +097.5, +79.4], [01.775, -22.9, +034.6, -137.0, +098.5, +78.2], [04.042, -27.8, -064.5, +074.5, +088.4, +73.6], [07.937, -23.6, -032.9, +127.7, +091.3, +78.3], [09.424, -24.8, +052.6, -119.6, +103.8, +87.0], [09.708, -30.0, -132.1, -009.1, +080.3, +70.6], [12.525, -27.7, +077.2, -083.8, +086.5, +72.9], ], dtype=np.float64, ), # Table 7.7.1-5: CDL-E CDLType.E: np.array( [ [00.0000, -00.03, +00.0, -180.0, +099.6, +80.4], [00.0000, -22.03, +00.0, -180.0, +099.6, +80.4], [00.5133, -15.80, +57.5, +018.2, +104.2, +80.4], [00.5440, -18.10, +57.5, +018.2, +104.2, +80.4], [00.5630, -19.80, +57.5, +018.2, +104.2, +80.4], [00.5440, -22.90, -20.1, +101.8, +099.4, +80.8], [00.7112, -22.40, +16.2, +112.9, +100.8, +86.3], [1.90920, -18.60, +09.3, -155.5, +098.8, +82.7], [1.92930, -20.80, +09.3, -155.5, +098.8, +82.7], [1.95890, -22.60, +09.3, -155.5, +098.8, +82.7], [2.64260, -22.30, +19.0, -143.3, +100.8, +82.9], [3.71360, -25.60, +32.7, -094.7, +096.4, +88.0], [5.45240, -20.20, +00.5, +147.0, +098.9, +81.0], [12.0034, -29.80, +55.9, -036.2, +095.6, +88.6], [20.6419, -29.20, +57.6, -026.0, +104.6, +78.3], ], dtype=np.float64, ), } CDL_Per_Cluster_Parameters: np.ndarray = np.array( [ [05.0, 11 - 0, 3.0, 3.0, 10.0, False], # Table 7.7.1-1: CDL-A [10.0, 22.0, 3.0, 7.0, 08.0, False], # Table 7.7.1-2: CDL-B [02.0, 15.0, 3.0, 7.0, 07.0, False], # Table 7.7.1-2: CDL-C [05.0, 08.0, 3.0, 3.0, 11.0, True], # Table 7.7.1-4: CDL-D [05.0, 11.0, 3.0, 7.0, 08.0, True], # Table 7.7.1-5: CDL-E ] )
[docs] class CDLRealization(ChannelRealization[ClusterDelayLineSample]): """Realization of a static cluster delay line model for link-level simulations. Generated by the :meth:`_realize<.CDL._realize>` method of the :class:`CDL` class. """ def __init__( self, type: CDLType, rms_delay: float, rayleigh_factor: float, angle_coupling_indices: np.ndarray, consistent_realization: ConsistentRealization, xpr_phase: ConsistentUniform, sample_hooks: Set[ChannelSampleHook[ClusterDelayLineSample]], gain: float, ) -> None: """ Args: type: Type of the cluster delay line model. rms_delay: Root mean square delay spread of the channel. rayleigh_factor: Rayleigh K-factor of the channel. angle_coupling_indices: Indices for the coupling of rays within a cluster. consistent_realization: Realization of the consistent distribution. xpr_phase: Realization of the cross-polarization phase. sample_hooks: Hooks to be called after the channel sample has been generated. gain: Linear channel gain factor. """ # Initialize base class ChannelRealization.__init__(self, sample_hooks, gain) # Store parameters self.__type = type self.__rms_delay = rms_delay self.__rayleigh_factor = rayleigh_factor self.__angle_coupling_indices = angle_coupling_indices self.__consistent_realization = consistent_realization self.__xpr_phase = xpr_phase
[docs] def _sample(self, state: LinkState) -> ClusterDelayLineSample: # Sample the consistent distribution consistent_sample = self.__consistent_realization.sample( state.transmitter.position, state.receiver.position ) # Fetch the cluster parameters parameters = CDL_Cluster_Parameters[self.__type] per_cluster_parameters = CDL_Per_Cluster_Parameters[self.__type.value, :] normalized_cluster_delays = parameters[:, 0] cluster_powers = 10 ** (parameters[:, 1] / 10) cluster_aods = parameters[:, 2] cluster_aoas = parameters[:, 3] cluster_zods = parameters[:, 4] cluster_zoas = parameters[:, 5] rms_asd_spreads = per_cluster_parameters[0] rms_asa_spreads = per_cluster_parameters[1] rms_zsd_spreads = per_cluster_parameters[2] rms_zsa_spreads = per_cluster_parameters[3] XPR_dB = per_cluster_parameters[4] line_of_sight = bool(per_cluster_parameters[5]) # Generate cluster delays cluster_delays = self.__rms_delay * normalized_cluster_delays # Step 1: Generate departure and arrival angles # Equation 7.7-0a in ETSI TR 138.901 v17.0.0 ray_aods = np.add.outer( cluster_aods, rms_asd_spreads * ClusterDelayLineRealization._ray_offset_angles ) ray_aoas = np.add.outer( cluster_aoas, rms_asa_spreads * ClusterDelayLineRealization._ray_offset_angles ) ray_zods = np.add.outer( cluster_zods, rms_zsd_spreads * ClusterDelayLineRealization._ray_offset_angles ) ray_zoas = np.add.outer( cluster_zoas, rms_zsa_spreads * ClusterDelayLineRealization._ray_offset_angles ) # Step 2: Coupling of rays within a cluster for both azimuth and zenith # Equation 7.7-0b in ETSI TR 138.901 v17.0.0 shuffled_ray_aods = np.take_along_axis( ray_aods, self.__angle_coupling_indices[0, :], axis=1 ) shuffled_ray_aoas = np.take_along_axis( ray_aoas, self.__angle_coupling_indices[1, :], axis=1 ) shuffled_ray_zods = np.take_along_axis( ray_zods, self.__angle_coupling_indices[2, :], axis=1 ) shuffled_ray_zoas = np.take_along_axis( ray_zoas, self.__angle_coupling_indices[3, :], axis=1 ) # Draw initial random phases (step 10) # A single 2x2 slice represents the jones matrix transforming the polarization of a single ray cross_polarization_factor = 10 ** (XPR_dB / 10) polarization_transformations = np.exp( 2j * np.pi * self.__xpr_phase.sample(consistent_sample) ) polarization_transformations[0, 1, ::] *= cross_polarization_factor polarization_transformations[1, 0, ::] *= cross_polarization_factor return ClusterDelayLineSample( line_of_sight, self.__rayleigh_factor, np.pi / 180 * shuffled_ray_aoas, np.pi / 180 * shuffled_ray_zoas, np.pi / 180 * shuffled_ray_aods, np.pi / 180 * shuffled_ray_zods, 0, cluster_delays, self.__rms_delay, cluster_powers, polarization_transformations, state, )
def _reciprocal_sample( self, sample: ClusterDelayLineSample, state: LinkState ) -> ClusterDelayLineSample: return sample.reciprocal(state)
[docs] @override def serialize(self, process: SerializationProcess) -> None: process.serialize_object(self.__type, "type") process.serialize_floating(self.__rms_delay, "rms_delay") process.serialize_floating(self.__rayleigh_factor, "rayleigh_factor") process.serialize_array(self.__angle_coupling_indices, "angle_coupling_indices") process.serialize_object(self.__consistent_realization, "consistent_realization") process.serialize_object(self.__xpr_phase, "xpr_phase") ChannelRealization.serialize(self, process)
@classmethod @override def _DeserializeParameters(cls, process: DeserializationProcess) -> dict[str, object]: parameters = { "type": process.deserialize_object("type", CDLType), "rms_delay": process.deserialize_floating("rms_delay"), "rayleigh_factor": process.deserialize_floating("rayleigh_factor"), "angle_coupling_indices": process.deserialize_array("angle_coupling_indices", np.int64), "consistent_realization": process.deserialize_object( "consistent_realization", ConsistentRealization ), "xpr_phase": process.deserialize_object("xpr_phase", ConsistentUniform), } parameters.update(ChannelRealization._DeserializeParameters(process)) return parameters
[docs] @classmethod @override def Deserialize(cls, process: DeserializationProcess) -> CDLRealization: return CDLRealization( sample_hooks=set(), **cls._DeserializeParameters(process) # type: ignore[arg-type] )
[docs] class CDL(Channel[CDLRealization, ClusterDelayLineSample]): """Static cluster delay line model for link-level simulations.""" __DEFAULT_RAYLEIGH_FACTOR: float = 0.0 __DEFAULT_DECORRELATION_DISTANCE: float = 30.0 __model_type: CDLType __rms_delay: float __rayleigh_factor: float __decorrelation_distance: float __consistent_generator: ConsistentGenerator __xpr_phase: ConsistentUniform def __init__( self, model_type: CDLType, rms_delay: float, rayleigh_factor: float = __DEFAULT_RAYLEIGH_FACTOR, decorrelation_distance: float = __DEFAULT_DECORRELATION_DISTANCE, **kwargs, ) -> None: """ Args: model_type: Type of the cluster delay line model. rms_delay: Root mean square delay spread of the channel. rayleigh_factor: Rayleigh K-factor of the channel. decorrelation_distance: Decorrelation distance of the channel. \*\*kwargs: Additional parameters for the base class. """ # Initialize base class Channel.__init__(self, **kwargs) # Store parameters self.__model_type = model_type self.rms_delay = rms_delay self.rayleigh_factor = rayleigh_factor self.decorrelation_distance = decorrelation_distance self.__consistent_generator = ConsistentGenerator(self) self.__xpr_phase = self.__consistent_generator.uniform( ( 2, 2, CDL_Cluster_Parameters[model_type].shape[0], ClusterDelayLineRealization._ray_offset_angles.size, ) ) @property def model_type(self) -> CDLType: """Type of the cluster delay line model.""" return self.__model_type @property def rms_delay(self) -> float: """Root mean square delay spread of the channel. Raises: ValueError: If the delay spread is negative. """ return self.__rms_delay @rms_delay.setter def rms_delay(self, value: float) -> None: if value < 0: raise ValueError("The delay spread must be non-negative.") self.__rms_delay = value @property def rayleigh_factor(self) -> float: """Rayleigh K-factor of the channel. Raises: ValueError: If the K-factor is negative. """ return self.__rayleigh_factor @rayleigh_factor.setter def rayleigh_factor(self, value: float) -> None: if value < 0: raise ValueError("The K-factor must be non-negative.") self.__rayleigh_factor = value @property def decorrelation_distance(self) -> float: """Decorrelation distance of the channel. Raises: ValueError: If the decorrelation distance is negative. """ return self.__decorrelation_distance @decorrelation_distance.setter def decorrelation_distance(self, value: float) -> None: if value < 0: raise ValueError("The decorrelation distance must be non-negative.") self.__decorrelation_distance = value
[docs] def _realize(self) -> CDLRealization: angle_candidate_indices = np.arange(ClusterDelayLineRealization._ray_offset_angles.size) angle_coupling_indices = np.array( [ [ self._rng.permutation(angle_candidate_indices) for _ in range(CDL_Cluster_Parameters[self.model_type].shape[0]) ] for _ in range(4) ] ) return CDLRealization( self.model_type, self.rms_delay, self.rayleigh_factor, angle_coupling_indices, self.__consistent_generator.realize(self.decorrelation_distance), self.__xpr_phase, self.sample_hooks, self.gain, )
[docs] @override def serialize(self, process: SerializationProcess) -> None: process.serialize_object(self.model_type, "model_type") process.serialize_floating(self.rms_delay, "rms_delay") process.serialize_floating(self.rayleigh_factor, "rayleigh_factor") process.serialize_floating(self.decorrelation_distance, "decorrelation_distance") process.serialize_floating(self.gain, "gain")
[docs] @classmethod @override def Deserialize(cls, process: DeserializationProcess) -> CDL: return CDL( process.deserialize_object("model_type", CDLType), process.deserialize_floating("rms_delay"), process.deserialize_floating("rayleigh_factor", cls.__DEFAULT_RAYLEIGH_FACTOR), process.deserialize_floating( "decorrelation_distance", cls.__DEFAULT_DECORRELATION_DISTANCE ), gain=process.deserialize_floating("gain"), )