From 0483322b09f464d648dca180afc38ef7e0066ec9 Mon Sep 17 00:00:00 2001 From: Jan Adler Date: Tue, 28 May 2024 12:52:59 +0000 Subject: [PATCH] Propagation Loss SNR Calculations --- hermespy/channel/cdl/cluster_delay_lines.py | 4 + hermespy/channel/channel.py | 13 ++- hermespy/channel/delay/delay.py | 4 + hermespy/channel/fading/fading.py | 39 ++++--- hermespy/channel/ideal.py | 4 + hermespy/channel/quadriga/quadriga.py | 4 + hermespy/channel/radar/radar.py | 4 + hermespy/modem/waveform_single_carrier.py | 1 - hermespy/simulation/noise/level.py | 32 +++++- tests/integration_tests/test_links.py | 45 ++++---- tests/unit_tests/channel/test_cdl.py | 20 ++++ tests/unit_tests/channel/test_fading.py | 102 ++++++++++++++---- .../unit_tests/simulation/noise/test_level.py | 33 +++++- 13 files changed, 238 insertions(+), 67 deletions(-) diff --git a/hermespy/channel/cdl/cluster_delay_lines.py b/hermespy/channel/cdl/cluster_delay_lines.py index 9af37110..f36a393d 100644 --- a/hermespy/channel/cdl/cluster_delay_lines.py +++ b/hermespy/channel/cdl/cluster_delay_lines.py @@ -392,6 +392,10 @@ def delay_offset(self) -> float: return self.__delay_offset + @property + def expected_energy_scale(self) -> float: + return float(np.sum(self.cluster_powers)) + def __ray_impulse_generator( self, sampling_rate: float, num_samples: int, center_frequency: float ) -> Generator[Tuple[np.ndarray, np.ndarray, float], None, None]: diff --git a/hermespy/channel/channel.py b/hermespy/channel/channel.py index 8a61543b..d8ddef6e 100644 --- a/hermespy/channel/channel.py +++ b/hermespy/channel/channel.py @@ -310,6 +310,16 @@ def time(self) -> float: return self.__state.time + @property + @abstractmethod + def expected_energy_scale(self) -> float: + """Expected linear scaling of a propagated signal's energy at each receiving antenna. + + Required to compute the expected energy of a signal after propagation, + and therfore signal-to-noise ratios (SNRs) and signal-to-interference-plus-noise ratios (SINRs). + """ + ... # pragma: no cover + @abstractmethod def _propagate(self, signal: SignalBlock, interpolation: InterpolationMode) -> SignalBlock: """Propagate radio-frequency band signals over a channel instance. @@ -647,7 +657,6 @@ class Channel(ABC, RandomNode, Serializable, Generic[CRT, CST]): __scenario: SimulationScenario __gain: float - __interpolation_mode: InterpolationMode __sample_hooks: Set[ChannelSampleHook[CST]] def __init__(self, gain: float = 1.0, seed: Optional[int] = None) -> None: @@ -655,7 +664,7 @@ def __init__(self, gain: float = 1.0, seed: Optional[int] = None) -> None: Args: gain (float, optional): - Linear channel power gain factor. + Linear channel energy gain factor. Initializes the :meth:`gain` property. :math:`1.0` by default. diff --git a/hermespy/channel/delay/delay.py b/hermespy/channel/delay/delay.py index ccd6eca2..437c7c4e 100644 --- a/hermespy/channel/delay/delay.py +++ b/hermespy/channel/delay/delay.py @@ -65,6 +65,10 @@ def gain(self) -> float: return self.__gain + @property + def expected_energy_scale(self) -> float: + return self.__path_gain(self.carrier_frequency) + def __spatial_response(self) -> np.ndarray: receiver_position = self.receiver_state.position diff --git a/hermespy/channel/fading/fading.py b/hermespy/channel/fading/fading.py index 46bad45f..b15c18e2 100644 --- a/hermespy/channel/fading/fading.py +++ b/hermespy/channel/fading/fading.py @@ -274,10 +274,14 @@ def spatial_response(self) -> np.ndarray: @property def gain(self) -> float: - """Linear power gain factor a signal experiences when being propagated over this realization.""" + """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]: @@ -311,27 +315,22 @@ def __path_impulse_generator( self.nlos_phases, ): - impulse = ( - nlos_gain - * (num_sinusoids**-0.5) - * np.sum( - exp( - 1j - * np.outer( - nlos_time, - np.cos((2 * pi * n + nlos_angles + nlos_phases) / num_sinusoids), - ) - ), - axis=1, - keepdims=False, - ) + 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 - # ToDo: Check if the normalization by the number of paths is correct impulse *= (self.gain * power) ** 0.5 yield impulse, delay @@ -475,9 +474,9 @@ def _sample(self, state: LinkState) -> MultipathFadingSample: if isinstance(self.__los_angles_variable, float) else 2 * np.pi * self.__los_angles_variable.sample(consistent_sample) ) - nlos_angles = 2 * np.pi * self.__path_angles_variable.sample(consistent_sample) - los_phases = 2 * np.pi * self.__los_phases_variable.sample(consistent_sample) - nlos_phases = 2 * np.pi * self.__path_phases_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, @@ -693,7 +692,7 @@ def __init__( 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(0, 2 * pi) if los_angle is None else los_angle + 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 diff --git a/hermespy/channel/ideal.py b/hermespy/channel/ideal.py index 2f21a36c..f80e826f 100644 --- a/hermespy/channel/ideal.py +++ b/hermespy/channel/ideal.py @@ -49,6 +49,10 @@ def __init__(self, gain: float, state: LinkState) -> None: # Initialize class attributes self.__gain = gain + @property + def expected_energy_scale(self) -> float: + return self.__gain + def state( self, num_samples: int, diff --git a/hermespy/channel/quadriga/quadriga.py b/hermespy/channel/quadriga/quadriga.py index 9be23bf1..4756eeaa 100644 --- a/hermespy/channel/quadriga/quadriga.py +++ b/hermespy/channel/quadriga/quadriga.py @@ -81,6 +81,10 @@ def path_delays(self) -> np.ndarray: return self.__path_delays + @property + def expected_energy_scale(self) -> float: + return self.__gain * float(np.sum(self.__path_gains)) + def _propagate(self, signal: SignalBlock, interpolation: InterpolationMode) -> SignalBlock: max_delay_in_samples = int(np.round(np.max(self.path_delays) * self.bandwidth)) propagated_signal = np.zeros( diff --git a/hermespy/channel/radar/radar.py b/hermespy/channel/radar/radar.py index 0c7806a5..01a8b74a 100644 --- a/hermespy/channel/radar/radar.py +++ b/hermespy/channel/radar/radar.py @@ -85,6 +85,10 @@ def gain(self) -> float: return self.__gain + @property + def expected_energy_scale(self) -> float: + return self.__gain # ToDo: Consider the energy scale of the paths + def _propagate(self, signal: SignalBlock, interpolation: InterpolationMode) -> SignalBlock: delays = np.array( [ diff --git a/hermespy/modem/waveform_single_carrier.py b/hermespy/modem/waveform_single_carrier.py index d3996224..cc4653d5 100644 --- a/hermespy/modem/waveform_single_carrier.py +++ b/hermespy/modem/waveform_single_carrier.py @@ -832,7 +832,6 @@ def _base_filter(self) -> np.ndarray: ) ) impulse_response[idx_0_by_0] = 1 + self.roll_off * (4 / np.pi - 1) - return impulse_response / np.linalg.norm(impulse_response) diff --git a/hermespy/simulation/noise/level.py b/hermespy/simulation/noise/level.py index 044e857d..ce0fba21 100644 --- a/hermespy/simulation/noise/level.py +++ b/hermespy/simulation/noise/level.py @@ -4,6 +4,7 @@ from abc import abstractmethod from hermespy.core import Device, ScalarDimension, Transmitter, Receiver, Serializable +from hermespy.channel import Channel, ChannelSample __author__ = "Jan Adler" __copyright__ = "Copyright 2024, Barkhausen Institut gGmbH" @@ -132,12 +133,22 @@ class SNR(NoiseLevel): yaml_tag = "SNR" __snr: float __reference: Device | Transmitter | Receiver + __expected_channel_scale: float - def __init__(self, snr: float, reference: Device | Transmitter | Receiver) -> None: + def __init__( + self, snr: float, reference: Device | Transmitter | Receiver, channel: Channel | None = None + ) -> None: """ Args: - snr (float): Signal-to-noise ratio. - reference (Device |Transmitter | Receiver): Reference of the noise level. + snr (float): + Expected signal-to-noise ratio. + + reference (Device |Transmitter | Receiver): + Reference of the noise level, i.e. with which power / energy was the signal generated. + + channel (Channel, optional): + Channel instance over which the signal was propagated. + For channel models that consider propagation losses the noise power is scaled accordingly. """ # Initialize base class @@ -146,6 +157,19 @@ def __init__(self, snr: float, reference: Device | Transmitter | Receiver) -> No # Initialize class attributes self.snr = snr self.reference = reference + self.__expected_channel_scale = 1.0 + + if channel is not None: + channel.add_sample_hook(self.__update_expected_channel_scale) + + def __update_expected_channel_scale(self, sample: ChannelSample) -> None: + """Update the expected channel scale. + + Args: + sample (ChannelSample): Channel sample. + """ + + self.__expected_channel_scale = sample.expected_energy_scale @property def level(self) -> float: @@ -162,7 +186,7 @@ def level(self, value: float) -> None: self.snr = value def get_power(self) -> float: - return self.reference.power / self.snr + return self.reference.power / self.snr * self.__expected_channel_scale @property def title(self) -> str: diff --git a/tests/integration_tests/test_links.py b/tests/integration_tests/test_links.py index 45f62c0c..8f811779 100644 --- a/tests/integration_tests/test_links.py +++ b/tests/integration_tests/test_links.py @@ -4,6 +4,7 @@ from unittest import TestCase import numpy as np +from numpy.testing import assert_array_almost_equal from hermespy.channel import Channel, IdealChannel, TDL, Cost259, RandomDelayChannel, TDLType, UrbanMicrocells from hermespy.core import Transformation @@ -90,20 +91,20 @@ def __propagate(self, channel: Channel) -> None: channel.seed = 42 self.link.seed = 42 - device_transmission = self.tx_device.transmit() + + self.device_transmission = self.tx_device.transmit() channel_realization = channel.realize() - channel_sample = channel_realization.sample(self.tx_device, self.rx_device) - channel_propagation = channel_sample.propagate(device_transmission) + self.channel_sample = channel_realization.sample(self.tx_device, self.rx_device) + channel_propagation = self.channel_sample.propagate(self.device_transmission) self.rx_device.process_input(channel_propagation) - link_reception = self.link.receive() + self.link_reception = self.link.receive() # Debug: # - link_transmission = device_transmission.operator_transmissions[0] - tx = link_transmission.signal - state = channel_sample.state(tx.num_samples, tx.num_samples) - rx_prediction = state.propagate(tx) - return + # link_transmission = device_transmission.operator_transmissions[0] + # tx = link_transmission.signal + # state = self.channel_sample.state(tx.num_samples, tx.num_samples) + # rx_prediction = state.propagate(tx) # ======================= # Waveform configurations @@ -115,7 +116,7 @@ def __configure_single_carrier_waveform(self, channel: Channel) -> RootRaisedCos Returns: The configured waveform. """ - waveform = RootRaisedCosineWaveform(symbol_rate=1 / 10e-6, num_preamble_symbols=10, num_data_symbols=40, pilot_rate=10, oversampling_factor=8, roll_off=0.9) + waveform = RootRaisedCosineWaveform(symbol_rate=1 / 10e-6, num_preamble_symbols=10, num_data_symbols=160, pilot_rate=10, oversampling_factor=8, roll_off=0.9) waveform.synchronization = SingleCarrierCorrelationSynchronization() waveform.channel_estimation = SingleCarrierIdealChannelEstimation(channel, self.tx_device, self.rx_device) waveform.channel_equalization = SingleCarrierZeroForcingChannelEqualization() @@ -282,7 +283,7 @@ def __configure_COST259_channel(self) -> Cost259: Returns: The configured channel. """ - channel = Cost259(gain=0.9, doppler_frequency=self._doppler_frequency) + channel = Cost259(gain=1.0, doppler_frequency=self._doppler_frequency, num_sinusoids=100) return channel def __configure_5GTDL_channel(self) -> TDL: @@ -319,20 +320,28 @@ def __configure_delay_channel(self) -> RandomDelayChannel: # ======================= def __assert_link(self) -> None: + + # Test bit error rates ber_treshold = 1e-2 + + # Test signal powers + # transmitted_power = self.device_transmission.emerging_signals[0].power + # expected_transmit_power = self.link.power + self.assertGreaterEqual(ber_treshold, self.ber.evaluate().artifact().to_scalar()) + # assert_array_almost_equal(expected_transmit_power * np.ones_like(transmitted_power), transmitted_power, 1, "Transmitted waveform's power does not match expectations") def test_ideal_channel_chirp_fsk(self) -> None: """Verify a valid SISO link over an ideal channel with chirp frequency shift keying modulation""" self.__configure_chirp_fsk_waveform() - self.__propagate(IdealChannel()) + self.__propagate(IdealChannel(.9)) self.__assert_link() def test_ideal_channel_single_carrier(self) -> None: """Verify a valid SISO link over an ideal channel with single carrier modulation""" - channel = IdealChannel() + channel = IdealChannel(.9) self.__configure_single_carrier_waveform(channel) self.__propagate(channel) self.__assert_link() @@ -341,7 +350,7 @@ def test_ideal_channel_ocdm_ls_zf(self) -> None: """Verify a valid SISO link over an ideal channel with OCDM modulation, least-squares channel estimation and zero-forcing equalization""" - channel = IdealChannel() + channel = IdealChannel(.9) self.__configure_ocdm_waveform(channel) self.__propagate(channel) self.__assert_link() @@ -349,7 +358,7 @@ def test_ideal_channel_ocdm_ls_zf(self) -> None: def test_ideal_channel_ofdm(self) -> None: """Verify a valid SISO link over an ideal channel OFDM modulation""" - channel = IdealChannel() + channel = IdealChannel(.9) self.__configure_ofdm_waveform(channel) self.__propagate(channel) self.__assert_link() @@ -358,7 +367,7 @@ def test_ideal_channel_ofdm_ls_zf(self) -> None: """Verify a valid SISO link over an ideal channel with OFDM modulation, least-squares channel estimation and zero-forcing equalization""" - channel = IdealChannel() + channel = IdealChannel(.9) waveform = self.__configure_ofdm_waveform(channel) waveform.channel_estimation = OrthogonalLeastSquaresChannelEstimation() waveform.channel_equalization = OrthogonalZeroForcingChannelEqualization() @@ -370,7 +379,7 @@ def test_ideal_channel_ofdm_schmidl_cox(self) -> None: """Verify a valid link over an AWGN channel with OFDM modluation, Schmidl-Cox synchronization, least-squares channel estimation and zero-forcing equalization""" - channel = IdealChannel() + channel = IdealChannel(.9) waveform = self.__configure_ofdm_waveform(channel) waveform.pilot_section = SchmidlCoxPilotSection() waveform.synchronization = SchmidlCoxSynchronization() @@ -383,7 +392,7 @@ def test_ideal_channel_ofdm_schmidl_cox(self) -> None: def test_ideal_channel_otfs_ls_zf(self) -> None: """Verify a valid SISO link over an ideal channel with OTFS modulation""" - channel = IdealChannel() + channel = IdealChannel(.9) self.__configure_otfs_waveform(channel) self.__propagate(channel) self.__assert_link() diff --git a/tests/unit_tests/channel/test_cdl.py b/tests/unit_tests/channel/test_cdl.py index 986feabe..4a26692c 100644 --- a/tests/unit_tests/channel/test_cdl.py +++ b/tests/unit_tests/channel/test_cdl.py @@ -284,6 +284,26 @@ def test_expected_realization(self) -> None: sample: ClusterDelayLineSample = realization.sample(self.alpha_device, self.beta_device) self._test_propagation(sample) + def test_expected_scale(self) -> None: + """Test the expected amplitude scaling""" + + unit_energy_signal = DenseSignal(np.ones((self.alpha_device.num_transmit_antennas, 100)) / 10, self.bandwidth, self.carrier_frequency) + num_attempts = 100 + + cumulated_propagated_energy = np.zeros((self.beta_device.num_receive_antennas), dtype=np.float_) + cumulated_expected_scale = 0.0 + for _ in range(num_attempts): + realization = self.model.realize() + sample = realization.sample(self.alpha_device, self.beta_device) + propagated_signal = sample.propagate(unit_energy_signal) + cumulated_propagated_energy += propagated_signal.energy + cumulated_expected_scale += sample.expected_energy_scale + + mean_propagated_energy = cumulated_propagated_energy / num_attempts + mean_expected_energy = (cumulated_expected_scale / num_attempts) ** 2 + + self.assertAlmostEqual(mean_propagated_energy, mean_expected_energy, delta=1e-1) + def test_propagation_time_of_flight(self) -> None: """Test time of flight delay simulation""" diff --git a/tests/unit_tests/channel/test_fading.py b/tests/unit_tests/channel/test_fading.py index 21fb9e45..c7bee2d4 100644 --- a/tests/unit_tests/channel/test_fading.py +++ b/tests/unit_tests/channel/test_fading.py @@ -17,8 +17,8 @@ from hermespy.channel import MultipathFadingChannel, AntennaCorrelation, CustomAntennaCorrelation, TDL, Exponential, Cost259, StandardAntennaCorrelation, CorrelationType, Cost259Type, TDLType from hermespy.channel.channel import LinkState from hermespy.channel.fading.fading import MultipathFadingSample -from hermespy.core import AntennaMode, Signal, FloatingError -from hermespy.simulation import SimulatedDevice, SimulatedIdealAntenna, SimulatedUniformArray +from hermespy.core import AntennaMode, Signal, DenseSignal, Transformation +from hermespy.simulation import StaticTrajectory, SimulatedDevice, SimulatedIdealAntenna, SimulatedUniformArray from unit_tests.core.test_factory import test_yaml_roundtrip_serialization from unit_tests.utils import SimulationTestContext from unit_tests.utils import assert_signals_equal @@ -573,7 +573,7 @@ def test_channel_gain(self) -> None: channel_gain._rng = np.random.default_rng(42) # Reset random number rng propagation_gain = channel_gain.propagate(tx_signal, self.alpha_device, self.beta_device) - assert_array_almost_equal(propagation_no_gain[:, :] * gain**0.5, propagation_gain[:, :]) + assert_array_almost_equal(propagation_no_gain[:, :] * gain ** .5, propagation_gain[:, :]) def test_antenna_correlation(self) -> None: """Test channel simulation with antenna correlation modeling""" @@ -694,15 +694,11 @@ class TestCost259(unittest.TestCase): """Test the Cost256 template for the multipath fading channel model.""" def setUp(self) -> None: - self.alpha_device = Mock() - self.beta_device = Mock() - self.alpha_device.antennas.num_antennas = 1 - self.beta_device.antennas.num_antennas = 1 - self.alpha_device.position = np.array([100, 0, 0]) - self.beta_device.position = np.array([0, 100, 0]) - self.alpha_device.orientation = np.array([0, 0, 0]) - self.beta_device.orientation = np.array([0, 0, pi]) - + self.sampling_rate = 10e6 + self.carrier_frequency = 2.4e8 + self.alpha_device = SimulatedDevice(carrier_frequency=self.carrier_frequency, sampling_rate=self.sampling_rate, pose=StaticTrajectory(Transformation.From_Translation(np.array([100, 0, 0])))) + self.beta_device = SimulatedDevice(carrier_frequency=self.carrier_frequency, sampling_rate=self.sampling_rate, pose=StaticTrajectory(Transformation.From_Translation(np.array([100, 0, 0])))) + def test_init(self) -> None: """Test the template initializations.""" @@ -725,6 +721,27 @@ def test_model_type(self) -> None: for model_type in Cost259Type: channel = Cost259(model_type) self.assertEqual(model_type, channel.model_type) + + def test_expected_scale(self) -> None: + """Test the expected amplitude scaling""" + + model = Cost259(Cost259Type.HILLY, doppler_frequency=10.0, seed=42) + unit_energy_signal = DenseSignal(np.ones((self.alpha_device.num_transmit_antennas, 100)) / 10, self.sampling_rate, self.carrier_frequency) + num_attempts = 1000 + + cumulated_propagated_energy = np.zeros((self.beta_device.num_receive_antennas), dtype=np.float_) + cumulated_expected_scale = 0.0 + for _ in range(num_attempts): + realization = model.realize() + sample = realization.sample(self.alpha_device, self.beta_device) + propagated_signal = sample.propagate(unit_energy_signal) + cumulated_propagated_energy += propagated_signal.energy + cumulated_expected_scale += sample.expected_energy_scale + + mean_propagated_energy = cumulated_propagated_energy / num_attempts + mean_expected_energy = (cumulated_expected_scale / num_attempts) ** 2 + + self.assertAlmostEqual(mean_propagated_energy, mean_expected_energy, delta=1e-1) def test_serialization(self) -> None: """Test YAML serialization""" @@ -737,14 +754,11 @@ class Test5GTDL(unittest.TestCase): def setUp(self) -> None: self.rms_delay = 1e-6 - self.alpha_device = Mock() - self.beta_device = Mock() - self.alpha_device.antennas.num_antennas = 1 - self.beta_device.antennas.num_antennas = 1 - self.alpha_device.position = np.array([100, 0, 0]) - self.beta_device.position = np.array([0, 100, 0]) - self.alpha_device.orientation = np.array([0, 0, 0]) - self.beta_device.orientation = np.array([0, 0, pi]) + self.sampling_rate = 10e6 + self.carrier_frequency = 2.4e8 + self.alpha_device = SimulatedDevice(carrier_frequency=self.carrier_frequency, sampling_rate=self.sampling_rate, pose=StaticTrajectory(Transformation.From_Translation(np.array([100, 0, 0])))) + self.beta_device = SimulatedDevice(carrier_frequency=self.carrier_frequency, sampling_rate=self.sampling_rate, pose=StaticTrajectory(Transformation.From_Translation(np.array([100, 0, 0])))) + def test_init(self) -> None: """Test the template initializations.""" @@ -774,6 +788,27 @@ def test_model_type(self) -> None: for model_type in TDLType: channel = TDL(model_type=model_type) self.assertEqual(model_type, channel.model_type) + + def test_expected_scale(self) -> None: + """Test the expected amplitude scaling""" + + model = TDL(model_type=TDLType.E, doppler_frequency=10.0, seed=42) + unit_energy_signal = DenseSignal(np.ones((self.alpha_device.num_transmit_antennas, 100)) / 10, self.sampling_rate, self.carrier_frequency) + num_attempts = 1000 + + cumulated_propagated_energy = np.zeros((self.beta_device.num_receive_antennas), dtype=np.float_) + cumulated_expected_scale = 0.0 + for _ in range(num_attempts): + realization = model.realize() + sample = realization.sample(self.alpha_device, self.beta_device) + propagated_signal = sample.propagate(unit_energy_signal) + cumulated_propagated_energy += propagated_signal.energy + cumulated_expected_scale += sample.expected_energy_scale + + mean_propagated_energy = cumulated_propagated_energy / num_attempts + mean_expected_energy = (cumulated_expected_scale / num_attempts) ** 2 + + self.assertAlmostEqual(mean_propagated_energy, mean_expected_energy, delta=1e-1) def test_serialization(self) -> None: """Test YAML serialization""" @@ -788,9 +823,13 @@ class TestExponential(unittest.TestCase): def setUp(self) -> None: self.tap_interval = 1e-5 self.rms_delay = 1e-8 - self.channel = Exponential(tap_interval=self.tap_interval, rms_delay=self.rms_delay) + self.sampling_rate = 10e6 + self.carrier_frequency = 2.4e8 + self.alpha_device = SimulatedDevice(carrier_frequency=self.carrier_frequency, sampling_rate=self.sampling_rate, pose=StaticTrajectory(Transformation.From_Translation(np.array([100, 0, 0])))) + self.beta_device = SimulatedDevice(carrier_frequency=self.carrier_frequency, sampling_rate=self.sampling_rate, pose=StaticTrajectory(Transformation.From_Translation(np.array([100, 0, 0])))) + def test_init(self) -> None: """Initialization arguments should be properly parsed.""" pass @@ -803,6 +842,27 @@ def test_init_validation(self) -> None: with self.assertRaises(ValueError): _ = Exponential(1.0, 0.0) + + def test_expected_scale(self) -> None: + """Test the expected amplitude scaling""" + + unit_energy_signal = DenseSignal(np.ones((self.alpha_device.num_transmit_antennas, 100)) / 10, self.sampling_rate, self.carrier_frequency) + num_attempts = 1000 + + cumulated_propagated_energy = np.zeros((self.beta_device.num_receive_antennas), dtype=np.float_) + cumulated_expected_scale = 0.0 + for _ in range(num_attempts): + realization = self.channel.realize() + sample = realization.sample(self.alpha_device, self.beta_device) + propagated_signal = sample.propagate(unit_energy_signal) + cumulated_propagated_energy += propagated_signal.energy + cumulated_expected_scale += sample.expected_energy_scale + + mean_propagated_energy = cumulated_propagated_energy / num_attempts + mean_expected_energy = (cumulated_expected_scale / num_attempts) ** 2 + + self.assertAlmostEqual(mean_propagated_energy, mean_expected_energy, delta=1e-1) + def test_serialization(self) -> None: """Test YAML serialization""" diff --git a/tests/unit_tests/simulation/noise/test_level.py b/tests/unit_tests/simulation/noise/test_level.py index da33f942..1679189f 100644 --- a/tests/unit_tests/simulation/noise/test_level.py +++ b/tests/unit_tests/simulation/noise/test_level.py @@ -4,7 +4,11 @@ from unittest import TestCase from unittest.mock import patch, PropertyMock -from hermespy.simulation import NoiseLevel, N0, SimulatedDevice, SNR +import numpy as np + +from hermespy.core import DenseSignal, SignalReceiver +from hermespy.simulation import NoiseLevel, N0, SimulatedDevice, SNR, AWGN +from hermespy.channel import IdealChannel from unit_tests.core.test_factory import test_yaml_roundtrip_serialization __author__ = "Jan Adler" @@ -124,5 +128,32 @@ def test_get_power(self) -> None: mock_power.return_value = reference_power self.assertEqual(reference_power / self.level.snr, self.level.get_power()) + def test_power_scaling(self) -> None: + """Power should be scaled by the expected channel scale""" + + num_samples = 10000 + sampling_rate = 1e8 + expected_energy_scale = 0.5 + channel = IdealChannel(expected_energy_scale) + tx_device = self.reference + tx_device.sampling_rate = sampling_rate + rx_device = SimulatedDevice(sampling_rate=sampling_rate) + rx_device.noise_model = AWGN(seed=42) + + rx_dsp = SignalReceiver(num_samples, sampling_rate) + rx_dsp.device = rx_device + + noise_level = SNR(1.5, tx_device, channel) + rx_device.noise_level = noise_level + + unit_power_signal = DenseSignal(np.ones(num_samples), sampling_rate, 0) + + propagated_signal = channel.propagate(unit_power_signal, tx_device, rx_device) + rx_device.receive(propagated_signal) + + expected_received_power = (unit_power_signal.power * (1 + 1/noise_level.snr)) * expected_energy_scale + received_power = rx_dsp.signal.power[0] + self.assertAlmostEqual(expected_received_power, received_power, delta=.1) + del _TestNoiseLevel