Skip to content

Commit e631efe

Browse files
committed
Provide a tools to estimate the BER performance of a link model with Monte Carlo simulation.
This commit provides: * the tool (`commpy.utilities.link_performance`), * a test that check both SISO and MIMO channels, * some typo corrections, * an update of the documentation.
1 parent 6f9576d commit e631efe

File tree

6 files changed

+158
-30
lines changed

6 files changed

+158
-30
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Utilities
6666
- Hamming distance, Euclidean distance.
6767
- Upsample
6868
- Power of a discrete-time signal
69+
- Estimate the BER performance of a link model with Monte Carlo simulation.
6970

7071
FAQs
7172
----

commpy/channels.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ class SISOFlatChannel(_FlatChannel):
108108
----------
109109
noise_std : float, optional
110110
Noise standard deviation.
111-
Default value is None and then the value must set later.
111+
*Default* value is None and then the value must set later.
112112
113113
fading_param : tuple of 2 floats, optional
114-
Parameters of the fading (see attribute for details). Default value is (1,0) i.e. no fading.
114+
Parameters of the fading (see attribute for details).
115+
*Default* value is (1,0) i.e. no fading.
115116
116117
Attributes
117118
----------
@@ -255,11 +256,11 @@ class MIMOFlatChannel(_FlatChannel):
255256
256257
noise_std : float, optional
257258
Noise standard deviation.
258-
Default value is None and then the value must set later.
259+
*Default* value is None and then the value must set later.
259260
260261
fading_param : tuple of 3 floats, optional
261262
Parameters of the fading. The complete tuple must be set each time.
262-
Default value is (zeros((nb_rx, nb_tx)), identity(nb_tx), identity(nb_rx)) i.e. Rayleigh fading.
263+
*Default* value is (zeros((nb_rx, nb_tx)), identity(nb_tx), identity(nb_rx)) i.e. Rayleigh fading.
263264
264265
Attributes
265266
----------

commpy/modulation.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Authors: Veeresh Taranalli <[email protected]>
32
# License: BSD 3-Clause
43

@@ -48,17 +47,15 @@ def modulate(self, input_bits):
4847
4948
"""
5049
mapfunc = vectorize(lambda i:
51-
self.constellation[bitarray2dec(input_bits[i:i+self.num_bits_symbol])])
50+
self.constellation[bitarray2dec(input_bits[i:i + self.num_bits_symbol])])
5251

5352
baseband_symbols = mapfunc(arange(0, len(input_bits), self.num_bits_symbol))
5453

5554
return baseband_symbols
5655

57-
def demodulate(self, input_symbols, demod_type, noise_var = 0):
56+
def demodulate(self, input_symbols, demod_type, noise_var=0):
5857
""" Demodulate (map) a set of constellation symbols to corresponding bits.
5958
60-
Supports hard-decision demodulation only.
61-
6259
Parameters
6360
----------
6461
input_symbols : 1D ndarray of complex floats
@@ -78,10 +75,10 @@ def demodulate(self, input_symbols, demod_type, noise_var = 0):
7875
7976
"""
8077
if demod_type == 'hard':
81-
index_list = map(lambda i: argmin(abs(input_symbols[i] - self.constellation)), \
78+
index_list = map(lambda i: argmin(abs(input_symbols[i] - self.constellation)),
8279
range(0, len(input_symbols)))
8380
demod_bits = hstack(map(lambda i: dec2bitarray(i, self.num_bits_symbol),
84-
index_list))
81+
index_list))
8582
elif demod_type == 'soft':
8683
demod_bits = zeros(len(input_symbols) * self.num_bits_symbol)
8784
for i in arange(len(input_symbols)):
@@ -91,10 +88,12 @@ def demodulate(self, input_symbols, demod_type, noise_var = 0):
9188
llr_den = 0
9289
for const_index in self.symbol_mapping:
9390
if (const_index >> bit_index) & 1:
94-
llr_num = llr_num + exp((-abs(current_symbol - self.constellation[const_index])**2)/noise_var)
91+
llr_num = llr_num + exp(
92+
(-abs(current_symbol - self.constellation[const_index]) ** 2) / noise_var)
9593
else:
96-
llr_den = llr_den + exp((-abs(current_symbol - self.constellation[const_index])**2)/noise_var)
97-
demod_bits[i*self.num_bits_symbol + self.num_bits_symbol - 1 - bit_index] = log(llr_num/llr_den)
94+
llr_den = llr_den + exp(
95+
(-abs(current_symbol - self.constellation[const_index]) ** 2) / noise_var)
96+
demod_bits[i * self.num_bits_symbol + self.num_bits_symbol - 1 - bit_index] = log(llr_num / llr_den)
9897
else:
9998
pass
10099
# throw an error
@@ -105,8 +104,10 @@ def demodulate(self, input_symbols, demod_type, noise_var = 0):
105104
class PSKModem(Modem):
106105
""" Creates a Phase Shift Keying (PSK) Modem object. """
107106

107+
Es = 1
108+
108109
def _constellation_symbol(self, i):
109-
return cos(2*pi*(i-1)/self.m) + sin(2*pi*(i-1)/self.m)*(0+1j)
110+
return cos(2 * pi * (i - 1) / self.m) + sin(2 * pi * (i - 1) / self.m) * (0 + 1j)
110111

111112
def __init__(self, m):
112113
""" Creates a Phase Shift Keying (PSK) Modem object.
@@ -121,13 +122,13 @@ def __init__(self, m):
121122
self.num_bits_symbol = int(log2(self.m))
122123
self.symbol_mapping = arange(self.m)
123124
self.constellation = list(map(self._constellation_symbol,
124-
self.symbol_mapping))
125+
self.symbol_mapping))
125126

126127
class QAMModem(Modem):
127128
""" Creates a Quadrature Amplitude Modulation (QAM) Modem object."""
128129

129130
def _constellation_symbol(self, i):
130-
return (2*i[0]-1) + (2*i[1]-1)*(1j)
131+
return (2 * i[0] - 1) + (2 * i[1] - 1) * (1j)
131132

132133
def __init__(self, m):
133134
""" Creates a Quadrature Amplitude Modulation (QAM) Modem object.
@@ -142,9 +143,11 @@ def __init__(self, m):
142143
self.m = m
143144
self.num_bits_symbol = int(log2(self.m))
144145
self.symbol_mapping = arange(self.m)
145-
mapping_array = arange(1, sqrt(self.m)+1) - (sqrt(self.m)/2)
146+
mapping_array = arange(1, sqrt(self.m) + 1) - (sqrt(self.m) / 2)
146147
self.constellation = list(map(self._constellation_symbol,
147-
list(product(mapping_array, repeat=2))))
148+
list(product(mapping_array, repeat=2))))
149+
self.Es = 2 * (self.m - 1) / 3
150+
148151

149152
def ofdm_tx(x, nfft, nsc, cp_length):
150153
""" OFDM Transmit signal generation """
@@ -155,29 +158,31 @@ def ofdm_tx(x, nfft, nsc, cp_length):
155158
ofdm_tx_signal = array([])
156159

157160
for i in range(0, shape(x)[1]):
158-
symbols = x[:,i]
161+
symbols = x[:, i]
159162
ofdm_sym_freq = zeros(nfft, dtype=complex)
160-
ofdm_sym_freq[1:(nsc/2)+1] = symbols[nsc/2:]
161-
ofdm_sym_freq[-(nsc/2):] = symbols[0:nsc/2]
163+
ofdm_sym_freq[1:(nsc / 2) + 1] = symbols[nsc / 2:]
164+
ofdm_sym_freq[-(nsc / 2):] = symbols[0:nsc / 2]
162165
ofdm_sym_time = ifft(ofdm_sym_freq)
163166
cp = ofdm_sym_time[-cp_length:]
164167
ofdm_tx_signal = concatenate((ofdm_tx_signal, cp, ofdm_sym_time))
165168

166169
return ofdm_tx_signal
167170

171+
168172
def ofdm_rx(y, nfft, nsc, cp_length):
169173
""" OFDM Receive Signal Processing """
170174

171-
num_ofdm_symbols = int(len(y)/(nfft + cp_length))
175+
num_ofdm_symbols = int(len(y) / (nfft + cp_length))
172176
x_hat = zeros([nsc, num_ofdm_symbols], dtype=complex)
173177

174178
for i in range(0, num_ofdm_symbols):
175-
ofdm_symbol = y[i*nfft + (i+1)*cp_length:(i+1)*(nfft + cp_length)]
179+
ofdm_symbol = y[i * nfft + (i + 1) * cp_length:(i + 1) * (nfft + cp_length)]
176180
symbols_freq = fft(ofdm_symbol)
177-
x_hat[:,i] = concatenate((symbols_freq[-nsc/2:], symbols_freq[1:(nsc/2)+1]))
181+
x_hat[:, i] = concatenate((symbols_freq[-nsc / 2:], symbols_freq[1:(nsc / 2) + 1]))
178182

179183
return x_hat
180184

185+
181186
def mimo_ml(y, h, constellation):
182187
""" MIMO ML Detection.
183188
@@ -195,7 +200,7 @@ def mimo_ml(y, h, constellation):
195200
"""
196201
m = len(constellation)
197202
x_ideal = array([tile(constellation, m), repeat(constellation, m)])
198-
y_vector = tile(y, m*m)
203+
y_vector = tile(y, m * m)
199204
min_idx = argmin(sum(abs(y_vector - dot(h, x_ideal)), axis=0))
200205
x_r = x_ideal[:, min_idx]
201206

commpy/tests/test_channels.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def check_chan_gain(mod, chan):
277277
# Test with Rayleigh fading
278278
chan.fading_param = (zeros((nb_rx, nb_tx)), identity(nb_tx), identity(nb_rx))
279279
check_chan_gain(mod, chan)
280-
assert_allclose(chan.channel_gains.mean(), 0, atol=1e-2,
280+
assert_allclose(chan.channel_gains.mean(), 0, atol=2e-2,
281281
err_msg='Wrong channel mean with real channel')
282282
assert_allclose(chan.channel_gains.var(), 1, atol=5e-2,
283283
err_msg='Wrong channel variance with real channel')

commpy/tests/test_utilities.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Authors: Bastien Trotobas <[email protected]>
2+
# License: BSD 3-Clause
3+
4+
from __future__ import division # Python 2 compatibility
5+
6+
from numpy import arange, sqrt, identity, zeros, log10
7+
from numpy.random import seed
8+
from numpy.testing import run_module_suite, assert_allclose, dec
9+
from scipy.special import erfc
10+
11+
from commpy.channels import MIMOFlatChannel, SISOFlatChannel
12+
from commpy.modulation import QAMModem, kbest
13+
from commpy.utilities import link_performance
14+
15+
16+
@dec.slow
17+
def test_link_performance():
18+
# Apply link_performance to SISO QPSK and AWGN channel
19+
BERs = link_performance(QAMModem(4), SISOFlatChannel(fading_param=(1 + 0j, 0)), None,
20+
range(0, 9, 2), 600e4, 600)
21+
desired = erfc(sqrt(10**(arange(0, 9, 2) / 10) / 2)) / 2
22+
assert_allclose(BERs, desired, rtol=0.25, err_msg='Wrong performance for SISO QPSK and AWGN channel')
23+
24+
# Apply link_performance to MIMO 16QAM and 4x4 Rayleigh channel
25+
RayleighChannel = MIMOFlatChannel(4, 4, None, (zeros((4, 4), dtype=complex), identity(4), identity(4)))
26+
SNRs = range(0, 21, 5) + 10 * log10(4)
27+
28+
def kbest16(y, h, constellation):
29+
return kbest(y, h, constellation, 16)
30+
31+
BERs = link_performance(QAMModem(16), RayleighChannel, kbest16,
32+
SNRs, 600e4, 600)
33+
desired = (2e-1, 1e-1, 3e-2, 2e-3, 4e-5) # From reference
34+
assert_allclose(BERs, desired, rtol=1, err_msg='Wrong performance for MIMO 16QAM and 4x4 Rayleigh channel')
35+
36+
37+
if __name__ == "__main__":
38+
seed(17121996)
39+
run_module_suite()

commpy/utilities.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@
1515
euclid_dist -- Squared Euclidean distance.
1616
upsample -- Upsample by an integral factor (zero insertion).
1717
signal_power -- Compute the power of a discrete time signal.
18-
18+
link_performance -- Estimate the BER performance of a link model with Monte Carlo simulation.
1919
2020
"""
21+
from __future__ import division # Python 2 compatibility
22+
2123
import numpy as np
2224

23-
__all__ = ['dec2bitarray', 'bitarray2dec', 'hamming_dist', 'euclid_dist', 'upsample', 'signal_power']
25+
from commpy.channels import MIMOFlatChannel
26+
27+
__all__ = ['dec2bitarray', 'bitarray2dec', 'hamming_dist', 'euclid_dist', 'upsample',
28+
'signal_power', 'link_performance']
2429

2530

2631
def dec2bitarray(in_number, bit_width):
@@ -165,7 +170,84 @@ def signal_power(signal):
165170

166171
@np.vectorize
167172
def square_abs(s):
168-
return abs(s)**2
173+
return abs(s) ** 2
169174

170175
P = np.mean(square_abs(signal))
171176
return P
177+
178+
179+
def link_performance(modem, channel, detector, SNRs, send_max, err_min, send_chunck=None, code_rate=1):
180+
"""
181+
Estimate the BER performance of a link model with Monte Carlo simulation.
182+
183+
Parameters
184+
----------
185+
modem : modem object
186+
Modem used to modulate and demodulate.
187+
188+
channel : channel object
189+
Channel through which the message is propagated.
190+
191+
detector : function with prototype detector(y, h, constellation) or None
192+
Detector to decode channel output. See detectors in commpy.modulation for details on the prototype.
193+
194+
SNRs : 1D arraylike
195+
Signal to Noise ratio in dB defined as :math:`SNR_{dB} = (E_b/N_0)_{dB} + 10 \log_{10}(R_cM_c)`
196+
where :math:`Rc` is the code rate and :math:`Mc` the modulation rate.
197+
198+
send_max : int
199+
Maximum number of bits send for each SNR.
200+
201+
err_min : int
202+
link_performance send bits until it reach err_min errors (see also send_max).
203+
204+
send_chunck : int
205+
Number of bits to be send at each iteration.
206+
*Default*: send_chunck = err_min
207+
208+
code_rate : float in (0,1]
209+
Rate of the used code.
210+
*Default*: 1 i.e. no code.
211+
"""
212+
213+
# Initialization
214+
BERs = np.empty_like(SNRs, dtype=float)
215+
216+
# Handles the case detector is None
217+
if detector is None:
218+
def detector(y, H, constellation):
219+
return y
220+
221+
# Set chunck size and round it to be a multiple of num_bits_symbol*nb_tx to avoid padding
222+
if send_chunck is None:
223+
send_chunck = err_min
224+
divider = modem.num_bits_symbol * channel.nb_tx
225+
send_chunck = max(divider, send_chunck // divider * divider)
226+
227+
# Computations
228+
for id_SNR in range(len(SNRs)):
229+
channel.set_SNR_dB(SNRs[id_SNR], code_rate, modem.Es)
230+
bit_send = 0
231+
bit_err = 0
232+
while bit_send < send_max and bit_err < err_min:
233+
# Propagate some bits
234+
msg = np.random.choice((0, 1), send_chunck)
235+
symbs = modem.modulate(msg)
236+
channel_output = channel.propagate(symbs)
237+
238+
# Deals with MIMO channel
239+
if isinstance(channel, MIMOFlatChannel):
240+
nb_symb_vector = len(channel_output)
241+
detected_msg = np.empty(nb_symb_vector * channel.nb_tx, dtype=channel_output.dtype)
242+
for i in range(nb_symb_vector):
243+
detected_msg[channel.nb_tx * i:channel.nb_tx * (i+1)] = \
244+
detector(channel_output[i], channel.channel_gains[i], modem.constellation)
245+
else:
246+
detected_msg = channel_output
247+
248+
# Count errors
249+
received_msg = modem.demodulate(detected_msg, 'hard')
250+
bit_err += (msg != received_msg[:len(msg)]).sum() # Remove MIMO padding
251+
bit_send += send_chunck
252+
BERs[id_SNR] = bit_err / bit_send
253+
return BERs

0 commit comments

Comments
 (0)