def setUp(self) -> None: self.modem = Mock() self.encoder_alpha = StubEncoder(Mock(), 64) self.encoder_beta = StubEncoder(Mock(), 16) self.encoder_manager = EncoderManager(self.modem) self.rng = np.random.default_rng(42)
def test_to_yaml(self) -> None: """Serialization to YAML.""" safe_representer = SafeRepresenter() node = EncoderManager.to_yaml(safe_representer, self.encoder_manager) self.assertEqual(node.value, 'null', "YAML serialization produced unexpected result")
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
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
class TestEncoderManager(TestCase): """Test the `EncoderManager`, responsible for configuring arbitrary channel encodings.""" def setUp(self) -> None: self.modem = Mock() self.encoder_alpha = StubEncoder(Mock(), 64) self.encoder_beta = StubEncoder(Mock(), 16) self.encoder_manager = EncoderManager(self.modem) self.rng = np.random.default_rng(42) def test_init(self) -> None: """Test the object initialization behaviour.""" self.assertIs(self.modem, self.encoder_manager.modem, "Modem not properly initialized") self.assertIs(0, len(self.encoder_manager.encoders), "Encoder list not properly initialized") self.assertEqual(True, self.encoder_manager.allow_truncating, "Truncating flag not properly initialized") self.assertEqual(True, self.encoder_manager.allow_padding, "Padding flag not properly initialized") def test_to_yaml(self) -> None: """Serialization to YAML.""" safe_representer = SafeRepresenter() node = EncoderManager.to_yaml(safe_representer, self.encoder_manager) self.assertEqual(node.value, 'null', "YAML serialization produced unexpected result") def test_from_yaml(self) -> None: """Recall from YAML dump.""" pass def test_modem(self) -> None: """Modem getter must return setter value.""" modem = Mock() self.encoder_manager.modem = modem self.assertIs(modem, self.encoder_manager.modem, "Modem getter does not return setter value") def test_modem_getter_assert(self) -> None: """Modem getter must throw `RuntimeError` if manager is floating.""" self.encoder_manager.modem = None with self.assertRaises(RuntimeError): self.assertEqual(self.encoder_manager.modem, None, "This assert is never called") def test_add_encoder_registration(self) -> None: """Added encoders must refer back to the manager they have been added to.""" self.encoder_manager.add_encoder(self.encoder_alpha) self.encoder_manager.add_encoder(self.encoder_beta) self.assertIs(self.encoder_alpha.manager, self.encoder_manager, "Added encoder does not refer back to manager") self.assertIs(self.encoder_beta.manager, self.encoder_manager, "Added encoder does not refer back to manager") def test_encoder_sorting(self) -> None: """Test that encoders are automatically ordered in ascending order, depending on their expected number of input bits. """ self.encoder_manager.add_encoder( self.encoder_alpha) # Encoder alpha expects 32 input bits self.encoder_manager.add_encoder( self.encoder_beta) # Encoder beta expects 16 input bits self.assertIs(self.encoder_manager.encoders[1], self.encoder_alpha, "Encoders sorted in unexpected order") self.assertIs(self.encoder_manager.encoders[0], self.encoder_beta, "Encoders sorted in unexpected order") def test_bit_block_size(self) -> None: """Test the bit block size calculation.""" self.assertEqual( self.encoder_manager.bit_block_size, 1, "Bit block size calculation produced unexpected result") self.encoder_manager.add_encoder(self.encoder_alpha) self.encoder_manager.add_encoder(self.encoder_beta) self.assertEqual( self.encoder_manager.bit_block_size, 32, "Bit block size calculation produced unexpected result") def test_code_block_size(self) -> None: """Test the code block size calculation.""" self.assertEqual( self.encoder_manager.code_block_size, 1, "Code block size calculation produced unexpected result") self.encoder_manager.add_encoder(self.encoder_alpha) self.encoder_manager.add_encoder(self.encoder_beta) self.assertEqual(self.encoder_manager.code_block_size, 128) def test_rate(self) -> None: """Test the coding rate calculation.""" self.assertEqual(self.encoder_manager.rate, 1.0, "Coding rate calculation produced unexpected result") self.encoder_manager.add_encoder(self.encoder_alpha) self.assertEqual(self.encoder_manager.rate, self.encoder_alpha.rate, "Coding rate calculation produced unexpected result") self.encoder_manager.add_encoder(self.encoder_beta) self.assertEqual(self.encoder_manager.rate, self.encoder_alpha.rate * self.encoder_beta.rate, "Coding rate calculation produced unexpected result") def test_empty_encoding(self) -> None: """Test proper encoding without an encoder.""" data = np.arange(10) expected_code = data.copy() code = self.encoder_manager.encode(data) assert_array_equal(expected_code, code) def test_single_encoding(self) -> None: """Test proper encoding with a single encoder.""" self.encoder_manager.add_encoder(self.encoder_alpha) data = ((np.arange(self.encoder_manager.bit_block_size) % 2) == 1).astype(int) expected_code = data.repeat(2) code = self.encoder_manager.encode(data) assert_array_equal(code, expected_code) def test_chained_encodings(self) -> None: """Test proper encoding with multiple chained encoders.""" self.encoder_manager.add_encoder(self.encoder_alpha) self.encoder_manager.add_encoder(self.encoder_beta) data = ((np.arange(self.encoder_manager.bit_block_size) % 2) == 1).astype(int) expected_code = data.repeat(4) code = self.encoder_manager.encode(data) assert_array_equal(code, expected_code) def test_empty_decoding(self) -> None: """Test proper decoding without an encoder.""" code = np.arange(10) expected_data = code.copy() data = self.encoder_manager.decode(code) assert_array_equal(expected_data, data) def test_single_decoding(self) -> None: """Test proper decoding with a single decoder""" self.encoder_manager.add_encoder(self.encoder_alpha) expected_data = ((np.arange(self.encoder_manager.bit_block_size) % 2) == 1).astype(int) code = expected_data.repeat(2) data = self.encoder_manager.decode(code) assert_array_equal(data, expected_data) def test_chained_decoding(self) -> None: """Test proper decoding with multiple chained encoders.""" # Configured (mocked) encoder manager self.encoder_manager.add_encoder(self.encoder_alpha) self.encoder_manager.add_encoder(self.encoder_beta) expected_data = ((np.arange(self.encoder_manager.bit_block_size) % 2) == 1).astype(int) code = expected_data.repeat(4) data = self.encoder_manager.decode(code) assert_array_equal(data, expected_data) def test_padding(self) -> None: """Test the padding of zeros on insufficient code lengths.""" self.encoder_manager.add_encoder(self.encoder_alpha) data = np.ones(self.encoder_manager.bit_block_size, dtype=bool) code = self.encoder_manager.encode( data, self.encoder_manager.code_block_size + 2) self.assertEqual(len(code), self.encoder_manager.code_block_size + 2) def test_truncating(self) -> None: """Test the truncating of bits on overflowing data lengths""" self.encoder_manager.add_encoder(self.encoder_alpha) expected_data = (np.arange(self.encoder_manager.bit_block_size) % 2) == 1 code = expected_data.repeat(2) data = self.encoder_manager.decode(code, expected_data.shape[0] - 2) assert_array_equal(data, expected_data[:-2]) def test_encode_decode(self) -> None: """Encoding a bit set and subsequently decoding it should yield the original set""" self.encoder_manager.add_encoder(self.encoder_alpha) self.encoder_manager.add_encoder(self.encoder_beta) expected_data = self.rng.integers(0, 2, 20, dtype=bool) code = self.encoder_manager.encode(expected_data) data = self.encoder_manager.decode(code, len(expected_data)) assert_array_equal(expected_data, data)
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