├── .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 | [](https://travis-ci.com/kirlf/ModulationPy)
4 | [](https://pypi.org/project/ModulationPy/)
5 | [](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 |
%20%3D%20s_I(t)cos(2%5Cpi%20f_c%20t)%20-%20s_Q(t)cos(2%5Cpi%20f_c%20t)%20%5Cqquad%20(1))
22 |
23 | where
is the In-phase part,
is the Quadrature part,
is the carrier frequency, and
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 | %20%5Cqquad(2)%20)
35 |
36 | Modulation order relates to **gross bit rate** concept:
37 |
38 | %20%5Cqquad(3)%20)
39 |
40 | where
is the baud or symbol rate. Baud rate usually relates to the coherence bandwidth
(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 | %7D%20%5Cqquad%20(4)%0A)
83 |
84 | where
is the phase rotation,
is the mapped to decimals (in range between *0* and *M-1*) input symbol, and
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 | 
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 | %5Cfrac%7BE_b%7D%7BN_o%7D%7D%5Csin%5Cleft(%20%5Cfrac%7B%5Cpi%7D%7BM%7D%20%5Cright)%20%5Cright)%7D%7Blog_2(M)%7D%20%5Cqquad%20(5)%0A)
252 |
253 | where
,
denotes the [complementary error function](https://en.wikipedia.org/wiki/Error_function#Complementary_error_function),
is the energy per one bit, and
is the noise spectral density.
254 |
255 | In case of BPSK and QPSK the following formula should be used for error probability:
256 |
257 | %7D%20%5Cqquad%20(6)%0A)
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 | erfc%5Cleft(%20%5Csqrt%7B%5Cfrac%7B3%5Clog_2M%7D%7B2(M-1)%7D%20%5Cfrac%7BE_b%7D%7BN_0%7D%7D%20%5Cright)%20%5Cqquad%20(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 | %20%3D%20-%5Cfrac%7B1%7D%7B%5Csigma%5E2%7D%20%5Cleft(%20%5Cmin_%7Bs%20%5Cepsilon%20S_0%7D%20%5Cleft(%20(x%20-%20s_x)%5E2%20%2B%20(y%20-%20s_y)%5E2%20%5Cright)%20%20-%20%5Cmin_%7Bs%20%5Cepsilon%20S_1%7D%20%5Cleft(%20(x%20-%20s_x)%5E2%20%2B%20(y%20-%20s_y)%5E2%20%5Cright)%5Cright)%20%5Cqquad(8))
288 |
289 | where
is the In-phase coordinate of the received symbol,
is the Quadrature coordinate of the received symbol,
is the In-phase coordinate of ideal symbol or constellation point,
is the Quadrature coordinate of ideal symbol or constellation point,
is the ideal symbols or constellation points with bit 0, at the given bit position,
is the ideal symbols or constellation points with bit 1, at the given bit position,
is the transmitted bit (one of the K bits in an M-ary symbol, assuming all M symbols are equally probable,
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 |
--------------------------------------------------------------------------------