|
15 | 15 | ofdm_rx -- OFDM Receive Signal Processing |
16 | 16 | mimo_ml -- MIMO Maximum Likelihood (ML) Detection. |
17 | 17 | 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. |
20 | 21 |
|
21 | 22 | """ |
| 23 | +from bisect import insort |
22 | 24 | from itertools import product |
23 | 25 |
|
24 | 26 | import matplotlib.pyplot as plt |
25 | 27 | from numpy import arange, array, zeros, pi, cos, sin, sqrt, log2, argmin, \ |
26 | 28 | 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 |
28 | 30 | from numpy.fft import fft, ifft |
29 | 31 | from numpy.linalg import qr, norm |
30 | 32 |
|
31 | 33 | from commpy.utilities import bitarray2dec, dec2bitarray |
32 | 34 |
|
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'] |
34 | 37 |
|
35 | 38 |
|
36 | 39 | class Modem: |
@@ -346,6 +349,152 @@ def kbest(y, h, constellation, K, noise_var=0, output_type='hard', demode=None): |
346 | 349 | raise ValueError('output_type must be "hard" or "soft"') |
347 | 350 |
|
348 | 351 |
|
| 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 | + |
349 | 498 | def bit_lvl_repr(H, w): |
350 | 499 | """ Bit-level representation of matrix H with weights w. |
351 | 500 |
|
|
0 commit comments