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]
示例#4
0
    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
示例#5
0
    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)
示例#9
0
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