Skip to content

Commit bd4e96a

Browse files
committed
Merge branch 'MBF-FD'
2 parents 463b8ad + dccd6e3 commit bd4e96a

File tree

3 files changed

+158
-6
lines changed

3 files changed

+158
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Available Features
5454
- OFDM Tx/Rx signal processing
5555
- MIMO Maximum Likelihood (ML) Detection.
5656
- MIMO K-best Schnorr-Euchner Detection.
57+
- MIMO Best-First Detection.
5758
- Convert channel matrix to Bit-level representation.
5859
- Computation of LogLikelihood ratio using max-log approximation.
5960

commpy/links.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def link_performance(link_model, SNRs, send_max, err_min, send_chunk=None, code_
5959
"""
6060

6161
# Initialization
62-
BERs = np.empty_like(SNRs, dtype=float)
62+
BERs = np.zeros_like(SNRs)
6363
# Set chunk size and round it to be a multiple of num_bits_symbol*nb_tx to avoid padding
6464
if send_chunk is None:
6565
send_chunk = err_min
@@ -83,7 +83,7 @@ def link_performance(link_model, SNRs, send_max, err_min, send_chunk=None, code_
8383
# Deals with MIMO channel
8484
if isinstance(link_model.channel, MIMOFlatChannel):
8585
nb_symb_vector = len(channel_output)
86-
received_msg = np.empty(int(math.ceil(len(msg) / link_model.rate)), int)
86+
received_msg = np.empty(int(math.ceil(len(msg) / link_model.rate)))
8787
for i in range(nb_symb_vector):
8888
received_msg[receive_size * i:receive_size * (i + 1)] = \
8989
link_model.receive(channel_output[i], link_model.channel.channel_gains[i],
@@ -101,6 +101,8 @@ def link_performance(link_model, SNRs, send_max, err_min, send_chunk=None, code_
101101
bit_err += (msg != link_model.decoder(received_msg)[:len(msg)]).sum()
102102
bit_send += send_chunk
103103
BERs[id_SNR] = bit_err / bit_send
104+
if bit_err < err_min:
105+
break
104106
return BERs
105107

106108

commpy/modulation.py

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,25 @@
1515
ofdm_rx -- OFDM Receive Signal Processing
1616
mimo_ml -- MIMO Maximum Likelihood (ML) Detection.
1717
kbest -- MIMO K-best Schnorr-Euchner Detection.
18-
bit_lvl_repr -- Bit level representation.
19-
max_log_approx -- Max-log approximation.
18+
best_first_detector -- MIMO Best-First Detection.
19+
bit_lvl_repr -- Bit Level Representation.
20+
max_log_approx -- Max-Log Approximation.
2021
2122
"""
23+
from bisect import insort
2224
from itertools import product
2325

2426
import matplotlib.pyplot as plt
2527
from numpy import arange, array, zeros, pi, cos, sin, sqrt, log2, argmin, \
2628
hstack, repeat, tile, dot, shape, concatenate, exp, \
27-
log, vectorize, empty, eye, kron, inf
29+
log, vectorize, empty, eye, kron, inf, full, abs, newaxis, minimum, clip
2830
from numpy.fft import fft, ifft
2931
from numpy.linalg import qr, norm
3032

3133
from commpy.utilities import bitarray2dec, dec2bitarray
3234

33-
__all__ = ['PSKModem', 'QAMModem', 'ofdm_tx', 'ofdm_rx', 'mimo_ml', 'kbest', 'bit_lvl_repr', 'max_log_approx']
35+
__all__ = ['PSKModem', 'QAMModem', 'ofdm_tx', 'ofdm_rx', 'mimo_ml', 'kbest', 'best_first_detector',
36+
'bit_lvl_repr', 'max_log_approx']
3437

3538

3639
class Modem:
@@ -346,6 +349,152 @@ def kbest(y, h, constellation, K, noise_var=0, output_type='hard', demode=None):
346349
raise ValueError('output_type must be "hard" or "soft"')
347350

348351

352+
def best_first_detector(y, h, constellation, stack_size, noise_var, demode, llr_max):
353+
""" MIMO Best-First Detection.
354+
355+
Reference: G. He, X. Zhang, et Z. Liang, "Algorithm and Architecture of an Efficient MIMO Detector With Cross-Level
356+
Parallel Tree-Search", IEEE Transactions on Very Large Scale Integration (VLSI) Systems, 2019
357+
358+
359+
Parameters
360+
----------
361+
y : 1D ndarray
362+
Received complex symbols (length: num_receive_antennas)
363+
364+
h : 2D ndarray
365+
Channel Matrix (shape: num_receive_antennas x num_transmit_antennas)
366+
367+
constellation : 1D ndarray of floats
368+
Constellation used to modulate the symbols
369+
370+
stack_size : tuple of integers
371+
Size of each stack (length: num_transmit_antennas - 1)
372+
373+
noise_var : positive float
374+
Noise variance.
375+
*Default* value is 0.
376+
377+
demode : function with prototype binary_word = demode(point)
378+
Function that provide the binary word corresponding to a symbol vector.
379+
380+
llr_max : float
381+
Max value for LLR clipping
382+
383+
Returns
384+
-------
385+
x : 1D ndarray of Log-Likelihood Ratios.
386+
Detected vector (length: num_receive_antennas).
387+
"""
388+
389+
class _Node:
390+
""" Helper data model that implements __lt__ (aka '<') as required to use bisect.insort. """
391+
392+
def __init__(self, symb_vectors, partial_metrics):
393+
"""
394+
Recursive initializer that build a sequence of siblings.
395+
Inputs are assumed to be ordered based on metric
396+
"""
397+
if len(partial_metrics) == 1:
398+
# There is one node to build
399+
self.symb_vector = symb_vectors.reshape(-1) # Insure that self.symb_vector is a 1d-ndarray
400+
self.partial_metric = partial_metrics[0]
401+
self.best_sibling = None
402+
else:
403+
# Recursive call to build several nodes
404+
self.symb_vector = symb_vectors[:, 0].reshape(-1) # Insure that self.symb_vector is a 1d-ndarray
405+
self.partial_metric = partial_metrics[0]
406+
self.best_sibling = _Node(symb_vectors[:, 1:], partial_metrics[1:])
407+
408+
def __lt__(self, other):
409+
return self.partial_metric < other.partial_metric
410+
411+
def expand(self, yt, r, constellation):
412+
""" Build all children and return the best one. constellation must be a numpy ndarray."""
413+
# Construct children's symbol vector
414+
child_size = self.symb_vector.size + 1
415+
children_symb_vectors = empty((child_size, constellation.size), constellation.dtype)
416+
children_symb_vectors[1:] = self.symb_vector[:, newaxis]
417+
children_symb_vectors[0] = constellation
418+
419+
# Compute children's partial metric and sort
420+
children_metric = abs(yt[-child_size] - r[-child_size, -child_size:].dot(children_symb_vectors)) ** 2
421+
children_metric += self.partial_metric
422+
ordering = children_metric.argsort()
423+
424+
# Build children and return the best one
425+
return _Node(children_symb_vectors[:, ordering], children_metric[ordering])
426+
427+
# Extract information from arguments
428+
nb_tx, nb_rx = h.shape
429+
constellation = array(constellation)
430+
m = constellation.size
431+
modulation_order = int(log2(m))
432+
433+
# QR decomposition
434+
q, r = qr(h)
435+
yt = q.conj().T.dot(y)
436+
437+
# Initialisation
438+
map_metric = inf
439+
map_bit_vector = None
440+
counter_hyp_metric = full((nb_tx, modulation_order), inf)
441+
stacks = tuple([] for _ in range(nb_tx))
442+
443+
# Start process by adding the best root's child in the last stack
444+
stacks[-1].append(_Node(empty(0, constellation.dtype), array(0, float, ndmin=1)).expand(yt, r, constellation))
445+
446+
# While there is at least one non-empty stack (exempt the first one)
447+
while any(stacks[1:]):
448+
# Node processing
449+
for idx_next_stack in range(len(stacks) - 1):
450+
try:
451+
idx_this_stack = idx_next_stack + 1
452+
best_node = stacks[idx_this_stack].pop(0)
453+
454+
# Update search radius
455+
if map_bit_vector is None:
456+
radius = inf # No leaf has been reached yet so we keep all nodes
457+
else:
458+
bit_vector = demode(best_node.symb_vector).reshape(-1, modulation_order)
459+
bit_vector[bit_vector == 0] = -1
460+
461+
# Select the counter hyp metrics that could be affected by this node. Details: eq. (14)-(16) in [1].
462+
try:
463+
a2 = counter_hyp_metric[idx_this_stack:][map_bit_vector[idx_this_stack:] != bit_vector].max()
464+
except ValueError:
465+
a2 = inf # NumPy cannot compute max on an empty matrix
466+
radius = max(counter_hyp_metric[:idx_this_stack].max(), a2)
467+
468+
# Process best sibling
469+
if best_node.best_sibling is not None and best_node.best_sibling.partial_metric <= radius:
470+
insort(stacks[idx_this_stack], best_node.best_sibling)
471+
472+
# Process children
473+
best_child = best_node.expand(yt, r, constellation)
474+
if best_child.partial_metric <= radius:
475+
insort(stacks[idx_next_stack], best_child)
476+
except IndexError: # Raised when popping an empty stack
477+
pass
478+
479+
# LLR update if there is a new leaf
480+
if stacks[0]:
481+
if stacks[0][0].partial_metric < map_metric:
482+
minimum(counter_hyp_metric, map_metric, out=counter_hyp_metric)
483+
map_metric = stacks[0][0].partial_metric
484+
map_bit_vector = demode(stacks[0][0].symb_vector).reshape(-1, modulation_order)
485+
map_bit_vector[map_bit_vector == 0] = -1
486+
else:
487+
minimum(counter_hyp_metric, stacks[0][0].partial_metric, out=counter_hyp_metric)
488+
clip(counter_hyp_metric, map_metric - llr_max, map_metric + llr_max, counter_hyp_metric)
489+
490+
# Trimming stack according to requested max stack size
491+
del stacks[0][0:] # there is no stack for the leafs
492+
for idx_next_stack in range(len(stacks) - 1):
493+
del stacks[idx_next_stack + 1][stack_size[idx_next_stack]:]
494+
495+
return ((map_metric - counter_hyp_metric) * map_bit_vector).reshape(-1)
496+
497+
349498
def bit_lvl_repr(H, w):
350499
""" Bit-level representation of matrix H with weights w.
351500

0 commit comments

Comments
 (0)