#!/usr/bin/env python
"""Module containing single user channels. """
import math
from typing import List, Optional, Union
import numpy as np
from . import fading
from .fading_generators import JakesSampleGenerator, RayleighSampleGenerator
# Either a jakes or a Rayleigh model can be used
FadingGenerator = Union[JakesSampleGenerator, RayleighSampleGenerator]
# Type representing something that can be used to index a numpy array
Indexes = Union[np.ndarray, List[int], slice]
[docs]class SuChannel:
"""
Single User channel corresponding to a Tapped Delay Line channel model,
which corresponds to a multipath channel. You can use a single tap in
order to get a flat fading channel.
You can create a new SuChannel object either specifying the channel
profile or specifying both the channel tap powers and delays. If
only the fading_generator is specified then a single tap with
unitary power and delay zero will be assumed, which corresponds to a
flat fading channel model.
Parameters
----------
fading_generator : FadingGenerator
The instance of a fading generator in the `fading_generators`
module. It should be a subclass of FadingSampleGenerator. The
fading generator will be used to generate the channel samples. If
not provided then RayleighSampleGenerator will be used
channel_profile : fading.TdlChannelProfile
The channel profile, which specifies the tap powers and delays.
tap_powers_dB : np.ndarray
The powers of each tap (in dB). Dimension: `L x 1`
Note: The power of each tap will be a negative number (in dB).
tap_delays : np.ndarray
The delay of each tap (in seconds). Dimension: `L x 1`
Ts : float, optional
The sampling interval.
"""
def __init__(self,
fading_generator: Optional[FadingGenerator] = None,
channel_profile: Optional[fading.TdlChannelProfile] = None,
tap_powers_dB: Optional[np.ndarray] = None,
tap_delays: Optional[np.ndarray] = None,
Ts: Optional[float] = None):
fading_generator_param: FadingGenerator
if fading_generator is None:
fading_generator_param = RayleighSampleGenerator()
if channel_profile is None and Ts is None:
Ts = 1.0
else:
fading_generator_param = fading_generator
if (channel_profile is None and tap_powers_dB is None
and tap_delays is None):
# Only the fading generator was provided. Let's assume a flat
# fading channel
self._tdlchannel = fading.TdlChannel(fading_generator_param,
tap_powers_dB=np.zeros(1),
tap_delays=np.zeros(1),
Ts=Ts)
else:
# More parameters were provided. We will have then a TDL
# channel model. Let's just pass these parameters to the
# base class.
self._tdlchannel = fading.TdlChannel(fading_generator_param,
channel_profile,
tap_powers_dB, tap_delays, Ts)
# Path loss which will be multiplied by the impulse response when
# corrupt_data is called
self._pathloss_value: Optional[float] = None
[docs] def set_pathloss(self, pathloss_value: Optional[float] = None) -> None:
"""
Set the path loss (IN LINEAR SCALE).
The path loss will be accounted when calling the corrupt_data
method.
If you want to disable the path loss, set `pathloss_value` to
None.
Parameters
----------
pathloss_value : float | None
The path loss (IN LINEAR SCALE) from the transmitter to the
receiver. If you want to disable the path loss then set it to
None.
Notes
-----
Note that path loss is a power relation, which means that the
channel coefficients will be multiplied by the square root of
elements in `pathloss_value`.
"""
if pathloss_value is not None:
if pathloss_value < 0 or pathloss_value > 1:
raise ValueError("Pathloss must be between 0 and 1")
self._pathloss_value = pathloss_value
[docs] def set_num_antennas(self, num_rx_antennas: int,
num_tx_antennas: int) -> None:
"""
Set the number of transmit and receive antennas for MIMO
transmission.
Set both `num_rx_antennas` and `num_tx_antennas` to None for SISO
transmission
Parameters
----------
num_rx_antennas : int
The number of receive antennas.
num_tx_antennas : int
The number of transmit antennas.
"""
self._tdlchannel.set_num_antennas(num_rx_antennas, num_tx_antennas)
[docs] def corrupt_data(self, signal: np.ndarray) -> np.ndarray:
"""
Transmit the signal through the TDL channel.
Parameters
----------
signal : np.ndarray
The signal to be transmitted.
Returns
-------
np.ndarray
The received signal after transmission through the TDL channel.
"""
# output = super().corrupt_data(signal)
output = self._tdlchannel.corrupt_data(signal)
if self._pathloss_value is not None:
# noinspection PyTypeChecker
output *= math.sqrt(self._pathloss_value)
return output
[docs] def corrupt_data_in_freq_domain(
self,
signal: np.ndarray,
fft_size: int,
carrier_indexes: Optional[Indexes] = None) -> np.ndarray:
"""
Transmit the signal through the TDL channel, but in the frequency
domain.
This is ROUGHLY equivalent to modulating `signal` with OFDM using
`fft_size` subcarriers, transmitting through a regular TdlChannel,
and then demodulating with OFDM to recover the received signal.
One important difference is that here the channel is considered
constant during the transmission of `fft_size` elements in
`signal`, and then it is varied by the equivalent of the variation
for that number of elements. That is, the channel is block static.
Parameters
----------
signal : np.ndarray
The signal to be transmitted.
fft_size : int
The size of the Fourier transform to get the frequency
response.
carrier_indexes : slice | np.ndarray
The indexes of the subcarriers where signal is to be
transmitted. If it is None assume all subcarriers will be
used. This can be a slice object or a numpy array of integers.
Returns
-------
np.ndarray
The received signal after transmission through the TDL channel
"""
output = self._tdlchannel.corrupt_data_in_freq_domain(
signal, fft_size, carrier_indexes)
if self._pathloss_value is not None:
# noinspection PyTypeChecker
output *= math.sqrt(self._pathloss_value)
return output
[docs] def get_last_impulse_response(self) -> fading.TdlImpulseResponse:
"""
Get the last generated impulse response.
A new impulse response is generated when the method `corrupt_data`
is called. You can use the `get_last_impulse_response` method to
get the impulse response used to corrupt the last data.
Returns
-------
fading.TdlImpulseResponse
The impulse response of the channel that was used to corrupt
the last data.
"""
if self._pathloss_value is None:
return self._tdlchannel.get_last_impulse_response()
return math.sqrt(self._pathloss_value) * \
self._tdlchannel.get_last_impulse_response()
@property
def switched_direction(self) -> bool:
"""
Get the value of `switched_direction`.
Returns
-------
bool
True if direction is switched and False otherwise.
"""
return self._tdlchannel.switched_direction
@switched_direction.setter
def switched_direction(self, value: bool) -> None:
"""
Set the value of `switched_direction`.
Parameters
----------
value : bool
True to switch directions of false to use original direction.
"""
self._tdlchannel.switched_direction = value
@property
def num_taps(self) -> int:
"""
Get the number of taps in the profile.
Returns
-------
int
The number of taps in the channel (not including any zero
padding).
"""
return self._tdlchannel.num_taps
@property
def num_taps_with_padding(self) -> int:
"""
Get the number of taps in the profile including zero-padding
when the profile is discretized.
If the profile is not discretized an exception is raised.
Returns
-------
int
The number of taps in the channel (including any zero padding).
"""
return self._tdlchannel.num_taps_with_padding
@property
def channel_profile(self) -> fading.TdlChannelProfile:
"""
Return the channel profile.
Returns
-------
fading.TdlChannelProfile
The channel profile.
"""
return self._tdlchannel.channel_profile
@property
def num_tx_antennas(self) -> int:
"""
Get the number of transmit antennas.
Returns
-------
int
The number of transmit antennas.
"""
return self._tdlchannel.num_tx_antennas
@property
def num_rx_antennas(self) -> int:
"""
Get the number of receive antennas.
Returns
-------
int
The number of receive antennas.
"""
return self._tdlchannel.num_rx_antennas
[docs]class SuMimoChannel(SuChannel):
"""
Single User channel corresponding to a Tapped Delay Line channel model,
which corresponds to a multipath channel. You can use a single tap in
order to get a flat fading channel.
You can create a new SuMimoChannel object either specifying the
channel profile or specifying both the channel tap powers and
delays. If only the fading_generator is specified then a single tap
with unitary power and delay zero will be assumed, which corresponds
to a flat fading channel model.
Parameters
----------
num_antennas : int
Number of transmit and receive antennas.
fading_generator : FadingGenerator
The instance of a fading generator in the `fading_generators`
module. It should be a subclass of FadingSampleGenerator. The
fading generator will be used to generate the channel samples. If
not provided then RayleighSampleGenerator will be used
channel_profile : fading.TdlChannelProfile
The channel profile, which specifies the tap powers and delays.
tap_powers_dB : np.ndarray
The powers of each tap (in dB). Dimension: `L x 1`
Note: The power of each tap will be a negative number (in dB).
tap_delays : np.ndarray
The delay of each tap (in seconds). Dimension: `L x 1`
Ts : float, optional
The sampling interval.
"""
def __init__(self,
num_antennas: int,
fading_generator: Optional[FadingGenerator] = None,
channel_profile: Optional[fading.TdlChannelProfile] = None,
tap_powers_dB: Optional[np.ndarray] = None,
tap_delays: Optional[np.ndarray] = None,
Ts: Optional[float] = None):
# Before calling supper to initialize the base class we will set
# the shape of the fading generator
fading_generator_param: FadingGenerator
if fading_generator is None:
fading_generator_param = RayleighSampleGenerator()
if channel_profile is None and Ts is None:
Ts = 1.0
else:
fading_generator_param = fading_generator
# Set the shape of the fading generator.
fading_generator_param.shape = (num_antennas, num_antennas)
# Initialize attributes from base class
super(SuMimoChannel,
self).__init__(fading_generator_param, channel_profile,
tap_powers_dB, tap_delays, Ts)