Source code for hermespy.channel.multipath_fading_channel

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

from __future__ import annotations
from abc import abstractmethod, ABC
from typing import Any, Sequence, Type, TYPE_CHECKING, List

import matplotlib.pyplot as plt
import numpy as np
from h5py import Group
from numpy import cos, exp
from scipy.constants import pi
from sparse import GCXS  # type: ignore

from hermespy.core import (
    ChannelStateInformation,
    ChannelStateFormat,
    Device,
    HDFSerializable,
    Serializable,
    Signal,
    VAT,
    Visualizable,
)
from .channel import Channel, ChannelRealization, InterpolationMode

if TYPE_CHECKING:
    from hermespy.simulation import SimulatedDevice  # pragma: no cover

__author__ = "Andre Noll Barreto"
__copyright__ = "Copyright 2023, Barkhausen Institut gGmbH"
__credits__ = ["Andre Noll Barreto", "Tobias Kronauer", "Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.2.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"


[docs] class AntennaCorrelation(ABC): """Base class for statistical modeling of antenna array correlations.""" __channel: Channel | None __device: SimulatedDevice | None def __init__( self, channel: Channel | None = None, device: SimulatedDevice | None = None ) -> None: self.channel = channel self.device = device @property @abstractmethod def covariance(self) -> np.ndarray: """Antenna covariance matrix. Returns: Two-dimensional numpy array representing the covariance matrix. """ ... # pragma: no cover @property def channel(self) -> Channel | None: """The channel this correlation model configures. Returns: Handle to the channel. `None` if the model is currently considered floating """ return self.__channel @channel.setter def channel(self, value: Channel | None) -> None: self.__channel = value @property def device(self) -> SimulatedDevice | None: """The device this correlation model is based upon. Returns: Handle to the device. `None` if the device is currently unknown. """ return self.__device @device.setter def device(self, value: SimulatedDevice | None) -> None: self.__device = value
[docs] class CustomAntennaCorrelation(Serializable, AntennaCorrelation): """Customizable antenna correlations.""" yaml_tag = "CustomCorrelation" """YAML serialization tag""" __covariance_matrix: np.ndarray def __init__(self, covariance: np.ndarray) -> None: """ Args: covariance (numpy.ndarray): Postive definte square antenna covariance matrix. """ self.covariance = covariance @property def covariance(self) -> np.ndarray: if ( self.device is not None and self.device.num_antennas != self.__covariance_matrix.shape[0] ): raise RuntimeError( f"Device with {self.device.num_antennas} antennas does not match covariance matrix of magnitude {self.__covariance_matrix.shape[0]}" ) return self.__covariance_matrix @covariance.setter def covariance(self, value: np.ndarray) -> None: if value.ndim != 2 or not np.allclose(value, value.T.conj()): raise ValueError("Antenna correlation must be a hermitian matrix") if np.any(np.linalg.eigvals(value) <= 0.0): raise ValueError("Antenna correlation matrix must be positive definite") self.__covariance_matrix = value
[docs] class PathRealization(HDFSerializable): """A single delay path of a Multipath Fading channel realization. Represents the single propagation path equation .. math:: h_{\\ell}(t) = \\sqrt{\\frac{K_{\\ell}}{1 + K_{\\ell}}} \\mathrm{e}^{\\mathrm{j} t \\omega_{\\ell} \\cos(\\theta_{\\ell,0}) + \\mathrm{j} \\phi_{\\ell,0} } + \\sqrt{\\frac{1}{N(1 + K_{\\ell})}} \\sum_{n=1}^{N} \\mathrm{e}^{\\mathrm{j} t \\omega_{\\ell} \\cos\\left( \\frac{2\\pi n + \\theta_{\\ell,n}}{N} \\right) + \\mathrm{j} \\phi_{\\ell,n}} """ __los_gain: float __los_angle: float __los_phase: float __los_doppler: float __nlos_gain: float __nlos_angles: np.ndarray __nlos_phases: np.ndarray __nlos_doppler: float def __init__( self, power: float, delay: float, los_gain: float, los_angle: float, los_phase: float, los_doppler: float, nlos_gain: float, nlos_angles: np.ndarray, nlos_phases: np.ndarray, nlos_doppler: float, ) -> None: """ Args: power (float): Power of the represented path in Watts. Initializes the :attr:`.power` attribute. delay (float): Delay of the represented path in seconds. Initializes the :attr:`.delay` attribute. los_gain (float): Line of sight power of the represented path. Initializes the :attr:`.los_gain` attribute. los_angle (float): Line of sight doppler angle in radians. Initializes the :attr:`.los_angle` attribute. los_phase (float): Line of sight components phase in radians. Initializes the :attr:`.los_phase` attribute. los_doppler (float): Line of sight doppler frequency in :math:`\\mathrm{Hz}`. Initializes the :attr:`.los_doppler` attribute. nlos_gain (float): Non line of sight power of the represented path. Initializes the :attr:`.nlos_gain` attribute. nlos_angles (float): Non line of sight doppler angles in radians. Initializes the :attr:`.nlos_angles` attribute. nlos_phases (float): Non line of sight components phases in radians. Initializes the :attr:`.nlos_phases` attribute. nlos_doppler (float): Non line of sight doppler frequency in :math:`\\mathrm{Hz}`. Initializes the :attr:`.nlos_doppler` attribute. """ # Initialize class attributes self.__power = power self.__delay = delay self.__los_gain = los_gain self.__los_angle = los_angle self.__los_phase = los_phase self.__los_doppler = los_doppler self.__nlos_gain = nlos_gain self.__nlos_angles = nlos_angles self.__nlos_phases = nlos_phases self.__nlos_doppler = nlos_doppler
[docs] @classmethod def Realize( cls: Type[PathRealization], power: float, delay: float, los_gain: float, nlos_gain, los_doppler: float, nlos_doppler: float, los_angle: float | None = None, num_sinusoids: int = 20, rng: np.random.Generator | None = None, ) -> PathRealization: """Realize the path's random variables. Args: power (float): Power of the represented path in Watts. delay (float): Delay of the represented path in seconds. los_gain (float): Line of sight power component of the represented path. nlos_gain (_type_): Non line of sight power component of the represented path. los_doppler (float): Line of sight doppler frequency of the represented path. nlos_doppler (float): None line of sight doppler frequencs of the represented path. los_angle (float, optional): Line of sight doppler angle in radians. num_sinusoids (int, optional): Number of sinusoids. :math:`20` by default. rng (np.random.Generator, optional): Random generator used to realize the random variables. Returns: The realized path realization. """ # Initialize a new random generator if none was provided _rng = np.random.default_rng() if rng is None else rng # Draw random realizations for the path los_angle = _rng.uniform(0, 2 * pi) if los_angle is None else los_angle los_phase = _rng.uniform(0, 2 * pi) nlos_angles = _rng.uniform(0, 2 * pi, num_sinusoids) nlos_phases = _rng.uniform(0, 2 * pi, num_sinusoids) # Intialize object from random realizations return cls( power, delay, los_gain, los_angle, los_phase, los_doppler, nlos_gain, nlos_angles, nlos_phases, nlos_doppler, )
@property def power(self) -> float: """Power of the propagation path in Watts. Referred to as :math:`g_{\\ell}` within the respective equations. """ return self.__power @property def delay(self) -> float: """Delay of the propagation path in seconds. Referred to as :math:`\\tau_{\ell}` within the respective equations. """ return self.__delay @property def los_gain(self) -> float: """Gain of the path's specular line of sight component. Represented by .. math:: \\sqrt{\\frac{K_{\ell}}{1 + K_{\ell}}} within the respective equations. """ return self.__los_gain @property def los_angle(self) -> float: """Angle of the path's specular line of sight component in radians. Represented by :math:`\\theta_{\\ell}` within the respective equations. """ return self.__los_angle @property def los_phase(self) -> float: """Phase of the path's specular line of sight component in radians. Represented by :math:`\\phi_{\\ell}` within the respective equations. """ return self.__los_phase @property def los_doppler(self) -> float: """Doppler frequency of the path's specular line of sight component in Hz. Represented by :math:`\\omega_{\\ell}` within the respective equations. """ return self.__los_doppler @property def nlos_gain(self) -> float: """Gain of the path's non-specular components. Represented by .. math:: \\sqrt{\\frac{1}{1 + K_{\ell}}} within the respective equations. """ return self.__nlos_gain @property def nlos_angles(self) -> np.ndarray: """Angles of the path's non-specular components in radians. Represented by the sequence .. math:: \\left[\\theta_{\\ell,1},\\, \\dotsc,\\, \\theta_{\\ell,N} \\right]^{\\mathsf{T}} \\in [0, 2\\pi)^{N} of :math:`N` angles in radians within the respective equations. """ return self.__nlos_angles @property def nlos_phases(self) -> np.ndarray: """Phases of the path's non-specular components in radians. Represented by the sequence .. math:: \\left[\\phi_{\\ell,1},\\, \\dotsc,\\, \\phi_{\\ell,N} \\right]^{\\mathsf{T}} \\in [0, 2\\pi)^{N} of :math:`N` angles in radians within the respective equations. """ return self.__nlos_phases @property def nlos_doppler(self) -> float: """Doppler frequency of the path's non-specular components in Hz. Represented by :math:`\\omega_{\\ell}` within the respective equations. """ return self.__nlos_doppler def _impulse_response(self, timestamps: np.ndarray) -> np.ndarray: """Compute the impulse response of the represented multipath component. Args: timestamps (numpy.ndarray): Timestamps in seconds at which to sample the impulse response. Returns: The sampled impulse response. """ num_sinusoids = len(self.__nlos_angles) # Initialize empty impulse response impulse_response = np.zeros(len(timestamps), dtype=np.complex_) # Sum up and normalize all non-specular components for s, (nlos_angle, nlos_phase) in enumerate(zip(self.nlos_angles, self.nlos_phases)): impulse_response += exp( 1j * ( self.nlos_doppler * timestamps * cos((2 * pi * s + nlos_angle) / num_sinusoids) + nlos_phase ) ) impulse_response *= self.nlos_gain * (num_sinusoids**-0.5) # Add the specular component impulse_response += self.los_gain * exp( 1j * (self.los_doppler * timestamps * cos(self.los_angle) + self.los_phase) ) # Scale by the overall path power impulse_response *= self.power**0.5 return impulse_response
[docs] def propagate(self, signal: Signal) -> np.ndarray: """Propagate a signal along the represented multipath component. Args: signal (Signal): The signal to be propagated. Returns: The propagated samples. """ # Generate the path's impule response impulse_response = self._impulse_response(signal.timestamps) # Propagate the transmitted samples propagated_samples = signal.samples * impulse_response[np.newaxis, :] return propagated_samples
def to_HDF(self, group: Group) -> None: group.attrs["power"] = self.__power group.attrs["delay"] = self.__delay group.attrs["los_gain"] = self.__los_gain group.attrs["los_angle"] = self.__los_angle group.attrs["los_phase"] = self.__los_phase group.attrs["los_doppler"] = self.los_doppler group.attrs["nlos_gain"] = self.__nlos_gain group.attrs["nlos_doppler"] = self.nlos_doppler HDFSerializable._write_dataset(group, "nlos_angles", self.__nlos_angles) HDFSerializable._write_dataset(group, "nlos_phases", self.__nlos_phases) @classmethod def from_HDF(cls: Type[PathRealization], group: Group) -> PathRealization: power = group.attrs["power"] delay = group.attrs["delay"] los_gain = group.attrs["los_gain"] los_angle = group.attrs["los_angle"] los_phase = group.attrs["los_phase"] los_doppler = group.attrs["los_doppler"] nlos_gain = group.attrs["nlos_gain"] nlos_doppler = group.attrs["nlos_doppler"] nlos_angles = np.array(group["nlos_angles"], dtype=np.float_) nlos_phases = np.array(group["nlos_phases"], dtype=np.float_) return cls( power, delay, los_gain, los_angle, los_phase, los_doppler, nlos_gain, nlos_angles, nlos_phases, nlos_doppler, )
[docs] class MultipathFadingRealization(ChannelRealization, Visualizable): """Realization of a multipath fading channel. Generated by the :meth:`realize()<MultipathFadingChannel.realize>` routine of :class:`MultipathFadingChannels<MultipathFadingChannel>`. """ __path_realizations: Sequence[PathRealization] __spatial_response: np.ndarray __max_delay: float def __init__( self, alpha_device: Device, beta_device: Device, gain: float, path_realizations: Sequence[PathRealization], spatial_response: np.ndarray, max_delay: float, interpolation_mode: InterpolationMode = InterpolationMode.NEAREST, ) -> None: """ Args: alpha_device (Device): First device linked by the :class:`.MultipathFadingChannel` instance that generated this realization. beta_device (Device): Second device linked by the :class:`.MultipathFadingChannel` instance that generated this realization. gain (float): Linear power gain factor a signal experiences when being propagated over this realization. path_realizations (Sequence[PathRealization]): Realizations of the individual propagation paths. spatial_response (numpy.ndarray): Spatial response matrix of the channel realization considering `alpha_device` is the transmitter and `beta_device` is the receiver. interpolation_mode (InterpolationMode, optional): Interpolation behaviour of the channel realization's delay components with respect to the proagated signal's sampling rate. """ # Initialize base class ChannelRealization.__init__(self, alpha_device, beta_device, gain, interpolation_mode) Visualizable.__init__(self) # Initialize class attributes self.__path_realizations = path_realizations self.__spatial_response = spatial_response # Infer additional parameters self.__max_delay = ( max(path.delay for path in path_realizations) if max_delay is None else max_delay )
[docs] @classmethod def Realize( cls: Type[MultipathFadingRealization], alpha_device: SimulatedDevice, beta_device: SimulatedDevice, gain: float, power_profile: np.ndarray, delays: np.ndarray, los_gains: np.ndarray, nlos_gains: np.ndarray, los_doppler: float, nlos_doppler: float, alpha_correlation: AntennaCorrelation | None = None, beta_correlation: AntennaCorrelation | None = None, los_angle: float | None = None, num_sinusoids: int = 20, rng: np.random.Generator | None = None, ) -> MultipathFadingRealization: """Realize the random variables of a multipath fading channel. Args: alpha_device (SimulatedDevice): First device linked by the channel. beta_device (SimulatedDevice): Second device linked by the channel. gain (float): Overall channel gain factor. power_profile (numpy.ndarray): Powers of each propagation path. delays (numpy.ndarray): Delays of each propgation path. los_gains (numpy.ndarray): Line of sight powers of each propagation path. nlos_gains (numpy.ndarray): Non line lof sight powers of each proapgation path. los_doppler (float): Line of sight doppler frequency of each propagation path. nlos_doppler (float): Non line of sight dopller frequency of each propagation path. alpha_correlation (AntennaCorrelation, optional): Antenna correlations at `alpha_device`. beta_correlation (AntennaCorrelation, optional): Antennna correlations at `beta_device`. los_angle (float, optional): Line of sight doppler angle in radians. num_sinusoids (int, optional): Number of model sinusoids. Defaults to 20. rng (numpy.random.Generator, optional): Random generator used to realize the random variables. Returns: The realized realization. """ # Initialize a new random generator if none was provided _rng = np.random.default_rng() if rng is None else rng # Generate MIMO channel response spatial_response = np.exp( 1j * _rng.uniform(0, 2 * pi, (beta_device.num_antennas, alpha_device.num_antennas)) ) # Apply antenna array correlation models if alpha_correlation is not None: spatial_response = spatial_response @ alpha_correlation.covariance if beta_correlation is not None: spatial_response = beta_correlation.covariance @ spatial_response # Generate path realizations path_realizations: List[PathRealization] = [] for power, delay, los_gain, nlos_gain in zip(power_profile, delays, los_gains, nlos_gains): path_realizations.append( PathRealization.Realize( power, delay, los_gain, nlos_gain, los_doppler, nlos_doppler, los_angle, num_sinusoids, _rng, ) ) max_delay = delays.max() return cls(alpha_device, beta_device, gain, path_realizations, spatial_response, max_delay)
@property def path_realizations(self) -> Sequence[PathRealization]: """Realizations of the individual propagation paths.""" return self.__path_realizations def __directive_spatial_response(self, transmitter: Device, receiver: Device) -> np.ndarray: """Infer the spatial response for the given transmitter and receiver. Subroutine of :meth:`state<MultipathFadingRealization.state>` and :meth:`_propagate<MultipathFadingRealization.propagate>`. Args: transmitter (Device): The transmitter device. receiver (Device): The receiver device. Returns: The spatial channel response matrix. Raises: ValueError: If the provided transmitter and receiver do not match the devices the channel was realized for. """ if transmitter == self.alpha_device and receiver == self.beta_device: return self.__spatial_response if transmitter == self.beta_device and receiver == self.alpha_device: return self.__spatial_response.T raise ValueError( "The provided transmitter and receiver do not match the devices the channel was realized for" )
[docs] def state( self, transmitter: Device, receiver: Device, delay: float, sampling_rate: float, num_samples: int, max_num_taps: int, ) -> ChannelStateInformation: spatial_response = self.__directive_spatial_response(transmitter, receiver) num_taps = min(1 + int(self.__max_delay * sampling_rate), max_num_taps) timestamps = np.arange(num_samples) / sampling_rate + delay siso_csi = np.zeros((num_samples, num_taps), dtype=np.complex_) for path_realization in self.path_realizations: tap_index = int(path_realization.delay * sampling_rate) # Skip paths with delays larger than the maximum delay required by the CSI request if tap_index > num_taps: continue siso_csi[:, tap_index] = siso_csi[:, tap_index] + path_realization._impulse_response( timestamps ) # For the multipath fading model, the MIMO CSI is the outer product of the SISO CSI with the spatial response # The resulting multidimensional array is sparse in its fourth dimension and converted to a GCXS array for memory efficiency mimo_csi = GCXS.from_numpy( np.einsum("ij,kl->ijkl", spatial_response * self.gain**0.5, siso_csi), compressed_axes=(0, 1, 2), ) state = ChannelStateInformation( ChannelStateFormat.IMPULSE_RESPONSE, mimo_csi, num_delay_taps=num_taps ) return state
def _propagate( self, signal: Signal, transmitter: Device, receiver: Device, interpolation: InterpolationMode, ) -> Signal: # Infer propagation direction and transmpose spatial response if necessary spatial_response = self.__directive_spatial_response(transmitter, receiver) sampling_rate = signal.sampling_rate max_delay_in_samples = int(self.__max_delay * sampling_rate) num_transmitted_samples = signal.num_samples + max_delay_in_samples # Propagate the transmitted samples propagated_samples = np.zeros( (spatial_response.shape[0], num_transmitted_samples), dtype=np.complex_ ) for path_realization in self.path_realizations: num_delay_samples = int(path_realization.delay * sampling_rate) propagated_samples[ :, num_delay_samples : num_delay_samples + signal.num_samples ] += path_realization.propagate(signal) # Apply the channel's spatial response propagated_samples = spatial_response @ propagated_samples # Return the result propagated_signal = Signal( propagated_samples, sampling_rate, carrier_frequency=signal.carrier_frequency, delay=signal.delay, noise_power=signal.noise_power, ) return propagated_signal def _plot(self, axes: VAT) -> None: ax: plt.Axes = axes.flat[0] delays = np.array([path.delay for path in self.path_realizations]) powers = np.array([path.power for path in self.path_realizations]) ax.stem(delays, powers) ax.set_xlabel("Delay [s]") ax.set_ylabel("Power [Watts]") ax.set_yscale("log")
[docs] def to_HDF(self, group: Group) -> None: ChannelRealization.to_HDF(self, group) group.attrs["num_path_realizations"] = len(self.__path_realizations) group.attrs["max_delay"] = self.__max_delay HDFSerializable._write_dataset(group, "spatial_response", self.__spatial_response) for r, path_realization in enumerate(self.__path_realizations): path_realization.to_HDF( HDFSerializable._create_group(group, f"path_realization_{r:02d}") )
[docs] @classmethod def From_HDF( cls: Type[MultipathFadingRealization], group: Group, alpha_device: Device, beta_device: Device, ) -> MultipathFadingRealization: initialization_parameters = cls._parameters_from_HDF(group) num_path_realizations = group.attrs["num_path_realizations"] initialization_parameters["max_delay"] = group.attrs["max_delay"] initialization_parameters["path_realizations"] = [ PathRealization.from_HDF(group[f"path_realization_{r:02d}"]) for r in range(num_path_realizations) ] initialization_parameters["spatial_response"] = np.array( group["spatial_response"], dtype=np.complex_ ) return cls(alpha_device, beta_device, **initialization_parameters)
[docs] class MultipathFadingChannel(Channel[MultipathFadingRealization], Serializable): """Base class for the implementation of stochastic multipath fading channels. Allows for the direct configuration of the Multipath Fading Channel's parameters .. math:: \\mathbf{g} &= \\left[ g_{1}, g_{2}, \\,\\dotsc,\\, g_{L} \\right]^\mathsf{T} \\in \\mathbb{C}^{L} \\\\ \\mathbf{k} &= \\left[ K_{1}, K_{2}, \\,\\dotsc,\\, K_{L} \\right]^\mathsf{T} \\in \\mathbb{R}^{L} \\\\ \\mathbf{\\tau} &= \\left[ \\tau_{1}, \\tau_{2}, \\,\\dotsc,\\, \\tau_{L} \\right]^\mathsf{T} \\in \\mathbb{R}^{L} \\\\ directly. Refer to :doc:`/api/channel.multipath_fading_channel` for a detailed description of the channel model. The following minimal example outlines how to configure the channel model within the context of a :doc:`simulation.simulation.Simulation`: .. literalinclude:: ../scripts/examples/channel_MultipathFadingChannel.py :language: python :linenos: :lines: 12-40 """ yaml_tag = "MultipathFading" __delays: np.ndarray __power_profile: np.ndarray __rice_factors: np.ndarray __max_delay: float __num_resolvable_paths: int __num_sinusoids: int __los_angle: float | None __los_gains: np.ndarray __doppler_frequency: float __los_doppler_frequency: float | None __alpha_correlation: AntennaCorrelation | None __beta_correlation: AntennaCorrelation | None def __init__( self, delays: np.ndarray | List[float], power_profile: np.ndarray | List[float], rice_factors: np.ndarray | List[float], alpha_device: SimulatedDevice | None = None, beta_device: SimulatedDevice | None = None, gain: float = 1.0, num_sinusoids: int | None = None, los_angle: float | None = None, doppler_frequency: float | None = None, los_doppler_frequency: float | None = None, alpha_correlation: AntennaCorrelation | None = None, beta_correlation: AntennaCorrelation | None = None, **kwargs: Any, ) -> None: """ Args: delays (numpy.ndarray): Delay in seconds of each individual multipath tap. Denoted by :math:`\\tau_{\\ell}` within the respective equations. power_profile (numpy.ndarray): Power loss factor of each individual multipath tap. Denoted by :math:`g_{\\ell}` within the respective equations. rice_factors (numpy.ndarray): Rice factor balancing line of sight and multipath in each individual channel tap. Denoted by :math:`K_{\\ell}` within the respective equations. alpha_device (Device, optional): First device linked by the :class:`.MultipathFadingChannel` instance that generated this realization. beta_device (Device, otional): Second device linked by the :class:`.MultipathFadingChannel` instance that generated this realization. gain (float, optional): Linear power gain factor a signal experiences when being propagated over this realization. :math:`1.0` by default. num_sinusoids (int, optional): Number of sinusoids used to sample the statistical distribution. Denoted by :math:`N` within the respective equations. los_angle (float, optional): Angle phase of the line of sight component within the statistical distribution. doppler_frequency (float, optional): Doppler frequency shift of the statistical distribution. Denoted by :math:`\\omega_{\\ell}` within the respective equations. alpha_correlation(AntennaCorrelation, optional): Antenna correlation model at the first device. By default, the channel assumes ideal correlation, i.e. no cross correlations. beta_correlation(AntennaCorrelation, optional): Antenna correlation model at the second device. By default, the channel assumes ideal correlation, i.e. no cross correlations. **kwargs (Any, optional): Channel base class initialization parameters. Raises: ValueError: If the length of `delays`, `power_profile` and `rice_factors` is not identical. ValueError: If delays are smaller than zero. ValueError: If power factors are smaller than zero. ValueError: If rice factors are smaller than zero. """ # Convert delays, power profile and rice factors to numpy arrays if they were provided as lists self.__delays = np.array(delays) if isinstance(delays, list) else delays self.__power_profile = ( np.array(power_profile) if isinstance(power_profile, list) else power_profile ) self.__rice_factors = ( np.array(rice_factors) if isinstance(rice_factors, list) else rice_factors ) if ( self.__delays.ndim != 1 or self.__power_profile.ndim != 1 or self.__rice_factors.ndim != 1 ): raise ValueError("Delays, power profile and rice factors must be vectors") if len(delays) < 1: raise ValueError("Configuration must contain at least one delay tap") if len(delays) != len(power_profile) or len(power_profile) != len(rice_factors): raise ValueError( "Delays, power profile and rice factor vectors must be of equal length" ) if np.any(self.__delays < 0.0): raise ValueError("Delays must be greater or equal to zero") if np.any(self.__power_profile < 0.0): raise ValueError("Power profile factors must be greater or equal to zero") if np.any(self.__rice_factors < 0.0): raise ValueError("Rice factors must be greater or equal to zero") # Sort delays sorting = np.argsort(delays) self.__delays = self.__delays[sorting] self.__power_profile = self.__power_profile[sorting] self.__rice_factors = self.__rice_factors[sorting] self.__num_sinusoids = 20 if num_sinusoids is None else num_sinusoids self.los_angle = los_angle self.doppler_frequency = 0.0 if doppler_frequency is None else doppler_frequency self.__los_doppler_frequency = None self.alpha_correlation = None self.beta_correlation = None if los_doppler_frequency is not None: self.los_doppler_frequency = los_doppler_frequency # Infer additional parameters self.__max_delay = max(self.__delays) self.__num_resolvable_paths = len(self.__delays) rice_inf_pos = np.isposinf(self.__rice_factors) rice_num_pos = np.invert(rice_inf_pos) self.__los_gains = np.empty(self.num_resolvable_paths, dtype=float) self.__non_los_gains = np.empty(self.num_resolvable_paths, dtype=float) self.__los_gains[rice_inf_pos] = 1.0 self.__los_gains[rice_num_pos] = np.sqrt( self.__rice_factors[rice_num_pos] / (1 + self.__rice_factors[rice_num_pos]) ) self.__non_los_gains[rice_num_pos] = 1 / np.sqrt(1 + self.__rice_factors[rice_num_pos]) self.__non_los_gains[rice_inf_pos] = 0.0 # Initialize base class Channel.__init__(self, alpha_device, beta_device, gain, **kwargs) # Update correlations (required here to break dependency cycle during init) self.alpha_correlation = alpha_correlation self.beta_correlation = beta_correlation @property def delays(self) -> np.ndarray: """Delays for each propagation path in seconds. Represented by the sequence .. math:: \\left[\\tau_{1},\\, \\dotsc,\\, \\tau_{L} \\right]^{\\mathsf{T}} \\in \\mathbb{R}_{+}^{L} of :math:`L` propagtion delays within the respective equations. """ return self.__delays @property def power_profile(self) -> np.ndarray: """Gain factors of each propagation path. Represented by the sequence .. math:: \\left[g_{1},\\, \\dotsc,\\, g_{L} \\right]^{\\mathsf{T}} \\in \\mathbb{R}_{+}^{L} of :math:`L` propagtion factors within the respective equations. """ return self.__power_profile @property def rice_factors(self) -> np.ndarray: """Rice factors balancing line of sight and non-line of sight power components for each propagation path. Represented by the sequence .. math:: \\left[K_{1},\\, \\dotsc,\\, K_{L} \\right]^{\\mathsf{T}} \\in \\mathbb{R}_{+}^{L} of :math:`L` factors within the respective equations. """ return self.__rice_factors @property def doppler_frequency(self) -> float: """Doppler frequency in :math:`Hz`. Represented by :math:`\\omega` within the respective equations. """ return self.__doppler_frequency @doppler_frequency.setter def doppler_frequency(self, frequency: float) -> None: self.__doppler_frequency = frequency @property def los_doppler_frequency(self) -> float: """Line of sight Doppler frequency in :math:`Hz`. Represented by :math:`\\omega` within the respective equations. """ if self.__los_doppler_frequency is None: return self.doppler_frequency return self.__los_doppler_frequency @los_doppler_frequency.setter def los_doppler_frequency(self, frequency: float | None) -> None: self.__los_doppler_frequency = frequency @property def max_delay(self) -> float: """Maximum propagation delay in seconds.""" return self.__max_delay @property def num_resolvable_paths(self) -> int: """Number of dedicated propagation paths. Represented by :math:`L` within the respective equations. """ return self.__num_resolvable_paths @property def num_sinusoids(self) -> int: """Number of sinusoids assumed to model the fading in time-domain. Represented by :math:`N` within the respective equations. Raises: ValueError: For values smaller than zero. """ return self.__num_sinusoids @num_sinusoids.setter def num_sinusoids(self, num: int) -> None: if num < 0: raise ValueError("Number of sinusoids must be greater or equal to zero") self.__num_sinusoids = num @property def los_angle(self) -> float | None: """Line of sight doppler angle in radians. Represented by :math:`\\theta_{0}` within the respective equations. """ return self.__los_angle @los_angle.setter def los_angle(self, angle: float | None) -> None: self.__los_angle = angle def _realize(self) -> MultipathFadingRealization: return MultipathFadingRealization.Realize( self.alpha_device, self.beta_device, self.gain, self.__power_profile, self.__delays, self.__los_gains, self.__non_los_gains, self.los_doppler_frequency, self.doppler_frequency, self.alpha_correlation, self.beta_correlation, self.los_angle, self.__num_sinusoids, self._rng, ) @property def alpha_correlation(self) -> AntennaCorrelation | None: """Antenna correlation at the first device. Returns: Handle to the correlation model. :py:obj:`None`, if no model was configured and ideal correlation is assumed. """ return self.__alpha_correlation @alpha_correlation.setter def alpha_correlation(self, value: AntennaCorrelation | None) -> None: if value is not None: value.channel = self value.device = self.alpha_device self.__alpha_correlation = value @property def beta_correlation(self) -> AntennaCorrelation | None: """Antenna correlation at the second device. Returns: Handle to the correlation model. :py:obj:`None`, if no model was configured and ideal correlation is assumed. """ return self.__beta_correlation @beta_correlation.setter def beta_correlation(self, value: AntennaCorrelation | None) -> None: if value is not None: value.channel = self value.device = self.beta_device self.__beta_correlation = value @Channel.alpha_device.setter # type: ignore def alpha_device(self, value: SimulatedDevice) -> None: Channel.alpha_device.fset(self, value) # type: ignore # Register new device at correlation model if self.alpha_correlation is not None: self.alpha_correlation.device = value @Channel.beta_device.setter # type: ignore def beta_device(self, value: SimulatedDevice) -> None: Channel.beta_device.fset(self, value) # type: ignore # Register new device at correlation model if self.beta_correlation is not None: self.beta_correlation.device = value
[docs] def recall_realization(self, group: Group) -> MultipathFadingRealization: return MultipathFadingRealization.From_HDF(group, self.alpha_device, self.beta_device)