def test_num_sinusoids_validation(self) -> None: """Number of sinusoids property setter should raise ValueError on invalid arguments.""" channel = MultipathFadingChannel(**self.channel_params) with self.assertRaises(ValueError): channel.num_sinusoids = -1
def test_num_sinusoids_setget(self) -> None: """Number of sinusoids property getter should return setter argument.""" channel = MultipathFadingChannel(**self.channel_params) num_sinusoids = 100 channel.num_sinusoids = num_sinusoids self.assertEqual(num_sinusoids, channel.num_sinusoids)
def test_doppler_frequency_setget(self) -> None: """Doppler frequency property getter should return setter argument.""" channel = MultipathFadingChannel(**self.channel_params) doppler_frequency = 5 channel.doppler_frequency = doppler_frequency self.assertEqual(doppler_frequency, channel.doppler_frequency)
def __init__(self, tap_interval: float = 0.0, rms_delay: float = 0.0, **kwargs: Any) -> None: """Exponential Multipath Channel Model initialization. Args: tap_interval (float, optional): Tap interval in seconds. rms_delay (float, optional): Root-Mean-Squared delay in seconds. kwargs (Any): `MultipathFadingChannel` initialization parameters. Raises: ValueError: On invalid arguments. """ if tap_interval <= 0.0: raise ValueError("Tap interval must be greater than zero") if rms_delay <= 0.0: raise ValueError( "Root-Mean-Squared delay must be greater than zero") self.__tap_interval = tap_interval self.__rms_delay = rms_delay rms_norm = rms_delay / tap_interval # Calculate the decay exponent alpha based on an infinite power delay profile, in which case # rms_delay = exp(-alpha/2)/(1-exp(-alpha)), cf. geometric distribution. # Truncate the distributions for paths whose average power is very # small (less than exponential_truncation). alpha = -2 * np.log( (-1 + np.sqrt(1 + 4 * rms_norm**2)) / (2 * rms_norm)) max_delay_in_samples = int( np.ceil( np.log(MultipathFadingExponential.__exponential_truncation) / alpha)) delays = np.arange(max_delay_in_samples + 1) * tap_interval power_profile = np.exp(-alpha * np.arange(max_delay_in_samples + 1)) rice_factors = np.zeros(delays.shape) # Init base class with pre-defined model parameters MultipathFadingChannel.__init__(self, delays=delays, power_profile=power_profile, rice_factors=rice_factors, **kwargs)
def test_los_angle_setget(self) -> None: """Line of sight angle property getter should return setter argument.""" channel = MultipathFadingChannel(**self.channel_params) los_angle = 15 channel.los_angle = los_angle self.assertEqual(los_angle, channel.los_angle) channel.los_angle = None self.assertEqual(None, channel.los_angle)
def test_los_doppler_frequency_setget(self) -> None: """Line-of-Sight Doppler frequency property getter should return setter argument, alternatively the global Doppler.""" channel = MultipathFadingChannel(**self.channel_params) los_doppler_frequency = 5 channel.los_doppler_frequency = los_doppler_frequency self.assertEqual(los_doppler_frequency, channel.los_doppler_frequency) doppler_frequency = 4 channel.doppler_frequency = doppler_frequency channel.los_doppler_frequency = None self.assertEqual(doppler_frequency, channel.los_doppler_frequency)
def test_multipath_fading(self) -> None: self.channel_params['delays'] = np.zeros(1, dtype=float) self.channel_params['power_profile'] = np.ones(1, dtype=float) self.channel_params['rice_factors'] = np.zeros(1, dtype=float) LOW, HIGH = self.add_sync_offsets_to_params() ch = MultipathFadingChannel(**self.channel_params) self.serialized_channel_contains_sync_offsets(ch, LOW, HIGH)
def test_rayleigh(self) -> None: """ Test if the amplitude of a path is Rayleigh distributed. Verify that both real and imaginary components are zero-mean normal random variables with the right variance and uncorrelated. """ max_number_of_drops = 200 samples_per_drop = 1000 self.doppler_frequency = 200 self.channel_params['delays'][0] = 0. self.channel_params['power_profile'][0] = 1. self.channel_params['rice_factors'][0] = 0. self.channel_params['doppler_frequency'] = self.doppler_frequency channel = MultipathFadingChannel(**self.channel_params) samples = np.array([]) is_rayleigh = False alpha = .05 max_corr = 0.05 number_of_drops = 0 while not is_rayleigh and number_of_drops < max_number_of_drops: channel_gains = channel.impulse_response(samples_per_drop, self.doppler_frequency) samples = np.append(samples, channel_gains.ravel()) _, p_real = stats.kstest(np.real(samples), 'norm', args=(0, 1 / np.sqrt(2))) _, p_imag = stats.kstest(np.imag(samples), 'norm', args=(0, 1 / np.sqrt(2))) corr = np.corrcoef(np.real(samples), np.imag(samples)) corr = corr[0, 1] if p_real > alpha and p_imag > alpha and abs(corr) < max_corr: is_rayleigh = True number_of_drops += 1 self.assertTrue(is_rayleigh)
def test_max_delay_get(self) -> None: """Max delay property should return maximum of delays.""" self.channel_params['delays'] = np.array([1, 2, 3]) self.channel_params['power_profile'] = np.zeros(3) self.channel_params['rice_factors'] = np.ones(3) channel = MultipathFadingChannel(**self.channel_params) self.assertEqual(max(self.channel_params['delays']), channel.max_delay)
def test_num_sequences_get(self) -> None: """Number of fading sequences property should return core parameter lengths.""" self.channel_params['delays'] = np.array([1, 2, 3]) self.channel_params['power_profile'] = np.zeros(3) self.channel_params['rice_factors'] = np.ones(3) channel = MultipathFadingChannel(**self.channel_params) self.assertEqual(len(self.channel_params['delays']), channel.num_resolvable_paths)
def test_propagation_siso_no_fading(self) -> None: """ Test the propagation through a SISO multipath channel model without fading Check if the output sizes are consistent Check the output of a SISO multipath channel model without fading (K factor of Rice distribution = inf) """ self.rice_factors[0] = float('inf') self.delays[0] = 10 / self.sampling_rate channel = MultipathFadingChannel(**self.channel_params) timestamps = np.arange(self.num_samples) / self.sampling_rate transmission = exp(1j * timestamps * self.transmit_frequency).reshape( 1, self.num_samples) output, _, _ = channel.propagate( Signal(transmission, self.sampling_rate)) self.assertEqual(10, output[0].num_samples - transmission.shape[1], "Propagation impulse response has unexpected length")
def test_init_validation(self) -> None: """Object initialization should raise ValueError on invalid arguments.""" with self.assertRaises(ValueError): params = deepcopy(self.channel_params) params['delays'] = np.array([1, 2]) _ = MultipathFadingChannel(**params) with self.assertRaises(ValueError): params = deepcopy(self.channel_params) params['power_profile'] = np.array([1, 2]) _ = MultipathFadingChannel(**params) with self.assertRaises(ValueError): params = deepcopy(self.channel_params) params['rice_factors'] = np.array([1, 2]) _ = MultipathFadingChannel(**params) with self.assertRaises(ValueError): params = deepcopy(self.channel_params) params['delays'] = np.array([-1.0]) _ = MultipathFadingChannel(**params) with self.assertRaises(ValueError): params = deepcopy(self.channel_params) params['power_profile'] = np.array([-1.0]) _ = MultipathFadingChannel(**params) with self.assertRaises(ValueError): params = deepcopy(self.channel_params) params['rice_factors'] = np.array([-1.0]) _ = MultipathFadingChannel(**params)
def test_power_delay_profile(self) -> None: """ Test if the resulting power delay profile matches with the one specified in the parameters. Test also an interpolated channel (should have the same rms delay spread) """ max_number_of_drops = 100 samples_per_drop = 1000 max_delay_spread_dev = 12 / self.sampling_rate # Check what is acceptable here self.doppler_frequency = 50 self.channel_params['doppler_frequency'] = self.doppler_frequency self.channel_params['delays'] = np.zeros(5) self.channel_params['power_profile'] = np.ones(5) self.channel_params['rice_factors'] = np.zeros(5) self.channel_params['delays'] = np.array([0, 3, 6, 7, 8 ]) / self.sampling_rate mean_delay = np.mean(self.channel_params['delays']) config_delay_spread = np.mean( (self.channel_params['delays'] - mean_delay)**2) delayed_channel = MultipathFadingChannel(**self.channel_params) for s in range(max_number_of_drops): delayed_channel.random_generator = np.random.default_rng(s + 10) delayed_response = delayed_channel.impulse_response( samples_per_drop, self.sampling_rate) delayed_time = np.arange( delayed_response.shape[-1]) / self.sampling_rate delay_diff = (delayed_time - np.mean(delayed_time))**2 delayed_power = delayed_response.real**2 + delayed_response.imag**2 delay_spread = np.sqrt( np.mean(delayed_power @ delay_diff) / np.mean(delayed_power)) spread_delta = abs(config_delay_spread - delay_spread) self.assertTrue( spread_delta < max_delay_spread_dev, msg=f"{spread_delta} larger than max {max_delay_spread_dev}")
def test_impulse_response_seed(self) -> None: """Re-setting the random rng seed should result in identical impulse responses.""" channel = MultipathFadingChannel(**self.channel_params) channel.set_seed(100) first_draw = channel.impulse_response(self.num_samples, self.sampling_rate) channel.set_seed(100) second_draw = channel.impulse_response(self.num_samples, self.sampling_rate) assert_array_almost_equal(first_draw, second_draw)
def test_propagation_fading(self) -> None: """ Test the propagation through a SISO multipath channel with fading. """ test_delays = np.array([1., 2., 3., 4.], dtype=float) / self.sampling_rate reference_params = self.channel_params.copy() delayed_params = self.channel_params.copy() reference_params['delays'] = np.array([0.0]) reference_channel = MultipathFadingChannel(**reference_params) timestamps = np.arange(self.num_samples) / self.sampling_rate transmit_samples = np.exp(2j * pi * timestamps * self.transmit_frequency).reshape( (1, self.num_samples)) transmit_signal = Signal(transmit_samples, self.sampling_rate) for d, delay in enumerate(test_delays): delayed_params['delays'] = reference_params['delays'] + delay delayed_channel = MultipathFadingChannel(**delayed_params) reference_channel.set_seed(d) reference_propagation, _, _ = reference_channel.propagate( transmit_signal) delayed_channel.set_seed(d) delayed_propagation, _, _ = delayed_channel.propagate( transmit_signal) zero_pads = int(self.sampling_rate * float(delay)) npt.assert_array_almost_equal( reference_propagation[0].samples, delayed_propagation[0].samples[:, zero_pads:])
def test_rice(self) -> None: """ Test if the amplitude of a path is Ricean distributed. """ max_number_of_drops = 100 doppler_frequency = 200 samples_per_drop = 1000 self.channel_params['delays'][0] = 0. self.channel_params['power_profile'][0] = 1. self.channel_params['rice_factors'][0] = 1. self.channel_params['doppler_frequency'] = doppler_frequency channel = MultipathFadingChannel(**self.channel_params) samples = np.array([]) is_rice = False alpha = .05 number_of_drops = 0 while not is_rice and number_of_drops < max_number_of_drops: channel_gains = channel.impulse_response(samples_per_drop, doppler_frequency) samples = np.append(samples, channel_gains.ravel()) dummy, p_real = stats.kstest(np.abs(samples), 'rice', args=(np.sqrt(2), 0, 1 / 2)) if p_real > alpha: is_rice = True number_of_drops += 1 self.assertTrue(is_rice)
def test_init(self) -> None: """The object initialization should properly store all parameters.""" channel = MultipathFadingChannel(**self.channel_params) self.assertIs(self.transmitter, channel.transmitter, "Unexpected transmitter parameter initialization") self.assertIs(self.receiver, channel.receiver, "Unexpected receiver parameter initialization") self.assertEqual(self.active, channel.active, "Unexpected active parameter initialization") self.assertEqual(self.gain, channel.gain, "Unexpected gain parameter initialization") self.assertEqual(self.num_sinusoids, channel.num_sinusoids) self.assertEqual(self.doppler_frequency, channel.doppler_frequency) self.assertEqual(self.sync_offset_low, channel.sync_offset_low) self.assertEqual(self.sync_offset_high, channel.sync_offset_high)
def test_delays_get(self) -> None: """Delays getter should return init param.""" channel = MultipathFadingChannel(**self.channel_params) np.testing.assert_array_almost_equal(self.delays, channel.delays)
def __init__(self, model_type: TYPE = TYPE.URBAN, los_angle: Optional[float] = None, doppler_frequency: Optional[float] = None, los_doppler_frequency: Optional[float] = None, **kwargs: Any) -> None: """Model initialization. Args: model_type (TYPE): The model type.. 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. kwargs (Any): `MultipathFadingChannel` initialization parameters. Raises: ValueError: If `model_type` is not supported. If `los_angle` is defined in HILLY model type. """ if model_type == self.TYPE.URBAN: delays = 1e-6 * np.array([ 0, .217, .512, .514, .517, .674, .882, 1.230, 1.287, 1.311, 1.349, 1.533, 1.535, 1.622, 1.818, 1.836, 1.884, 1.943, 2.048, 2.140 ]) power_db = np.array([ -5.7, -7.6, -10.1, -10.2, -10.2, -11.5, -13.4, -16.3, -16.9, -17.1, -17.4, -19.0, -19.0, -19.8, -21.5, -21.6, -22.1, -22.6, -23.5, -24.3 ]) rice_factors = np.zeros(delays.shape) elif model_type == self.TYPE.RURAL: delays = 1e-6 * np.array( [0, .042, .101, .129, .149, .245, .312, .410, .469, .528]) power_db = np.array([ -5.2, -6.4, -8.4, -9.3, -10.0, -13.1, -15.3, -18.5, -20.4, -22.4 ]) rice_factors = np.zeros(delays.shape) elif model_type == self.TYPE.HILLY: if los_angle is not None: raise ValueError( "Model type HILLY does not support line of sight angle configuration" ) delays = 1e-6 * np.array([ 0, .356, .441, .528, .546, .609, .625, .842, .916, .941, 15.0, 16.172, 16.492, 16.876, 16.882, 16.978, 17.615, 17.827, 17.849, 18.016 ]) power_db = np.array([ -3.6, -8.9, -10.2, -11.5, -11.8, -12.7, -13.0, -16.2, -17.3, -17.7, -17.6, -22.7, -24.1, -25.8, -25.8, -26.2, -29.0, -29.9, -30.0, -30.7 ]) rice_factors = np.hstack( [np.array([np.inf]), np.zeros(delays.size - 1)]) los_angle = np.arccos(.7) else: raise ValueError("Requested model type not supported") self.__model_type = model_type # Convert power and normalize power_profile = 10**(power_db / 10) power_profile /= sum(power_profile) # Init base class with pre-defined model parameters MultipathFadingChannel.__init__( self, delays=delays, power_profile=power_profile, rice_factors=rice_factors, los_angle=los_angle, doppler_frequency=doppler_frequency, los_doppler_frequency=los_doppler_frequency, **kwargs)
def __init__(self, model_type: TYPE = 0, rms_delay: float = 0.0, doppler_frequency: Optional[float] = None, los_doppler_frequency: Optional[float] = None, **kwargs: Any) -> None: """Model initialization. Args: model_type (TYPE): The model type. rms_delay (float): Root-Mean-Squared delay in seconds. num_sinusoids (int, optional): Number of sinusoids used to sample the statistical distribution. doppler_frequency (float, optional) Doppler frequency shift of the statistical distribution. kwargs (Any): `MultipathFadingChannel` initialization parameters. Raises: ValueError: If `model_type` is not supported. If `rms_delay` is smaller than zero. If `los_angle` is specified in combination with `model_type` D or E. """ if rms_delay < 0.0: raise ValueError( "Root-Mean-Squared delay must be greater or equal to zero") self.__rms_delay = rms_delay if model_type == self.TYPE.A: normalized_delays = np.array([ 0, 0.3819, 0.4025, 0.5868, 0.4610, 0.5375, 0.6708, 0.5750, 0.7618, 1.5375, 1.8978, 2.2242, 2.1717, 2.4942, 2.5119, 3.0582, 4.0810, 4.4579, 4.5695, 4.7966, 5.0066, 5.3043, 9.6586 ]) power_db = np.array([ -13.4, 0, -2.2, -4, -6, -8.2, -9.9, -10.5, -7.5, -15.9, -6.6, -16.7, -12.4, -15.2, -10.8, -11.3, -12.7, -16.2, -18.3, -18.9, -16.6, -19.9, -29.7 ]) rice_factors = np.zeros(normalized_delays.shape) elif model_type == self.TYPE.B: normalized_delays = np.array([ 0, 0.1072, 0.2155, 0.2095, 0.2870, 0.2986, 0.3752, 0.5055, 0.3681, 0.3697, 0.5700, 0.5283, 1.1021, 1.2756, 1.5474, 1.7842, 2.0169, 2.8294, 3.0219, 3.6187, 4.1067, 4.2790, 4.7834 ]) power_db = np.array([ 0, -2.2, -4, -3.2, -9.8, -3.2, -3.4, -5.2, -7.6, -3, -8.9, -9, -4.8, -5.7, -7.5, -1.9, -7.6, -12.2, -9.8, -11.4, -14.9, -9.2, -11.3 ]) rice_factors = np.zeros(normalized_delays.shape) elif model_type == self.TYPE.C: normalized_delays = np.array([ 0, 0.2099, 0.2219, 0.2329, 0.2176, 0.6366, 0.6448, 0.6560, 0.6584, 0.7935, 0.8213, 0.9336, 1.2285, 1.3083, 2.1704, 2.7105, 4.2589, 4.6003, 5.4902, 5.6077, 6.3065, 6.6374, 7.0427, 8.6523 ]) power_db = np.array([ -4.4, -1.2, -3.5, -5.2, -2.5, 0, -2.2, -3.9, -7.4, -7.1, -10.7, -11.1, -5.1, -6.8, -8.7, -13.2, -13.9, -13.9, -15.8, -17.1, -16, -15.7, -21.6, -22.8 ]) rice_factors = np.zeros(normalized_delays.shape) elif model_type == self.TYPE.D: if los_doppler_frequency is not None: raise ValueError( "Model type D does not support line of sight doppler frequency configuration" ) normalized_delays = np.array([ 0, 0.035, 0.612, 1.363, 1.405, 1.804, 2.596, 1.775, 4.042, 7.937, 9.424, 9.708, 12.525 ]) power_db = np.array([ -13.5, -18.8, -21, -22.8, -17.9, -20.1, -21.9, -22.9, -27.8, -23.6, -24.8, -30.0, -27.7 ]) rice_factors = np.zeros(normalized_delays.shape) rice_factors[0] = 13.3 los_doppler_frequency = 0.7 elif model_type == self.TYPE.E: if los_doppler_frequency is not None: raise ValueError( "Model type E does not support line of sight doppler frequency configuration" ) normalized_delays = np.array([ 0, 0.5133, 0.5440, 0.5630, 0.5440, 0.7112, 1.9092, 1.9293, 1.9589, 2.6426, 3.7136, 5.4524, 12.0034, 20.6519 ]) power_db = np.array([ -22.03, -15.8, -18.1, -19.8, -22.9, -22.4, -18.6, -20.8, -22.6, -22.3, -25.6, -20.2, -29.8, -29.2 ]) rice_factors = np.zeros(normalized_delays.shape) rice_factors[0] = 22 los_doppler_frequency = 0.7 else: raise ValueError("Requested model type not supported") self.__model_type = model_type # Convert power and normalize power_profile = 10**(power_db / 10) power_profile /= sum(power_profile) # Scale delays delays = rms_delay * normalized_delays # Init base class with pre-defined model parameters MultipathFadingChannel.__init__( self, delays=delays, power_profile=power_profile, rice_factors=rice_factors, doppler_frequency=doppler_frequency, los_doppler_frequency=los_doppler_frequency, **kwargs)
def test_rice_factors_get(self) -> None: """Rice factors getter should return init param.""" channel = MultipathFadingChannel(**self.channel_params) np.testing.assert_array_almost_equal(self.rice_factors, channel.rice_factors)
def test_channel_gain(self) -> None: """ Test if channel gain is applied correctly on both propagation and channel impulse response """ gain = 10 doppler_frequency = 200 signal_length = 1000 self.channel_params['delays'][0] = 0. self.channel_params['power_profile'][0] = 1. self.channel_params['rice_factors'][0] = 0. self.channel_params['doppler_frequency'] = doppler_frequency channel_no_gain = MultipathFadingChannel(**self.channel_params) self.channel_params['gain'] = gain channel_gain = MultipathFadingChannel(**self.channel_params) frame_size = (1, signal_length) tx_samples = rand.normal( 0, 1, frame_size) + 1j * rand.normal(0, 1, frame_size) tx_signal = Signal(tx_samples, self.sampling_rate) channel_no_gain.random_generator = np.random.default_rng( 42) # Reset random number rng signal_out_no_gain, _, _ = channel_no_gain.propagate(tx_signal) channel_gain.random_generator = np.random.default_rng( 42) # Reset random number rng signal_out_gain, _, _ = channel_gain.propagate(tx_signal) assert_array_almost_equal(signal_out_gain[0].samples, signal_out_no_gain[0].samples * gain) timestamps = np.array([0, 100, 500]) / self.sampling_rate channel_no_gain.random_generator = np.random.default_rng( 50) # Reset random number rng channel_state_info_no_gain = channel_no_gain.impulse_response( len(timestamps), self.sampling_rate) channel_gain.random_generator = np.random.default_rng( 50) # Reset random number rng channel_state_info_gain = channel_gain.impulse_response( len(timestamps), self.sampling_rate) npt.assert_array_almost_equal(channel_state_info_gain, channel_state_info_no_gain * gain)
def test_power_profiles_get(self) -> None: """Power profiles getter should return init param.""" channel = MultipathFadingChannel(**self.channel_params) np.testing.assert_array_almost_equal(self.power_profile, channel.power_profile)