Source code for sionna.mimo.utils

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Utility functions and layers for the MIMO package."""

import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Layer
from abc import ABC, abstractmethod
from sionna.utils import matrix_sqrt_inv, expand_to_rank, insert_dims

[docs] def complex2real_vector(z): # pylint: disable=line-too-long r"""Transforms a complex-valued vector into its real-valued equivalent. Transforms the last dimension of a complex-valued tensor into its real-valued equivalent by stacking the real and imaginary parts on top of each other. For a vector :math:`\mathbf{z}\in \mathbb{C}^M` with real and imaginary parts :math:`\mathbf{x}\in \mathbb{R}^M` and :math:`\mathbf{y}\in \mathbb{R}^M`, respectively, this function returns the vector :math:`\left[\mathbf{x}^{\mathsf{T}}, \mathbf{y}^{\mathsf{T}} \right ]^{\mathsf{T}}\in\mathbb{R}^{2M}`. Input ----- : [...,M], tf.complex Output ------ : [...,2M], tf.complex.real_dtype """ x = tf.math.real(z) y = tf.math.imag(z) return tf.concat([x, y], axis=-1)
[docs] def real2complex_vector(z): # pylint: disable=line-too-long r"""Transforms a real-valued vector into its complex-valued equivalent. Transforms the last dimension of a real-valued tensor into its complex-valued equivalent by interpreting the first half as the real and the second half as the imaginary part. For a vector :math:`\mathbf{z}=\left[\mathbf{x}^{\mathsf{T}}, \mathbf{y}^{\mathsf{T}} \right ]^{\mathsf{T}}\in \mathbb{R}^{2M}` with :math:`\mathbf{x}\in \mathbb{R}^M` and :math:`\mathbf{y}\in \mathbb{R}^M`, this function returns the vector :math:`\mathbf{x}+j\mathbf{y}\in\mathbb{C}^M`. Input ----- : [...,2M], tf.float Output ------ : [...,M], tf.complex """ x, y = tf.split(z, 2, -1) return tf.complex(x, y)
[docs] def complex2real_matrix(z): # pylint: disable=line-too-long r"""Transforms a complex-valued matrix into its real-valued equivalent. Transforms the last two dimensions of a complex-valued tensor into their real-valued matrix equivalent representation. For a matrix :math:`\mathbf{Z}\in \mathbb{C}^{M\times K}` with real and imaginary parts :math:`\mathbf{X}\in \mathbb{R}^{M\times K}` and :math:`\mathbf{Y}\in \mathbb{R}^{M\times K}`, respectively, this function returns the matrix :math:`\tilde{\mathbf{Z}}\in \mathbb{R}^{2M\times 2K}`, given as .. math:: \tilde{\mathbf{Z}} = \begin{pmatrix} \mathbf{X} & -\mathbf{Y}\\ \mathbf{Y} & \mathbf{X} \end{pmatrix}. Input ----- : [...,M,K], tf.complex Output ------ : [...,2M, 2K], tf.complex.real_dtype """ x = tf.math.real(z) y = tf.math.imag(z) row1 = tf.concat([x, -y], axis=-1) row2 = tf.concat([y, x], axis=-1) return tf.concat([row1, row2], axis=-2)
[docs] def real2complex_matrix(z): # pylint: disable=line-too-long r"""Transforms a real-valued matrix into its complex-valued equivalent. Transforms the last two dimensions of a real-valued tensor into their complex-valued matrix equivalent representation. For a matrix :math:`\tilde{\mathbf{Z}}\in \mathbb{R}^{2M\times 2K}`, satisfying .. math:: \tilde{\mathbf{Z}} = \begin{pmatrix} \mathbf{X} & -\mathbf{Y}\\ \mathbf{Y} & \mathbf{X} \end{pmatrix} with :math:`\mathbf{X}\in \mathbb{R}^{M\times K}` and :math:`\mathbf{Y}\in \mathbb{R}^{M\times K}`, this function returns the matrix :math:`\mathbf{Z}=\mathbf{X}+j\mathbf{Y}\in\mathbb{C}^{M\times K}`. Input ----- : [...,2M,2K], tf.float Output ------ : [...,M, 2], tf.complex """ m = tf.shape(z)[-2]//2 k = tf.shape(z)[-1]//2 x = z[...,:m,:k] y = z[...,m:,:k] return tf.complex(x, y)
[docs] def complex2real_covariance(r): # pylint: disable=line-too-long r"""Transforms a complex-valued covariance matrix to its real-valued equivalent. Assume a proper complex random variable :math:`\mathbf{z}\in\mathbb{C}^M` [ProperRV]_ with covariance matrix :math:`\mathbf{R}= \in\mathbb{C}^{M\times M}` and real and imaginary parts :math:`\mathbf{x}\in \mathbb{R}^M` and :math:`\mathbf{y}\in \mathbb{R}^M`, respectively. This function transforms the given :math:`\mathbf{R}` into the covariance matrix of the real-valued equivalent vector :math:`\tilde{\mathbf{z}}=\left[\mathbf{x}^{\mathsf{T}}, \mathbf{y}^{\mathsf{T}} \right ]^{\mathsf{T}}\in\mathbb{R}^{2M}`, which is computed as [CovProperRV]_ .. math:: \mathbb{E}\left[\tilde{\mathbf{z}}\tilde{\mathbf{z}}^{\mathsf{H}} \right] = \begin{pmatrix} \frac12\Re\{\mathbf{R}\} & -\frac12\Im\{\mathbf{R}\}\\ \frac12\Im\{\mathbf{R}\} & \frac12\Re\{\mathbf{R}\} \end{pmatrix}. Input ----- : [...,M,M], tf.complex Output ------ : [...,2M, 2M], tf.complex.real_dtype """ q = complex2real_matrix(r) scale = tf.cast(2, q.dtype) return q/scale
[docs] def real2complex_covariance(q): # pylint: disable=line-too-long r"""Transforms a real-valued covariance matrix to its complex-valued equivalent. Assume a proper complex random variable :math:`\mathbf{z}\in\mathbb{C}^M` [ProperRV]_ with covariance matrix :math:`\mathbf{R}= \in\mathbb{C}^{M\times M}` and real and imaginary parts :math:`\mathbf{x}\in \mathbb{R}^M` and :math:`\mathbf{y}\in \mathbb{R}^M`, respectively. This function transforms the given covariance matrix of the real-valued equivalent vector :math:`\tilde{\mathbf{z}}=\left[\mathbf{x}^{\mathsf{T}}, \mathbf{y}^{\mathsf{T}} \right ]^{\mathsf{T}}\in\mathbb{R}^{2M}`, which is given as [CovProperRV]_ .. math:: \mathbb{E}\left[\tilde{\mathbf{z}}\tilde{\mathbf{z}}^{\mathsf{H}} \right] = \begin{pmatrix} \frac12\Re\{\mathbf{R}\} & -\frac12\Im\{\mathbf{R}\}\\ \frac12\Im\{\mathbf{R}\} & \frac12\Re\{\mathbf{R}\} \end{pmatrix}, into is complex-valued equivalent :math:`\mathbf{R}`. Input ----- : [...,2M,2M], tf.float Output ------ : [...,M, M], tf.complex """ r = real2complex_matrix(q) scale = tf.cast(2, r.dtype) return r*scale
[docs] def complex2real_channel(y, h, s): # pylint: disable=line-too-long r"""Transforms a complex-valued MIMO channel into its real-valued equivalent. Assume the canonical MIMO channel model .. math:: \mathbf{y} = \mathbf{H}\mathbf{x} + \mathbf{n} where :math:`\mathbf{y}\in\mathbb{C}^M` is the received signal vector, :math:`\mathbf{x}\in\mathbb{C}^K` is the vector of transmitted symbols, :math:`\mathbf{H}\in\mathbb{C}^{M\times K}` is the known channel matrix, and :math:`\mathbf{n}\in\mathbb{C}^M` is a noise vector with covariance matrix :math:`\mathbf{S}\in\mathbb{C}^{M\times M}`. This function returns the real-valued equivalent representations of :math:`\mathbf{y}`, :math:`\mathbf{H}`, and :math:`\mathbf{S}`, which are used by a wide variety of MIMO detection algorithms (Section VII) [YH2015]_. These are obtained by applying :meth:`~sionna.mimo.complex2real_vector` to :math:`\mathbf{y}`, :meth:`~sionna.mimo.complex2real_matrix` to :math:`\mathbf{H}`, and :meth:`~sionna.mimo.complex2real_covariance` to :math:`\mathbf{S}`. Input ----- y : [...,M], tf.complex 1+D tensor containing the received signals. h : [...,M,K], tf.complex 2+D tensor containing the channel matrices. s : [...,M,M], tf.complex 2+D tensor containing the noise covariance matrices. Output ------ : [...,2M], tf.complex.real_dtype 1+D tensor containing the real-valued equivalent received signals. : [...,2M,2K], tf.complex.real_dtype 2+D tensor containing the real-valued equivalent channel matrices. : [...,2M,2M], tf.complex.real_dtype 2+D tensor containing the real-valued equivalent noise covariance matrices. """ yr = complex2real_vector(y) hr = complex2real_matrix(h) sr = complex2real_covariance(s) return yr, hr, sr
[docs] def real2complex_channel(y, h, s): # pylint: disable=line-too-long r"""Transforms a real-valued MIMO channel into its complex-valued equivalent. Assume the canonical MIMO channel model .. math:: \mathbf{y} = \mathbf{H}\mathbf{x} + \mathbf{n} where :math:`\mathbf{y}\in\mathbb{C}^M` is the received signal vector, :math:`\mathbf{x}\in\mathbb{C}^K` is the vector of transmitted symbols, :math:`\mathbf{H}\in\mathbb{C}^{M\times K}` is the known channel matrix, and :math:`\mathbf{n}\in\mathbb{C}^M` is a noise vector with covariance matrix :math:`\mathbf{S}\in\mathbb{C}^{M\times M}`. This function transforms the real-valued equivalent representations of :math:`\mathbf{y}`, :math:`\mathbf{H}`, and :math:`\mathbf{S}`, as, e.g., obtained with the function :meth:`~sionna.mimo.complex2real_channel`, back to their complex-valued equivalents (Section VII) [YH2015]_. Input ----- y : [...,2M], tf.float 1+D tensor containing the real-valued received signals. h : [...,2M,2K], tf.float 2+D tensor containing the real-valued channel matrices. s : [...,2M,2M], tf.float 2+D tensor containing the real-valued noise covariance matrices. Output ------ : [...,M], tf.complex 1+D tensor containing the complex-valued equivalent received signals. : [...,M,K], tf.complex 2+D tensor containing the complex-valued equivalent channel matrices. : [...,M,M], tf.complex 2+D tensor containing the complex-valued equivalent noise covariance matrices. """ yc = real2complex_vector(y) hc = real2complex_matrix(h) sc = real2complex_covariance(s) return yc, hc, sc
[docs] def whiten_channel(y, h, s, return_s=True): # pylint: disable=line-too-long r"""Whitens a canonical MIMO channel. Assume the canonical MIMO channel model .. math:: \mathbf{y} = \mathbf{H}\mathbf{x} + \mathbf{n} where :math:`\mathbf{y}\in\mathbb{C}^M(\mathbb{R}^M)` is the received signal vector, :math:`\mathbf{x}\in\mathbb{C}^K(\mathbb{R}^K)` is the vector of transmitted symbols, :math:`\mathbf{H}\in\mathbb{C}^{M\times K}(\mathbb{R}^{M\times K})` is the known channel matrix, and :math:`\mathbf{n}\in\mathbb{C}^M(\mathbb{R}^M)` is a noise vector with covariance matrix :math:`\mathbf{S}\in\mathbb{C}^{M\times M}(\mathbb{R}^{M\times M})`. This function whitens this channel by multiplying :math:`\mathbf{y}` and :math:`\mathbf{H}` from the left by :math:`\mathbf{S}^{-\frac{1}{2}}`. Optionally, the whitened noise covariance matrix :math:`\mathbf{I}_M` can be returned. Input ----- y : [...,M], tf.float or tf.complex 1+D tensor containing the received signals. h : [...,M,K], tf.float or tf.complex 2+D tensor containing the channel matrices. s : [...,M,M], tf.float or complex 2+D tensor containing the noise covariance matrices. return_s : bool If `True`, the whitened covariance matrix is returned. Defaults to `True`. Output ------ : [...,M], tf.float or tf.complex 1+D tensor containing the whitened received signals. : [...,M,K], tf.float or tf.complex 2+D tensor containing the whitened channel matrices. : [...,M,M], tf.float or tf.complex 2+D tensor containing the whitened noise covariance matrices. Only returned if ``return_s`` is `True`. """ # Compute whitening matrix s_inv_1_2 = matrix_sqrt_inv(s) s_inv_1_2 = expand_to_rank(s_inv_1_2, tf.rank(h), 0) # Whiten obervation and channel matrix yw = tf.expand_dims(y, -1) yw = tf.matmul(s_inv_1_2, yw) yw = tf.squeeze(yw, axis=-1) hw = tf.matmul(s_inv_1_2, h) if return_s: # Ideal interference covariance matrix after whitening sw = tf.eye(tf.shape(s)[-2], dtype=s.dtype) sw = expand_to_rank(sw, tf.rank(s), 0) return yw, hw, sw else: return yw, hw
[docs] class List2LLR(ABC): # pylint: disable=line-too-long r"""List2LLR() Abstract class defining a callable to compute LLRs from a list of candidate vectors (or paths) provided by a MIMO detector. The following channel model is assumed .. math:: \bar{\mathbf{y}} = \mathbf{R}\bar{\mathbf{x}} + \bar{\mathbf{n}} where :math:`\bar{\mathbf{y}}\in\mathbb{C}^S` are the channel outputs, :math:`\mathbf{R}\in\mathbb{C}^{S\times S}` is an upper-triangular matrix, :math:`\bar{\mathbf{x}}\in\mathbb{C}^S` is the transmitted vector whose entries are uniformly and independently drawn from the constellation :math:`\mathcal{C}`, and :math:`\bar{\mathbf{n}}\in\mathbb{C}^S` is white noise with :math:`\mathbb{E}\left[\bar{\mathbf{n}}\right]=\mathbf{0}` and :math:`\mathbb{E}\left[\bar{\mathbf{n}}\bar{\mathbf{n}}^{\mathsf{H}}\right]=\mathbf{I}`. It is assumed that a MIMO detector such as :class:`~sionna.mimo.KBestDetector` produces :math:`K` candidate solutions :math:`\bar{\mathbf{x}}_k\in\mathcal{C}^S` and their associated distance metrics :math:`d_k=\lVert \bar{\mathbf{y}} - \mathbf{R}\bar{\mathbf{x}}_k \rVert^2` for :math:`k=1,\dots,K`. This layer can also be used with the real-valued representation of the channel. Input ----- (y, r, dists, path_inds, path_syms) : Tuple: y : [...,M], tf.complex or tf.float Channel outputs of the whitened channel r : [...,num_streams, num_streams], same dtype as ``y`` Upper triangular channel matrix of the whitened channel dists : [...,num_paths], tf.float Distance metric for each path (or candidate) path_inds : [...,num_paths,num_streams], tf.int32 Symbol indices for every stream of every path (or candidate) path_syms : [...,num_path,num_streams], same dtype as ``y`` Constellation symbol for every stream of every path (or candidate) Output ------ llr : [...num_streams,num_bits_per_symbol], tf.float LLRs for all bits of every stream Note ---- An implementation of this class does not need to make use of all of the provided inputs which enable various different implementations. """ @abstractmethod def __call__(self, inputs): raise NotImplementedError
[docs] class List2LLRSimple(Layer, List2LLR): # pylint: disable=line-too-long r"""List2LLRSimple(num_bits_per_symbol, llr_clip_val=20.0, **kwargs) Computes LLRs from a list of candidate vectors (or paths) provided by a MIMO detector. The following channel model is assumed: .. math:: \bar{\mathbf{y}} = \mathbf{R}\bar{\mathbf{x}} + \bar{\mathbf{n}} where :math:`\bar{\mathbf{y}}\in\mathbb{C}^S` are the channel outputs, :math:`\mathbf{R}\in\mathbb{C}^{S\times S}` is an upper-triangular matrix, :math:`\bar{\mathbf{x}}\in\mathbb{C}^S` is the transmitted vector whose entries are uniformly and independently drawn from the constellation :math:`\mathcal{C}`, and :math:`\bar{\mathbf{n}}\in\mathbb{C}^S` is white noise with :math:`\mathbb{E}\left[\bar{\mathbf{n}}\right]=\mathbf{0}` and :math:`\mathbb{E}\left[\bar{\mathbf{n}}\bar{\mathbf{n}}^{\mathsf{H}}\right]=\mathbf{I}`. It is assumed that a MIMO detector such as :class:`~sionna.mimo.KBestDetector` produces :math:`K` candidate solutions :math:`\bar{\mathbf{x}}_k\in\mathcal{C}^S` and their associated distance metrics :math:`d_k=\lVert \bar{\mathbf{y}} - \mathbf{R}\bar{\mathbf{x}}_k \rVert^2` for :math:`k=1,\dots,K`. This layer can also be used with the real-valued representation of the channel. The LLR for the :math:`i\text{th}` bit of the :math:`k\text{th}` stream is computed as .. math:: \begin{align} LLR(k,i) &= \log\left(\frac{\Pr(b_{k,i}=1|\bar{\mathbf{y}},\mathbf{R})}{\Pr(b_{k,i}=0|\bar{\mathbf{y}},\mathbf{R})}\right)\\ &\approx \min_{j \in \mathcal{C}_{k,i,0}}d_j - \min_{j \in \mathcal{C}_{k,i,1}}d_j \end{align} where :math:`\mathcal{C}_{k,i,1}` and :math:`\mathcal{C}_{k,i,0}` are the set of indices in the list of candidates for which the :math:`i\text{th}` bit of the :math:`k\text{th}` stream is equal to 1 and 0, respectively. The LLRs are clipped to :math:`\pm LLR_\text{clip}` which can be configured through the parameter ``llr_clip_val``. If :math:`\mathcal{C}_{k,i,0}` is empty, :math:`LLR(k,i)=LLR_\text{clip}`; if :math:`\mathcal{C}_{k,i,1}` is empty, :math:`LLR(k,i)=-LLR_\text{clip}`. Parameters ---------- num_bits_per_symbol : int Number of bits per constellation symbol llr_clip_val : float The absolute values of LLRs are clipped to this value. Defaults to 20.0. Can also be a trainable variable. Input ----- (y, r, dists, path_inds, path_syms) : Tuple: y : [...,M], tf.complex or tf.float Channel outputs of the whitened channel r : [...,num_streams, num_streams], same dtype as ``y`` Upper triangular channel matrix of the whitened channel dists : [...,num_paths], tf.float Distance metric for each path (or candidate) path_inds : [...,num_paths,num_streams], tf.int32 Symbol indices for every stream of every path (or candidate) path_syms : [...,num_path,num_streams], same dtype as ``y`` Constellation symbol for every stream of every path (or candidate) Output ------ llr : [...num_streams,num_bits_per_symbol], tf.float LLRs for all bits of every stream """ def __init__(self, num_bits_per_symbol, llr_clip_val=20.0, **kwargs): super().__init__(**kwargs) # Array composed of binary representations of all symbols indices num_points = 2**num_bits_per_symbol a = np.zeros([num_points, num_bits_per_symbol]) for i in range(num_points): a[i, :] = np.array(list(np.binary_repr(i, num_bits_per_symbol)), dtype=np.int32) # Compute symbol indices for which the bits are 0 or 1, e.g.,: # The ith column of c0 provides all symbol indices for which # the ith bit is 0. c0 = np.zeros([int(num_points/2), num_bits_per_symbol]) c1 = np.zeros([int(num_points/2), num_bits_per_symbol]) for i in range(num_bits_per_symbol): c0[:,i] = np.where(a[:,i]==0)[0] c1[:,i] = np.where(a[:,i]==1)[0] # Convert to tensor and add dummy dimensions needed for broadcasting self._c0 = expand_to_rank(tf.constant(c0, tf.int32), 5, 0) self._c1 = expand_to_rank(tf.constant(c1, tf.int32), 5, 0) # Assign this absolute value to all LLRs without counter-hypothesis self.llr_clip_val = llr_clip_val @property def llr_clip_val(self): return self._llr_clip_val @llr_clip_val.setter def llr_clip_val(self, value): self._llr_clip_val = value def __call__(self, inputs): # dists : [batch_size, num_paths] # path_inds : [batch_size, num_paths, num_streams] dists, path_inds = inputs[2:4] # Scaled by 0.5 to account for the reduced noise power in each complex # dimension if real channel representation is used. if inputs[0].dtype.is_floating: dists = dists/2.0 # Compute for every symbol in every path which bits are 0 or 1 # b0/b1: [batch_size, num_path, num_streams, num_bits_per_symbol] # The reduce_any op is forced to run in XLA mode to be able to # work with very large tensors. There seems to an int32 indexing issue # for all TF reduce CUDA kernels. path_inds = insert_dims(path_inds, 2, axis=-1) b0 = tf.equal(path_inds, self._c0) b1 = tf.equal(path_inds, self._c1) b0 = tf.function(tf.reduce_any, jit_compile=True)(b0, axis=-2) b1 = tf.function(tf.reduce_any, jit_compile=True)(b1, axis=-2) # Compute distances for all bits in all paths, set distance to inf # if the bit does not have the correct value dists = expand_to_rank(dists, tf.rank(b0), axis=-1) d0 = tf.where(b0, dists, tf.constant(np.inf, dists.dtype)) d1 = tf.where(b1, dists, tf.constant(np.inf, dists.dtype)) # Compute minimum distance for each bit in each stream # l0/l1: [batch_size, num_streams, num_bits_per_symbol] l0 = tf.reduce_min(d0, axis=1) l1 = tf.reduce_min(d1, axis=1) # Compute LLRs llr = l0-l1 # Clip LLRs llr = tf.clip_by_value(llr, -self.llr_clip_val, self.llr_clip_val) return llr