anomaly-detection-material-parameters-calibration

Sionna param calibration (research proj)
git clone https://git.ea.contact/anomaly-detection-material-parameters-calibration
Log | Files | Refs | README

cdl.py (29184B)


      1 #
      2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
      3 # SPDX-License-Identifier: Apache-2.0
      4 #
      5 """Clustered delay line (CDL) channel model from 3GPP TR38.901 specification"""
      6 
      7 
      8 import json
      9 from importlib_resources import files
     10 import tensorflow as tf
     11 from tensorflow import cos, sin
     12 import numpy as np
     13 
     14 from sionna.channel.utils import deg_2_rad
     15 from sionna.channel import ChannelModel
     16 from sionna import PI
     17 from sionna import config
     18 from sionna.utils.tensors import insert_dims
     19 from . import Topology, ChannelCoefficientsGenerator
     20 from . import Rays
     21 
     22 from . import models # pylint: disable=relative-beyond-top-level
     23 
     24 class CDL(ChannelModel):
     25     # pylint: disable=line-too-long
     26     r"""CDL(model, delay_spread, carrier_frequency, ut_array, bs_array, direction, min_speed=0., max_speed=None, dtype=tf.complex64)
     27 
     28     Clustered delay line (CDL) channel model from the 3GPP [TR38901]_ specification.
     29 
     30     The power delay profiles (PDPs) are normalized to have a total energy of one.
     31 
     32     If a minimum speed and a maximum speed are specified such that the
     33     maximum speed is greater than the minimum speed, then UTs speeds are
     34     randomly and uniformly sampled from the specified interval for each link
     35     and each batch example.
     36 
     37     The CDL model only works for systems with a single transmitter and a single
     38     receiver. The transmitter and receiver can be equipped with multiple
     39     antennas.
     40 
     41     Example
     42     --------
     43 
     44     The following code snippet shows how to setup a CDL channel model assuming
     45     an OFDM waveform:
     46 
     47     >>> # Panel array configuration for the transmitter and receiver
     48     >>> bs_array = PanelArray(num_rows_per_panel = 4,
     49     ...                       num_cols_per_panel = 4,
     50     ...                       polarization = 'dual',
     51     ...                       polarization_type = 'cross',
     52     ...                       antenna_pattern = '38.901',
     53     ...                       carrier_frequency = 3.5e9)
     54     >>> ut_array = PanelArray(num_rows_per_panel = 1,
     55     ...                       num_cols_per_panel = 1,
     56     ...                       polarization = 'single',
     57     ...                       polarization_type = 'V',
     58     ...                       antenna_pattern = 'omni',
     59     ...                       carrier_frequency = 3.5e9)
     60     >>> # CDL channel model
     61     >>> cdl = CDL(model = "A",
     62     >>>           delay_spread = 300e-9,
     63     ...           carrier_frequency = 3.5e9,
     64     ...           ut_array = ut_array,
     65     ...           bs_array = bs_array,
     66     ...           direction = 'uplink')
     67     >>> channel = OFDMChannel(channel_model = cdl,
     68     ...                       resource_grid = rg)
     69 
     70     where ``rg`` is an instance of :class:`~sionna.ofdm.ResourceGrid`.
     71 
     72     Notes
     73     ------
     74 
     75     The following tables from [TR38901]_ provide typical values for the delay
     76     spread.
     77 
     78     +--------------------------+-------------------+
     79     | Model                    | Delay spread [ns] |
     80     +==========================+===================+
     81     | Very short delay spread  | :math:`10`        |
     82     +--------------------------+-------------------+
     83     | Short short delay spread | :math:`10`        |
     84     +--------------------------+-------------------+
     85     | Nominal delay spread     | :math:`100`       |
     86     +--------------------------+-------------------+
     87     | Long delay spread        | :math:`300`       |
     88     +--------------------------+-------------------+
     89     | Very long delay spread   | :math:`1000`      |
     90     +--------------------------+-------------------+
     91 
     92     +-----------------------------------------------+------+------+----------+-----+----+-----+
     93     |              Delay spread [ns]                |             Frequency [GHz]             |
     94     +                                               +------+------+----+-----+-----+----+-----+
     95     |                                               |   2  |   6  | 15 |  28 |  39 | 60 |  70 |
     96     +========================+======================+======+======+====+=====+=====+====+=====+
     97     | Indoor office          | Short delay profile  | 20   | 16   | 16 | 16  | 16  | 16 | 16  |
     98     |                        +----------------------+------+------+----+-----+-----+----+-----+
     99     |                        | Normal delay profile | 39   | 30   | 24 | 20  | 18  | 16 | 16  |
    100     |                        +----------------------+------+------+----+-----+-----+----+-----+
    101     |                        | Long delay profile   | 59   | 53   | 47 | 43  | 41  | 38 | 37  |
    102     +------------------------+----------------------+------+------+----+-----+-----+----+-----+
    103     | UMi Street-canyon      | Short delay profile  | 65   | 45   | 37 | 32  | 30  | 27 | 26  |
    104     |                        +----------------------+------+------+----+-----+-----+----+-----+
    105     |                        | Normal delay profile | 129  | 93   | 76 | 66  | 61  | 55 | 53  |
    106     |                        +----------------------+------+------+----+-----+-----+----+-----+
    107     |                        | Long delay profile   | 634  | 316  | 307| 301 | 297 | 293| 291 |
    108     +------------------------+----------------------+------+------+----+-----+-----+----+-----+
    109     | UMa                    | Short delay profile  | 93   | 93   | 85 | 80  | 78  | 75 | 74  |
    110     |                        +----------------------+------+------+----+-----+-----+----+-----+
    111     |                        | Normal delay profile | 363  | 363  | 302| 266 | 249 |228 | 221 |
    112     |                        +----------------------+------+------+----+-----+-----+----+-----+
    113     |                        | Long delay profile   | 1148 | 1148 | 955| 841 | 786 | 720| 698 |
    114     +------------------------+----------------------+------+------+----+-----+-----+----+-----+
    115     | RMa / RMa O2I          | Short delay profile  | 32   | 32   | N/A| N/A | N/A | N/A| N/A |
    116     |                        +----------------------+------+------+----+-----+-----+----+-----+
    117     |                        | Normal delay profile | 37   | 37   | N/A| N/A | N/A | N/A| N/A |
    118     |                        +----------------------+------+------+----+-----+-----+----+-----+
    119     |                        | Long delay profile   | 153  | 153  | N/A| N/A | N/A | N/A| N/A |
    120     +------------------------+----------------------+------+------+----+-----+-----+----+-----+
    121     | UMi / UMa O2I          | Normal delay profile | 242                                     |
    122     |                        +----------------------+-----------------------------------------+
    123     |                        | Long delay profile   | 616                                     |
    124     +------------------------+----------------------+-----------------------------------------+
    125 
    126     Parameters
    127     -----------
    128 
    129     model : str
    130         CDL model to use. Must be one of "A", "B", "C", "D" or "E".
    131 
    132     delay_spread : float
    133         RMS delay spread [s].
    134 
    135     carrier_frequency : float
    136         Carrier frequency [Hz].
    137 
    138     ut_array : PanelArray
    139         Panel array used by the UTs. All UTs share the same antenna array
    140         configuration.
    141 
    142     bs_array : PanelArray
    143         Panel array used by the Bs. All BSs share the same antenna array
    144         configuration.
    145 
    146     direction : str
    147         Link direction. Must be either "uplink" or "downlink".
    148 
    149     ut_orientation : `None` or Tensor of shape [3], tf.float
    150         Orientation of the UT. If set to `None`, [:math:`\pi`, 0, 0] is used.
    151         Defaults to `None`.
    152 
    153     bs_orientation : `None` or Tensor of shape [3], tf.float
    154         Orientation of the BS. If set to `None`, [0, 0, 0] is used.
    155         Defaults to `None`.
    156 
    157     min_speed : float
    158         Minimum speed [m/s]. Defaults to 0.
    159 
    160     max_speed : None or float
    161         Maximum speed [m/s]. If set to `None`,
    162         then ``max_speed`` takes the same value as ``min_speed``.
    163         Defaults to `None`.
    164 
    165     dtype : Complex tf.DType
    166         Defines the datatype for internal calculations and the output
    167         dtype. Defaults to `tf.complex64`.
    168 
    169     Input
    170     -----
    171 
    172     batch_size : int
    173         Batch size
    174 
    175     num_time_steps : int
    176         Number of time steps
    177 
    178     sampling_frequency : float
    179         Sampling frequency [Hz]
    180 
    181     Output
    182     -------
    183     a : [batch size, num_rx = 1, num_rx_ant, num_tx = 1, num_tx_ant, num_paths, num_time_steps], tf.complex
    184         Path coefficients
    185 
    186     tau : [batch size, num_rx = 1, num_tx = 1, num_paths], tf.float
    187         Path delays [s]
    188 
    189     """
    190 
    191     # Number of rays per cluster is set to 20 for CDL
    192     NUM_RAYS = 20
    193 
    194     def __init__(   self,
    195                     model,
    196                     delay_spread,
    197                     carrier_frequency,
    198                     ut_array,
    199                     bs_array,
    200                     direction,
    201                     ut_orientation=None,
    202                     bs_orientation=None,
    203                     min_speed=0.,
    204                     max_speed=None,
    205                     dtype=tf.complex64):
    206 
    207         assert dtype.is_complex, "dtype must be a complex datatype"
    208         self._dtype = dtype
    209         real_dtype = dtype.real_dtype
    210         self._real_dtype = real_dtype
    211 
    212         assert direction in('uplink', 'downlink'), "Invalid link direction"
    213         self._direction = direction
    214 
    215         # If no orientation is defined by the user, set to default values
    216         # that make sense
    217         if ut_orientation is None:
    218             ut_orientation = tf.constant([PI, 0.0, 0.0], real_dtype)
    219         if bs_orientation is None:
    220             bs_orientation = tf.zeros([3], real_dtype)
    221 
    222         # Setting which from UT or BS is the transmitter and which is the
    223         # receiver according to the link direction
    224         if self._direction == 'downlink':
    225             self._moving_end = 'rx'
    226             self._tx_array = bs_array
    227             self._rx_array = ut_array
    228             self._tx_orientation = bs_orientation
    229             self._rx_orientation = ut_orientation
    230         elif self._direction == 'uplink':
    231             self._moving_end = 'tx'
    232             self._tx_array = ut_array
    233             self._rx_array = bs_array
    234             self._tx_orientation = ut_orientation
    235             self._rx_orientation = bs_orientation
    236 
    237         self._carrier_frequency = tf.constant(carrier_frequency, real_dtype)
    238         self._delay_spread = tf.constant(delay_spread, real_dtype)
    239         self._min_speed = tf.constant(min_speed, real_dtype)
    240         if max_speed is None:
    241             self._max_speed = self._min_speed
    242         else:
    243             assert max_speed >= min_speed, \
    244                 "min_speed cannot be larger than max_speed"
    245             self._max_speed = tf.constant(max_speed, real_dtype)
    246 
    247         # Loading the model parameters
    248         assert model in ("A", "B", "C", "D", "E"), "Invalid CDL model"
    249         if model == 'A':
    250             parameters_fname = "CDL-A.json"
    251         elif model == 'B':
    252             parameters_fname = "CDL-B.json"
    253         elif model == 'C':
    254             parameters_fname = "CDL-C.json"
    255         elif model == 'D':
    256             parameters_fname = "CDL-D.json"
    257         else: # 'E'
    258             parameters_fname = "CDL-E.json"
    259         self._load_parameters(parameters_fname)
    260 
    261         # Channel coefficient generator for sampling channel impulse responses
    262         self._cir_sampler = ChannelCoefficientsGenerator(carrier_frequency,
    263                                                          self._tx_array,
    264                                                          self._rx_array,
    265                                                          subclustering=False,
    266                                                          dtype=dtype)
    267 
    268     def __call__(self, batch_size, num_time_steps, sampling_frequency):
    269 
    270         ## Topology for generating channel coefficients
    271         # Sample random velocities
    272         v_r = config.tf_rng.uniform(shape=[batch_size, 1],
    273                                     minval=self._min_speed,
    274                                     maxval=self._max_speed,
    275                                     dtype=self._real_dtype)
    276         v_phi = config.tf_rng.uniform(shape=[batch_size, 1],
    277                                       minval=0.0,
    278                                       maxval=2.*PI,
    279                                       dtype=self._real_dtype)
    280         v_theta = config.tf_rng.uniform(shape=[batch_size, 1],
    281                                         minval=0.0,
    282                                         maxval=PI,
    283                                         dtype=self._real_dtype)
    284         velocities = tf.stack([v_r*cos(v_phi)*sin(v_theta),
    285                                v_r*sin(v_phi)*sin(v_theta),
    286                                v_r*cos(v_theta)], axis=-1)
    287         los = tf.fill([batch_size, 1, 1], self._los)
    288         los_aoa = tf.tile(self._los_aoa, [batch_size, 1, 1])
    289         los_zoa = tf.tile(self._los_zoa, [batch_size, 1, 1])
    290         los_aod = tf.tile(self._los_aod, [batch_size, 1, 1])
    291         los_zod = tf.tile(self._los_zod, [batch_size, 1, 1])
    292         distance_3d = tf.zeros([batch_size, 1, 1], self._real_dtype)
    293         tx_orientation = tf.tile(insert_dims(self._tx_orientation, 2, 0),
    294                                  [batch_size, 1, 1])
    295         rx_orientation = tf.tile(insert_dims(self._rx_orientation, 2, 0),
    296                                  [batch_size, 1, 1])
    297         k_factor = tf.tile(self._k_factor, [batch_size, 1, 1])
    298         topology = Topology(velocities=velocities,
    299                             moving_end=self._moving_end,
    300                             los_aoa=los_aoa,
    301                             los_zoa=los_zoa,
    302                             los_aod=los_aod,
    303                             los_zod=los_zod,
    304                             los=los,
    305                             distance_3d=distance_3d,
    306                             tx_orientations=tx_orientation,
    307                             rx_orientations=rx_orientation)
    308 
    309         # Rays used to generate the channel model
    310         delays = tf.tile(self._delays*self._delay_spread, [batch_size, 1, 1, 1])
    311         powers = tf.tile(self._powers, [batch_size, 1, 1, 1])
    312         aoa = tf.tile(self._aoa, [batch_size, 1, 1, 1, 1])
    313         aod = tf.tile(self._aod, [batch_size, 1, 1, 1, 1])
    314         zoa = tf.tile(self._zoa, [batch_size, 1, 1, 1, 1])
    315         zod = tf.tile(self._zod, [batch_size, 1, 1, 1, 1])
    316         xpr = tf.tile(self._xpr, [batch_size, 1, 1, 1, 1])
    317 
    318        # Random coupling
    319         aoa, aod, zoa, zod = self._random_coupling(aoa, aod, zoa, zod)
    320 
    321         rays = Rays(delays=delays,
    322                     powers=powers,
    323                     aoa=aoa,
    324                     aod=aod,
    325                     zoa=zoa,
    326                     zod=zod,
    327                     xpr=xpr)
    328 
    329         # Sampling channel impulse responses
    330         # pylint: disable=unbalanced-tuple-unpacking
    331         h, delays = self._cir_sampler(num_time_steps, sampling_frequency,
    332                                       k_factor, rays, topology)
    333 
    334         # Reshaping to match the expected output
    335         h = tf.transpose(h, [0, 2, 4, 1, 5, 3, 6])
    336         delays = tf.transpose(delays, [0, 2, 1, 3])
    337 
    338         # Stop gadients to avoid useless backpropagation
    339         h = tf.stop_gradient(h)
    340         delays = tf.stop_gradient(delays)
    341 
    342         return h, delays
    343 
    344     @property
    345     def num_clusters(self):
    346         r"""Number of paths (:math:`M`)"""
    347         return self._num_clusters
    348 
    349     @property
    350     def los(self):
    351         r"""`True` is this is a LoS model. `False` otherwise."""
    352         return self._los
    353 
    354     @property
    355     def k_factor(self):
    356         r"""K-factor in linear scale. Only available with LoS models."""
    357         assert self._los, "This property is only available for LoS models"
    358         # We return the K-factor for the path with zero-delay, and not for the
    359         # entire PDP.
    360         return self._k_factor[0,0,0]/self._powers[0,0,0,0]
    361 
    362     @property
    363     def delays(self):
    364         r"""Path delays [s]"""
    365         return self._delays[0,0,0]*self._delay_spread
    366 
    367     @property
    368     def powers(self):
    369         r"""Path powers in linear scale"""
    370         if self.los:
    371             k_factor = self._k_factor[0,0,0]
    372             nlos_powers = self._powers[0,0,0]
    373             # Power of the LoS path
    374             p0 = k_factor + nlos_powers[0]
    375             returned_powers = tf.tensor_scatter_nd_update(nlos_powers,
    376                                                             [[0]], [p0])
    377             returned_powers = returned_powers / (k_factor+1.)
    378         else:
    379             returned_powers = self._powers[0,0,0]
    380         return returned_powers
    381 
    382     @property
    383     def delay_spread(self):
    384         r"""RMS delay spread [s]"""
    385         return self._delay_spread
    386 
    387     @delay_spread.setter
    388     def delay_spread(self, value):
    389         self._delay_spread = value
    390 
    391     ###########################################
    392     # Utility functions
    393     ###########################################
    394 
    395     def _load_parameters(self, fname):
    396         r"""Load parameters of a CDL model.
    397 
    398         The model parameters are stored as JSON files with the following keys:
    399         * los : boolean that indicates if the model is a LoS model
    400         * num_clusters : integer corresponding to the number of clusters (paths)
    401         * delays : List of path delays in ascending order normalized by the RMS
    402             delay spread
    403         * powers : List of path powers in dB scale
    404         * aod : Paths AoDs [degree]
    405         * aoa : Paths AoAs [degree]
    406         * zod : Paths ZoDs [degree]
    407         * zoa : Paths ZoAs [degree]
    408         * cASD : Cluster ASD
    409         * cASA : Cluster ASA
    410         * cZSD : Cluster ZSD
    411         * cZSA : Cluster ZSA
    412         * xpr : XPR in dB
    413 
    414         For LoS models, the two first paths have zero delay, and are assumed
    415         to correspond to the specular and NLoS component, in this order.
    416 
    417         Input
    418         ------
    419         fname : str
    420             File from which to load the parameters.
    421 
    422         Output
    423         ------
    424         None
    425         """
    426 
    427         # Load the JSON configuration file
    428         source = files(models).joinpath(fname)
    429         # pylint: disable=unspecified-encoding
    430         with open(source) as parameter_file:
    431             params = json.load(parameter_file)
    432 
    433         # LoS scenario ?
    434         self._los = tf.cast(params['los'], tf.bool)
    435 
    436         # Loading cluster delays and powers
    437         self._num_clusters = tf.constant(params['num_clusters'], tf.int32)
    438 
    439         # Loading the rays components, all of shape [num clusters]
    440         delays = tf.constant(params['delays'], self._real_dtype)
    441         powers = tf.constant(np.power(10.0, np.array(params['powers'])/10.0),
    442                                                             self._real_dtype)
    443 
    444         # Normalize powers
    445         norm_fact = tf.reduce_sum(powers)
    446         powers = powers / norm_fact
    447 
    448         # Loading the angles and angle spreads of arrivals and departure
    449         c_aod = tf.constant(params['cASD'], self._real_dtype)
    450         aod = tf.constant(params['aod'], self._real_dtype)
    451         c_aoa = tf.constant(params['cASA'], self._real_dtype)
    452         aoa = tf.constant(params['aoa'], self._real_dtype)
    453         c_zod = tf.constant(params['cZSD'], self._real_dtype)
    454         zod = tf.constant(params['zod'], self._real_dtype)
    455         c_zoa = tf.constant(params['cZSA'], self._real_dtype)
    456         zoa = tf.constant(params['zoa'], self._real_dtype)
    457 
    458         # If LoS, compute the model K-factor following 7.7.6 of TR38.901 and
    459         # the LoS path angles of arrival and departure.
    460         # We remove the specular component from the arrays, as it will be added
    461         # separately when computing the channel coefficients
    462         if self._los:
    463             # Extract the specular component, as it will be added separately by
    464             # the CIR generator.
    465             los_power = powers[0]
    466             powers = powers[1:]
    467             delays = delays[1:]
    468             los_aod = aod[0]
    469             aod = aod[1:]
    470             los_aoa = aoa[0]
    471             aoa = aoa[1:]
    472             los_zod = zod[0]
    473             zod = zod[1:]
    474             los_zoa = zoa[0]
    475             zoa = zoa[1:]
    476 
    477             # The CIR generator scales all NLoS powers by 1/(K+1),
    478             # where K = k_factor, and adds to the path with zero delay a
    479             # specular component with power K/(K+1).
    480             # Note that all the paths are scaled by 1/(K+1), including the ones
    481             # with non-zero delays.
    482             # We re-normalized the NLoS power paths to ensure total unit energy
    483             # after scaling
    484             norm_fact = tf.reduce_sum(powers)
    485             powers = powers / norm_fact
    486             # To ensure that the path with zero delay the ratio between the
    487             # specular component and the NLoS component has the same ratio as
    488             # in the CDL PDP, we need to set the K-factor to to the value of
    489             # the specular component. The ratio between the other paths is
    490             # preserved as all paths are scaled by 1/(K+1).
    491             # Note that because of the previous normalization of the NLoS paths'
    492             # powers, which ensured that their total power is 1,
    493             # this is equivalent to defining the K factor as done in 3GPP
    494             # specifications (see step 11):
    495             # K = (power of specular component)/(total power of the NLoS paths)
    496             k_factor = los_power/norm_fact
    497 
    498             los_aod = deg_2_rad(los_aod)
    499             los_aoa = deg_2_rad(los_aoa)
    500             los_zod = deg_2_rad(los_zod)
    501             los_zoa = deg_2_rad(los_zoa)
    502         else:
    503             # For NLoS models, we need to give value to the K-factor and LoS
    504             # angles, but they will not be used.
    505             k_factor = tf.ones((), self._real_dtype)
    506 
    507             los_aod = tf.zeros((), self._real_dtype)
    508             los_aoa = tf.zeros((), self._real_dtype)
    509             los_zod = tf.zeros((), self._real_dtype)
    510             los_zoa = tf.zeros((), self._real_dtype)
    511 
    512         # Generate clusters rays and convert angles to radian
    513         aod = self._generate_rays(aod, c_aod) # [num clusters, num rays]
    514         aod = deg_2_rad(aod) # [num clusters, num rays]
    515         aoa = self._generate_rays(aoa, c_aoa) # [num clusters, num rays]
    516         aoa = deg_2_rad(aoa) # [num clusters, num rays]
    517         zod = self._generate_rays(zod, c_zod) # [num clusters, num rays]
    518         zod = deg_2_rad(zod) # [num clusters, num rays]
    519         zoa = self._generate_rays(zoa, c_zoa) # [num clusters, num rays]
    520         zoa = deg_2_rad(zoa) # [num clusters, num rays]
    521 
    522         # Store LoS power
    523         if self._los:
    524             self._los_power = los_power
    525 
    526         # Reshape the as expected by the channel impulse response generator
    527         self._k_factor = self._reshape_for_cir_computation(k_factor)
    528         los_aod  = self._reshape_for_cir_computation(los_aod)
    529         los_aoa  = self._reshape_for_cir_computation(los_aoa)
    530         los_zod  = self._reshape_for_cir_computation(los_zod)
    531         los_zoa  = self._reshape_for_cir_computation(los_zoa)
    532         self._delays = self._reshape_for_cir_computation(delays)
    533         self._powers = self._reshape_for_cir_computation(powers)
    534         aod = self._reshape_for_cir_computation(aod)
    535         aoa = self._reshape_for_cir_computation(aoa)
    536         zod = self._reshape_for_cir_computation(zod)
    537         zoa = self._reshape_for_cir_computation(zoa)
    538 
    539         # Setting angles of arrivals and departures according to the link
    540         # direction
    541         if self._direction == 'downlink':
    542             self._los_aoa = los_aoa
    543             self._los_zoa = los_zoa
    544             self._los_aod = los_aod
    545             self._los_zod = los_zod
    546             self._aoa = aoa
    547             self._zoa = zoa
    548             self._aod = aod
    549             self._zod = zod
    550         elif self._direction == 'uplink':
    551             self._los_aoa = los_aod
    552             self._los_zoa = los_zod
    553             self._los_aod = los_aoa
    554             self._los_zod = los_zoa
    555             self._aoa = aod
    556             self._zoa = zod
    557             self._aod = aoa
    558             self._zod = zoa
    559 
    560         # XPR
    561         xpr = params['xpr']
    562         xpr = np.power(10.0, xpr/10.0)
    563         xpr = tf.constant(xpr, self._real_dtype)
    564         xpr = tf.fill([self._num_clusters, CDL.NUM_RAYS], xpr)
    565         self._xpr = self._reshape_for_cir_computation(xpr)
    566 
    567     def _generate_rays(self, angles, c):
    568         r"""
    569         Generate rays from ``angles`` (which could be ZoD, ZoA, AoD, or AoA) and
    570         the angle spread ``c`` using equation 7.7-0a of TR38.901 specifications
    571 
    572         Input
    573         -------
    574         angles : [num cluster], float
    575             Tensor of angles with shape `[num_clusters]`
    576 
    577         c : float
    578             Angle spread
    579 
    580         Output
    581         -------
    582         ray_angles : float
    583             A tensor of shape [num clusters, num rays] containing the angle of
    584             each ray
    585         """
    586 
    587         # Basis vector of offset angle from table 7.5-3 from specfications
    588         # TR38.901
    589         basis_vector = tf.constant([0.0447, -0.0447,
    590                                     0.1413, -0.1413,
    591                                     0.2492, -0.2492,
    592                                     0.3715, -0.3715,
    593                                     0.5129, -0.5129,
    594                                     0.6797, -0.6797,
    595                                     0.8844, -0.8844,
    596                                     1.1481, -1.1481,
    597                                     1.5195, -1.5195,
    598                                     2.1551, -2.1551], self._real_dtype)
    599 
    600         # Reshape for broadcasting
    601         # [1, num rays = 20]
    602         basis_vector = tf.expand_dims(basis_vector, axis=0)
    603         # [num clusters, 1]
    604         angles = tf.expand_dims(angles, axis=1)
    605 
    606         # Generate rays following 7.7-0a
    607         # [num clusters, num rays = 20]
    608         ray_angles = angles + c*basis_vector
    609 
    610         return ray_angles
    611 
    612     def _reshape_for_cir_computation(self, array):
    613         r"""
    614         Add three leading dimensions to array, with shape [1, num_tx, num_rx],
    615         to reshape it as expected by the channel impulse response sampler.
    616 
    617         Input
    618         -------
    619         array : Any shape, float
    620             Array to reshape
    621 
    622         Output
    623         -------
    624         reshaped_array : Tensor, float
    625             The tensor ``array`` expanded with 3 dimensions for the batch,
    626             number of tx, and number of rx.
    627         """
    628 
    629         array_rank = tf.rank(array)
    630         tiling = tf.constant([1, 1, 1], tf.int32)
    631         if array_rank > 0:
    632             tiling = tf.concat([tiling, tf.ones([array_rank],tf.int32)], axis=0)
    633 
    634         array = insert_dims(array, 3, 0)
    635         array = tf.tile(array, tiling)
    636 
    637         return array
    638 
    639     def _shuffle_angles(self, angles):
    640         # pylint: disable=line-too-long
    641         """
    642         Randomly shuffle a tensor carrying azimuth/zenith angles
    643         of arrival/departure.
    644 
    645         Input
    646         ------
    647         angles : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    648             Angles to shuffle
    649 
    650         Output
    651         -------
    652         shuffled_angles : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    653             Shuffled ``angles``
    654         """
    655 
    656         # Create randomly shuffled indices by arg-sorting samples from a random
    657         # normal distribution
    658         random_numbers = config.tf_rng.normal(tf.shape(angles))
    659         shuffled_indices = tf.argsort(random_numbers)
    660         # Shuffling the angles
    661         shuffled_angles = tf.gather(angles,shuffled_indices, batch_dims=4)
    662         return shuffled_angles
    663 
    664     def _random_coupling(self, aoa, aod, zoa, zod):
    665         # pylint: disable=line-too-long
    666         """
    667         Randomly couples the angles within a cluster for both azimuth and
    668         elevation.
    669 
    670         Step 8 in TR 38.901 specification.
    671 
    672         Input
    673         ------
    674         aoa : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    675             Paths azimuth angles of arrival [degree] (AoA)
    676 
    677         aod : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    678             Paths azimuth angles of departure (AoD) [degree]
    679 
    680         zoa : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    681             Paths zenith angles of arrival [degree] (ZoA)
    682 
    683         zod : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    684             Paths zenith angles of departure [degree] (ZoD)
    685 
    686         Output
    687         -------
    688         shuffled_aoa : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    689             Shuffled `aoa`
    690 
    691         shuffled_aod : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    692             Shuffled `aod`
    693 
    694         shuffled_zoa : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    695             Shuffled `zoa`
    696 
    697         shuffled_zod : [batch size, num of BSs, num of UTs, maximum number of clusters, number of rays], tf.float
    698             Shuffled `zod`
    699         """
    700         shuffled_aoa = self._shuffle_angles(aoa)
    701         shuffled_aod = self._shuffle_angles(aod)
    702         shuffled_zoa = self._shuffle_angles(zoa)
    703         shuffled_zod = self._shuffle_angles(zod)
    704 
    705         return shuffled_aoa, shuffled_aod, shuffled_zoa, shuffled_zod