def decode( self, symbol_stream: np.ndarray, channel_state: ChannelStateInformation, stream_noises: np.ndarray ) -> Tuple[np.ndarray, ChannelStateInformation, np.ndarray]: for time_idx, (symbols, csi, noise) in enumerate( zip(symbol_stream.T, channel_state.samples(), stream_noises.T)): # Combine the responses of all superimposed transmit antennas for equalization transform = np.sum(csi.linear[:, :, 0, :], axis=2, keepdims=False) # Compute the pseudo-inverse from the singular-value-decomposition of the linear channel transform # noinspection PyTupleAssignmentBalance u, s, vh = svd(transform.todense(), full_matrices=False, check_finite=False) u /= s equalizer = (u @ vh).conj().T symbol_stream[:, time_idx] = equalizer @ symbols channel_state.state[:, :, time_idx, :] = np.tensordot(equalizer, csi.linear[:, :, 0, :], axes=(1, 0)) stream_noises[:, time_idx] = noise * s return symbol_stream, channel_state, stream_noises
def test_synchronize_validation(self) -> None: """Synchronization should raise a ValueError if the signal shape does match the stream response shape.""" with self.assertRaises(ValueError): _ = self.waveform_generator.synchronization.synchronize( np.zeros(10), ChannelStateInformation(ChannelStateFormat.IMPULSE_RESPONSE, np.zeros((10, 2)))) with self.assertRaises(ValueError): _ = self.waveform_generator.synchronization.synchronize( np.zeros((10, 2)), ChannelStateInformation(ChannelStateFormat.IMPULSE_RESPONSE, np.zeros((10, 2))))
def test_synchronization(self) -> None: """Synchronization should properly partition signal samples into frame sections.""" # Generate frame signal models num_samples = 2 * self.max_offset + self.num_frames * self.waveform.samples_in_frame csi = ChannelStateInformation.Ideal(num_samples) samples = np.zeros((1, num_samples), dtype=complex) expected_frames = [] pilot_indices = self.rng.integers( 0, self.max_offset, self.num_frames) + np.arange( self.num_frames) * self.waveform.samples_in_frame for p in pilot_indices: data_symbols = Symbols( self.rng.integers(0, self.waveform.modulation_order, self.waveform.symbols_per_frame)) signal_samples = self.waveform.modulate(data_symbols).samples samples[:, p:p + self.waveform.samples_in_frame] += signal_samples expected_frames.append(samples[:, p:p + self.waveform.samples_in_frame]) synchronized_frames = self.synchronization.synchronize(samples, csi) if len(synchronized_frames) != len(expected_frames): self.fail() for expected_frame, (synchronized_frame, _) in zip(expected_frames, synchronized_frames): assert_array_equal(expected_frame, synchronized_frame)
def test_synchronize(self) -> None: """Test the proper estimation of delays during Schmidl-Cox synchronization""" for d, n in product(self.delays_in_samples, self.num_frames): symbols = np.exp( 2j * pi * self.rng.uniform(0, 1, (n, self.frame.symbols_per_frame))) frames = [ np.exp(2j * pi * self.rng.uniform(0, 1, (self.num_streams, 1))) @ self.frame.modulate(Symbols(symbols[f, :])).samples for f in range(n) ] signal = np.empty((self.num_streams, 0), dtype=complex) for frame in frames: signal = np.concatenate( (signal, np.zeros( (self.num_streams, d), dtype=complex), frame), axis=1) channel_state = ChannelStateInformation.Ideal( len(signal), self.num_streams) synchronization = self.synchronization.synchronize( signal, channel_state) self.assertEqual(n, len(synchronization)) for frame, (synchronized_frame, _) in zip(frames, synchronization): assert_array_equal(frame, synchronized_frame)
def test_synchronize(self) -> None: """Default synchronization routine should properly split signals into frame-sections.""" num_streams = 3 num_samples_test = [50, 100, 150, 200] for num_samples in num_samples_test: signal = np.exp( 2j * self.rnd.uniform(0, pi, (num_streams, 1))) @ np.exp( 2j * self.rnd.uniform(0, pi, (1, num_samples))) channel_state = ChannelStateInformation.Ideal( num_samples, num_streams) synchronized_frames = self.waveform_generator.synchronization.synchronize( signal, channel_state) # Number of frames is the number of frames that fit into the samples num_frames = len(synchronized_frames) expected_num_frames = int( floor(num_samples / self.waveform_generator.samples_in_frame)) self.assertEqual(expected_num_frames, num_frames) # Frames and channel states should each contain the correct amount of samples for frame_signal, frame_channel_state in synchronized_frames: self.assertEqual(num_streams, frame_signal.shape[0]) self.assertEqual(self.waveform_generator.samples_in_frame, frame_signal.shape[1]) self.assertEqual(self.waveform_generator.samples_in_frame, frame_channel_state.num_samples)
def decode( self, symbol_stream: np.ndarray, channel_state: ChannelStateInformation, stream_noises: np.ndarray ) -> Tuple[np.ndarray, ChannelStateInformation, np.ndarray]: equalized_symbols = np.empty( (channel_state.num_receive_streams, channel_state.num_samples), dtype=complex) equalized_noises = np.empty( (channel_state.num_receive_streams, channel_state.num_samples), dtype=float) equalized_channel_state = ChannelStateInformation( channel_state.state_format) # Equalize in space in a first step for idx, (symbols, stream_state, noise) in enumerate( zip(symbol_stream, channel_state.received_streams(), stream_noises)): # Combine the responses of all superimposed transmit antennas for equalization linear_state = stream_state.linear transform = np.sum(linear_state[0, ::], axis=0, keepdims=False) # Compute the pseudo-inverse from the singular-value-decomposition of the linear channel transform # noinspection PyTupleAssignmentBalance u, s, vh = svd(transform.todense(), full_matrices=False, check_finite=False) u /= s equalizer = (u @ vh).conj().T equalized_symbols[idx, :] = equalizer @ symbols equalized_csi_slice = tensordot(equalizer, linear_state, axes=(1, 2)).transpose( (1, 2, 0, 3)) equalized_channel_state.append_linear(equalized_csi_slice, 0) equalized_noises[idx, :] = noise[:stream_state.num_samples] * s return equalized_symbols, channel_state, equalized_noises
def test_modulate_demodulate_no_dc_suppression(self) -> None: """Modulating and subsequently de-modulating an OFDM symbol section should yield correct symbols.""" expected_symbols = np.exp( 2j * self.rnd.uniform(0, pi, self.section.num_symbols)) modulated_signal = self.section.modulate(expected_symbols) channel_state = ChannelStateInformation.Ideal( num_samples=modulated_signal.shape[0]) ofdm_grid, _ = self.section.demodulate(modulated_signal, channel_state) symbols = ofdm_grid.T[self.section.resource_mask[ ElementType.DATA.value].T] assert_array_almost_equal(expected_symbols, symbols)
def test_encode_decode_circular(self) -> None: """Encoding and subsequently decoding a data stream should lead to identical symbols.""" input_stream = self.generator.random((1, 400)) channel_state = ChannelStateInformation( ChannelStateFormat.IMPULSE_RESPONSE, self.generator.random((4, 1, 100, 1))) stream_noise = self.generator.random((4, 100)) encoded_stream = self.precoder.encode(input_stream) decoded_stream, decoded_responses, _ = self.precoder.decode( encoded_stream, channel_state, stream_noise) assert_array_equal(input_stream, decoded_stream)
def decode( self, symbol_stream: np.ndarray, channel_state: ChannelStateInformation, stream_noises: np.ndarray ) -> Tuple[np.ndarray, ChannelStateInformation, np.ndarray]: # Collect data symbols from the stream symbol_stream = np.reshape(symbol_stream, (1, -1)) # 'F' stream_noises = np.reshape(stream_noises, (1, -1)) # 'F' state = channel_state.state channel_state.state = state.reshape( (1, state.shape[1], -1, state.shape[3])) # 'F' return symbol_stream, channel_state, stream_noises
def test_modulate_demodulate(self) -> None: """Modulating and subsequently de-modulating a data frame should yield identical symbols.""" expected_symbols = Symbols( np.exp(2j * self.rng.uniform(0, pi, self.generator.symbols_per_frame)) * np.arange(1, 1 + self.generator.symbols_per_frame)) baseband_signal = self.generator.modulate(expected_symbols) channel_state = ChannelStateInformation.Ideal( num_samples=baseband_signal.num_samples) symbols, _, _ = self.generator.demodulate( baseband_signal.samples[0, :], channel_state) assert_array_almost_equal(expected_symbols.raw, symbols.raw)
def test_synchronize(self) -> None: """Default synchronization should properly split signals into frame-sections.""" num_streams = 3 num_frames = 5 num_offset_samples = 2 num_samples = num_frames * self.waveform_generator.samples_in_frame + num_offset_samples signal = np.exp( 2j * self.rng.uniform(0, pi, (num_streams, 1))) @ np.exp( 2j * self.rng.uniform(0, pi, (1, num_samples))) csi = ChannelStateInformation.Ideal(num_samples, num_streams) frames = self.synchronization.synchronize(signal, csi) self.assertEqual(num_frames, len(frames))
def test_reference_based_channel_estimation(self) -> None: """Reference-based channel estimation should properly estimate channel at reference points.""" self.generator.channel_estimation_algorithm = ChannelEstimation.REFERENCE expected_bits = self.rng.integers(0, 2, self.generator.bits_per_frame) expected_symbols = self.generator.map(expected_bits) signal = self.generator.modulate(expected_symbols) expected_csi = ChannelStateInformation.Ideal(signal.num_samples) symbols, csi, _ = self.generator.demodulate(signal.samples[0, :], expected_csi) assert_array_almost_equal(np.ones(csi.state.shape, dtype=complex), csi.state)
def test_rx_signal_properly_demodulated(self) -> None: """Verify the proper demodulation of received signals.""" rx_signal = self.__read_saved_results_from_file('rx_signal.npy') channel_state = ChannelStateInformation.Ideal( num_samples=rx_signal.shape[0]) noise_variance = 0.0 received_symbols, _, _ = self.generator.demodulate( rx_signal, channel_state, noise_variance) received_bits = self.generator.unmap(received_symbols) received_bits_expected = self.__read_saved_results_from_file( 'received_bits.npy').ravel() np.testing.assert_array_equal( received_bits[:len(received_bits_expected)], received_bits_expected)
def test_decode_noiseless(self) -> None: """Decode should properly equalize the provided stream response in the absence of noise.""" num_samples = 100 num_streams = 4 expected_symbols = np.exp( 1j * self.generator.uniform(0, 2 * pi, (num_streams, num_samples))) stream_responses = np.exp( 1j * self.generator.uniform(0, 2 * pi, (num_streams, num_streams, num_samples, 1))) channel_state = ChannelStateInformation( ChannelStateFormat.IMPULSE_RESPONSE, stream_responses) propagated_symbol_stream = np.empty((num_streams, num_samples), dtype=complex) expected_equalized_responses = np.zeros( (num_streams, num_streams, num_samples, 1), dtype=complex) for symbol_idx, (response, symbol) in enumerate( zip(stream_responses.transpose((2, 0, 1, 3)), expected_symbols.T)): propagated_symbol_stream[:, symbol_idx] = response[:, :, 0] @ symbol expected_equalized_responses[:, :, symbol_idx, 0] = np.identity(num_streams, dtype=complex) stream_noises = np.zeros((num_streams, num_samples), dtype=float) equalized_symbols, equalized_csi, equalized_noises = self.precoder.decode( propagated_symbol_stream, channel_state, stream_noises) assert_array_almost_equal(expected_symbols, equalized_symbols) assert_array_almost_equal(expected_equalized_responses, equalized_csi.state) assert_array_almost_equal( equalized_noises, np.zeros((num_streams, num_samples), dtype=float))
def demodulate( self, baseband_signal: np.ndarray, channel_state: ChannelStateInformation, noise_variance: float ) -> Tuple[Symbols, ChannelStateInformation, np.ndarray]: # Assess number of frames contained within this signal samples_in_chirp = self.samples_in_chirp samples_in_pilot_section = samples_in_chirp * self.num_pilot_chirps prototypes, _ = self._prototypes() data_frame = baseband_signal[samples_in_pilot_section:] symbol_signals = data_frame.reshape(-1, self.samples_in_chirp) symbol_metrics = abs(symbol_signals @ prototypes.T.conj()) # ToDo: Unfortunately the demodulation-scheme is non-linear. Is there a better way? symbols = np.argmax(symbol_metrics, axis=1) channel_state = ChannelStateInformation.Ideal( len(symbols), channel_state.num_transmit_streams, channel_state.num_receive_streams) noises = np.repeat(noise_variance, self.num_data_chirps) return Symbols(symbols), channel_state, noises
def test_decode_validation(self) -> None: """Decoding should result in a RuntimeError, if multiple streams result.""" num_samples = 10 num_streams = 2 precoder = Mock() precoder.decode = lambda symbols, channels, noise: (symbols, channels, noise) self.precoding[0] = precoder input_symbols = self.generator.random((num_streams, num_samples)) input_channel = ChannelStateInformation.Ideal( num_samples=num_samples, num_receive_streams=num_streams) input_noise = self.generator.random((num_streams, num_samples)) with self.assertRaises(RuntimeError): _ = self.precoding.decode(input_symbols, input_channel, input_noise) with self.assertRaises(ValueError): _ = self.precoding.decode(input_symbols[0, :], input_channel, input_noise)
def receive(self) -> Tuple[Signal, Symbols, np.ndarray]: signal = self._receiver.signal.resample(self.waveform_generator.sampling_rate) if signal is None: raise RuntimeError("No signal received by modem") # signal = Signal.empty(sampling_rate=self.device.sampling_rate) csi = self._receiver.csi if csi is None: csi = ChannelStateInformation.Ideal(signal.num_samples) # Workaround for non-matching csi and signal model pairs elif signal.num_samples > (csi.num_samples + csi.num_delay_taps - 1): csi = ChannelStateInformation.Ideal(signal.num_samples) # Pull signal and channel state from the registered device slot noise_power = signal.noise_power num_samples = signal.num_samples # Number of frames within the received samples frames_per_stream = int(floor(num_samples / self.waveform_generator.samples_in_frame)) # 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) # Data bits required by the bit encoder to generate the input bits for the waveform generator num_data_bits = self.encoder_manager.required_num_data_bits(num_code_bits) # Apply stream decoding, for instance beam-forming # TODO: Not yet supported. # Synchronize all streams into frames synchronized_frames = self.waveform_generator.synchronization.synchronize(signal.samples, csi) # Abort at this point if no frames have been detected if len(synchronized_frames) < 1: return signal, Symbols(), np.empty(0, dtype=complex) # Demodulate signal frame by frame decoded_raw_symbols = np.empty(0, dtype=complex) for frame_samples, frame_csi in synchronized_frames: stream_symbols: List[np.ndarray] = [] stream_csis: List[ChannelStateInformation] = [] stream_noises: List[np.ndarray] = [] # Demodulate each stream within each frame independently for stream_samples, stream_csi in zip(frame_samples, frame_csi.received_streams()): symbols, csi, noise_powers = self.waveform_generator.demodulate(stream_samples, stream_csi, noise_power) stream_symbols.append(symbols.raw) stream_csis.append(csi) stream_noises.append(noise_powers) frame_symbols = np.array(stream_symbols, dtype=complex) frame_csi = ChannelStateInformation.concatenate(stream_csis, ChannelStateDimension.RECEIVE_STREAMS) frame_noises = np.array(stream_noises, dtype=float) decoded_frame_symbols = self.precoding.decode(frame_symbols, frame_csi, frame_noises) decoded_raw_symbols = np.append(decoded_raw_symbols, decoded_frame_symbols) # Convert decoded symbols to from array to symbols decoded_symbols = Symbols(decoded_raw_symbols) # Map the symbols to code bits code_bits = self.waveform_generator.unmap(decoded_symbols) # Decode the coded bit stream to plain data bits data_bits = self.encoder_manager.decode(code_bits, num_data_bits) # Cache receptions self.__received_bits = data_bits self.__received_symbols = decoded_symbols # We're finally done, blow the fanfares, throw confetti, etc. return signal, decoded_symbols, data_bits
def test_demodulate(self) -> None: """Demodulation should return an empty tuple.""" _ = self.section.demodulate(np.empty(0, dtype=complex), ChannelStateInformation.Ideal(0))