Source code for hermespy.channel.fading.fading

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

from __future__ import annotations
from abc import ABC
from typing import Any, Generator, Set, Tuple, 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 (
    AntennaArrayState,
    AntennaMode,
    ChannelStateInformation,
    ChannelStateFormat,
    HDFSerializable,
    Serializable,
    SignalBlock,
    VAT,
)
from ..channel import (
    Channel,
    ChannelSample,
    LinkState,
    ChannelSampleHook,
    ChannelRealization,
    InterpolationMode,
)
from ..consistent import ConsistentUniform, ConsistentGenerator, ConsistentRealization

__author__ = "Andre Noll Barreto"
__copyright__ = "Copyright 2024, Barkhausen Institut gGmbH"
__credits__ = ["Andre Noll Barreto", "Tobias Kronauer", "Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.3.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 def __init__(self, channel: Channel | None = None) -> None: """ Args: channel (Channel, optional): Channel this correlation model configures. `None` if the model is currently considered floating. """ self.channel = channel
[docs] def sample_covariance(self, antennas: AntennaArrayState, mode: AntennaMode) -> np.ndarray: """Sample the covariance matrix of a given antenna array. Args: antennas (AntennaArrayState): State of the antenna array. mode (AntennaMode): Mode of the antenna array, i.e. transmit or receive. 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
[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
[docs] def sample_covariance(self, antennas: AntennaArrayState, mode: AntennaMode) -> np.ndarray: num_antennas = ( antennas.num_transmit_antennas if mode == AntennaMode.TX else antennas.num_receive_antennas ) if self.__covariance_matrix.shape[0] < num_antennas: raise ValueError("Antenna correlation matrix does not match the number of antennas") return self.__covariance_matrix[:num_antennas, :num_antennas]
@property def covariance(self) -> np.ndarray: """Postive definte square antenna covariance matrix.""" 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 MultipathFadingSample(ChannelSample): """Immutable sample of a statistical multipath fading channel. Generated by the :meth:`sample()<MultipathFadingRealization.sample>` routine of a :class:`MultipathFadingRealization<MultipathFadingRealization>`. """ __spatial_response: np.ndarray __max_delay: float def __init__( self, power_profile: np.ndarray, delay_profile: np.ndarray, los_angles: np.ndarray, nlos_angles: np.ndarray, los_phases: np.ndarray, nlos_phases: np.ndarray, los_gains: np.ndarray, nlos_gains: np.ndarray, los_doppler: float, nlos_doppler: float, spatial_response: np.ndarray, gain: float, state: LinkState, ) -> None: """ Args: transmitter_state (DeviceState): State of the transmitting device at the time of sampling. receiver_state (DeviceState): State of the receiving device at the time of sampling. carrier_frequency (float): Carrier frequency of the channel in Hz. bandwidth (float): Bandwidth of the propagated signal in Hz. 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 ChannelSample.__init__(self, state) # Initialize class attributes self.__power_profile = power_profile self.__delay_profile = delay_profile self.__los_angles = los_angles self.__nlos_angles = nlos_angles self.__los_phases = los_phases self.__nlos_phases = nlos_phases self.__los_gains = los_gains self.__nlos_gains = nlos_gains self.__los_doppler = los_doppler self.__nlos_doppler = nlos_doppler self.__spatial_response = spatial_response self.__gain = gain # Infer additional parameters self.__max_delay = self.__delay_profile.max() @property def power_profile(self) -> np.ndarray: """Power loss factor of each individual multipath tap.""" return self.__power_profile @property def delay_profile(self) -> np.ndarray: """Delay in seconds of each individual multipath tap.""" return self.__delay_profile @property def los_angles(self) -> np.ndarray: """Angle of arrival of the line-of sight components.""" return self.__los_angles @property def nlos_angles(self) -> np.ndarray: """Angle of arrival of the non-line-of sight components.""" return self.__nlos_angles @property def los_phases(self) -> np.ndarray: """Phase of the line-of sight components.""" return self.__los_phases @property def nlos_phases(self) -> np.ndarray: """Phase of the non-line-of sight components.""" return self.__nlos_phases @property def los_gains(self) -> np.ndarray: """Gain factors of the line-of sight components.""" return self.__los_gains @property def nlos_gains(self) -> np.ndarray: """Gain factors of the non-line-of sight components.""" return self.__nlos_gains @property def los_doppler(self) -> float: """Doppler frequency shift of the line-of sight components.""" return self.__los_doppler @property def nlos_doppler(self) -> float: """Doppler frequency shift of the non-line-of sight components.""" return self.__nlos_doppler @property def spatial_response(self) -> np.ndarray: """Spatial response matrix of the channel realization considering `alpha_device` is the transmitter and `beta_device` is the receiver.""" return self.__spatial_response @property def gain(self) -> float: """Linear energy gain factor a signal experiences when being propagated over this realization.""" return self.__gain @property def expected_energy_scale(self) -> float: return self.gain * np.sum(self.power_profile) def __path_impulse_generator( self, num_samples: int ) -> Generator[Tuple[np.ndarray, int], None, None]: path_delay_samples = np.rint(self.delay_profile * self.bandwidth).astype(int) num_sinusoids = self.__nlos_angles.shape[1] n = 1 + np.arange(num_sinusoids) timestamps = np.arange(num_samples) / self.bandwidth nlos_time = self.nlos_doppler * timestamps los_time = self.los_doppler * timestamps for ( delay, power, los_gain, nlos_gain, los_angle, nlos_angles, los_phase, nlos_phases, ) in zip( path_delay_samples, self.power_profile, self.los_gains, self.nlos_gains, self.los_angles, self.nlos_angles, self.los_phases, self.nlos_phases, ): impulse = nlos_gain * np.sum( np.exp( 1j * ( np.outer(nlos_time, np.cos((2 * pi * n + nlos_angles) / num_sinusoids)) + nlos_phases[None, :] ) ), axis=1, keepdims=False, ) # Add the specular component impulse += los_gain * exp(1j * (los_time * cos(los_angle) + los_phase)) # Scale by the overall path power impulse *= (self.gain * power) ** 0.5 yield impulse, delay
[docs] def state( self, num_samples: int, max_num_taps: int, interpolation_mode: InterpolationMode = InterpolationMode.NEAREST, ) -> ChannelStateInformation: num_taps = min(1 + round(self.__max_delay * self.bandwidth), max_num_taps) siso_csi = np.zeros((num_samples, num_taps), dtype=np.complex_) for path_impulse, path_delay_samples in self.__path_impulse_generator(num_samples): # Skip paths with delays larger than the maximum delay required by the CSI request if path_delay_samples > num_taps: continue siso_csi[:, path_delay_samples] += path_impulse # 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", self.spatial_response, 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: SignalBlock, interpolation: InterpolationMode) -> SignalBlock: max_delay_in_samples = round(self.__max_delay * self.bandwidth) num_propagated_samples = signal.num_samples + max_delay_in_samples # Propagate the transmitted samples propagated_samples = np.zeros( (self.spatial_response.shape[0], num_propagated_samples), dtype=np.complex_ ) for path_impulse, path_num_delay_samples in self.__path_impulse_generator( signal.num_samples ): propagated_samples[ :, path_num_delay_samples : path_num_delay_samples + signal.num_samples ] += (signal * path_impulse[np.newaxis, :]) # Apply the channel's spatial response propagated_samples = self.spatial_response @ propagated_samples # Return the result propagated_block = SignalBlock(propagated_samples, signal.offset) return propagated_block
[docs] def plot_power_delay(self, axes: VAT | None = None) -> Tuple[plt.Figure, VAT]: if axes: _axes = axes figure = axes[0, 0].get_figure() else: figure, _axes = plt.subplots(1, 1, squeeze=False) figure.suptitle("Power Delay Profile") ax: plt.Axes = _axes.flat[0] ax.stem(self.__delay_profile, self.__power_profile) ax.set_xlabel("Delay [s]") ax.set_ylabel("Power [Watts]") ax.set_yscale("log") return figure, _axes
[docs] class MultipathFadingRealization(ChannelRealization[MultipathFadingSample]): """Realization of a statistical multipath fading channel. Generated by the :meth:`realize()<MultipathFadingChannel.realize>` routine of a :class:`MultipathFadingChannel<MultipathFadingChannel>`. """ def __init__( self, random_realization: ConsistentRealization, antenna_correlation_variable: ConsistentUniform, los_angles_variable: float | ConsistentUniform, nlos_angles_variable: ConsistentUniform, los_phases_variable: ConsistentUniform, nlos_phases_variable: ConsistentUniform, power_profile: np.ndarray, delay_profile: np.ndarray, los_gains: np.ndarray, nlos_gains: np.ndarray, los_doppler: float, nlos_doppler: float, antenna_correlation: AntennaCorrelation | None, sample_hooks: Set[ChannelSampleHook[MultipathFadingSample]], gain: float, ) -> None: # Initialize base class ChannelRealization.__init__(self, sample_hooks, gain) # Initialize class attributes self.__random_realization = random_realization self.__antenna_correlation_variable = antenna_correlation_variable self.__los_angles_variable = los_angles_variable self.__path_angles_variable = nlos_angles_variable self.__los_phases_variable = los_phases_variable self.__path_phases_variable = nlos_phases_variable self.__power_profile = power_profile self.__delay_profile = delay_profile self.__los_gains = los_gains self.__nlos_gains = nlos_gains self.__los_doppler = los_doppler self.__nlos_doppler = nlos_doppler self.__antenna_correlation = antenna_correlation def _sample(self, state: LinkState) -> MultipathFadingSample: # Sample the spatiall consistent random realization consistent_sample = self.__random_realization.sample( state.transmitter.pose.translation, state.receiver.pose.translation ) # Generate MIMO channel response spatial_response = np.exp( 2j * np.pi * self.__antenna_correlation_variable.sample(consistent_sample) )[ : state.receiver.antennas.num_receive_antennas, : state.transmitter.antennas.num_transmit_antennas, ] if self.__antenna_correlation is not None: spatial_response = ( self.__antenna_correlation.sample_covariance( state.receiver.antennas, AntennaMode.RX )
[docs] @ spatial_response @ self.__antenna_correlation.sample_covariance( state.transmitter.antennas, AntennaMode.TX ) ) # Sample multipath components los_angles = ( self.__los_angles_variable * np.ones_like(self.__power_profile) if isinstance(self.__los_angles_variable, float) else 2 * np.pi * self.__los_angles_variable.sample(consistent_sample) ) nlos_angles = -np.pi + 2 * np.pi * self.__path_angles_variable.sample(consistent_sample) los_phases = -np.pi + 2 * np.pi * self.__los_phases_variable.sample(consistent_sample) nlos_phases = -np.pi + 2 * np.pi * self.__path_phases_variable.sample(consistent_sample) return MultipathFadingSample( self.__power_profile, self.__delay_profile, los_angles, nlos_angles, los_phases, nlos_phases, self.__los_gains, self.__nlos_gains, self.__los_doppler, self.__nlos_doppler, spatial_response, self.gain, state, ) def to_HDF(self, group: Group) -> None: # Serialize class attributes self.__random_realization.to_HDF(group.create_group("random_realization")) HDFSerializable._write_dataset(group, "power_profile", self.__power_profile) HDFSerializable._write_dataset(group, "delay_profile", self.__delay_profile) HDFSerializable._write_dataset(group, "los_gains", self.__los_gains) HDFSerializable._write_dataset(group, "nlos_gains", self.__nlos_gains) group.attrs["los_doppler"] = self.__los_doppler group.attrs["nlos_doppler"] = self.__nlos_doppler group.attrs["gain"] = self.gain
[docs] @staticmethod def From_HDF( group: Group, random_realization: ConsistentRealization, antenna_correlation_variable: ConsistentUniform, los_angles_variable: float | ConsistentUniform, nlos_angles_variable: ConsistentUniform, los_phases_variable: ConsistentUniform, nlos_phases_variable: ConsistentUniform, antenna_correlation: AntennaCorrelation | None, sample_hooks: Set[ChannelSampleHook[MultipathFadingSample]], ) -> MultipathFadingRealization: # Deserialize base class random_realization = ConsistentRealization.from_HDF(group["random_realization"]) power_profile = np.array(group["power_profile"], dtype=np.float_) delay_profile = np.array(group["delay_profile"], dtype=np.float_) los_gains = np.array(group["los_gains"], dtype=np.float_) nlos_gains = np.array(group["nlos_gains"], dtype=np.float_) los_doppler = group.attrs["los_doppler"] nlos_doppler = group.attrs["nlos_doppler"] gain = group.attrs["gain"] return MultipathFadingRealization( random_realization, antenna_correlation_variable, los_angles_variable, nlos_angles_variable, los_phases_variable, nlos_phases_variable, power_profile, delay_profile, los_gains, nlos_gains, los_doppler, nlos_doppler, antenna_correlation, sample_hooks, gain, )
def _reciprocal_sample( self, sample: MultipathFadingSample, state: LinkState ) -> MultipathFadingSample: return MultipathFadingSample( sample.power_profile, sample.delay_profile, sample.los_angles, sample.nlos_angles, sample.los_phases, sample.nlos_phases, sample.los_gains, sample.nlos_gains, sample.los_doppler, sample.nlos_doppler, sample.spatial_response.T, sample.gain, state, )
[docs] class MultipathFadingChannel( Channel[MultipathFadingRealization, MultipathFadingSample], Serializable ): """Base class for the implementation of stochastic multipath fading channels.""" 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 __antenna_correlation: AntennaCorrelation | None def __init__( self, delays: np.ndarray | List[float], power_profile: np.ndarray | List[float], rice_factors: np.ndarray | List[float], correlation_distance: float = float("inf"), num_sinusoids: int | None = None, los_angle: float | None = None, doppler_frequency: float | None = None, los_doppler_frequency: float | None = None, antenna_correlation: AntennaCorrelation | None = None, gain: float = 1.0, **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. correlation_distance (float, optional): Distance at which channel samples are considered to be uncorrelated. :math:`\\infty` by default, i.e. the channel is considered to be fully correlated in space. 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. antenna_correlation (AntennaCorrelation, optional): Antenna correlation model. By default, the channel assumes ideal correlation, i.e. no cross correlations. gain (float, optional): Linear power gain factor a signal experiences when being propagated over this realization. :math:`1.0` by default. \**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") # Initialize base class self.__antenna_correlation = None Channel.__init__(self, gain=gain, **kwargs) # 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 = self._rng.uniform(-pi, pi) if los_angle is None else los_angle self.doppler_frequency = 0.0 if doppler_frequency is None else doppler_frequency self.__los_doppler_frequency = 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] = np.sqrt( 1 / ((1 + self.__rice_factors[rice_num_pos]) * self.__num_sinusoids) ) self.__non_los_gains[rice_inf_pos] = 0.0 # Update correlations (required here to break dependency cycle during init) self.antenna_correlation = antenna_correlation self.correlation_distance = correlation_distance self.__rng = ConsistentGenerator(self) # self.__antenna_correlation_variable = self.__rng.uniform((self.beta_device.num_antennas, self.alpha_device.num_antennas)) self.__antenna_correlation_variable = self.__rng.uniform((10, 10)) self.__los_angles_variable = ( self.__rng.uniform((self.__num_resolvable_paths,)) if self.__los_angle is not None else self.__los_angle ) self.__nlos_angles_variable = self.__rng.uniform( (self.num_resolvable_paths, self.__num_sinusoids) ) self.__los_phases_variable = self.__rng.uniform((self.num_resolvable_paths,)) self.__nlos_phases_variable = self.__rng.uniform( (self.num_resolvable_paths, self.__num_sinusoids) ) @property def correlation_distance(self) -> float: """Correlation distance in meters. Represents the distance over which the antenna correlation is assumed to be constant. """ return self.__correlation_distance @correlation_distance.setter def correlation_distance(self, distance: float) -> None: if distance < 0: raise ValueError("Correlation distance must be greater or equal to zero") self.__correlation_distance = distance @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( self.__rng.realize(self.correlation_distance), self.__antenna_correlation_variable, self.__los_angles_variable, self.__nlos_angles_variable, self.__los_phases_variable, self.__nlos_phases_variable, self.__power_profile, self.__delays, self.__los_gains, self.__non_los_gains, self.los_doppler_frequency, self.doppler_frequency, self.antenna_correlation, self.sample_hooks, self.gain, ) @property def antenna_correlation(self) -> AntennaCorrelation | None: """Antenna correlations. Returns: Handle to the correlation model. :py:obj:`None`, if no model was configured and ideal correlation is assumed. """ return self.__antenna_correlation @antenna_correlation.setter def antenna_correlation(self, value: AntennaCorrelation | None) -> None: if value is not None: value.channel = self self.__alpha_correlation = value
[docs] def recall_realization(self, group: Group) -> MultipathFadingRealization: return MultipathFadingRealization.From_HDF( group, self.__rng.realize(self.correlation_distance), self.__antenna_correlation_variable, self.__los_angles_variable, self.__nlos_angles_variable, self.__los_phases_variable, self.__nlos_phases_variable, self.antenna_correlation, self.sample_hooks, )