def test_propagate_MISO(self) -> None: """Test valid propagation for the Multiple-Input-Single-Output channel.""" num_transmit_antennas = 3 self.transmitter.antennas.num_antennas = num_transmit_antennas self.receiver.antennas.num_antennas = 1 for num_samples in self.propagate_signal_lengths: for gain in self.propagate_signal_gains: forwards_samples = (np.random.rand(num_transmit_antennas, num_samples) + 1j * np.random.rand(num_transmit_antennas, num_samples)) backwards_samples = np.random.rand(1, num_samples) + 1j * np.random.rand(1, num_samples) forwards_input = Signal(forwards_samples, self.sampling_rate) backwards_input = Signal(backwards_samples, self.sampling_rate) self.channel.gain = gain expected_forwards_samples = gain * np.sum(forwards_samples, axis=0, keepdims=True) expected_backwards_samples = gain * np.repeat(backwards_samples, num_transmit_antennas, axis=0) forwards_signal, backwards_signal, _ = self.channel.propagate(forwards_input, backwards_input) assert_array_equal(expected_forwards_samples, forwards_signal[0].samples) assert_array_equal(expected_backwards_samples, backwards_signal[0].samples)
def test_propagate_validation(self) -> None: """Propagation routine must raise errors in case of unsupported scenarios.""" with self.assertRaises(ValueError): _ = self.channel.propagate(Signal(np.array([1, 2, 3]), self.sampling_rate)) with self.assertRaises(ValueError): self.transmitter.num_antennas = 1 _ = self.channel.propagate(Signal(np.array([[1, 2, 3], [4, 5, 6]]), self.sampling_rate)) with self.assertRaises(RuntimeError): floating_channel = Channel() _ = floating_channel.propagate(Signal(np.array([[1, 2, 3]]), self.sampling_rate))
def test_append_stream_assert(self) -> None: """Appending to a signal model should raise a ValueError if the models don't match.""" with self.assertRaises(ValueError): samples = self.signal.samples[:, 0] append_signal = Signal(samples, self.signal.sampling_rate, self.signal.carrier_frequency) self.signal.append_streams(append_signal) with self.assertRaises(ValueError): samples = self.signal.samples append_signal = Signal(samples, self.signal.sampling_rate, 0.) self.signal.append_streams(append_signal)
def test_equalize_5GTDL(self) -> None: """Test equalization of 5GTDL multipath fading channels.""" for model_type, num_antennas in product(MultipathFading5GTDL.TYPE, self.num_antennas): self.transmitter.antennas.num_antennas = num_antennas self.receiver.antennas.num_antennas = num_antennas samples = np.exp( 2j * self.rng.uniform(0., pi, (num_antennas, self.num_samples))) signal = Signal(samples, self.sampling_rate) noise = np.zeros((num_antennas, self.num_samples)) channel = MultipathFading5GTDL(model_type=model_type, rms_delay=0., transmitter=self.transmitter, receiver=self.receiver) channel.random_mother = self.random_mother propagated_signal, _, channel_state = channel.propagate(signal) equalized_signal, _, _ = self.equalizer.decode( propagated_signal[0].samples, channel_state, noise) assert_array_almost_equal(samples, equalized_signal)
def test_quantization_complex(self): """ Test correct quantization of complex numbers""" max_amplitude = 100 self.quantizer.gain = Gain(1 / max_amplitude) # randomly choose quantization levels quantization_idx = self.rng.integers( self.quantizer.num_quantization_levels, size=(2, self.num_samples)) quantization_step = 2 * max_amplitude / self.quantizer.num_quantization_levels quantization_levels = -max_amplitude + quantization_step * ( quantization_idx + .5) # add random noise within quantization interval random_noise = self.rng.uniform(-quantization_step / 2, quantization_step / 2, size=(2, self.num_samples)) input_signal = quantization_levels + random_noise quantization_levels = quantization_levels[ 0, :] + 1j * quantization_levels[1, :] input_signal = input_signal[0, :] + 1j * input_signal[1, :] input_signal = Signal(samples=input_signal, sampling_rate=1.) output_signal = self.quantizer.convert(input_signal) np.testing.assert_almost_equal(output_signal.samples.flatten(), quantization_levels)
def test_quantization_rms(self): """ Test correct quantizer output with gain control to rms amplitude""" rms_amplitude = 576577.79 self.quantizer.gain = AutomaticGainControl( GainControlType.RMS_AMPLITUDE) # create signal with desired rms input_signal = self.rng.normal(size=self.num_samples) measured_rms = rms_value(input_signal) input_signal = input_signal / measured_rms * rms_amplitude test_signal = deepcopy(input_signal) # create non-adaptive quantizer with desired amplitude quantizer_no_gain_control = deepcopy(self.quantizer) quantizer_no_gain_control.gain = Gain(1 / rms_amplitude) # Output of both quantizers must be the same input_signal = Signal(samples=input_signal, sampling_rate=1.) output_signal_adaptive = self.quantizer.convert(input_signal) output_signal_non_adaptive = quantizer_no_gain_control.convert( input_signal) np.testing.assert_almost_equal(output_signal_adaptive.samples, output_signal_non_adaptive.samples)
def test_quantization_max_amplitude(self): """ Test correct quantizer output with gain control to maximum amplitude""" self.quantizer.gain = AutomaticGainControl( GainControlType.MAX_AMPLITUDE) max_amplitude = 123.7 # randomly choose quantization levels quantization_idx = self.rng.integers( self.quantizer.num_quantization_levels, size=self.num_samples) quantization_step = 2 * max_amplitude / self.quantizer.num_quantization_levels quantization_levels = -max_amplitude + quantization_step * ( quantization_idx + .5) # add random noise within quantization interval random_noise = self.rng.uniform(-quantization_step / 2, quantization_step / 2, size=self.num_samples) input_signal = quantization_levels + random_noise # add maximum amplitude value to input_vector input_signal = np.append(input_signal, [max_amplitude]) quantization_levels = np.append( quantization_levels, [max_amplitude - quantization_step / 2]) input_signal = Signal(samples=input_signal, sampling_rate=1.) output_signal = self.quantizer.convert(input_signal) np.testing.assert_almost_equal( np.real(output_signal.samples.flatten()), quantization_levels)
def test_quantization_no_gain_control(self): """ Test correct quantizer output without gain control""" max_amplitude = 100 self.quantizer.gain = Gain(1 / max_amplitude) # randomly choose quantization levels quantization_idx = self.rng.integers( self.quantizer.num_quantization_levels, size=self.num_samples) quantization_step = 2 * max_amplitude / self.quantizer.num_quantization_levels quantization_levels = -max_amplitude + quantization_step * ( quantization_idx + .5) # add random noise within quantization interval random_noise = self.rng.uniform(-quantization_step / 2, quantization_step / 2, size=self.num_samples) input_signal = quantization_levels + random_noise # add saturated values max_quantization_level = max_amplitude - quantization_step / 2 quantization_levels = np.append( quantization_levels, [-max_quantization_level, max_quantization_level]) saturated_level = max_amplitude + 10. input_signal = np.append(input_signal, [-saturated_level, saturated_level]) input_signal = Signal(samples=input_signal, sampling_rate=1.) output_signal = self.quantizer.convert(input_signal) np.testing.assert_almost_equal( np.real(output_signal.samples.flatten()), quantization_levels)
def test_synchronization_offset(self) -> None: """The synchronization offset should be applied properly by adding a delay to the propgated signal.""" self.transmitter.antennas.num_antennas = 1 self.receiver.antennas.num_antennas = 1 mock_generator = Mock() self.random_node._rng = mock_generator for num_samples in self.propagate_signal_lengths: for offset in [0, 1., -10., 100.]: samples = np.random.rand(1, num_samples) + 1j * np.random.rand(1, num_samples) signal_delay = 0.11 signal = Signal(samples, self.sampling_rate, delay=signal_delay) mock_generator.uniform.return_value = 0. instant_signal, _, instant_channel = self.channel.propagate(signal) mock_generator.uniform.return_value = offset offset_signal, _, offset_channel = self.channel.propagate(signal) # The propagated signal should be delayed by the offset, while the CSI does not change assert_array_equal(instant_signal[0].samples, offset_signal[0].samples) assert_array_equal(instant_channel.state, offset_channel.state) self.assertEqual(signal_delay + offset, offset_signal[0].delay)
def transmit(self, duration: float = 0.) -> Tuple[Signal]: silence = Signal(np.zeros( (self.device.num_antennas, self.__num_samples), dtype=complex), sampling_rate=self.sampling_rate, carrier_frequency=self.device.carrier_frequency) self.slot.add_transmission(self, silence) return silence,
def setUp(self) -> None: self.random = default_rng(42) self.num_streams = 3 self.num_samples = 100 self.sampling_rate = 1e4 self.carrier_frequency = 1e3 self.delay = 0. self.samples = (self.random.random( (self.num_streams, self.num_samples)) + 1j * self.random.random( (self.num_streams, self.num_samples))) self.signal = Signal(samples=self.samples, sampling_rate=self.sampling_rate, carrier_frequency=self.carrier_frequency, delay=self.delay)
def test_estimate_noise_power(self, mock_trigger) -> None: """Noise power estimation should return the correct power estimate.""" num_samples = 10000 expected_noise_power = .1 samples = 2 ** -.5 * (self.rng.normal(size=num_samples, scale=expected_noise_power ** .5) + 1j * self.rng.normal(size=num_samples, scale=expected_noise_power ** .5)) signal = Signal(samples, sampling_rate=self.sampling_rate) mock_trigger.side_effect = lambda: [receiver.cache_reception(signal) for receiver in self.device.receivers] noise_power = self.device.estimate_noise_power(num_samples) self.assertAlmostEqual(expected_noise_power, noise_power[0], places=2)
def test_append_streams(self) -> None: """Appending a signal model should yield the proper result.""" samples = self.signal.samples.copy() append_samples = self.signal.samples + 1j append_signal = Signal(append_samples, self.signal.sampling_rate, self.signal.carrier_frequency) self.signal.append_streams(append_signal) assert_array_equal(np.append(samples, append_samples, axis=0), self.signal.samples)
def test_empty(self) -> None: """Using the empty initializer should result in an empty signal model.""" sampling_rate = 2 num_streams = 5 num_samples = 6 empty_signal = Signal.empty(sampling_rate, num_streams=num_streams, num_samples=num_samples) self.assertEqual(sampling_rate, empty_signal.sampling_rate) self.assertEqual(num_samples, empty_signal.num_samples) self.assertEqual(num_streams, empty_signal.num_streams)
def test_no_quantization(self) -> None: """ Test correct quantizer output with infinite resolution""" self.quantizer.num_quantization_bits = 0 input_signal = (np.random.normal(size=self.num_samples) + 1j * np.random.normal(size=self.num_samples)) input_signal = Signal(sampling_rate=1., samples=input_signal) output_signal = self.quantizer.convert(input_signal) np.testing.assert_array_equal(input_signal.samples, output_signal.samples)
def receive(self, signal: Signal) -> None: """Receive a new signal at this physical device. Args: signal (Signal): Signal model to be received. """ # Signal is now a baseband-signal signal.carrier_frequency = 0. # Account for negative calibrations delays by # appending zeros to the signal start if self.__calibration_delay < 0.: num_padding_zeros = int(-self.__calibration_delay * signal.sampling_rate) signal.samples = signal.samples[:, num_padding_zeros:] # Cache reception at each operator for receiver in self.receivers: receiver.cache_reception(signal)
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_add_noise_power(self) -> None: """Added noise should have correct power""" signal = np.zeros(1000000, dtype=complex) powers = np.array([0, 1, 100, 1000]) for expected_noise_power in powers: noisy_signal = Signal(signal, sampling_rate=1.) self.noise.add(noisy_signal, expected_noise_power) noise_power = np.var(noisy_signal.samples) self.assertTrue( abs(noise_power - expected_noise_power) <= (0.001 * expected_noise_power))
def test_channel_state_information(self) -> None: """Propagating over the linear channel state model should return identical results.""" self.transmitter.antennas.num_antennas = 1 self.receiver.antennas.num_antennas = 1 for num_samples in self.propagate_signal_lengths: for gain in self.propagate_signal_gains: samples = np.random.rand(1, num_samples) + 1j * np.random.rand(1, num_samples) signal = Signal(samples, self.sampling_rate) self.channel.gain = gain forwards, backwards, csi = self.channel.propagate(signal, signal) expected_csi_signal = csi.linear[0, 0, ::].todense() @ samples.T assert_array_equal(forwards[0].samples, expected_csi_signal.T) assert_array_equal(backwards[0].samples, expected_csi_signal.T)
def pilot_signal(self) -> Signal: """Samples of the frame's pilot section. Returns: samples (np.ndarray): Pilot samples. """ # Generate single pilot chirp prototype prototypes, _ = self._prototypes() pilot_samples = np.empty(self.samples_in_chirp * self.num_pilot_chirps, dtype=complex) for pilot_idx in range(self.num_pilot_chirps): pilot_samples[pilot_idx*self.samples_in_chirp:(1+pilot_idx)*self.samples_in_chirp] = \ prototypes[pilot_idx % len(prototypes)] return Signal(pilot_samples, self.sampling_rate)
def test_propagate_SISO(self) -> None: """Test valid propagation for the Single-Input-Single-Output channel.""" self.transmitter.antennas.num_antennas = 1 self.receiver.antennas.num_antennas = 1 for num_samples in self.propagate_signal_lengths: for gain in self.propagate_signal_gains: samples = np.random.rand(1, num_samples) + 1j * np.random.rand(1, num_samples) signal = Signal(samples, self.sampling_rate) self.channel.gain = gain expected_propagated_samples = gain * samples forwards_signal, backwards_signal, _ = self.channel.propagate(signal, signal) assert_array_equal(expected_propagated_samples, forwards_signal[0].samples) assert_array_equal(expected_propagated_samples, backwards_signal[0].samples)
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 setUp(self) -> None: self.precoding = SymbolPrecoding() self.equalizer = MMSETimeEqualizer() self.precoding[0] = self.equalizer self.transmitter = Mock() self.transmitter.antennas.num_antennas = 1 self.receiver = Mock() self.receiver.antennas.num_antennas = 1 self.rng = np.random.default_rng(42) self.random_mother = Mock() self.random_mother._rng = self.rng self.num_samples = 1000 self.sampling_rate = 1e6 self.samples = np.exp(2j * self.rng.uniform(0., pi, self.num_samples)) self.signal = Signal(self.samples, self.sampling_rate) self.noise_variances = [0, 1e-4] self.rms_delays = [0., 1e-5]
def modulate(self, data_symbols: Symbols) -> Signal: prototypes, _ = self._prototypes() samples = np.empty(self.samples_in_frame, dtype=complex) sample_idx = 0 samples_in_chirp = self.samples_in_chirp # Add pilot samples pilot_samples = self.pilot_signal.samples.flatten() num_pilot_samples = len(pilot_samples) samples[:num_pilot_samples] = pilot_samples sample_idx += num_pilot_samples # Modulate data symbols for symbol in data_symbols.raw[0, :]: samples[sample_idx:sample_idx + samples_in_chirp] = prototypes[symbol, :] sample_idx += samples_in_chirp return Signal(samples, self.sampling_rate)
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_quantization_mid_tread(self): """ Test correct mid-tread quantizer output without gain control""" max_amplitude = 150. self.quantizer.gain = Gain(1 / max_amplitude) self.quantizer.quantizer_type = QuantizerType.MID_TREAD # randomly choose quantization levels quantization_idx = self.rng.integers( self.quantizer.num_quantization_levels, size=self.num_samples) quantization_step = 2 * max_amplitude / self.quantizer.num_quantization_levels quantization_levels = -max_amplitude + quantization_step * quantization_idx # add random noise within quantization interval random_noise = self.rng.uniform(-quantization_step / 2, quantization_step / 2, size=self.num_samples) input_signal = quantization_levels + random_noise # add saturated values max_quantization_level = max_amplitude - quantization_step min_quantization_level = -max_amplitude quantization_levels = np.append( quantization_levels, [min_quantization_level, max_quantization_level]) saturated_level_pos = max_quantization_level + 10. saturated_level_neg = min_quantization_level - 10. input_signal = np.append(input_signal, [saturated_level_neg, saturated_level_pos]) input_signal = Signal(samples=input_signal, sampling_rate=1.) output_signal = self.quantizer.convert(input_signal) np.testing.assert_almost_equal( np.real(output_signal.samples.flatten()), quantization_levels)
def modulate(self, data_symbols: np.ndarray) -> Signal: return Signal(data_symbols, self.sampling_rate)
class TestSignal(unittest.TestCase): """Test the signal model base class.""" def setUp(self) -> None: self.random = default_rng(42) self.num_streams = 3 self.num_samples = 100 self.sampling_rate = 1e4 self.carrier_frequency = 1e3 self.delay = 0. self.samples = (self.random.random( (self.num_streams, self.num_samples)) + 1j * self.random.random( (self.num_streams, self.num_samples))) self.signal = Signal(samples=self.samples, sampling_rate=self.sampling_rate, carrier_frequency=self.carrier_frequency, delay=self.delay) def test_init(self) -> None: """Initialization arguments should be properly stored as object attributes.""" assert_array_equal(self.samples, self.signal.samples) self.assertEqual(self.sampling_rate, self.signal.sampling_rate) self.assertEqual(self.carrier_frequency, self.signal.carrier_frequency) self.assertEqual(self.delay, self.signal.delay) def test_empty(self) -> None: """Using the empty initializer should result in an empty signal model.""" sampling_rate = 2 num_streams = 5 num_samples = 6 empty_signal = Signal.empty(sampling_rate, num_streams=num_streams, num_samples=num_samples) self.assertEqual(sampling_rate, empty_signal.sampling_rate) self.assertEqual(num_samples, empty_signal.num_samples) self.assertEqual(num_streams, empty_signal.num_streams) def test_samples_setget(self) -> None: """Samples property getter should return setter argument.""" samples = (self.random.random( (self.num_streams + 1, self.num_samples + 1)) + 1j * self.random.random( (self.num_streams + 1, self.num_samples + 1))) self.signal.samples = samples assert_array_equal(samples, self.signal.samples) def test_samples_validation(self) -> None: """Samples property setter should raise ValueError on invalid arguments.""" with self.assertRaises(ValueError): self.signal.samples = self.random.random((1, 2, 3)) def test_num_streams(self) -> None: """Number of streams property should return the correct number of streams.""" self.assertEqual(self.num_streams, self.signal.num_streams) def test_num_samples(self) -> None: """Number of samples property should return the correct number of samples.""" self.assertEqual(self.num_samples, self.signal.num_samples) def test_sampling_rate_setget(self) -> None: """Sampling rate property getter should return setter argument.""" sampling_rate = 1.123e4 self.signal.sampling_rate = sampling_rate self.assertEqual(sampling_rate, self.signal.sampling_rate) def test_sampling_rate_validation(self) -> None: """Sampling rate property setter should raise ValueError on invalid arguments.""" with self.assertRaises(ValueError): self.signal.sampling_rate = -1.23 with self.assertRaises(ValueError): self.signal.sampling_rate = 0. def test_carrier_frequency_setget(self) -> None: """Carrier frequency property getter should return setter argument.""" carrier_frequency = 1.123 self.signal.carrier_frequency = carrier_frequency self.assertEqual(carrier_frequency, self.signal.carrier_frequency) def test_carrier_frequency_validation(self) -> None: """Carrier frequency setter should raise ValueError on invalid arguments.""" with self.assertRaises(ValueError): self.signal.carrier_frequency = -1.0 try: self.signal.carrier_frequency = 0. except ValueError: self.fail() def test_copy(self) -> None: """Copying a signal model should result in a completely independent instance.""" samples = self.signal.samples.copy() signal_copy = self.signal.copy() signal_copy.samples += 1j assert_array_equal(samples, self.signal.samples) def test_resampling_power_up(self) -> None: """Resampling to a higher sampling rate should not affect the signal power.""" # Create an oversampled sinusoid signal frequency = 0.1 * self.sampling_rate self.num_samples = 1000 timestamps = np.arange(self.num_samples) / self.sampling_rate samples = np.outer(np.exp(2j * pi * np.array([0, .33, .66])), np.exp(2j * pi * timestamps * frequency)) self.signal.samples = samples expected_sampling_rate = 2 * self.sampling_rate resampled_signal = self.signal.resample(expected_sampling_rate) expected_power = self.signal.power resampled_power = resampled_signal.power assert_array_almost_equal(expected_power, resampled_power, decimal=3) self.assertEqual(expected_sampling_rate, resampled_signal.sampling_rate) def test_resampling_power_down(self) -> None: """Resampling to a lower sampling rate should not affect the signal power.""" # Create an oversampled sinusoid signal frequency = 0.1 * self.sampling_rate self.num_samples = 1000 timestamps = np.arange(self.num_samples) / self.sampling_rate samples = np.outer(np.exp(2j * pi * np.array([0, .33, .66])), np.exp(2j * pi * timestamps * frequency)) self.signal.samples = samples expected_sampling_rate = .5 * self.sampling_rate resampled_signal = self.signal.resample(expected_sampling_rate) expected_power = self.signal.power resampled_power = resampled_signal.power assert_array_almost_equal(expected_power, resampled_power, decimal=3) self.assertEqual(expected_sampling_rate, resampled_signal.sampling_rate) def test_resampling_circular(self) -> None: """Up- and subsequently down-sampling a signal model should result in the identical signal.""" # Create an oversampled sinusoid signal frequency = 0.3 * self.sampling_rate timestamps = np.arange(self.num_samples) / self.sampling_rate samples = np.outer(np.exp(2j * pi * np.array([0, 0.33, 0.66])), np.exp(2j * pi * timestamps * frequency)) self.signal.samples = samples # Up-sample and down-sample again up_signal = self.signal.resample(1.5 * self.sampling_rate) down_signal = up_signal.resample(self.sampling_rate) # Compare to the initial samples assert_array_almost_equal(samples, down_signal.samples, decimal=1) self.assertEqual(self.sampling_rate, down_signal.sampling_rate) def test_superimpose_power(self) -> None: """Superimposing two signal models should yield approximately the sum of both model's individual power.""" expected_power = 4 * self.signal.power self.signal.superimpose(self.signal) assert_array_almost_equal(expected_power, self.signal.power) def test_timestamps(self) -> None: """Timestamps property should return the correct sampling times.""" expected_timestamps = np.arange(self.num_samples) / self.sampling_rate assert_array_equal(expected_timestamps, self.signal.timestamps) def test_plot(self) -> None: """The plot routine should not raise any exceptions.""" pass def test_append_samples(self) -> None: """Appending a signal model should yield the proper result.""" samples = self.signal.samples.copy() append_samples = self.signal.samples + 1j append_signal = Signal(append_samples, self.signal.sampling_rate, self.signal.carrier_frequency) self.signal.append_samples(append_signal) assert_array_equal(np.append(samples, append_samples, axis=1), self.signal.samples) def test_append_samples_assert(self) -> None: """Appending to a signal model should raise a ValueError if the models don't match.""" with self.assertRaises(ValueError): samples = self.signal.samples[0, :] append_signal = Signal(samples, self.signal.sampling_rate, self.signal.carrier_frequency) self.signal.append_samples(append_signal) with self.assertRaises(ValueError): samples = self.signal.samples append_signal = Signal(samples, self.signal.sampling_rate, 0.) self.signal.append_samples(append_signal) def test_append_streams(self) -> None: """Appending a signal model should yield the proper result.""" samples = self.signal.samples.copy() append_samples = self.signal.samples + 1j append_signal = Signal(append_samples, self.signal.sampling_rate, self.signal.carrier_frequency) self.signal.append_streams(append_signal) assert_array_equal(np.append(samples, append_samples, axis=0), self.signal.samples) def test_append_stream_assert(self) -> None: """Appending to a signal model should raise a ValueError if the models don't match.""" with self.assertRaises(ValueError): samples = self.signal.samples[:, 0] append_signal = Signal(samples, self.signal.sampling_rate, self.signal.carrier_frequency) self.signal.append_streams(append_signal) with self.assertRaises(ValueError): samples = self.signal.samples append_signal = Signal(samples, self.signal.sampling_rate, 0.) self.signal.append_streams(append_signal)
def transmit(self, duration: float = 0.) -> Tuple[Signal, Symbols, np.ndarray]: """Returns an array with the complex base-band samples of a waveform generator. The signal may be distorted by RF impairments. Args: duration (float, optional): Length of signal in seconds. Returns: transmissions (tuple): signal (Signal): Signal model carrying the `data_bits` in multiple streams, each stream encoding multiple radio frequency band communication frames. symbols (Symbols): Symbols to which `data_bits` were mapped, used to modulate `signal`. data_bits (np.ndarray): Vector of bits mapped to `data_symbols`. """ # By default, the drop duration will be exactly one frame if duration <= 0.: duration = self.frame_duration # Number of data symbols per transmitted frame symbols_per_frame = self.waveform_generator.symbols_per_frame # Number of frames fitting into the selected drop duration frames_per_stream = int(duration / self.waveform_generator.frame_duration) # Generate data bits data_bits = self.generate_data_bits() # Number of code bits required to generate all frames for all streams num_code_bits = int(self.waveform_generator.bits_per_frame * frames_per_stream / self.precoding.rate) # Encode the data bits code_bits = self.encoder_manager.encode(data_bits, num_code_bits) # Map data bits to symbols symbols = self.waveform_generator.map(code_bits) # Apply symbol precoding symbol_streams = Symbols(self.precoding.encode(symbols.raw)) # Check that the number of symbol streams matches the number of required symbol streams if symbol_streams.num_streams != self.num_streams: raise RuntimeError("Invalid precoding configuration, the number of resulting streams does not " "match the number of transmit antennas") # Generate a dedicated base-band signal for each symbol stream signal = Signal(np.empty((0, 0), dtype=complex), self.waveform_generator.sampling_rate) for stream_idx, stream_symbols in enumerate(symbol_streams): stream_signal = Signal(np.empty((0, 0), dtype=complex), self.waveform_generator.sampling_rate) for frame_idx in range(frames_per_stream): data_symbols = stream_symbols[frame_idx*symbols_per_frame:(1+frame_idx)*symbols_per_frame] frame_signal = self.waveform_generator.modulate(data_symbols) stream_signal.append_samples(frame_signal) signal.append_streams(stream_signal) # Apply stream coding, for instance beam-forming # TODO: Not yet supported. # Change the signal carrier # signal.carrier_frequency = self.carrier_frequency # Transmit signal over the occupied device slot (if the modem is attached to a device) if self._transmitter.attached: self._transmitter.slot.add_transmission(self._transmitter, signal) # Cache transmissions self.__transmitted_bits = data_bits self.__transmitted_symbols = symbols # We're finally done, blow the fanfares, throw confetti, etc. return signal, symbols, data_bits
def calibrate(self, max_delay: float, calibration_file: str, num_iterations: int = 10, wait: float = 0.) -> plt.Figure: """Calibrate the hardware. Currently the calibration will only compensate possible time-delays. Ideally, the transmit and receive channels of the device should be connected by a patch cable. WARNING: An attenuator element may be required! Be careful!!!! Args: max_delay (float): The maximum expected delay which the calibration should compensate for in seconds. calibration_file (str): Location of the calibration dump within the filesystem. num_iterations (int, optional): Number of calibration iterations. Default is 10. wait (float, optional): Idle time between iteration transmissions in seconds. Returns: plt.Figure: A figure of information regarding the calibration. """ if num_iterations < 1: raise ValueError( "The number of iterations must be greater or equal to one") if wait < 0.: raise ValueError( "The waiting time must be greater or equal to zero") # Clear transmit caches self.transmitters.clear_cache() num_samples = int(2 * max_delay * self.sampling_rate) if num_samples <= 1: raise ValueError( "The assumed maximum delay is not resolvable by the configured sampling rate" ) # Register operators calibration_transmitter = SignalTransmitter(num_samples) self.transmitters.add(calibration_transmitter) calibration_receiver = SignalReceiver(num_samples) self.receivers.add(calibration_receiver) dirac_index = int(max_delay * self.sampling_rate) waveform = np.zeros((self.num_antennas, num_samples), dtype=complex) waveform[:, dirac_index] = 1. calibration_signal = Signal(waveform, self.sampling_rate, self.carrier_frequency) propagated_signals: List[Signal] = [] propagated_dirac_indices = np.empty(num_iterations, dtype=int) # Make multiple iteration calls for calibration for i in range(num_iterations): # Send and receive calibration waveform calibration_transmitter.transmit(calibration_signal) self.trigger() propagated_signal = calibration_receiver.receive() # Infer the implicit delay by estimating the sample index of the propagated dirac propagated_signals.append(propagated_signal) propagated_dirac_indices = np.argmax( abs(propagated_signal.samples[0, :])) # Wait the configured amount of time between iterations sleep(wait) # Un-register operators self.transmitters.remove(calibration_transmitter) self.receivers.remove(calibration_receiver) # Compute calibration delay mean_dirac_index = np.mean( propagated_dirac_indices) # This is just a ML estimation calibration_delay = ( dirac_index - mean_dirac_index) / propagated_signal.sampling_rate # Dump calibration to pickle savefile with open(calibration_file, 'wb') as file_stream: dump(calibration_delay, file_stream) # Save register calibration delay internally self.__calibration_delay = calibration_delay # Visualize the results fig, axes = plt.subplots(2, 1) fig.suptitle('Device Delay Calibration') axes[0].plot(calibration_signal.timestamps, abs(calibration_signal.samples[0, :])) for sig in propagated_signals: axes[1].plot(sig.timestamps, abs(sig.samples[0, :]), color='blue') axes[1].axvline(x=(dirac_index / self.sampling_rate - calibration_delay), color='red')