diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml
new file mode 100644
index 0000000..8739b9e
--- /dev/null
+++ b/.github/workflows/install.yml
@@ -0,0 +1,71 @@
+#
+# install.yml
+#
+# Copyright The PyModulation Contributors.
+#
+# This file is part of PyModulation.
+#
+# PyModulation is free software; you can redistribute it
+# and/or modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# PyModulation 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with PyModulation; if not, see .
+#
+#
+#
+
+name: Install test
+
+on:
+ push:
+ branches: [dev]
+ pull_request:
+ branches: [main, dev]
+
+ # 'workflow_dispatch' allows manual execution of this workflow under the repository's 'Actions' tab
+ workflow_dispatch:
+
+jobs:
+
+ install-test:
+ name: Installing test
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.12', '3.13']
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Upgrade pip and build tools
+ run: |
+ python -m pip install --upgrade pip
+ pip install setuptools wheel build
+
+ - name: Build package (sdist and wheel)
+ run: |
+ python setup.py sdist bdist_wheel
+
+ - name: Check built distributions
+ run: |
+ pip install twine
+ twine check dist/*
+
+ - name: Install package
+ run: |
+ python setup.py install
diff --git a/.github/workflows/sphinx-build.yml b/.github/workflows/sphinx-build.yml
index e263cce..add493d 100644
--- a/.github/workflows/sphinx-build.yml
+++ b/.github/workflows/sphinx-build.yml
@@ -20,28 +20,31 @@
#
#
-name: Documentation build
+name: Documentation build test
on:
push:
- branches: [dev, main]
+ branches: [dev]
pull_request:
- branches: [dev, main]
+ branches: [main, dev]
-jobs:
- build:
+ # 'workflow_dispatch' allows manual execution of this workflow under the repository's 'Actions' tab
+ workflow_dispatch:
+jobs:
+ build-doc:
+ name: Documentation building
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- # Standard drop-in approach that should work for most people.
- - uses: ammaraskar/sphinx-action@master
- with:
- docs-folder: "docs/"
- pre-build-command: "pip install --upgrade pip"
- # Create an artifact of the html output.
- - uses: actions/upload-artifact@v4
- with:
- name: documentation_html
- path: docs/_build/html/
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Build pages
+ uses: Kjuly/sphinx-builder@main
+ with:
+ source_root: "docs/"
+ - name: Create artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: Documentation HTML
+ path: docs/_build/html/
diff --git a/.github/workflows/sphinx-deploy.yml b/.github/workflows/sphinx-deploy.yml
index 4087162..2f90d86 100644
--- a/.github/workflows/sphinx-deploy.yml
+++ b/.github/workflows/sphinx-deploy.yml
@@ -46,7 +46,7 @@ jobs:
# Create an artifact of the html output
- uses: actions/upload-artifact@v4
with:
- name: documentation_html
+ name: Documentation HTML
path: docs/_build/html/
# Publish built docs to gh-pages branch
- name: Commit documentation changes
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 12cdd1a..7100377 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -43,6 +43,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
- name: Install dependencies
run: sudo apt install -y python3-pytest
diff --git a/CHANGELOG b/CHANGELOG
index c9e5ea4..62fa7ff 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,12 @@
+
+===========================================
+
+v0.1 - 2026/01/09
+
+- First release
+- GFSK/GMSK support
+
+===========================================
===========================================
v0.0 - 2025/03/30
diff --git a/README.md b/README.md
index a79cad2..e095734 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# PyModulation
+
+
+
@@ -15,11 +17,20 @@
## Overview
-> TODO
+PyModulation is a Python library that implements a collection of digital modulation and demodulation techniques with a strong focus on Software-Defined Radio (SDR) applications. The library is designed to provide a simple, consistent, and extensible interface for working with different modulation schemes, enabling rapid development, testing, and prototyping of wireless communication systems.
+
+The main objective of PyModulation is to allow the direct use of supported modulation techniques with SDR hardware, while remaining flexible enough to be used in simulations, offline signal processing, and educational contexts. By abstracting common modulation tasks, the library helps users focus on system design and experimentation rather than low-level signal handling.
+
+PyModulation is suitable for a wide range of applications, including SDR-based transmitters and receivers, communication protocol prototyping, academic research, and teaching digital communications concepts. Its modular architecture makes it easy to extend with new modulation schemes and integrate with existing Python-based SDR and signal-processing toolchains.
+
+The following modulations are currently supported:
+
+* GFSK/GMSK
## Dependencies
-* None
+* NumPy
+* SciPy
## Installing
diff --git a/docs/conf.py b/docs/conf.py
index 688c1f2..babd59e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -3,26 +3,28 @@
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
+import sphinx_rtd_theme
+
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'PyModulation'
copyright = 'Copyright The PyModulation Contributors'
author = 'Gabriel Mariano Marcelino'
-release = 'v0.0.0'
+release = 'v0.1'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-extensions = []
+extensions = [
+ 'sphinx_rtd_theme',
+]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
-
-
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
-html_theme = 'alabaster'
+html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
diff --git a/docs/img/logo.jpg b/docs/img/logo.jpg
new file mode 100644
index 0000000..471551a
Binary files /dev/null and b/docs/img/logo.jpg differ
diff --git a/docs/img/logo.png b/docs/img/logo.png
new file mode 100644
index 0000000..99d7071
Binary files /dev/null and b/docs/img/logo.png differ
diff --git a/docs/index.rst b/docs/index.rst
index 5bead8e..18980cd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,18 +3,22 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
-Welcome to PyModulation's documentation!
-========================================
+.. image:: img/logo.jpg
+ :width: 400
-.. toctree::
- :maxdepth: 2
- :caption: Contents:
+PyModulation is a Python library that implements a collection of digital modulation and demodulation techniques with a strong focus on Software-Defined Radio (SDR) applications. The library is designed to provide a simple, consistent, and extensible interface for working with different modulation schemes, enabling rapid development, testing, and prototyping of wireless communication systems.
+
+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
+ installation
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..5f95688
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,23 @@
+************
+Installation
+************
+
+The simplest way to install the PyModulation library is doing it from the PyPI repository [1]_:
+
+::
+
+ pip install pymodulation
+
+It is also possible to install directly from source files available in the git repository [2]_:
+
+::
+
+ python setup.py install
+
+With the github releases, there is also some ready to install distribuition packages (*.rpm*, *.tar.gz*, etc.).
+
+References
+==========
+
+.. [1] https://pypi.org/project/pymodulation/
+.. [2] https://github.com/mgm8/pymodulation
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..31cfba8
--- /dev/null
+++ b/docs/overview.rst
@@ -0,0 +1,9 @@
+********
+Overview
+********
+
+PyModulation is a Python library that implements a collection of digital modulation and demodulation techniques with a strong focus on Software-Defined Radio (SDR) applications. The library is designed to provide a simple, consistent, and extensible interface for working with different modulation schemes, enabling rapid development, testing, and prototyping of wireless communication systems.
+
+The main objective of PyModulation is to allow the direct use of supported modulation techniques with SDR hardware, while remaining flexible enough to be used in simulations, offline signal processing, and educational contexts. By abstracting common modulation tasks, the library helps users focus on system design and experimentation rather than low-level signal handling.
+
+PyModulation is suitable for a wide range of applications, including SDR-based transmitters and receivers, communication protocol prototyping, academic research, and teaching digital communications concepts. Its modular architecture makes it easy to extend with new modulation schemes and integrate with existing Python-based SDR and signal-processing toolchains.
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..6b104fb
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+Sphinx
+sphinx-rtd-theme
diff --git a/docs/usage.rst b/docs/usage.rst
new file mode 100644
index 0000000..e87344f
--- /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 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 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..ea879b0 100644
--- a/pymodulation/__init__.py
+++ b/pymodulation/__init__.py
@@ -20,5 +20,7 @@
#
#
-from pymodulation.pymodulation import PyModulation
from pymodulation.version import __version__
+
+from pymodulation.gfsk import GFSK
+from pymodulation.gmsk import GMSK
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/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