From 14a0b6282d20bbe32457964b075f054d027adcb4 Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Thu, 3 Apr 2025 21:48:57 -0300 Subject: [PATCH 1/7] Adding a class implementing the GFSK modulation #3 --- README.md | 3 +- pymodulation/__init__.py | 1 - pymodulation/gfsk.py | 284 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + setup.py | 14 +- tests/test_gfsk.py | 46 +++++++ 6 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 pymodulation/gfsk.py create mode 100644 tests/test_gfsk.py 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/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..ec4a8af --- /dev/null +++ b/pymodulation/gfsk.py @@ -0,0 +1,284 @@ +# +# 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) + :param L: oversampling factor + + :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) + :param L: oversampling factor + + :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 (used in gmsk_mod). + + :param Tb: bit period + :param L: oversampling factor (number of samples per bit) + :param k: span length of the pulse (bit interval) + + :return h_norm: normalized filter coefficients of Gaussian LPF + """ + 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: TODO + :param iq_samples: TODO + + :return res: TODO + """ + 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: TODO + + :return res: 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 + + :return res: TODO + """ + 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/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.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..506697a --- /dev/null +++ b/tests/test_gfsk.py @@ -0,0 +1,46 @@ +# +# 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 + +from gfsk import GFSK + +def test_modulator_demodulator(): + data = [random.randint(0, 255) for _ in range(1000)] + + gfsk = GFSK(1.5, 0.5, 1200) + + samples, fs, dur = gfsk.modulate(data) + + demod_bits, signal = gfsk.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 data == data_res From 04b766c1d54f28a94448a0d4d832ca8c6d6179f2 Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Thu, 3 Apr 2025 21:49:33 -0300 Subject: [PATCH 2/7] Adding a class implementing the GMSK modulation #3 --- pymodulation/gmsk.py | 49 +++++++++++++++++++++++++++++++++++++++++++ tests/test_gmsk.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 pymodulation/gmsk.py create mode 100644 tests/test_gmsk.py diff --git a/pymodulation/gmsk.py b/pymodulation/gmsk.py new file mode 100644 index 0000000..159408a --- /dev/null +++ b/pymodulation/gmsk.py @@ -0,0 +1,49 @@ +# +# 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): + """ + """ + 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/tests/test_gmsk.py b/tests/test_gmsk.py new file mode 100644 index 0000000..b4ed0ca --- /dev/null +++ b/tests/test_gmsk.py @@ -0,0 +1,50 @@ +# +# 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 + +from gmsk import GMSK + +def test_modulator_demodulator(): + data = [random.randint(0, 255) for _ in range(1000)] + + gmsk = GMSK(0.5, 4800) + + print(gmsk.get_modulation_index()) + samples, fs, dur = gmsk.modulate(data) + print(samples) + print(fs) + print(dur) + + demod_bits, signal = gmsk.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 data == data_res From c2a4a205a957c5d1aba4ff2a2b5f5333f13f199c Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Tue, 8 Apr 2025 01:25:47 -0300 Subject: [PATCH 3/7] tests: Improving the unit test of the GFSK modulator #3 --- pymodulation/gfsk.py | 21 ++++++- tests/test_gfsk.py | 139 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 152 insertions(+), 8 deletions(-) diff --git a/pymodulation/gfsk.py b/pymodulation/gfsk.py index ec4a8af..8a17774 100644 --- a/pymodulation/gfsk.py +++ b/pymodulation/gfsk.py @@ -136,7 +136,10 @@ 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 @@ -176,7 +179,10 @@ 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 @@ -197,13 +203,19 @@ def modulate_time_domain(self, data, L=_GFSK_DEFAULT_OVERSAMPLING_FACTOR): def _gaussian_lpf(self, Tb, L, k): """ - Generate filter coefficients of Gaussian low pass filter (used in gmsk_mod). + 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 @@ -223,7 +235,7 @@ def _int_list_to_bit_list(self, n): :rtype: list """ res = list() - + for i in n: res = res + [int(digit) for digit in bin(i)[2:].zfill(8)] @@ -234,7 +246,9 @@ def demodulate(self, fs, iq_samples): Perform GFSK demodulation. :param fs: TODO + :param iq_samples: TODO + :type: no.array :return res: TODO """ @@ -275,7 +289,10 @@ def _gaussian_filter(self, L, sps): :param L: TODO + :param sps: TODO + :return res: TODO + :rtype: """ alpha = np.sqrt(np.log(2)) / (self.get_bt() * sps) t = np.arange(-L, L + 1) diff --git a/tests/test_gfsk.py b/tests/test_gfsk.py index 506697a..73fa5fd 100644 --- a/tests/test_gfsk.py +++ b/tests/test_gfsk.py @@ -22,16 +22,143 @@ import random +import pytest +import numpy as np + from gfsk import GFSK -def test_modulator_demodulator(): - data = [random.randint(0, 255) for _ in range(1000)] +# 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) - gfsk = GFSK(1.5, 0.5, 1200) + 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 - samples, fs, dur = gfsk.modulate(data) +def test_modulator_demodulator(gfsk_modulator, test_data): + """Test modulation and demoulation""" + samples, fs, dur = gfsk_modulator.modulate(test_data) - demod_bits, signal = gfsk.demodulate(fs, samples) + demod_bits, signal = gfsk_modulator.demodulate(fs, samples) data_res = list() @@ -43,4 +170,4 @@ def test_modulator_demodulator(): pos -= 1 data_res.append(result) - assert data == data_res + assert test_data == data_res From 0241041b25ae33c6dc3c3c00abf96652b3c6c0e4 Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Mon, 14 Apr 2025 02:01:52 -0300 Subject: [PATCH 4/7] docs: Adding a page for describing the supported modulations #3 --- docs/index.rst | 22 ++++++++++++---------- docs/modulations.rst | 17 +++++++++++++++++ docs/overview.rst | 3 +++ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 docs/modulations.rst create mode 100644 docs/overview.rst diff --git a/docs/index.rst b/docs/index.rst index 5bead8e..1ce48b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,18 +3,20 @@ 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 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 +******** From 6f30b87ca11008e09e03e569f76e311b27ec1a5b Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Sat, 12 Jul 2025 17:05:42 -0300 Subject: [PATCH 5/7] Addding documentation to the GMSK class #3 --- pymodulation/gmsk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymodulation/gmsk.py b/pymodulation/gmsk.py index 159408a..126c9f0 100644 --- a/pymodulation/gmsk.py +++ b/pymodulation/gmsk.py @@ -42,6 +42,14 @@ def __init__(self, bt, baud): def set_modulation_index(self, modidx): """ + Sets the modulation index. + + :note: For GMSK, the modulation 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!") From a96225161e2f97444c744cb5da6f5575b1c98cb8 Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Thu, 8 Jan 2026 19:31:23 -0300 Subject: [PATCH 6/7] tests: Improving the GMSK class unit tests #3 --- tests/test_gmsk.py | 149 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 10 deletions(-) diff --git a/tests/test_gmsk.py b/tests/test_gmsk.py index b4ed0ca..07c379a 100644 --- a/tests/test_gmsk.py +++ b/tests/test_gmsk.py @@ -22,20 +22,149 @@ import random +import pytest +import numpy as np + from gmsk import GMSK -def test_modulator_demodulator(): - data = [random.randint(0, 255) for _ in range(1000)] +# 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) - gmsk = GMSK(0.5, 4800) + 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 - print(gmsk.get_modulation_index()) - samples, fs, dur = gmsk.modulate(data) - print(samples) - print(fs) - print(dur) +def test_modulator_demodulator(gmsk_modulator, test_data): + """Test modulation and demoulation""" + samples, fs, dur = gmsk_modulator.modulate(test_data) - demod_bits, signal = gmsk.demodulate(fs, samples) + demod_bits, signal = gmsk_modulator.demodulate(fs, samples) data_res = list() @@ -47,4 +176,4 @@ def test_modulator_demodulator(): pos -= 1 data_res.append(result) - assert data == data_res + assert test_data == data_res From 3fbba6e437d88c1fa432f9c3dc4b3018ea90f255 Mon Sep 17 00:00:00 2001 From: Gabriel Mariano Marcelino Date: Thu, 8 Jan 2026 23:59:30 -0300 Subject: [PATCH 7/7] Adding usage examples for GFSK and GMSK modulations and minor improvements. closes #3 --- docs/index.rst | 1 + docs/usage.rst | 49 ++++++++++++++++++++++++++++++++++++ pymodulation/gfsk.py | 18 +++++++------ pymodulation/gmsk.py | 2 +- pymodulation/pymodulation.py | 2 ++ pymodulation/version.py | 2 +- setup.cfg | 2 +- 7 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 docs/usage.rst diff --git a/docs/index.rst b/docs/index.rst index 1ce48b4..7f1a1d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,3 +20,4 @@ Contents overview modulations + usage 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/gfsk.py b/pymodulation/gfsk.py index 8a17774..42afc6f 100644 --- a/pymodulation/gfsk.py +++ b/pymodulation/gfsk.py @@ -245,12 +245,16 @@ def demodulate(self, fs, iq_samples): """ Perform GFSK demodulation. - :param fs: TODO + :param fs: Sample rate in S/s - :param iq_samples: TODO - :type: no.array + :param iq_samples: IQ samples + :type: np.array - :return res: TODO + :return: The demodulated bitstream. + :rtype: list + + :return: The baseband signal in NRZ format. + :rtype: list """ sps = int(fs/self.get_baudrate()) @@ -273,9 +277,9 @@ def _frequency_discriminator(self, iq_samples): """ Extract frequency deviations using phase changes in IQ samples. - :param iq_samples: TODO + :param iq_samples: IQ samples. - :return res: TODO + :return: TODO """ phase = np.angle(iq_samples) # Extract phase unwrapped_phase = np.unwrap(phase) # Unwrap to avoid phase discontinuities @@ -291,7 +295,7 @@ def _gaussian_filter(self, L, sps): :param sps: TODO - :return res: TODO + :return: TODO :rtype: """ alpha = np.sqrt(np.log(2)) / (self.get_bt() * sps) diff --git a/pymodulation/gmsk.py b/pymodulation/gmsk.py index 126c9f0..7c97df8 100644 --- a/pymodulation/gmsk.py +++ b/pymodulation/gmsk.py @@ -44,7 +44,7 @@ def set_modulation_index(self, modidx): """ Sets the modulation index. - :note: For GMSK, the modulation must always be 0.5. + :note: For GMSK, the modulation index must always be 0.5. :param modidx: The new modulation index (always 0.5). :type: float 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/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