Source code for hermespy.channel.fading.fading

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

from __future__ import annotations
from typing import Any, Generator, Set, Tuple, List
from typing_extensions import override

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

from hermespy.core import (
    AntennaArrayState,
    AntennaMode,
    ChannelStateInformation,
    ChannelStateFormat,
    DeserializationProcess,
    Serializable,
    SerializationProcess,
    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.5.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"


[docs] class AntennaCorrelation(Serializable): """Base class for statistical modeling of antenna array correlations.""" __channel: Channel | None def __init__(self, channel: Channel | None = None) -> None: """ Args: channel: 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: State of the antenna array. mode: 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(AntennaCorrelation): """Customizable antenna correlations.""" __covariance_matrix: np.ndarray def __init__(self, covariance: np.ndarray) -> None: """ Args: covariance: 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] @override def serialize(self, process: SerializationProcess) -> None: process.serialize_array(self.__covariance_matrix, "covariance_matrix")
[docs] @classmethod @override def Deserialize(cls, process: DeserializationProcess) -> CustomAntennaCorrelation: return CustomAntennaCorrelation( process.deserialize_array("covariance_matrix", np.complex128) )
[docs] class MultipathFadingSample(ChannelSample): """Immutable sample of a statistical multipath fading channel. Generated by the 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: State of the transmitting device at the time of sampling. receiver_state: State of the receiving device at the time of sampling. carrier_frequency: Carrier frequency of the channel in Hz. bandwidth: Bandwidth of the propagated signal in Hz. path_realizations: Realizations of the individual propagation paths. spatial_response: Spatial response matrix of the channel realization considering `alpha_device` is the transmitter and `beta_device` is the receiver. interpolation_mode: 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.complex128) 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.complex128 ) 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 ) @ 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 _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] @override def serialize(self, process: SerializationProcess) -> None: process.serialize_object(self.__random_realization, "random_realization") process.serialize_object( self.__antenna_correlation_variable, "antenna_correlation_variable" ) if isinstance(self.__los_angles_variable, float): process.serialize_floating(self.__los_angles_variable, "los_angles_variable") else: process.serialize_object(self.__los_angles_variable, "los_angles_variable") process.serialize_object(self.__path_angles_variable, "path_angles_variable") process.serialize_object(self.__los_phases_variable, "los_phases_variable") process.serialize_object(self.__path_phases_variable, "path_phases_variable") process.serialize_array(self.__power_profile, "power_profile") process.serialize_array(self.__delay_profile, "delay_profile") process.serialize_array(self.__los_gains, "los_gains") process.serialize_array(self.__nlos_gains, "nlos_gains") process.serialize_floating(self.__los_doppler, "los_doppler") process.serialize_floating(self.__nlos_doppler, "nlos_doppler") if self.__antenna_correlation is not None: process.serialize_object(self.__antenna_correlation, "antenna_correlation") ChannelRealization.serialize(self, process)
[docs] @classmethod @override def Deserialize(cls, process: DeserializationProcess) -> MultipathFadingRealization: return MultipathFadingRealization( process.deserialize_object("random_realization", ConsistentRealization), process.deserialize_object("antenna_correlation_variable", ConsistentUniform), process.deserialize_object("los_angles_variable", ConsistentUniform), process.deserialize_object("path_angles_variable", ConsistentUniform), process.deserialize_object("los_phases_variable", ConsistentUniform), process.deserialize_object("path_phases_variable", ConsistentUniform), process.deserialize_array("power_profile", np.float64), process.deserialize_array("delay_profile", np.float64), process.deserialize_array("los_gains", np.float64), process.deserialize_array("nlos_gains", np.float64), process.deserialize_floating("los_doppler"), process.deserialize_floating("nlos_doppler"), process.deserialize_object("antenna_correlation", AntennaCorrelation, None), set(), **Channel._DeserializeParameters(process), # type: ignore[arg-type] )
[docs] class MultipathFadingChannel( Channel[MultipathFadingRealization, MultipathFadingSample], Serializable ): """Base class for the implementation of stochastic multipath fading channels.""" _DEFAULT_DECORRELATION_DISTANCE = float("inf") _DEFAULT_NUM_SINUSOIDS = 20 _DEFAULT_DOPPLER_FREQUENCY = 0.0 __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 = _DEFAULT_DECORRELATION_DISTANCE, num_sinusoids: int = _DEFAULT_NUM_SINUSOIDS, los_angle: float | None = None, doppler_frequency: float = _DEFAULT_DOPPLER_FREQUENCY, los_doppler_frequency: float | None = None, antenna_correlation: AntennaCorrelation | None = None, gain: float = Channel._DEFAULT_GAIN, seed: int | None = None, ) -> None: """ Args: delays: Delay in seconds of each individual multipath tap. Denoted by :math:`\\tau_{\\ell}` within the respective equations. power_profile: Power loss factor of each individual multipath tap. Denoted by :math:`g_{\\ell}` within the respective equations. rice_factors: Rice factor balancing line of sight and multipath in each individual channel tap. Denoted by :math:`K_{\\ell}` within the respective equations. correlation_distance: 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: Number of sinusoids used to sample the statistical distribution. Denoted by :math:`N` within the respective equations. los_angle: Angle phase of the line of sight component within the statistical distribution. doppler_frequency: Doppler frequency shift of the statistical distribution. Denoted by :math:`\\omega_{\\ell}` within the respective equations. antenna_correlation: Antenna correlation model. By default, the channel assumes ideal correlation, i.e. no cross correlations. gain: Linear power gain factor a signal experiences when being propagated over this realization. :math:`1.0` by default. seed: Seed used to initialize the pseudo-random number generator. 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, seed) # 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 = num_sinusoids self.los_angle = self._rng.uniform(-pi, pi) if los_angle is None else los_angle self.doppler_frequency = doppler_frequency 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
[docs] 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] @override def serialize(self, process: SerializationProcess) -> None: process.serialize_array(self.__delays, "delays") process.serialize_array(self.__power_profile, "power_profile") process.serialize_array(self.__rice_factors, "rice_factors") process.serialize_integer(self.num_sinusoids, "num_sinusoids") process.serialize_floating(self.__correlation_distance, "correlation_distance") process.serialize_floating(self.__doppler_frequency, "doppler_frequency") process.serialize_floating(self.__los_doppler_frequency, "los_doppler_frequency") if self.antenna_correlation is not None: process.serialize_object(self.antenna_correlation, "antenna_correlation") Channel.serialize(self, process)
@classmethod @override def _DeserializeParameters(cls, process: DeserializationProcess) -> dict[str, Any]: parameters = Channel._DeserializeParameters(process) parameters.update( { "delays": process.deserialize_array("delays", np.float64), "power_profile": process.deserialize_array("power_profile", np.float64), "rice_factors": process.deserialize_array("rice_factors", np.float64), "num_sinusoids": process.deserialize_integer( "num_sinusoids", cls._DEFAULT_NUM_SINUSOIDS ), "correlation_distance": process.deserialize_floating( "correlation_distance", cls._DEFAULT_DECORRELATION_DISTANCE ), "doppler_frequency": process.deserialize_floating( "doppler_frequency", cls._DEFAULT_DOPPLER_FREQUENCY ), "los_doppler_frequency": process.deserialize_floating("los_doppler_frequency"), "antenna_correlation": process.deserialize_object( "antenna_correlation", AntennaCorrelation, None ), } ) return parameters
[docs] @classmethod @override def Deserialize(cls, process: DeserializationProcess) -> MultipathFadingChannel: return MultipathFadingChannel(**cls._DeserializeParameters(process))