# -*- 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))