├── .coveragerc ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── ModulationPy ├── ModulationPy.py └── __init__.py ├── README.md ├── docs ├── CommPy_vs_ModulationPy.ipynb ├── Linear_modulation_16_08_19.pptx ├── PSK_BER.py ├── QAM_BER.py ├── img │ ├── modulationpy_logo.png │ ├── psk_ber.png │ ├── qam_signconst.PNG │ ├── qpsk_signconst.PNG │ └── simulator_scheme.PNG └── psk.xlsx ├── requirements.txt ├── setup.py └── tests.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = ModulationPy 4 | include = */ModulationPy/* 5 | omit = 6 | */setup.py 7 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: pyHPRks7EyYvOEFiFd8NKyPATd2FeUTLg 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # static files generated from Django application using `collectstatic` 142 | media 143 | static -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | before_install: 5 | - python --version 6 | - pip install -U pip 7 | - pip install -r requirements.txt 8 | - pip install -U pytest 9 | - pip install codecov 10 | script: python3 tests.py # run tests 11 | after_success: 12 | - codecov # submit coverage 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Vladimir Fadeev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /ModulationPy/ModulationPy.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | 7 | class Modem: 8 | def __init__(self, M, gray_map=True, bin_input=True, soft_decision=True, bin_output=True): 9 | 10 | N = np.log2(M) # bits per symbol 11 | if N != np.round(N): 12 | raise ValueError("M should be 2**n, with n=1, 2, 3...") 13 | if soft_decision == True and bin_output == False: 14 | raise ValueError("Non-binary output is available only for hard decision") 15 | 16 | self.M = M # modulation order 17 | self.N = int(N) # bits per symbol 18 | self.m = [i for i in range(self.M)] 19 | self.gray_map = gray_map 20 | self.bin_input = bin_input 21 | self.soft_decision = soft_decision 22 | self.bin_output = bin_output 23 | 24 | ''' SERVING METHODS ''' 25 | 26 | def __gray_encoding(self, dec_in): 27 | """ Encodes values by Gray encoding rule. 28 | 29 | Parameters 30 | ---------- 31 | dec_in : list of ints 32 | Input sequence of decimals to be encoded by Gray. 33 | Returns 34 | ------- 35 | gray_out: list of ints 36 | Output encoded by Gray sequence. 37 | """ 38 | 39 | bin_seq = [np.binary_repr(d, width=self.N) for d in dec_in] 40 | gray_out = [] 41 | for bin_i in bin_seq: 42 | gray_vals = [str(int(bin_i[idx]) ^ int(bin_i[idx - 1])) 43 | if idx != 0 else bin_i[0] 44 | for idx in range(0, len(bin_i))] 45 | gray_i = "".join(gray_vals) 46 | gray_out.append(int(gray_i, 2)) 47 | return gray_out 48 | 49 | def create_constellation(self, m, s): 50 | """ Creates signal constellation. 51 | Parameters 52 | ---------- 53 | m : list of ints 54 | Possible decimal values of the signal constellation (0 ... M-1). 55 | s : list of complex values 56 | Possible coordinates of the signal constellation. 57 | Returns 58 | ------- 59 | dict_out: dict 60 | Output dictionary where 61 | key is the bit sequence or decimal value and 62 | value is the complex coordinate. 63 | """ 64 | 65 | if self.bin_input == False and self.gray_map == False: 66 | dict_out = {k: v for k, v in zip(m, s)} 67 | elif self.bin_input == False and self.gray_map == True: 68 | mg = self.__gray_encoding(m) 69 | dict_out = {k: v for k, v in zip(mg, s)} 70 | elif self.bin_input == True and self.gray_map == False: 71 | mb = self.de2bin(m) 72 | dict_out = {k: v for k, v in zip(mb, s)} 73 | elif self.bin_input == True and self.gray_map == True: 74 | mg = self.__gray_encoding(m) 75 | mgb = self.de2bin(mg) 76 | dict_out = {k: v for k, v in zip(mgb, s)} 77 | return dict_out 78 | 79 | def llr_preparation(self): 80 | """ Creates the coordinates 81 | where either zeros or ones can be placed in the signal constellation.. 82 | Returns 83 | ------- 84 | zeros : list of lists of complex values 85 | The coordinates where zeros can be placed in the signal constellation. 86 | ones : list of lists of complex values 87 | The coordinates where ones can be placed in the signal constellation. 88 | """ 89 | code_book = self.code_book 90 | 91 | zeros = [[] for i in range(self.N)] 92 | ones = [[] for i in range(self.N)] 93 | 94 | bin_seq = self.de2bin(self.m) 95 | 96 | for bin_idx, bin_symb in enumerate(bin_seq): 97 | if self.bin_input == True: 98 | key = bin_symb 99 | else: 100 | key = bin_idx 101 | for possition, digit in enumerate(bin_symb): 102 | if digit == '0': 103 | zeros[possition].append(code_book[key]) 104 | else: 105 | ones[possition].append(code_book[key]) 106 | return zeros, ones 107 | 108 | ''' DEMODULATION ALGORITHMS ''' 109 | 110 | def __ApproxLLR(self, x, noise_var): 111 | """ Calculates approximate Log-likelihood Ratios (LLRs) [1]. 112 | Parameters 113 | ---------- 114 | x : 1-D ndarray of complex values 115 | Received complex-valued symbols to be demodulated. 116 | noise_var: float 117 | Additive noise variance. 118 | Returns 119 | ------- 120 | result: 1-D ndarray of floats 121 | Output LLRs. 122 | Reference: 123 | [1] Viterbi, A. J., "An Intuitive Justification and a 124 | Simplified Implementation of the MAP Decoder for Convolutional Codes," 125 | IEEE Journal on Selected Areas in Communications, 126 | vol. 16, No. 2, pp 260–264, Feb. 1998 127 | 128 | """ 129 | 130 | zeros = self.zeros 131 | ones = self.ones 132 | LLR = [] 133 | for (zero_i, one_i) in zip(zeros, ones): 134 | num = [((np.real(x) - np.real(z)) ** 2) 135 | + ((np.imag(x) - np.imag(z)) ** 2) 136 | for z in zero_i] 137 | denum = [((np.real(x) - np.real(o)) ** 2) 138 | + ((np.imag(x) - np.imag(o)) ** 2) 139 | for o in one_i] 140 | 141 | num_post = np.amin(num, axis=0, keepdims=True) 142 | denum_post = np.amin(denum, axis=0, keepdims=True) 143 | 144 | llr = np.transpose(num_post[0]) - np.transpose(denum_post[0]) 145 | LLR.append(-llr / noise_var) 146 | 147 | result = np.zeros((len(x) * len(zeros))) 148 | for i, llr in enumerate(LLR): 149 | result[i::len(zeros)] = llr 150 | return result 151 | 152 | ''' METHODS TO EXECUTE ''' 153 | 154 | def modulate(self, msg): 155 | """ Modulates binary or decimal stream. 156 | Parameters 157 | ---------- 158 | x : 1-D ndarray of ints 159 | Decimal or binary stream to be modulated. 160 | Returns 161 | ------- 162 | modulated : 1-D array of complex values 163 | Modulated symbols (signal envelope). 164 | """ 165 | 166 | if (self.bin_input == True) and ((len(msg) % self.N) != 0): 167 | raise ValueError("The length of the binary input should be a multiple of log2(M)") 168 | 169 | if (self.bin_input == True) and ((max(msg) > 1.) or (min(msg) < 0.)): 170 | raise ValueError("The input values should be 0s or 1s only!") 171 | if (self.bin_input == False) and ((max(msg) > (self.M - 1)) or (min(msg) < 0.)): 172 | raise ValueError("The input values should be in following range: [0, ... M-1]!") 173 | 174 | if self.bin_input: 175 | msg = [str(bit) for bit in msg] 176 | splited = ["".join(msg[i:i + self.N]) 177 | for i in range(0, len(msg), self.N)] # subsequences of bits 178 | modulated = [self.code_book[s] for s in splited] 179 | else: 180 | modulated = [self.code_book[dec] for dec in msg] 181 | return np.array(modulated) 182 | 183 | def demodulate(self, x, noise_var=1.): 184 | """ Demodulates complex symbols. 185 | 186 | Yes, MathWorks company provides several algorithms to demodulate 187 | BPSK, QPSK, 8-PSK and other M-PSK modulations in hard output manner: 188 | https://www.mathworks.com/help/comm/ref/mpskdemodulatorbaseband.html 189 | 190 | However, to reduce the number of implemented schemes the following way is used in our project: 191 | - calculate LLRs (soft decision) 192 | - map LLR to bits according to the sign of LLR (inverse of NRZ) 193 | We guess the complexity issues are not the critical part due to hard output demodulators are not so popular. 194 | This phenomenon depends on channel decoders properties: 195 | e.g., Convolutional codes, Turbo convolutional codes and LDPC codes work better with LLR. 196 | 197 | Parameters 198 | ---------- 199 | x : 1-D ndarray of complex symbols 200 | Decimal or binary stream to be demodulated. 201 | noise_var: float 202 | Additive noise variance. 203 | Returns 204 | ------- 205 | result : 1-D array floats 206 | Demodulated message (LLRs or binary sequence). 207 | """ 208 | 209 | if self.soft_decision: 210 | result = self.__ApproxLLR(x, noise_var) 211 | else: 212 | if self.bin_output: 213 | llr = self.__ApproxLLR(x, noise_var) 214 | result = (np.sign(-llr) + 1) / 2 # NRZ-to-bin 215 | else: 216 | llr = self.__ApproxLLR(x, noise_var) 217 | result = self.bin2de((np.sign(-llr) + 1) / 2) 218 | return result 219 | 220 | 221 | class PSKModem(Modem): 222 | def __init__(self, M, phi=0, gray_map=True, bin_input=True, soft_decision=True, bin_output=True): 223 | super().__init__(M, gray_map, bin_input, soft_decision, bin_output) 224 | self.phi = phi # phase rotation 225 | self.s = list(np.exp(1j * self.phi + 1j * 2 * np.pi * np.array(self.m) / self.M)) 226 | self.code_book = self.create_constellation(self.m, self.s) 227 | self.zeros, self.ones = self.llr_preparation() 228 | 229 | def de2bin(self, decs): 230 | """ Converts values from decimal to binary representation. 231 | If the input is binary, the conversion from binary to decimal should be done before. 232 | Therefore, this supportive method is implemented. 233 | 234 | This method has an additional heuristic: 235 | the bit sequence of "even" modulation schemes (e.g., QPSK) should be read right to left. 236 | 237 | Parameters 238 | ---------- 239 | decs : list of ints 240 | Input decimal values. 241 | Returns 242 | ------- 243 | bin_out : list of ints 244 | Output binary sequences. 245 | """ 246 | if self.N % 2 == 0: 247 | bin_out = [np.binary_repr(d, width=self.N)[::-1] 248 | for d in decs] 249 | else: 250 | bin_out = [np.binary_repr(d, width=self.N) 251 | for d in decs] 252 | return bin_out 253 | 254 | def bin2de(self, bin_in): 255 | """ Converts values from binary to decimal representation. 256 | Parameters 257 | ---------- 258 | bin_in : list of ints 259 | Input binary values. 260 | Returns 261 | ------- 262 | dec_out : list of ints 263 | Output decimal values. 264 | """ 265 | 266 | dec_out = [] 267 | N = self.N # bits per modulation symbol (local variables are tiny bit faster) 268 | Ndecs = int(len(bin_in) / N) # length of the decimal output 269 | for i in range(Ndecs): 270 | bin_seq = bin_in[i * N:i * N + N] # binary equivalent of the one decimal value 271 | str_o = "".join([str(int(b)) for b in bin_seq]) # binary sequence to string 272 | if N % 2 == 0: 273 | str_o = str_o[::-1] 274 | dec_out.append(int(str_o, 2)) 275 | return dec_out 276 | 277 | def plot_const(self): 278 | """ Plots signal constellation """ 279 | 280 | const = self.code_book 281 | fig = plt.figure(figsize=(6, 4), dpi=150) 282 | for i in list(const): 283 | x = np.real(const[i]) 284 | y = np.imag(const[i]) 285 | plt.plot(x, y, 'o', color='green') 286 | if x < 0: 287 | h = 'right' 288 | xadd = -.03 289 | else: 290 | h = 'left' 291 | xadd = .03 292 | if y < 0: 293 | v = 'top' 294 | yadd = -.03 295 | else: 296 | v = 'bottom' 297 | yadd = .03 298 | if abs(x) < 1e-9 and abs(y) > 1e-9: 299 | h = 'center' 300 | elif abs(x) > 1e-9 and abs(y) < 1e-9: 301 | v = 'center' 302 | plt.annotate(i, (x + xadd, y + yadd), ha=h, va=v) 303 | if self.M == 2: 304 | M = 'B' 305 | elif self.M == 4: 306 | M = 'Q' 307 | else: 308 | M = str(self.M) + "-" 309 | 310 | if self.gray_map: 311 | mapping = 'Gray' 312 | else: 313 | mapping = 'Binary' 314 | 315 | if self.bin_input: 316 | inputs = 'Binary' 317 | else: 318 | inputs = 'Decimal' 319 | 320 | plt.grid() 321 | plt.axvline(linewidth=1.0, color='black') 322 | plt.axhline(linewidth=1.0, color='black') 323 | plt.axis([-1.5, 1.5, -1.5, 1.5]) 324 | plt.title(M + 'PSK, phase rotation: ' + str(round(self.phi, 5)) + \ 325 | ', Mapping: ' + mapping + ', Input: ' + inputs) 326 | plt.show() 327 | 328 | 329 | class QAMModem(Modem): 330 | def __init__(self, M, gray_map=True, bin_input=True, soft_decision=True, bin_output=True): 331 | super().__init__(M, gray_map, bin_input, soft_decision, bin_output) 332 | 333 | if np.sqrt(M) != np.fix(np.sqrt(M)) or np.log2(np.sqrt(M)) != np.fix(np.log2(np.sqrt(M))): 334 | raise ValueError('M must be a square of a power of 2') 335 | 336 | self.m = [i for i in range(self.M)] 337 | self.s = self.__qam_symbols() 338 | self.code_book = self.create_constellation(self.m, self.s) 339 | 340 | if self.gray_map: 341 | self.__gray_qam_arange() 342 | 343 | self.zeros, self.ones = self.llr_preparation() 344 | 345 | def __qam_symbols(self): 346 | """ Creates M-QAM complex symbols.""" 347 | 348 | c = np.sqrt(self.M) 349 | b = -2 * (np.array(self.m) % c) + c - 1 350 | a = 2 * np.floor(np.array(self.m) / c) - c + 1 351 | s = list((a + 1j * b)) 352 | return s 353 | 354 | def __gray_qam_arange(self): 355 | """ This method re-arranges complex coordinates according to Gray coding requirements. 356 | To implement correct Gray mapping the additional heuristic is used: 357 | the even "columns" in the signal constellation is complex conjugated. 358 | """ 359 | 360 | for idx, (key, item) in enumerate(self.code_book.items()): 361 | if (np.floor(idx / np.sqrt(self.M)) % 2) != 0: 362 | self.code_book[key] = np.conj(item) 363 | 364 | def de2bin(self, decs): 365 | """ Converts values from decimal to binary representation. 366 | Parameters 367 | ---------- 368 | decs : list of ints 369 | Input decimal values. 370 | Returns 371 | ------- 372 | bin_out : list of ints 373 | Output binary sequences. 374 | """ 375 | bin_out = [np.binary_repr(d, width=self.N) for d in decs] 376 | return bin_out 377 | 378 | def bin2de(self, bin_in): 379 | """ Converts values from binary to decimal representation. 380 | Parameters 381 | ---------- 382 | bin_in : list of ints 383 | Input binary values. 384 | Returns 385 | ------- 386 | dec_out : list of ints 387 | Output decimal values. 388 | """ 389 | 390 | dec_out = [] 391 | N = self.N # bits per modulation symbol (local variables are tiny bit faster) 392 | Ndecs = int(len(bin_in) / N) # length of the decimal output 393 | for i in range(Ndecs): 394 | bin_seq = bin_in[i * N:i * N + N] # binary equivalent of the one decimal value 395 | str_o = "".join([str(int(b)) for b in bin_seq]) # binary sequence to string 396 | dec_out.append(int(str_o, 2)) 397 | return dec_out 398 | 399 | def plot_const(self): 400 | """ Plots signal constellation """ 401 | 402 | if self.M <= 16: 403 | limits = np.log2(self.M) 404 | size = 'small' 405 | elif self.M == 64: 406 | limits = 1.5 * np.log2(self.M) 407 | size = 'x-small' 408 | else: 409 | limits = 2.25 * np.log2(self.M) 410 | size = 'xx-small' 411 | 412 | const = self.code_book 413 | fig = plt.figure(figsize=(6, 4), dpi=150) 414 | for i in list(const): 415 | x = np.real(const[i]) 416 | y = np.imag(const[i]) 417 | plt.plot(x, y, 'o', color='red') 418 | if x < 0: 419 | h = 'right' 420 | xadd = -.05 421 | else: 422 | h = 'left' 423 | xadd = .05 424 | if y < 0: 425 | v = 'top' 426 | yadd = -.05 427 | else: 428 | v = 'bottom' 429 | yadd = .05 430 | if abs(x) < 1e-9 and abs(y) > 1e-9: 431 | h = 'center' 432 | elif abs(x) > 1e-9 and abs(y) < 1e-9: 433 | v = 'center' 434 | plt.annotate(i, (x + xadd, y + yadd), ha=h, va=v, size=size) 435 | M = str(self.M) 436 | if self.gray_map: 437 | mapping = 'Gray' 438 | else: 439 | mapping = 'Binary' 440 | 441 | if self.bin_input: 442 | inputs = 'Binary' 443 | else: 444 | inputs = 'Decimal' 445 | 446 | plt.grid() 447 | plt.axvline(linewidth=1.0, color='black') 448 | plt.axhline(linewidth=1.0, color='black') 449 | plt.axis([-limits, limits, -limits, limits]) 450 | plt.title(M + '-QAM, Mapping: ' + mapping + ', Input: ' + inputs) 451 | plt.show() 452 | -------------------------------------------------------------------------------- /ModulationPy/__init__.py: -------------------------------------------------------------------------------- 1 | from ModulationPy.ModulationPy import * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Build Status](https://travis-ci.com/kirlf/ModulationPy.svg?branch=master)](https://travis-ci.com/kirlf/ModulationPy) 4 | [![PyPi](https://badge.fury.io/py/ModulationPy.svg)](https://pypi.org/project/ModulationPy/) 5 | [![Coverage](https://coveralls.io/repos/kirlf/ModulationPy/badge.svg)](https://coveralls.io/r/kirlf/ModulationPy) 6 | 7 | # ModulationPy 8 | 9 | Digital baseband linear modems: M-PSK and M-QAM. 10 | 11 | ## Motivation 12 | 13 | The main idea is to develop a Python module that allows replacing related to **baseband digital linear modulations** MatLab/Octave functions and objects. This project is inspired by [CommPy](https://github.com/veeresht/CommPy) open-source project. 14 | 15 | ## Theory basics 16 | 17 | ### 1. Linearity 18 | 19 | Linear modulation schemes have a canonical form \[1\]: 20 | 21 |

 s(t) = s_I(t)cos(2\pi f_c t) - s_Q(t)cos(2\pi f_c t) \qquad (1)

22 | 23 | where s_I(t) is the In-phase part, s_Q(t) is the Quadrature part, f_c is the carrier frequency, and t is the time moment. In-phase and Quadrature parts are low-pass signals that **linearly** correlate with an information signal. 24 | 25 | ### 2. Baseband representation 26 | 27 | Modulation scheme can also be modeled without consideration of the carrier frequency and bit duration. The baseband analogs can be used for research due to the main properties depend on the envelope (complex symbols). 28 | 29 | ### 3. Modulation order 30 | 31 | Modulation order means number of possible modulation symbols. The number of bits per modulation symbol 32 | depend on the modulation order: 33 | 34 |

 N = log_2(M) \qquad(2)

35 | 36 | Modulation order relates to **gross bit rate** concept: 37 | 38 |

 R_b = R_slog_2(N) \qquad(3)

39 | 40 | where R_s is the baud or symbol rate. Baud rate usually relates to the coherence bandwidth B_c (see more in \[2\]). 41 | 42 | > See more in ["Basics of linear digital modulations"](https://speakerdeck.com/kirlf/linear-digital-modulations) (slides). 43 | 44 | ## Installation 45 | 46 | Released version on PyPi: 47 | 48 | ``` bash 49 | $ pip install ModulationPy 50 | ``` 51 | 52 | To build by sources, clone from github and install as follows: 53 | 54 | ```bash 55 | $ git clone https://github.com/kirlf/ModulationPy.git 56 | $ cd ModulationPy 57 | $ python3 setup.py install 58 | ``` 59 | 60 | ## What are modems available? 61 | 62 | - **M-PSK**: **P**hase **S**hift **K**eying 63 | - **M-QAM**: **Q**uadratured **A**mplitude **M**odulation 64 | 65 | where **M** is the modulation order. 66 | 67 | ### 1. M-PSK 68 | 69 | M-PSK modem is available in ```class PSKModem``` with the following parameters: 70 | 71 | | Parameter | Possible values | Description | 72 | | ------------- |:-------------| :-----| 73 | | ``` M ``` | positive integer values power of 2 | Modulation order. Specify the number of points in the signal constellation as scalar value that is a positive integer power of two.| 74 | | ```phi``` | float values | Phase rotation. Specify the phase offset of the signal constellation, in radians, as a real scalar value. The default is 0.| 75 | | ```gray_map``` | ```True``` or ```False``` | Specifies mapping rule. If parametr is ```True``` the modem works with Gray mapping, else it works with Binary mapping. The default is ```True```.| 76 | | ```bin_input``` | ```True``` or ```False```| Specify whether the input of ```modulate()``` method is bits or integers. When you set this property to ```True```, the ```modulate()``` method input requires a column vector of bit values. The length of this vector must an integer multiple of log2(M). The default is ```True```.| 77 | | ```soft_decision``` | ```True``` or ```False``` | Specify whether the output values of ```demodulate()``` method is demodulated as hard or soft decisions. If parametr is ```True``` the output will be Log-likelihood ratios (LLR's), else binary symbols. The default is ```True```.| 78 | | ```bin_output``` | ```True``` or ```False```|Specify whether the output of ```demodulate()``` method is bits or integers. The default is ```True```.| 79 | 80 | The mapping of into the modulation symbols is done by the following formula \[3\]: 81 | 82 |

 r = exp(j\phi + j2\pi m/M)

83 | 84 | where  \phi is the phase rotation,  m is the mapped to decimals (in range between *0* and *M-1*) input symbol, and  M is the modulation order. 85 | 86 | 87 | ### 2. M-QAM 88 | 89 | M-QAM modem is available in ```class QAMModem``` with the following parameters: 90 | 91 | | Parameter | Possible values | Description | 92 | | ------------- |:-------------| :-----| 93 | | ``` M ``` | positive integer values power of 2 | Modulation order. Specify the number of points in the signal constellation as scalar value that is a positive integer power of two.| 94 | | ```gray_map``` | ```True``` or ```False``` | Specifies mapping rule. If parametr is ```True``` the modem works with Gray mapping, else it works with Binary mapping. The default is ```True```.| 95 | | ```bin_input``` | ```True``` or ```False```| Specify whether the input of ```modulate()``` method is bits or integers. When you set this property to ```True```, the ```modulate()``` method input requires a column vector of bit values. The length of this vector must an integer multiple of log2(M). The default is ```True```.| 96 | | ```soft_decision``` | ```True``` or ```False``` | Specify whether the output values of ```demodulate()``` method is demodulated as hard or soft decisions. If parametr is ```True``` the output will be Log-likelihood ratios (LLR's), else binary symbols. The default is ```True```.| 97 | | ```bin_output``` | ```True``` or ```False```|Specify whether the output of ```demodulate()``` method is bits or integers. The default is ```True```.| 98 | 99 | Now the QAM modulation is designed as in **qammod** Octave function \[4\]. It requires only "even" (in sense, that ```log2(M)``` is even) modulation schemes (4-QAM, 16-QAM, 64-QAM and so on - universal and simple). 100 | Anyway, there are no "odd" modulation schemes in [popular wireless communication standards](https://www.quora.com/What-different-modulation-techniques-are-used-in-1G-2G-3G-4G-and-5G). 101 | 102 | 103 | ## How to use? 104 | 105 | ### 1. Initialization. 106 | 107 | E.g., **QPSK** with the pi/4 phase offset, binary input and Gray mapping: 108 | 109 | ```python 110 | from ModulationPy import PSKModem, QAMModem 111 | import numpy as np 112 | 113 | modem = PSKModem(4, np.pi/4, 114 | gray_map=True, 115 | bin_input=True) 116 | ``` 117 | 118 | To show signal constellation use the ``` plot_const()``` method: 119 | 120 | ```python 121 | 122 | modem.plot_const() 123 | 124 | ``` 125 | 126 | 127 | 128 | 129 | E.g. **16-QAM** with decimal input and Gray mapping 130 | 131 | ```python 132 | modem = QAMModem(16, 133 | gray_map=True, 134 | bin_input=False) 135 | 136 | modem.plot_const() 137 | ``` 138 | 139 | 140 | 141 | 142 | ### 2. Modulation and demodulation 143 | 144 | To modulate and demodulate use ```modulate()``` and ```demodulate()``` methods. 145 | 146 | The method ```modulate()``` has the one input argument: 147 | 148 | - decimal or binary stream to be modulated (```1-D ndarray of ints```). 149 | 150 | The method ```demodulate()``` has the two input arguments: 151 | 152 | - data stream to be demodulated (```1-D ndarray of complex symbols```) and 153 | 154 | - additive noise variance (```float```, default is 1.0). 155 | 156 | E.g., QPSK (binary input/output): 157 | 158 | ```python 159 | 160 | import numpy as np 161 | from ModulationPy import PSKModem 162 | 163 | modem = PSKModem(4, np.pi/4, 164 | bin_input=True, 165 | soft_decision=False, 166 | bin_output=True) 167 | 168 | msg = np.array([0, 0, 0, 1, 1, 0, 1, 1]) # input message 169 | 170 | modulated = modem.modulate(msg) # modulation 171 | demodulated = modem.demodulate(modulated) # demodulation 172 | 173 | print("Modulated message:\n"+str(modulated)) 174 | print("Demodulated message:\n"+str(demodulated)) 175 | 176 | >>> Modulated message: 177 | [ 0.70710678+0.70710678j 0.70710678-0.70710678j -0.70710678+0.70710678j 178 | -0.70710678-0.70710678j] 179 | 180 | >>> Demodulated message: 181 | [0. 0. 0. 1. 1. 0. 1. 1.] 182 | 183 | ``` 184 | 185 | E.g., QPSK (decimal input/output): 186 | 187 | ``` python 188 | 189 | import numpy as np 190 | from ModulationPy import PSKModem 191 | 192 | modem = PSKModem(4, np.pi/4, 193 | bin_input=False, 194 | soft_decision=False, 195 | bin_output=False) 196 | 197 | msg = np.array([0, 1, 2, 3]) # input message 198 | 199 | modulated = modem.modulate(msg) # modulation 200 | demodulated = modem.demodulate(modulated) # demodulation 201 | 202 | print("Modulated message:\n"+str(modulated)) 203 | print("Demodulated message:\n"+str(demodulated)) 204 | 205 | >>> Modulated message: 206 | [ 0.70710678+0.70710678j -0.70710678+0.70710678j 0.70710678-0.70710678j 207 | -0.70710678-0.70710678j] 208 | 209 | >>> Demodulated message: 210 | [0, 1, 2, 3] 211 | 212 | ``` 213 | 214 | E.g., 16-QAM (decimal input/output): 215 | 216 | ``` python 217 | 218 | import numpy as np 219 | from ModulationPy import QAMModem 220 | 221 | modem = PSKModem(16, 222 | bin_input=False, 223 | soft_decision=False, 224 | bin_output=False) 225 | 226 | msg = np.array([i for i in range(16)]) # input message 227 | 228 | modulated = modem.modulate(msg) # modulation 229 | demodulated = modem.demodulate(modulated) # demodulation 230 | 231 | print("Demodulated message:\n"+str(demodulated)) 232 | 233 | >>> Demodulated message: 234 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 235 | 236 | ``` 237 | 238 | 239 | ### 3. Bit-error ratio performance 240 | 241 | Let us demonstrate this at example with the following system model: 242 | 243 | ![](https://raw.githubusercontent.com/kirlf/ModulationPy/master/docs/img/simulator_scheme.PNG) 244 | 245 | *Fig. 1. The structural scheme of the test communication model.* 246 | 247 | The simulation results will be compared with theoretical curves \[5\]: 248 | 249 |

 
250 | P_b = \frac{erfc \left( \sqrt{log_2(M)\frac{E_b}{N_o}}\sin\left( \frac{\pi}{M} \right) \right)}{log_2(M)}
251 |

252 | 253 | where M >4 , erfc(*) denotes the [complementary error function](https://en.wikipedia.org/wiki/Error_function#Complementary_error_function), E_b is the energy per one bit, and N_o is the noise spectral density. 254 | 255 | In case of BPSK and QPSK the following formula should be used for error probability: 256 | 257 |

 
258 | P_{b, BQ} = \frac{1}{2}erfc \left( \sqrt{\frac{E_b}{N_o}}\right)} \qquad (6)
259 |

260 | 261 | The results are presented below. 262 | 263 | 264 | 265 | *Fig. 2. Bit-error ratio curves in presence of AWGN (M-PSK).* 266 | 267 | The theoretical BER curves for M-QAM case can be obtained via the following formula [5]: 268 | 269 |

P_{b,QAM} = 2 \left(\frac{\sqrt{M}-1}{M\log_2M}\right)erfc\left( \sqrt{\frac{3\log_2M}{2(M-1)} \frac{E_b}{N_0}} \right) \qquad (7)

270 | 271 | The theoretical BER curve for 4-QAM is identical to the BPSK/QPSK case. 272 | 273 | 274 | 275 | *Fig. 3. Bit-error ratio curves in presence of AWGN (M-QAM).* 276 | 277 | > The source codes of the simulation are available here: [M-PSK](https://github.com/kirlf/ModulationPy/blob/master/docs/PSK_BER.py), [M-QAM](https://github.com/kirlf/ModulationPy/blob/master/docs/QAM_BER.py). 278 | 279 | The dismatchings are appeared cause of small number of averages. Anyway, it works. 280 | 281 | ### 4. Execution time performance 282 | 283 | To demonstrate execution time performance let us comparate our package with another implementation, e.g. with the mentioned above [CommPy](https://github.com/veeresht/CommPy). 284 | 285 | The demodulation algorithm is developed according to following fomula in our project \[6\],\[7\]: 286 | 287 |

 L(b) = -\frac{1}{\sigma^2} \left( \min_{s \epsilon S_0} \left( (x - s_x)^2 + (y - s_y)^2 \right)  - \min_{s \epsilon S_1} \left( (x - s_x)^2 + (y - s_y)^2 \right)\right) \qquad(7)

288 | 289 | where  x is the In-phase coordinate of the received symbol,  y is the Quadrature coordinate of the received symbol,  s_x is the In-phase coordinate of ideal symbol or constellation point,  s_y is the Quadrature coordinate of ideal symbol or constellation point,  S_0 is the ideal symbols or constellation points with bit 0, at the given bit position,  S_1 is the ideal symbols or constellation points with bit 1, at the given bit position,  b is the transmitted bit (one of the K bits in an M-ary symbol, assuming all M symbols are equally probable,  \sigma^2 is the noise variance of baseband signal. 290 | 291 | Comparison information: 292 | 293 | - The script: [CommPy_vs_ModulationPy.ipynb](https://github.com/kirlf/ModulationPy/blob/master/docs/CommPy_vs_ModulationPy.ipynb) 294 | - Platform: https://jupyter.org/try (Classic Notebook) 295 | - The frame length: 10 000 (bits) 296 | 297 | Results: 298 | 299 | | Method (package) | Average execution time (ms)| 300 | | ------------- |:-------------:| 301 | | modulation (ModulationPy): QPSK | 10.3 | 302 | | modulation (CommPy): QPSK | 15.7 | 303 | | demodulation (ModulationPy): QPSK | 0.4 | 304 | | demodulation (CommPy): QPSK | 319 | 305 | | modulation (ModulationPy): 256-QAM | 8.9 | 306 | | modulation (CommPy): 256-QAM | 11.3 | 307 | | demodulation (ModulationPy): 256-QAM | 42.6 | 308 | | demodulation (CommPy): 256-QAM | 22 000 | 309 | 310 | 311 | Yes, I admit that our implementation is slower than MatLab blocks and functions (see ["Examples"](https://ch.mathworks.com/matlabcentral/fileexchange/72860-fast-qpsk-implementation?s_tid=prof_contriblnk)). However, I believe that this is a good start! 312 | 313 | ## References 314 | 315 | 1. Haykin S. Communication systems. – John Wiley & Sons, 2008. — p. 93 316 | 2. Goldsmith A. Wireless communications. – Cambridge university press, 2005. – p. 88-92 317 | 3. MathWorks: comm.PSKModulator (https://www.mathworks.com/help/comm/ref/comm.pskmodulator-system-object.html?s_tid=doc_ta) 318 | 4. Octave: qammod (https://octave.sourceforge.io/communications/function/qammod.html) 319 | 5. Link Budget Analysis: Digital Modulation, Part 3 (www.AtlantaRF.com) 320 | 6. Viterbi, A. J. (1998). An intuitive justification and a simplified implementation of the MAP decoder for convolutional codes. IEEE Journal on Selected Areas in Communications, 16(2), 260-264. 321 | 7. MathWorks: Approximate LLR Algorithm (https://www.mathworks.com/help/comm/ug/digital-modulation.html#brc6ymu) 322 | -------------------------------------------------------------------------------- /docs/CommPy_vs_ModulationPy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 3, 6 | "metadata": { 7 | "colab": {}, 8 | "colab_type": "code", 9 | "id": "ZZc-hhpItgMH" 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "import numpy as np\n", 14 | "import commpy.modulation as commpy_modulation\n", 15 | "from ModulationPy import PSKModem, QAMModem" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 4, 21 | "metadata": { 22 | "colab": {}, 23 | "colab_type": "code", 24 | "id": "y77TcvujtgML" 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "msg = np.random.randint(0, 2, int(1e4))" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": { 34 | "colab_type": "text", 35 | "id": "FjrlnawKtgMO" 36 | }, 37 | "source": [ 38 | "## ModulationPy" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 5, 44 | "metadata": { 45 | "colab": {}, 46 | "colab_type": "code", 47 | "id": "SQ0OoxBgtgMP" 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "modem = PSKModem(4) # our class initialization\n", 52 | "m = modem.modulate(msg)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 6, 58 | "metadata": { 59 | "colab": { 60 | "base_uri": "https://localhost:8080/", 61 | "height": 34 62 | }, 63 | "colab_type": "code", 64 | "id": "XC4av2DXtgMT", 65 | "outputId": "3b6ecef5-fb6e-4ef6-d8c0-cfe117ab4477" 66 | }, 67 | "outputs": [ 68 | { 69 | "name": "stdout", 70 | "output_type": "stream", 71 | "text": [ 72 | "10.3 ms ± 347 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 73 | ] 74 | } 75 | ], 76 | "source": [ 77 | "%%timeit\n", 78 | "m = modem.modulate(msg)" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 7, 84 | "metadata": { 85 | "colab": { 86 | "base_uri": "https://localhost:8080/", 87 | "height": 34 88 | }, 89 | "colab_type": "code", 90 | "id": "3ukIC8DStgMW", 91 | "outputId": "079e7758-52be-4bc6-95d1-fe246c880a18" 92 | }, 93 | "outputs": [ 94 | { 95 | "name": "stdout", 96 | "output_type": "stream", 97 | "text": [ 98 | "398 µs ± 41.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" 99 | ] 100 | } 101 | ], 102 | "source": [ 103 | "%%timeit\n", 104 | "modem.demodulate(m) # demodulation # demodulation" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 8, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "modem = QAMModem(256) # our class initialization\n", 114 | "m = modem.modulate(msg)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 9, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "name": "stdout", 124 | "output_type": "stream", 125 | "text": [ 126 | "8.86 ms ± 125 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 127 | ] 128 | } 129 | ], 130 | "source": [ 131 | "%%timeit\n", 132 | "m = modem.modulate(msg)" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 10, 138 | "metadata": {}, 139 | "outputs": [ 140 | { 141 | "name": "stdout", 142 | "output_type": "stream", 143 | "text": [ 144 | "42.6 ms ± 4.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 145 | ] 146 | } 147 | ], 148 | "source": [ 149 | "%%timeit\n", 150 | "modem.demodulate(m) # demodulation # demodulation" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": { 156 | "colab_type": "text", 157 | "id": "zLQNPPjatgMZ" 158 | }, 159 | "source": [ 160 | "## CommPy" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 11, 166 | "metadata": { 167 | "colab": {}, 168 | "colab_type": "code", 169 | "id": "qxnSclywtgMZ" 170 | }, 171 | "outputs": [], 172 | "source": [ 173 | "modem = commpy_modulation.PSKModem(4)\n", 174 | "m = modem.modulate(msg) " 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 12, 180 | "metadata": { 181 | "colab": { 182 | "base_uri": "https://localhost:8080/", 183 | "height": 34 184 | }, 185 | "colab_type": "code", 186 | "id": "wtTOnlX5tgMd", 187 | "outputId": "c7d610c6-eaa8-4738-a00f-c6faeffd82c0" 188 | }, 189 | "outputs": [ 190 | { 191 | "name": "stdout", 192 | "output_type": "stream", 193 | "text": [ 194 | "15.7 ms ± 851 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 195 | ] 196 | } 197 | ], 198 | "source": [ 199 | "%%timeit\n", 200 | "m = modem.modulate(msg) " 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 13, 206 | "metadata": { 207 | "colab": { 208 | "base_uri": "https://localhost:8080/", 209 | "height": 170 210 | }, 211 | "colab_type": "code", 212 | "id": "NfhCU81EtgMi", 213 | "outputId": "0b970e72-9e62-445a-ec7c-1ab2a3aa7187" 214 | }, 215 | "outputs": [ 216 | { 217 | "name": "stderr", 218 | "output_type": "stream", 219 | "text": [ 220 | "/Users/vladimir/opt/anaconda3/lib/python3.7/site-packages/commpy/modulation.py:130: RuntimeWarning: divide by zero encountered in double_scalars\n", 221 | " llr_den += exp((-abs(current_symbol - symbol) ** 2) / noise_var)\n", 222 | "/Users/vladimir/opt/anaconda3/lib/python3.7/site-packages/commpy/modulation.py:128: RuntimeWarning: divide by zero encountered in double_scalars\n", 223 | " llr_num += exp((-abs(current_symbol - symbol) ** 2) / noise_var)\n", 224 | "/Users/vladimir/opt/anaconda3/lib/python3.7/site-packages/commpy/modulation.py:128: RuntimeWarning: invalid value encountered in double_scalars\n", 225 | " llr_num += exp((-abs(current_symbol - symbol) ** 2) / noise_var)\n", 226 | "/Users/vladimir/opt/anaconda3/lib/python3.7/site-packages/commpy/modulation.py:130: RuntimeWarning: invalid value encountered in double_scalars\n", 227 | " llr_den += exp((-abs(current_symbol - symbol) ** 2) / noise_var)\n" 228 | ] 229 | }, 230 | { 231 | "name": "stdout", 232 | "output_type": "stream", 233 | "text": [ 234 | "319 ms ± 9.18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 235 | ] 236 | } 237 | ], 238 | "source": [ 239 | "%%timeit\n", 240 | "modem.demodulate(m, demod_type='soft')" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": 14, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "modem = commpy_modulation.QAMModem(256)\n", 250 | "m = modem.modulate(msg) " 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 15, 256 | "metadata": {}, 257 | "outputs": [ 258 | { 259 | "name": "stdout", 260 | "output_type": "stream", 261 | "text": [ 262 | "11.3 ms ± 1.26 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 263 | ] 264 | } 265 | ], 266 | "source": [ 267 | "%%timeit\n", 268 | "m = modem.modulate(msg) " 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 16, 274 | "metadata": {}, 275 | "outputs": [ 276 | { 277 | "name": "stdout", 278 | "output_type": "stream", 279 | "text": [ 280 | "22.4 s ± 2.01 s per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 281 | ] 282 | } 283 | ], 284 | "source": [ 285 | "%%timeit\n", 286 | "modem.demodulate(m, demod_type='soft')" 287 | ] 288 | } 289 | ], 290 | "metadata": { 291 | "colab": { 292 | "name": "CommPy-vs-ModulationPy.ipynb", 293 | "provenance": [] 294 | }, 295 | "kernelspec": { 296 | "display_name": "Python 3", 297 | "language": "python", 298 | "name": "python3" 299 | }, 300 | "language_info": { 301 | "codemirror_mode": { 302 | "name": "ipython", 303 | "version": 3 304 | }, 305 | "file_extension": ".py", 306 | "mimetype": "text/x-python", 307 | "name": "python", 308 | "nbconvert_exporter": "python", 309 | "pygments_lexer": "ipython3", 310 | "version": "3.7.4" 311 | } 312 | }, 313 | "nbformat": 4, 314 | "nbformat_minor": 1 315 | } 316 | -------------------------------------------------------------------------------- /docs/Linear_modulation_16_08_19.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/Linear_modulation_16_08_19.pptx -------------------------------------------------------------------------------- /docs/PSK_BER.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ModulationPy import PSKModem 3 | 4 | def BER_calc(a, b): 5 | num_ber = np.sum(np.abs(a - b)) 6 | ber = np.mean(np.abs(a - b)) 7 | return int(num_ber), ber 8 | 9 | def BER_psk(M, EbNo): 10 | EbNo_lin = 10**(EbNo/10) 11 | if M > 4: 12 | P = special.erfc(np.sqrt(EbNo_lin*np.log2(M))*np.sin(np.pi/M)) 13 | / np.log2(M) 14 | else: 15 | P = 0.5*special.erfc(np.sqrt(EbNo_lin)) 16 | return P 17 | 18 | EbNos = np.array([i for i in range(30)]) # array of Eb/No in dBs 19 | N = 100000 # number of symbols per the frame 20 | N_c = 100 # number of trials 21 | 22 | Ms = [4, 8, 16, 32] # modulation orders 23 | 24 | ''' Simulation loops ''' 25 | 26 | mean_BER = np.empty((len(EbNos), len(Ms))) 27 | for idxM, M in enumerate(Ms): 28 | print(M) 29 | BER = np.empty((N_c,)) 30 | k = np.log2(M) #number of bit per modulation symbol 31 | 32 | modem = PSKModem(M, 33 | gray_map=True, 34 | bin_input=True, 35 | soft_decision=False, 36 | bin_output=True) 37 | 38 | for idxEbNo, EbNo in enumerate(EbNos): 39 | print(EbNo) 40 | snrdB = EbNo + 10*np.log10(k) # Signal-to-Noise ratio (in dB) 41 | noiseVar = 10**(-snrdB/10) # noise variance (power) 42 | 43 | for cntr in range(N_c): 44 | message_bits = np.random.randint(0, 2, int(N*k)) # message 45 | modulated = modem.modulate(message_bits) # modulation 46 | 47 | Es = np.mean(np.abs(modulated)**2) # symbol energy 48 | No = Es/((10**(EbNo/10))*np.log2(M)) # noise spectrum density 49 | 50 | noisy = modulated + np.sqrt(No/2)*\ 51 | (np.random.randn(modulated.shape[0])+\ 52 | 1j*np.random.randn(modulated.shape[0])) # AWGN 53 | 54 | demodulated = modem.demodulate(noisy, noise_var=noiseVar) 55 | NumErr, BER[cntr] = BER_calc(message_bits, 56 | demodulated) # bit-error ratio 57 | 58 | mean_BER[idxEbNo, idxM] = np.mean(BER) # averaged bit-error ratio 59 | 60 | 61 | ''' Theoretical results ''' 62 | 63 | BER_theor = np.empty((len(EbNos), len(Ms))) 64 | for idxM, M in enumerate(Ms): 65 | BER_theor[:, idxM] = BER_psk(M, EbNos) 66 | 67 | 68 | ''' Curves ''' 69 | 70 | fig, ax = plt.subplots(figsize=(10,7), dpi=300) 71 | 72 | plt.semilogy(EbNos, BER_theor[:,0], 'g-', label = 'QPSK (theory)') 73 | plt.semilogy(EbNos, BER_theor[:,1], 'b-', label = '8-PSK (theory)') 74 | plt.semilogy(EbNos, BER_theor[:,2], 'k-', label = '16-PSK (theory)') 75 | plt.semilogy(EbNos, BER_theor[:,3], 'r-', label = '32-PSK (theory)') 76 | 77 | plt.semilogy(EbNos, mean_BER[:,0], 'g-o', label = 'QPSK (simulation)') 78 | plt.semilogy(EbNos, mean_BER[:,1], 'b-o', label = '8-PSK (simulation)') 79 | plt.semilogy(EbNos, mean_BER[:,2], 'k-o', label = '16-PSK (simulation)') 80 | plt.semilogy(EbNos, mean_BER[:,3], 'r-o', label = '32-PSK (simulation)') 81 | 82 | ax.set_ylim(1e-7, 2) 83 | ax.set_xlim(0, 25.1) 84 | 85 | plt.title("M-PSK") 86 | plt.xlabel('EbNo (dB)') 87 | plt.ylabel('BER') 88 | plt.grid() 89 | plt.legend(loc='upper right') 90 | plt.savefig('psk_ber.png') 91 | -------------------------------------------------------------------------------- /docs/QAM_BER.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ModulationPy import QAMModem 3 | from scipy import special 4 | import matplotlib.pyplot as plt 5 | 6 | 7 | def BER_calc(a, b): 8 | num_ber = np.sum(np.abs(a - b)) 9 | ber = np.mean(np.abs(a - b)) 10 | return int(num_ber), ber 11 | 12 | 13 | def BER_qam(M, EbNo): 14 | EbNo_lin = 10 ** (EbNo / 10) 15 | if M > 4: 16 | P = 2 * np.sqrt((np.sqrt(M) - 1) / 17 | (np.sqrt(M) * np.log2(M))) * special.erfc(np.sqrt(EbNo_lin * 3 * np.log2(M) / 2 * (M - 1))) 18 | else: 19 | P = 0.5 * special.erfc(np.sqrt(EbNo_lin)) 20 | return P 21 | 22 | 23 | EbNos = np.array([i for i in range(30)]) # array of Eb/No in dBs 24 | N = 100000 # number of symbols per the frame 25 | N_c = 100 # number of trials 26 | 27 | Ms = [4, 16, 64, 256] # modulation orders 28 | 29 | ''' Simulation loops ''' 30 | 31 | mean_BER = np.empty((len(EbNos), len(Ms))) 32 | for idxM, M in enumerate(Ms): 33 | print("Modulation order: ", M) 34 | BER = np.empty((N_c,)) 35 | k = np.log2(M) # number of bit per modulation symbol 36 | 37 | modem = QAMModem(M, 38 | bin_input=True, 39 | soft_decision=False, 40 | bin_output=True) 41 | 42 | for idxEbNo, EbNo in enumerate(EbNos): 43 | print("EbNo: ", EbNo) 44 | snrdB = EbNo + 10 * np.log10(k) # Signal-to-Noise ratio (in dB) 45 | noiseVar = 10 ** (-snrdB / 10) # noise variance (power) 46 | 47 | for cntr in range(N_c): 48 | message_bits = np.random.randint(0, 2, int(N * k)) # message 49 | modulated = modem.modulate(message_bits) # modulation 50 | 51 | Es = np.mean(np.abs(modulated) ** 2) # symbol energy 52 | No = Es / ((10 ** (EbNo / 10)) * np.log2(M)) # noise spectrum density 53 | 54 | noisy = modulated + np.sqrt(No / 2) * \ 55 | (np.random.randn(modulated.shape[0]) + 56 | 1j * np.random.randn(modulated.shape[0])) # AWGN 57 | 58 | demodulated = modem.demodulate(noisy, noise_var=noiseVar) 59 | NumErr, BER[cntr] = BER_calc(message_bits, 60 | demodulated) # bit-error ratio 61 | mean_BER[idxEbNo, idxM] = np.mean(BER, axis=0) # averaged bit-error ratio 62 | 63 | ''' Theoretical results ''' 64 | 65 | BER_theor = np.empty((len(EbNos), len(Ms))) 66 | for idxM, M in enumerate(Ms): 67 | BER_theor[:, idxM] = BER_qam(M, EbNos) 68 | 69 | ''' Curves ''' 70 | 71 | fig, ax = plt.subplots(figsize=(10, 7), dpi=300) 72 | 73 | plt.semilogy(EbNos, BER_theor[:, 0], 'g-', label='4-QAM (theory)') 74 | plt.semilogy(EbNos, BER_theor[:, 1], 'b-', label='16-QAM (theory)') 75 | plt.semilogy(EbNos, BER_theor[:, 2], 'k-', label='64-QAM (theory)') 76 | plt.semilogy(EbNos, BER_theor[:, 3], 'r-', label='256-QAM (theory)') 77 | 78 | plt.semilogy(EbNos, mean_BER[:, 0], 'g-o', label='4-QAM (simulation)') 79 | plt.semilogy(EbNos, mean_BER[:, 1], 'b-o', label='16-QAM (simulation)') 80 | plt.semilogy(EbNos, mean_BER[:, 2], 'k-o', label='64-QAM (simulation)') 81 | plt.semilogy(EbNos, mean_BER[:, 3], 'r-o', label='256-QAM (simulation)') 82 | 83 | ax.set_ylim(1e-7, 2) 84 | ax.set_xlim(0, 25.1) 85 | 86 | plt.title("M-QAM") 87 | plt.xlabel('EbNo (dB)') 88 | plt.ylabel('BER') 89 | plt.grid() 90 | plt.legend(loc='upper right') 91 | plt.savefig('qam_ber.png') 92 | -------------------------------------------------------------------------------- /docs/img/modulationpy_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/img/modulationpy_logo.png -------------------------------------------------------------------------------- /docs/img/psk_ber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/img/psk_ber.png -------------------------------------------------------------------------------- /docs/img/qam_signconst.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/img/qam_signconst.PNG -------------------------------------------------------------------------------- /docs/img/qpsk_signconst.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/img/qpsk_signconst.PNG -------------------------------------------------------------------------------- /docs/img/simulator_scheme.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/img/simulator_scheme.PNG -------------------------------------------------------------------------------- /docs/psk.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirlf/ModulationPy/774a05d230a47e0afdead70b2e9590d8e925e17e/docs/psk.xlsx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.7.1 2 | matplotlib>=2.2.2 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | DESCRIPTION = 'Baseband Digital Linear Modems' 4 | LONG_DESCRIPTION = 'The documentation of this project can be obtained ' \ 5 | 'from our GitHub repository: https://github.com/kirlf/ModulationPy' 6 | MAINTAINER = 'Vladimir Fadeev' 7 | MAINTAINER_EMAIL = 'vovenur@gmail.com' 8 | URL = 'https://github.com/kirlf/ModulationPy' 9 | LICENSE = 'BSD 3-Clause' 10 | VERSION = '0.1.8' 11 | 12 | setup( 13 | name="ModulationPy", 14 | version=VERSION, 15 | 16 | # Project uses reStructuredText, so ensure that the docutils get 17 | # installed or upgraded on the target machine 18 | install_requires=[ 19 | 'numpy>=1.7.1', 20 | 'matplotlib>=2.2.2', 21 | ], 22 | 23 | python_requires='>=3.6.4', 24 | # package_dir = {'': 'src'}, 25 | packages=['ModulationPy'], 26 | 27 | # metadata to display on PyPI 28 | license=LICENSE, 29 | author=MAINTAINER, 30 | author_email=MAINTAINER_EMAIL, 31 | description=DESCRIPTION, 32 | long_description=LONG_DESCRIPTION, 33 | keywords="communications digital modulation demodulation psk qam", 34 | url=URL, # project home page, if any 35 | 36 | classifiers=[ 37 | 'License :: OSI Approved :: BSD License', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Intended Audience :: Science/Research', 40 | 'Intended Audience :: Telecommunications Industry', 41 | 'Operating System :: Unix', 42 | 'Topic :: Scientific/Engineering', 43 | 'Topic :: Software Development', 44 | ] 45 | 46 | ) 47 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | from ModulationPy.ModulationPy import QAMModem, PSKModem 4 | 5 | 6 | class TestThing(unittest.TestCase): 7 | def test_QAMModem(self): 8 | """ QAM modem modulation and demodulation tests""" 9 | Ms = [4, 16, 64] # modulation orders 10 | 11 | for M in Ms: 12 | 13 | print("Modulation order: "+str(M)) 14 | 15 | # Hard decision, Gray mapping, binary IO 16 | mlpr = int(np.log2(M)) #multiplier 17 | size = mlpr*int(1e4) 18 | msg = np.random.randint(2, size=size) 19 | 20 | Modem = QAMModem(M, gray_map=True, bin_input=True, soft_decision = False, bin_output = True) 21 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 22 | 23 | # Hard decision, binary mapping, binary IO 24 | Modem = QAMModem(M, gray_map=False, bin_input=True, soft_decision = False, bin_output = True) 25 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 26 | 27 | # Hard decision, Gray mapping, non-binary IO 28 | size = int(1e5) 29 | msg = np.random.randint(2, size=size) 30 | Modem = QAMModem(M, gray_map=True, bin_input=False, soft_decision = False, bin_output = False) 31 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 32 | 33 | # Hard decision, binary mapping, non-binary IO 34 | Modem = QAMModem(M, gray_map=False, bin_input=False, soft_decision = False, bin_output = False) 35 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 36 | 37 | 38 | def test_PSKModem(self): 39 | """ PSK modem modulation and demodulation tests""" 40 | Ms = [2, 4, 8, 16, 32] # modulation order 41 | for M in Ms: 42 | 43 | print("Modulation order: "+str(M)) 44 | 45 | # Hard decision, Gray mapping, binary IO 46 | mlpr = int(np.log2(M)) #multiplier 47 | size = mlpr*int(1e4) 48 | msg = np.random.randint(2, size=size) 49 | 50 | Modem = PSKModem(M, gray_map=True, bin_input=True, soft_decision = False, bin_output = True) 51 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 52 | 53 | # Hard decision, binary mapping, binary IO 54 | Modem = PSKModem(M, gray_map=False, bin_input=True, soft_decision = False, bin_output = True) 55 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 56 | 57 | # Hard decision, Gray mapping, non-binary IO 58 | size = int(1e5) 59 | msg = np.random.randint(2, size=size) 60 | Modem = PSKModem(M, gray_map=True, bin_input=False, soft_decision = False, bin_output = False) 61 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 62 | 63 | # Hard decision, binary mapping, non-binary IO 64 | Modem = PSKModem(M, gray_map=False, bin_input=False, soft_decision = False, bin_output = False) 65 | np.testing.assert_array_equal(msg, Modem.demodulate(Modem.modulate(msg))) 66 | 67 | if __name__ == '__main__': 68 | unittest.main() 69 | 70 | --------------------------------------------------------------------------------