Source code for phypy.modulators

"""Modulator Module that implements various wireless modulators

These modulators are meant to turn arbitrary bit patterns to analog waveforms for wireless
transmission.
"""

import numpy as np


[docs]class OFDM: """Class that creates OFDM signals. This class will set up an OFDM modulator to create random OFDM signals. Attributes: n_subcarriers: Number of subcarriers per OFDM symbol subcarrier_spacing: Spacing between subcarriers in Hz cp_length : Number of samples in the cyclic prefix fft_size: Size of the IFFT/FFT used. sampling_rate: The native sampling rate based on the FFT size and subcarrier spacing symbol_alphabet: The constellation points Todo: - Add an arbitrary bit input - Add a demodulator """ def __init__(self, n_subcarriers: int = 1200, subcarrier_spacing: int = 15000, cp_length: int = 144, constellation: str = 'QPSK', seed: int = 0): """OFDM Modulator Constructor. Construct an OFDM Modulator with custom number of subcarriers, subcarrier spacing, cyclic prefix length, and constellation on each subcarrier. Args: n_subcarriers: Number of subcarriers per OFDM symbol subcarrier_spacing: Spacing of the subcarriers in the frequency domain in Hertz cp_length: Number of samples in cyclic prefix constellation: Type of constellation used on each subcarrier. QPSK, 16QAM or 64QAM seed: Seed for the random number generator """ self.n_subcarriers = n_subcarriers self.subcarrier_spacing = subcarrier_spacing self.cp_length = cp_length self.fft_size = np.power(2, np.int(np.ceil(np.log2(n_subcarriers)))) self.sampling_rate = self.subcarrier_spacing * self.fft_size self.symbol_alphabet = self.qam_alphabet(constellation) self.seed = seed self.fd_symbols = None # We'll hold the last TX symbols for calculating error later
[docs] def use(self, n_symbols: int = 10): """Use the OFDM modulator to generate a random signal. Args: n_symbols: Number of OFDM symbols to generate Returns: A time-domain OFDM signal TODO: - Allow to pass in an arbitrary bit pattern for modulation. """ np.random.seed(self.seed) self.fd_symbols = self.symbol_alphabet[ np.random.randint(self.symbol_alphabet.size, size=(self.n_subcarriers, n_symbols))] out = np.zeros((self.fft_size + self.cp_length, n_symbols), dtype='complex64') for index, symbol in enumerate(self.fd_symbols.T): td_waveform = self.frequency_to_time_domain(symbol) out[:, index] = self.add_cyclic_prefix(td_waveform) return out.flatten(1)
[docs] def frequency_to_time_domain(self, fd_symbol): """Convert the frequency domain symbol to time domain via IFFT Args: fd_symbol: One frequency domain symbol Returns: time domain signal """ # TODO: Verify that the RB are mapping to the IFFT input correctly ifft_input = np.zeros((self.fft_size), dtype='complex64') # Index 0 is DC. Leave blank. The 1st half needs to be in negative frequency # so they go in the last IFFT inputs. ifft_input[1: np.int(self.n_subcarriers / 2) + 1] = \ fd_symbol[np.int(self.n_subcarriers / 2):] ifft_input[-np.int(self.n_subcarriers / 2):] = \ fd_symbol[:np.int(self.n_subcarriers / 2)] return np.fft.ifft(ifft_input)
[docs] def time_to_frequency_domain(self, td_symbol): full_fft_output = np.fft.fft(td_symbol, axis=0) fd_symbols = np.zeros(shape=self.fd_symbols.shape, dtype='complex64') fd_symbols[np.int(self.n_subcarriers / 2):, :] = full_fft_output[1:np.int(self.n_subcarriers/2) + 1, :] fd_symbols[:np.int(self.n_subcarriers / 2), :] = full_fft_output[-np.int(self.n_subcarriers / 2):, :] return fd_symbols
[docs] def add_cyclic_prefix(self, td_waveform): """Adds cyclic prefix Adds by taking the last few samples and appending it to the beginning of the signal Args: td_waveform: IFFT output signal. Returns: time domain signal with a cyclic prefix """ # TODO: verify my indexing out = np.zeros(td_waveform.size + self.cp_length, dtype='complex64') out[self.cp_length:] = td_waveform out[:self.cp_length] = td_waveform[-self.cp_length:] return out
[docs] def remove_cyclic_prefix(self, td_grid): w_out_cp = td_grid[-self.fft_size:, :] return w_out_cp
[docs] @staticmethod def qam_alphabet(constellation): """Returns constellation points for QPSK, 16QAM, or 64 QAM Args: constellation: String saying the desired constellation Returns: symbol alphabet on the complex plane """ constellation_dict = { "QPSK": 4, "16QAM": 16, "64QAM": 64 } n_points = constellation_dict[constellation] x = np.int(np.sqrt(n_points)) - 1 alpha_n_points = np.arange(-x, x + 1, 2, dtype=int) A = np.kron(np.ones((x + 1, 1)), alpha_n_points) B = np.flipud(A.transpose()) const_qam = A + 1j * B alphabet = const_qam.flatten(1) return alphabet
[docs] def demodulate(self, time_domain_rx_signal): """Demodulate a time domain signal back into the FD symbols""" # Reorganize to grid _, n_symbols = self.fd_symbols.shape td_grid = np.reshape(time_domain_rx_signal, (self.fft_size+self.cp_length, n_symbols), order='F') td_grid = self.remove_cyclic_prefix(td_grid) fd_symbols = self.time_to_frequency_domain(td_grid) evm = self.calculate_evm(fd_symbols) return fd_symbols, evm
[docs] def calculate_evm(self, fd_rx_signal): # Get error vectors e = fd_rx_signal - self.fd_symbols evm = 100 * np.linalg.norm(e) / np.linalg.norm(self.fd_symbols) return evm
if __name__ == "__main__": ofdm = OFDM() x = ofdm.use() y, evm_percent = ofdm.demodulate(x) 1 + 1