diff --git a/README.md b/README.md index a79cad2..86ea1f3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ ## Dependencies -* None +* NumPy +* SciPy ## Installing diff --git a/docs/index.rst b/docs/index.rst index 5bead8e..7f1a1d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,18 +3,21 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to PyModulation's documentation! -======================================== +PyModulation Library Documentation +================================== -.. toctree:: - :maxdepth: 2 - :caption: Contents: +TODO + +The project is fully open source and is available in a `GitHub repository `_. All contributions are welcome! If you found a bug, developed a new feature, or want to improve the documentation, there are two ways to do so: open an issue describing the suggested modification, or by opening a pull request. More information are available in the `CONTRIBUTING file `_. +Any questions or suggestions can also be addressed to Gabriel Mariano Marcelino <`gabriel.mm8@gmail.com `_>. +Contents +======== -Indices and tables -================== +.. toctree:: + :maxdepth: 2 -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + overview + modulations + usage diff --git a/docs/modulations.rst b/docs/modulations.rst new file mode 100644 index 0000000..5d53a89 --- /dev/null +++ b/docs/modulations.rst @@ -0,0 +1,17 @@ +*********** +Modulations +*********** + +GFSK +==== + +Gaussian Frequency Shift Keying (GFSK) is a modulation technique derived from Frequency Shift Keying (FSK), where digital data is transmitted by shifting the carrier frequency between discrete values. Unlike traditional FSK, GFSK applies a Gaussian filter to the baseband pulses before modulation, which smooths the phase transitions and reduces spectral bandwidth. This filtering minimizes abrupt frequency changes, resulting in a more compact power spectrum and reduced interference with adjacent channels. GFSK is particularly advantageous in wireless communication systems where efficient bandwidth utilization and low power consumption are critical. + +One of the most notable applications of GFSK is in Bluetooth technology, where it is used for its robustness and spectral efficiency. The Gaussian filtering helps mitigate intersymbol interference (ISI) and improves performance in noisy environments. Additionally, GFSK supports both coherent and non-coherent detection, offering flexibility in receiver design. Its constant envelope property ensures efficient power amplifier operation, making it suitable for battery-powered devices. Overall, GFSK strikes a balance between simplicity, spectral efficiency, and reliability, making it a popular choice for short-range wireless communication systems. + +GMSK +==== + +Gaussian Minimum Shift Keying (GMSK) is a continuous-phase modulation scheme derived from Frequency Shift Keying (FSK), where the digital signal is filtered using a Gaussian filter before modulation. This filtering smooths the phase transitions, resulting in a nearly constant envelope and significantly reduced spectral sidelobes compared to traditional FSK. The key feature of GMSK is its ability to achieve high spectral efficiency while maintaining low out-of-band emissions, making it ideal for bandwidth-constrained wireless systems. + +A notable application of GMSK is in the Global System for Mobile Communications (GSM), where it was chosen for its robustness against interference and efficient use of available spectrum. The modulation's constant envelope allows for the use of highly efficient nonlinear power amplifiers, reducing power consumption in mobile devices. Additionally, GMSK's resistance to multipath fading and phase noise enhances performance in challenging radio environments. Despite its slightly higher complexity in demodulation compared to simpler FSK schemes, GMSK remains a widely adopted modulation technique due to its excellent balance between spectral efficiency, power efficiency, and reliability in wireless communication systems. diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..45f7247 --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,3 @@ +******** +Overview +******** diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..b227ccb --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,49 @@ +***** +Usage +***** + +This section presents examples of how to use the library for each supported modulation type. + +GFSK +==== + +The GFSK modulation can be used through the *GFSK* class, using the modulate and demodulate methods. An example of usage can be seen in the code below: + +.. code-block:: python + + from pymodulation.gfsk import GFSK + + mod = GFSK(2.5, 0.5, 9600) # Modulation index = 2.5, BT = 0.5, Baudrate = 9600 bps + + data = list(range(100)) + + samples, fs, dur = mod.modulate(data) + + print("IQ Samples:", samples[:10]) + + bits, bb_sig = mod.demodulate(fs, samples) + + print("Demodulated bits:", list(map(int, bits))) + +The *modulate* method returns the IQ samples of the generated signal, the corresponding sampling rate, and the signal duration in seconds. The *demodulate* method allows the demodulation of a GFSK signal, taking the corresponding IQ samples and sampling rate as input, and producing as output the data bitstream contained in the signal and the baseband signal samples (in NRZ format). + +GMSK +==== + +This modulation can be used in a manner almost identical to GFSK modulation, with the difference that in this case the modulation index is fixed at 0.5, as expected for this type of modulation. An example of usage can be seen in the code below. + +.. code-block:: python + + from pymodulation.gmsk import GMSK + + mod = GMSK(0.5, 9600) # BT = 0.5, baudrate = 9600 bps + + data = list(range(100)) + + samples, fs, dur = mod.modulate(data) + + print("IQ Samples:", samples[:10]) + + bits, bb_sig = mod.demodulate(fs, samples) + + print("Demodulated bits:", list(map(int, bits))) diff --git a/pymodulation/__init__.py b/pymodulation/__init__.py index 275ccef..f09c51f 100644 --- a/pymodulation/__init__.py +++ b/pymodulation/__init__.py @@ -20,5 +20,4 @@ # # -from pymodulation.pymodulation import PyModulation from pymodulation.version import __version__ diff --git a/pymodulation/gfsk.py b/pymodulation/gfsk.py new file mode 100644 index 0000000..42afc6f --- /dev/null +++ b/pymodulation/gfsk.py @@ -0,0 +1,305 @@ +# +# gfsk.py +# +# Copyright The PyModulation Contributors. +# +# This file is part of PyModulation library. +# +# PyModulation library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PyModulation library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PyModulation library. If not, see . +# +# + +import numpy as np +from scipy.signal import upfirdn, lfilter + +_GFSK_DEFAULT_OVERSAMPLING_FACTOR = 100 + +class GFSK: + """ + GFSK modulator. + """ + def __init__(self, modidx, bt, baud): + """ + Class constructor with modulation initialization. + + :param modidx: Modulation index. + :type: float + + :param bt: BT product (bandwidth x bit period) for GFSK + :type: float + + :param baud: The desired data rate in bps + :type: int + + :return None + """ + self._mod_index = float() + self._bt = float() + self._baudrate = int() + + self.set_modulation_index(modidx) + self.set_bt(bt) + self.set_baudrate(baud) + + def set_modulation_index(self, modidx): + """ + Sets the modulation index. + + :param modidx: The new modulation index. + :type: float + + :return: None. + """ + self._mod_index = modidx + + def get_modulation_index(self): + """ + Gets the modulation index. + + :return: The configured modulation index. + :rtype: float + """ + return self._mod_index + + def set_bt(self, bt): + """ + Sets the bandwidth-time index. + + :param bt: The new bandwidth-time product. + :type: float + + :return: None + """ + self._bt = bt + + def get_bt(self): + """ + Gets the current bandwidth-time index. + + :return: The configured bandwidth-time product. + :rtype: float + """ + return self._bt + + def set_baudrate(self, baud): + """ + Sets the baudrate. + + :param baud: The new baudrate in bps; + :type: int + + :return: None. + """ + self._baudrate = baud + + def get_baudrate(self): + """ + Gets the current baudrate. + + :return: The configured baudrate in bps. + :rtype: int + """ + return self._baudrate + + def modulate(self, data, L=_GFSK_DEFAULT_OVERSAMPLING_FACTOR): + """ + Function to modulate an integer stream using GFSK modulation. + + :param data: input integer list to modulate (bytes as integers) + :type: list + + :param L: oversampling factor + :type: int + + :return: s_complex: baseband GFSK signal (I+jQ) + :return: samp: Sample rate S/s + :return: dur: Signal duration in seconds + """ + I, Q, fs, dur = self.get_iq(data, L) + s_complex = I + 1j*Q # Complex baseband representation + + return s_complex, fs, dur + + def get_iq(self, data, L=_GFSK_DEFAULT_OVERSAMPLING_FACTOR): + """ + Computes the IQ data of the GFSK modulated signal. + + :param data: input integer list to modulate (bytes as integers) + :type: list + + :param L: oversampling factor + :type: int + + :return: I: I data of the modulated signal + :return: Q: Q data of the modulated signal + :return: samp: Sample rate S/s + :return: dur: Signal duration in seconds + """ + # Convert to array of bits + data = self._int_list_to_bit_list(data) + + data = np.array(data) + + # Timing parameters + fc = self.get_baudrate() # Carrier frequency = Data transfer rate in bps + fs = L*fc # Sample frequency in Hz + Ts = np.float64(1.0)/fs # Sample period in seconds + Tb = L*Ts # Bit period in seconds + + c_t = upfirdn(h=[1]*L, x=2*data-1, up = L) # NRZ pulse train c(t) + k = 1 # Truncation length for Gaussian LPF + h_t = self._gaussian_lpf(Tb, L, k) # Gaussian LPF + b_t = np.convolve(h_t, c_t, 'full') # Convolve c(t) with Gaussian LPF to get b(t) + bnorm_t = b_t/np.max(np.abs(b_t)) # Normalize the output of Gaussian LPF to +/-1 + + # Integrate to get phase information + h = np.float64(self.get_modulation_index()) # Modulation index + phi_t = lfilter(b = [1], a=[1,-1], x=bnorm_t*Ts) * h*np.pi/Tb + I = np.cos(phi_t) + Q = np.sin(phi_t) # Cross-correlated baseband I/Q signals + + # Sampling values + dur = len(data)*Tb # Transmission duration in seconds + + return I, Q, fs, dur + + def modulate_time_domain(self, data, L=_GFSK_DEFAULT_OVERSAMPLING_FACTOR): + """ + Generates the GFSK modulated signal in time domain. + + :param data: input integer list to modulate (bytes as integers) + :type: list + + :param L: oversampling factor + :type: int + + :return: s_t: GFSK modulated signal with carrier s(t) (time domain) + :return: samp: Sample rate S/s + :return: dur: Signal duration in seconds + """ + I, Q, samp, dur = self.get_iq(data, L) + + fc = self.get_baudrate() # Carrier frequency = Data transfer rate in bps + fs = L*fc + Ts = 1/fs + + t = Ts*np.arange(start=0, stop=len(I)) # Time base for RF carrier + sI_t = I*np.cos(2*np.pi*fc*t) + sQ_t = Q*np.sin(2*np.pi*fc*t) + s_t = sI_t - sQ_t # s(t) - GFSK with RF carrier + + return s_t, t, samp, dur + + def _gaussian_lpf(self, Tb, L, k): + """ + Generate filter coefficients of Gaussian low pass filter. + + :param Tb: bit period + :type: float + + :param L: oversampling factor (number of samples per bit) + :type: int + + :param k: span length of the pulse (bit interval) + :type: float + + :return h_norm: normalized filter coefficients of Gaussian LPF + :rtype: list + """ + B = self.get_bt()/Tb # Bandwidth of the filter + # Truncated time limits for the filter + t = np.arange(start = -k*Tb, stop = k*Tb + Tb/L, step = Tb/L) + h = B*np.sqrt(2*np.pi/(np.log(2)))*np.exp(-2 * (t*np.pi*B)**2 /(np.log(2))) + h_norm = h / np.sum(h) + return h_norm + + def _int_list_to_bit_list(self, n): + """ + Converts a integer list (bytes) to a bit list. + + :param n: An integer list. + :type: list + + :return res: The given integer list as a bit list + :rtype: list + """ + res = list() + + for i in n: + res = res + [int(digit) for digit in bin(i)[2:].zfill(8)] + + return res + + def demodulate(self, fs, iq_samples): + """ + Perform GFSK demodulation. + + :param fs: Sample rate in S/s + + :param iq_samples: IQ samples + :type: np.array + + :return: The demodulated bitstream. + :rtype: list + + :return: The baseband signal in NRZ format. + :rtype: list + """ + sps = int(fs/self.get_baudrate()) + + # Frequency discriminator + freq_deviation = self._frequency_discriminator(iq_samples) + + # Apply Gaussian matched filter + gaussian_filter = self._gaussian_filter(3 * sps, sps) + filtered_signal = np.convolve(freq_deviation, gaussian_filter, mode='same') + + # Downsample to symbol rate + sampled_signal = filtered_signal[sps // 2 :: sps] + + # Decision thresholding + demodulated_bits = (sampled_signal > 0).astype(int) + + return list(demodulated_bits), sampled_signal + + def _frequency_discriminator(self, iq_samples): + """ + Extract frequency deviations using phase changes in IQ samples. + + :param iq_samples: IQ samples. + + :return: TODO + """ + phase = np.angle(iq_samples) # Extract phase + unwrapped_phase = np.unwrap(phase) # Unwrap to avoid phase discontinuities + freq_deviation = np.diff(unwrapped_phase) # Phase derivative + + return np.concatenate([[0], freq_deviation]) # Keep length consistent + + def _gaussian_filter(self, L, sps): + """ + Generate a Gaussian matched filter. + + :param L: TODO + + :param sps: TODO + + :return: TODO + :rtype: + """ + alpha = np.sqrt(np.log(2)) / (self.get_bt() * sps) + t = np.arange(-L, L + 1) + g = np.exp(-0.5 * (alpha * t) ** 2) + + return g / np.sum(g) diff --git a/pymodulation/gmsk.py b/pymodulation/gmsk.py new file mode 100644 index 0000000..7c97df8 --- /dev/null +++ b/pymodulation/gmsk.py @@ -0,0 +1,57 @@ +# +# gmsk.py +# +# Copyright The PyModulation Contributors. +# +# This file is part of PyModulation library. +# +# PyModulation library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PyModulation library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PyModulation library. If not, see . +# +# + +from pymodulation.gfsk import GFSK + +class GMSK(GFSK): + """ + GMSK modulator. + """ + def __init__(self, bt, baud): + """ + Class constructor with modulation initialization. + + :param bt: BT product (bandwidth x bit period) for GMSK + :type: float + + :param baud: The desired data rate in bps + :type: int + + :return: None. + """ + super().__init__(0.5, bt, baud) + + def set_modulation_index(self, modidx): + """ + Sets the modulation index. + + :note: For GMSK, the modulation index must always be 0.5. + + :param modidx: The new modulation index (always 0.5). + :type: float + + :return: None. + """ + if modidx != 0.5: + raise ValueError("The modulation index of GMSK must be always 0.5! If you change the modulation index it will not be GMSK anymore!") + else: + super().set_modulation_index(modidx) diff --git a/pymodulation/pymodulation.py b/pymodulation/pymodulation.py index 5aa585d..5d22e4d 100644 --- a/pymodulation/pymodulation.py +++ b/pymodulation/pymodulation.py @@ -20,3 +20,5 @@ # # +from pymodulation.gfsk import GFSK +from pymodulation.gmsk import GMSK diff --git a/pymodulation/version.py b/pymodulation/version.py index dcc6275..d654cd3 100644 --- a/pymodulation/version.py +++ b/pymodulation/version.py @@ -24,7 +24,7 @@ __copyright__ = "Copyright The PyModulation Contributors" __credits__ = ["Gabriel Mariano Marcelino"] __license__ = "LGPLv3" -__version__ = "0.0.0" +__version__ = "0.1.0" __maintainer__ = "Gabriel Mariano Marcelino" __email__ = "gabriel.mm8@gmail.com" __status__ = "Development" diff --git a/requirements.txt b/requirements.txt index e69de29..6bad103 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,2 @@ +numpy +scipy diff --git a/setup.cfg b/setup.cfg index 07f2f9d..f1079fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [bdist_rpm] packager = Gabriel Mariano Marcelino -requires = python3 +requires = python3, python3-numpy, python3-scipy diff --git a/setup.py b/setup.py index 2e6dbf6..b01e823 100644 --- a/setup.py +++ b/setup.py @@ -2,24 +2,24 @@ # # setup.py -# +# # Copyright The PyModulation Contributors. -# +# # This file is part of PyModulation library. -# +# # PyModulation library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # PyModulation library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public License # along with PyModulation library. If not, see . -# +# # import setuptools @@ -62,5 +62,5 @@ ], download_url = "https://github.com/mgm8/pymodulation/releases", packages = setuptools.find_packages(), - install_requires = [], + install_requires = ['numpy', 'scipy'], ) diff --git a/tests/test_gfsk.py b/tests/test_gfsk.py new file mode 100644 index 0000000..73fa5fd --- /dev/null +++ b/tests/test_gfsk.py @@ -0,0 +1,173 @@ +# +# test_gfsk.py +# +# Copyright The PyModulation Contributors. +# +# This file is part of PyModulation library. +# +# PyModulation library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PyModulation library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PyModulation library. If not, see . +# +# + +import random + +import pytest +import numpy as np + +from gfsk import GFSK + +# Parameterized test cases +MODULATION_INDICES = [0.3, 0.5, 1.0] +BT_PRODUCTS = [0.3, 0.5, 1.0] +BAUD_RATES = [1200, 9600, 19200] + +# Test fixtures +@pytest.fixture +def gfsk_modulator(): + """Fixture providing a default GFSK modulator instance""" + return GFSK(modidx=0.5, bt=0.3, baud=9600) + +@pytest.fixture +def test_data(): + """Fixture providing test data (simple byte sequence)""" + return [random.randint(0, 255) for _ in range(1000)] + +def test_initialization(gfsk_modulator): + """Test that initialization sets the correct parameters""" + assert gfsk_modulator.get_modulation_index() == 0.5 + assert gfsk_modulator.get_bt() == 0.3 + assert gfsk_modulator.get_baudrate() == 9600 + +@pytest.mark.parametrize("modidx", MODULATION_INDICES) +def test_modulation_index_setter(gfsk_modulator, modidx): + """Test modulation index setter/getter""" + gfsk_modulator.set_modulation_index(modidx) + assert gfsk_modulator.get_modulation_index() == modidx + +@pytest.mark.parametrize("bt", BT_PRODUCTS) +def test_bt_setter(gfsk_modulator, bt): + """Test BT product setter/getter""" + gfsk_modulator.set_bt(bt) + assert gfsk_modulator.get_bt() == bt + +@pytest.mark.parametrize("baud", BAUD_RATES) +def test_baudrate_setter(gfsk_modulator, baud): + """Test baudrate setter/getter""" + gfsk_modulator.set_baudrate(baud) + assert gfsk_modulator.get_baudrate() == baud + +def test_modulate_output_shapes(gfsk_modulator, test_data): + """Test that modulate returns outputs with correct shapes/types""" + s_complex, fs, dur = gfsk_modulator.modulate(test_data) + + assert isinstance(s_complex, np.ndarray) + assert isinstance(fs, (int, float)) + assert isinstance(dur, float) + assert len(s_complex) > 0 + +def test_modulate_time_domain_output(gfsk_modulator, test_data): + """Test time domain modulation output""" + s_t, t, samp, dur = gfsk_modulator.modulate_time_domain(test_data) + + assert isinstance(s_t, np.ndarray) + assert isinstance(t, np.ndarray) + assert isinstance(samp, (int, float)) + assert isinstance(dur, float) + assert len(s_t) == len(t) + +def test_get_iq_output(gfsk_modulator, test_data): + """Test IQ generation output""" + I, Q, fs, dur = gfsk_modulator.get_iq(test_data) + + assert isinstance(I, np.ndarray) + assert isinstance(Q, np.ndarray) + assert isinstance(fs, (int, float)) + assert isinstance(dur, float) + assert len(I) == len(Q) + +def test_gaussian_lpf(gfsk_modulator): + """Test Gaussian LPF coefficient generation""" + Tb = 1/9600 + L = 100 + k = 1 + h_norm = gfsk_modulator._gaussian_lpf(Tb, L, k) + + assert isinstance(h_norm, np.ndarray) + assert len(h_norm) > 0 + assert np.isclose(np.sum(h_norm), 1.0, rtol=1e-5) # Should be normalized + +def test_int_to_bit_conversion(gfsk_modulator): + """Test integer to bit list conversion""" + input_data = [0x01, 0x03] # 00000001, 00000011 + expected_output = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1] + + result = gfsk_modulator._int_list_to_bit_list(input_data) + assert result == expected_output + +def test_demodulation(gfsk_modulator, test_data): + """Test demodulation round-trip""" + # Modulate the test data + s_complex, fs, _ = gfsk_modulator.modulate(test_data) + + # Demodulate + demod_bits, sampled_signal = gfsk_modulator.demodulate(fs, s_complex) + + # Convert original data to bits for comparison + original_bits = gfsk_modulator._int_list_to_bit_list(test_data) + + # We can't expect perfect reconstruction, but basic checks: + assert len(demod_bits) > 0 + assert isinstance(demod_bits, list) + assert isinstance(sampled_signal, np.ndarray) + assert len(demod_bits)-2 <= len(original_bits) # May lose some bits at edges + +def test_frequency_discriminator(gfsk_modulator): + """Test frequency discriminator""" + # Create a simple IQ signal with known frequency deviation + t = np.linspace(0, 1, 1000) + freq_dev = 0.1 + iq_samples = np.exp(1j * 2 * np.pi * freq_dev * t) + + result = gfsk_modulator._frequency_discriminator(iq_samples) + + assert isinstance(result, np.ndarray) + assert len(result) == len(iq_samples) + +def test_gaussian_filter(gfsk_modulator): + """Test Gaussian filter generation""" + L = 10 + sps = 100 + g = gfsk_modulator._gaussian_filter(L, sps) + + assert isinstance(g, np.ndarray) + assert len(g) == 2 * L + 1 + assert np.isclose(np.sum(g), 1.0, rtol=1e-5) # Should be normalized + +def test_modulator_demodulator(gfsk_modulator, test_data): + """Test modulation and demoulation""" + samples, fs, dur = gfsk_modulator.modulate(test_data) + + demod_bits, signal = gfsk_modulator.demodulate(fs, samples) + + data_res = list() + + for i in range(1, len(demod_bits) - 1, 8): + result = int() + pos = 8 - 1 + for j in range(8): + result = result | (demod_bits[i + j] << pos) + pos -= 1 + data_res.append(result) + + assert test_data == data_res diff --git a/tests/test_gmsk.py b/tests/test_gmsk.py new file mode 100644 index 0000000..07c379a --- /dev/null +++ b/tests/test_gmsk.py @@ -0,0 +1,179 @@ +# +# test_gmsk.py +# +# Copyright The PyModulation Contributors. +# +# This file is part of PyModulation library. +# +# PyModulation library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PyModulation library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PyModulation library. If not, see . +# +# + +import random + +import pytest +import numpy as np + +from gmsk import GMSK + +# Parameterized test cases +MODULATION_INDICES = [0.3, 0.5, 1.0] +BT_PRODUCTS = [0.3, 0.5, 1.0] +BAUD_RATES = [1200, 9600, 19200] + +# Test fixtures +@pytest.fixture +def gmsk_modulator(): + """Fixture providing a default GMSK modulator instance""" + return GMSK(bt=0.3, baud=9600) + +@pytest.fixture +def test_data(): + """Fixture providing test data (simple byte sequence)""" + return [random.randint(0, 255) for _ in range(1000)] + +def test_initialization(gmsk_modulator): + """Test that initialization sets the correct parameters""" + assert gmsk_modulator.get_modulation_index() == 0.5 + assert gmsk_modulator.get_bt() == 0.3 + assert gmsk_modulator.get_baudrate() == 9600 + +@pytest.mark.parametrize("modidx", MODULATION_INDICES) +def test_modulation_index_setter(gmsk_modulator, modidx): + """Test modulation index setter/getter""" + if modidx == 0.5: + gmsk_modulator.set_modulation_index(modidx) + assert gmsk_modulator.get_modulation_index() == modidx + else: + with pytest.raises(Exception) as exc_info: + gmsk_modulator.set_modulation_index(modidx) + + assert exc_info.type is ValueError + +@pytest.mark.parametrize("bt", BT_PRODUCTS) +def test_bt_setter(gmsk_modulator, bt): + """Test BT product setter/getter""" + gmsk_modulator.set_bt(bt) + assert gmsk_modulator.get_bt() == bt + +@pytest.mark.parametrize("baud", BAUD_RATES) +def test_baudrate_setter(gmsk_modulator, baud): + """Test baudrate setter/getter""" + gmsk_modulator.set_baudrate(baud) + assert gmsk_modulator.get_baudrate() == baud + +def test_modulate_output_shapes(gmsk_modulator, test_data): + """Test that modulate returns outputs with correct shapes/types""" + s_complex, fs, dur = gmsk_modulator.modulate(test_data) + + assert isinstance(s_complex, np.ndarray) + assert isinstance(fs, (int, float)) + assert isinstance(dur, float) + assert len(s_complex) > 0 + +def test_modulate_time_domain_output(gmsk_modulator, test_data): + """Test time domain modulation output""" + s_t, t, samp, dur = gmsk_modulator.modulate_time_domain(test_data) + + assert isinstance(s_t, np.ndarray) + assert isinstance(t, np.ndarray) + assert isinstance(samp, (int, float)) + assert isinstance(dur, float) + assert len(s_t) == len(t) + +def test_get_iq_output(gmsk_modulator, test_data): + """Test IQ generation output""" + I, Q, fs, dur = gmsk_modulator.get_iq(test_data) + + assert isinstance(I, np.ndarray) + assert isinstance(Q, np.ndarray) + assert isinstance(fs, (int, float)) + assert isinstance(dur, float) + assert len(I) == len(Q) + +def test_gaussian_lpf(gmsk_modulator): + """Test Gaussian LPF coefficient generation""" + Tb = 1/9600 + L = 100 + k = 1 + h_norm = gmsk_modulator._gaussian_lpf(Tb, L, k) + + assert isinstance(h_norm, np.ndarray) + assert len(h_norm) > 0 + assert np.isclose(np.sum(h_norm), 1.0, rtol=1e-5) # Should be normalized + +def test_int_to_bit_conversion(gmsk_modulator): + """Test integer to bit list conversion""" + input_data = [0x01, 0x03] # 00000001, 00000011 + expected_output = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1] + + result = gmsk_modulator._int_list_to_bit_list(input_data) + assert result == expected_output + +def test_demodulation(gmsk_modulator, test_data): + """Test demodulation round-trip""" + # Modulate the test data + s_complex, fs, _ = gmsk_modulator.modulate(test_data) + + # Demodulate + demod_bits, sampled_signal = gmsk_modulator.demodulate(fs, s_complex) + + # Convert original data to bits for comparison + original_bits = gmsk_modulator._int_list_to_bit_list(test_data) + + # We can't expect perfect reconstruction, but basic checks: + assert len(demod_bits) > 0 + assert isinstance(demod_bits, list) + assert isinstance(sampled_signal, np.ndarray) + assert len(demod_bits)-2 <= len(original_bits) # May lose some bits at edges + +def test_frequency_discriminator(gmsk_modulator): + """Test frequency discriminator""" + # Create a simple IQ signal with known frequency deviation + t = np.linspace(0, 1, 1000) + freq_dev = 0.1 + iq_samples = np.exp(1j * 2 * np.pi * freq_dev * t) + + result = gmsk_modulator._frequency_discriminator(iq_samples) + + assert isinstance(result, np.ndarray) + assert len(result) == len(iq_samples) + +def test_gaussian_filter(gmsk_modulator): + """Test Gaussian filter generation""" + L = 10 + sps = 100 + g = gmsk_modulator._gaussian_filter(L, sps) + + assert isinstance(g, np.ndarray) + assert len(g) == 2 * L + 1 + assert np.isclose(np.sum(g), 1.0, rtol=1e-5) # Should be normalized + +def test_modulator_demodulator(gmsk_modulator, test_data): + """Test modulation and demoulation""" + samples, fs, dur = gmsk_modulator.modulate(test_data) + + demod_bits, signal = gmsk_modulator.demodulate(fs, samples) + + data_res = list() + + for i in range(1, len(demod_bits) - 1, 8): + result = int() + pos = 8 - 1 + for j in range(8): + result = result | (demod_bits[i + j] << pos) + pos -= 1 + data_res.append(result) + + assert test_data == data_res