def setUp(self) -> None: # Random rng self.generator = np.random.default_rng(42) # Mock modem self.modem = Mock() # Precoder to be tested self.precoding = SymbolPrecoding(modem=self.modem)
class TestSymbolPrecoding(unittest.TestCase): def setUp(self) -> None: # Random rng self.generator = np.random.default_rng(42) # Mock modem self.modem = Mock() # Precoder to be tested self.precoding = SymbolPrecoding(modem=self.modem) def test_init(self) -> None: """Object init arguments should be properly stored as class attributes.""" self.assertIs(self.modem, self.precoding.modem) def test_rate(self) -> None: """Rate should be the multiplication of all precoder-rates.""" precoder_alpha = Mock() precoder_beta = Mock() precoder_alpha.rate = Fraction(1, 2) precoder_beta.rate = Fraction(1, 6) self.precoding[0] = precoder_alpha self.precoding[1] = precoder_beta expected_rate = precoder_alpha.rate * precoder_beta.rate self.assertEqual(expected_rate, self.precoding.rate) 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 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 __init__(self, encoding: Optional[EncoderManager] = None, precoding: Optional[SymbolPrecoding] = None, waveform: Optional[WaveformGenerator] = None, seed: Optional[int] = None) -> None: """ Args: encoding (EncoderManager, optional): Bit coding configuration. Encodes communication bit frames during transmission and decodes them during reception. precoding (SymbolPrecoding, optional): Modulation symbol coding configuration. waveform (WaveformGenerator, optional): The waveform to be transmitted by this modem. seed (int, optional): Seed used to initialize the pseudo-random number generator. """ # Base class initialization RandomNode.__init__(self, seed=seed) DuplexOperator.__init__(self) self.__encoder_manager = EncoderManager() self.__precoding = SymbolPrecoding(modem=self) self.__waveform_generator = None self.__power = 1.0 self.__transmitted_bits = np.empty(0, dtype=int) self.__transmitted_symbols = Symbols() self.__received_bits = np.empty(0, dtype=int) self.__received_symbols = Symbols() self.bits_source = RandomBitsSource() self.encoder_manager = EncoderManager() if encoding is None else encoding self.precoding = SymbolPrecoding(modem=self) if precoding is None else precoding self.waveform_generator = waveform
def setUp(self) -> None: self.random_generator = rnd.default_rng(42) self.random_node = Mock() self.random_node._rng = self.random_generator self.device = SimulatedDevice() self.encoding = EncoderManager() self.precoding = SymbolPrecoding() self.waveform = MockWaveformGenerator(oversampling_factor=4) self.modem = Modem(encoding=self.encoding, precoding=self.precoding, waveform=self.waveform) self.modem.device = self.device self.modem.random_mother = self.random_node
def setUp(self) -> None: self.precoding = SymbolPrecoding() self.equalizer = ZFSpaceEqualizer() 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 = 100 self.sampling_rate = 1e6 self.num_antennas = [1, 2, 4]
class TestMeanSquareTimeEqualization(unittest.TestCase): """Test Minimum-Mean-Square-Error channel equalization in time-domain.""" 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 test_equalize_5GTDL(self) -> None: """Test equalization of 5GTDL multipath fading channels.""" for model_type, rms_delay, noise_variance in product( MultipathFading5GTDL.TYPE, self.rms_delays, self.noise_variances): channel = MultipathFading5GTDL(model_type=model_type, rms_delay=rms_delay, transmitter=self.transmitter, receiver=self.receiver) channel.random_mother = self.random_mother propagated_signal, _, channel_state = channel.propagate( self.signal) noise = (self.rng.normal(0., noise_variance**.5, propagated_signal[0].samples.shape) + 1j * self.rng.normal(0., noise_variance**.5, propagated_signal[0].samples.shape) ) * 2**-.5 noisy_signal = propagated_signal[0].samples + noise signal_power = np.var(self.signal.samples) equalized_signal = self.precoding.decode(noisy_signal, channel_state, noise_variance) equalized_signal_power = np.var(equalized_signal) self.assertAlmostEqual(signal_power, equalized_signal_power, places=1) def test_equalize_Cost256(self) -> None: """Test equalization of 5GTDL multipath fading channels.""" for model_type, noise_variance in product(MultipathFadingCost256.TYPE, self.noise_variances): channel = MultipathFadingCost256(model_type=model_type, transmitter=self.transmitter, receiver=self.receiver) channel.random_mother = self.random_mother propagated_signal, _, channel_state = channel.propagate( self.signal) noise = (self.rng.normal(0., noise_variance**.5, propagated_signal[0].samples.shape) + 1j * self.rng.normal(0., noise_variance**.5, propagated_signal[0].samples.shape) ) * 2**-.5 noisy_signal = propagated_signal[0].samples + noise signal_power = np.var(self.signal.samples) equalized_signal = self.precoding.decode(noisy_signal, channel_state, noise_variance) equalized_signal_power = np.var(equalized_signal) self.assertAlmostEqual(signal_power, equalized_signal_power, places=1)
class TestZeroForcingTimeEqualization(unittest.TestCase): """Test Zero-Forcing channel equalization in time-domain.""" def setUp(self) -> None: self.precoding = SymbolPrecoding() self.equalizer = ZFTimeEqualizer() self.precoding[0] = self.equalizer self.transmitter = Mock() self.transmitter.antennas.num_antennas = 1 self.receiver = Mock() self.receiver.antennas.num_antennas = 1 self.generator = np.random.default_rng(42) self.random_mother = Mock() self.random_mother._rng = self.generator self.num_samples = 100 self.sampling_rate = 1e6 self.samples = np.exp(2j * self.generator.uniform(0., pi, self.num_samples)) self.signal = Signal(self.samples, self.sampling_rate) self.rms_delays = [0., 1e-6, 1e-5] def test_equalize_5GTDL(self) -> None: """Test equalization of 5GTDL multipath fading channels.""" for model_type, rms_delay in product(MultipathFading5GTDL.TYPE, self.rms_delays): channel = MultipathFading5GTDL(model_type=model_type, rms_delay=rms_delay, transmitter=self.transmitter, receiver=self.receiver) channel.random_mother = self.random_mother propagated_signal, _, channel_state = channel.propagate( self.signal) expected_propagated_samples = channel_state.linear[ 0, 0, :, :].todense() @ self.samples assert_array_almost_equal(expected_propagated_samples, propagated_signal[0].samples[0, :]) equalized_signal = self.precoding.decode( propagated_signal[0].samples, channel_state, 0.) assert_array_almost_equal(self.samples, equalized_signal) def test_equalize_Cost256(self) -> None: """Test equalization of 5GTDL multipath fading channels.""" for model_type in MultipathFadingCost256.TYPE: channel = MultipathFadingCost256(model_type=model_type, transmitter=self.transmitter, receiver=self.receiver) channel.random_mother = self.random_mother propagated_signal, _, channel_state = channel.propagate( self.signal) expected_propagated_samples = channel_state.linear[ 0, 0, :, :].todense() @ self.samples assert_array_almost_equal(expected_propagated_samples, propagated_signal[0].samples[0, :]) equalized_signal = self.precoding.decode( propagated_signal[0].samples, channel_state, 0.) assert_array_almost_equal(self.samples, equalized_signal)
class Modem(RandomNode, DuplexOperator, SerializableArray): """HermesPy representation of a wireless communication modem. Modems may transmit or receive information in form of bit streams. In HermesPy, a modem is the basis of every simulation entity which may transmit or receive electromagnetic waveforms. The modem consists of an analog RF chain, a waveform generator, and can be used either for transmission or reception of a given technology. """ yaml_tag = u'Modem' """YAML serialization tag.""" __encoder_manager: EncoderManager __precoding: SymbolPrecoding __waveform_generator: Optional[WaveformGenerator] __bits_source: BitsSource __transmitted_bits: np.ndarray # Cache of recently transmitted bits __transmitted_symbols: Symbols # Cache of recently transmitted symbols __received_bits: np.ndarray # Cache of recently received bits __received_symbols: Symbols # Cache of recently received symbols def __init__(self, encoding: Optional[EncoderManager] = None, precoding: Optional[SymbolPrecoding] = None, waveform: Optional[WaveformGenerator] = None, seed: Optional[int] = None) -> None: """ Args: encoding (EncoderManager, optional): Bit coding configuration. Encodes communication bit frames during transmission and decodes them during reception. precoding (SymbolPrecoding, optional): Modulation symbol coding configuration. waveform (WaveformGenerator, optional): The waveform to be transmitted by this modem. seed (int, optional): Seed used to initialize the pseudo-random number generator. """ # Base class initialization RandomNode.__init__(self, seed=seed) DuplexOperator.__init__(self) self.__encoder_manager = EncoderManager() self.__precoding = SymbolPrecoding(modem=self) self.__waveform_generator = None self.__power = 1.0 self.__transmitted_bits = np.empty(0, dtype=int) self.__transmitted_symbols = Symbols() self.__received_bits = np.empty(0, dtype=int) self.__received_symbols = Symbols() self.bits_source = RandomBitsSource() self.encoder_manager = EncoderManager() if encoding is None else encoding self.precoding = SymbolPrecoding(modem=self) if precoding is None else precoding self.waveform_generator = waveform 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 @property def transmitted_bits(self) -> np.ndarray: """Recently transmitted data bits. Returns: np.ndarray: Numpy array of recently transmitted data bits. """ return self.__transmitted_bits.copy() @property def transmitted_symbols(self) -> Symbols: """Recently transmitted modulation symbols. Returns: Symbols: Recently transmitted symbol series. """ return self.__transmitted_symbols.copy() 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 @property def received_bits(self) -> np.ndarray: """Recently received data bits. Returns: np.ndarray: Numpy array of recently received data bits. """ return self.__received_bits.copy() @property def received_symbols(self) -> Symbols: """Recently received modulation symbols. Returns: Symbols: Recently received symbol series. """ return self.__received_symbols.copy() @property def num_streams(self) -> int: """The number of data streams handled by the modem. The number of data streams is always less or equal to the number of available antennas `num_antennas`. Returns: int: The number of data streams generated by the modem. """ # For now, stream compression will not be supported return self.device.num_antennas @property def bits_source(self) -> BitsSource: """Source of bits transmitted over the modem. Returns: bits_source (BitsSource): Handle to the bits source. """ return self.__bits_source @bits_source.setter def bits_source(self, value: BitsSource) -> None: """Set the source of bits transmitted over the modem""" self.__bits_source = value self.__bits_source.random_mother = self @property def encoder_manager(self) -> EncoderManager: """Access the modem's encoder management. Returns: EncoderManager: Handle to the modem's encoder instance. """ return self.__encoder_manager @encoder_manager.setter def encoder_manager(self, new_manager: EncoderManager) -> None: """Update the modem's encoder management. Args: new_manager (EncoderManger): The new encoder manager. """ self.__encoder_manager = new_manager new_manager.modem = self @property def waveform_generator(self) -> WaveformGenerator: """Communication waveform emitted by this modem. Returns: WaveformGenerator: Handle to the modem's `WaveformGenerator` instance. """ return self.__waveform_generator @waveform_generator.setter def waveform_generator(self, value: Optional[WaveformGenerator]) -> None: """Set the communication waveform emitted by this modem.""" self.__waveform_generator = value if value is not None: value.modem = self @property def precoding(self) -> SymbolPrecoding: """Access this modem's precoding configuration. Returns: SymbolPrecoding: Handle to the configuration. """ return self.__precoding @precoding.setter def precoding(self, coding: SymbolPrecoding) -> None: """Modify the modem's precoding configuration. Args: coding (SymbolPrecoding): The new precoding configuration. """ self.__precoding = coding self.__precoding.modem = self @property def num_data_bits_per_frame(self) -> int: """Compute the number of required data bits to generate a single frame. Returns: int: The number of data bits. """ num_code_bits = self.waveform_generator.bits_per_frame return self.encoder_manager.required_num_data_bits(num_code_bits) @property def frame_duration(self) -> float: return self.waveform_generator.frame_duration @property def sampling_rate(self) -> float: return self.waveform_generator.sampling_rate @property def energy(self) -> float: if self.waveform_generator is None: return 0. return self.waveform_generator.bit_energy def generate_data_bits(self) -> np.ndarray: """Generate data bits required to build a single transmit data frame for this modem. Returns: numpy.ndarray: A vector of hard data bits in 0/1 format. """ num_bits = int(self.num_data_bits_per_frame / self.precoding.rate) bits = self.bits_source.generate_bits(num_bits) return bits @classmethod def to_yaml(cls: Type[Modem], representer: SafeRepresenter, node: Modem) -> MappingNode: """Serialize a `Modem` object to YAML. Args: representer (Modem): A handle to a representer used to generate valid YAML code. The representer gets passed down the serialization tree to each node. node (Modem): The `Device` instance to be serialized. Returns: MappingNode: The serialized YAML node. """ state = {} if len(node.__encoder_manager.encoders) > 0: state['Encoding'] = node.__encoder_manager if len(node.__precoding) > 0: state['Precoding'] = node.__precoding if node.__waveform_generator is not None: state['Waveform'] = node.__waveform_generator return representer.represent_mapping(cls.yaml_tag, state) @classmethod def from_yaml(cls: Type[Modem], constructor: SafeConstructor, node: MappingNode) -> Modem: """Recall a new `Modem` class instance from YAML. Args: constructor (SafeConstructor): A handle to the constructor extracting the YAML information. node (MappingNode): YAML node representing the `Modem` serialization. Returns: Modem: Newly created serializable instance. """ state = constructor.construct_mapping(node, deep=True) encoding: List[Encoder] = state.pop('Encoding', []) precoding: List[SymbolPrecoder] = state.pop('Precoding', []) waveform: Optional[WaveformGenerator] = state.pop('Waveform', None) modem = cls.InitializationWrapper(state) for encoder in encoding: modem.encoder_manager.add_encoder(encoder) for precoder_idx, precoder in enumerate(precoding): modem.precoding[precoder_idx] = precoder if waveform is not None: modem.waveform_generator = waveform return modem