Source code for sionna.phy.mapping

## SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.# SPDX-License-Identifier: Apache-2.0#"""Blocks for (de)mapping, constellation class, and utility functions"""importnumpyasnpimporttensorflowastfimportmatplotlib.pyplotaspltfromsionna.phy.blockimportBlock,Objectfromsionna.phy.configimportconfig,dtypesfromsionna.phy.utilsimportexpand_to_rank,flatten_last_dims,\hard_decisions,split_dimdefpam_gray(b):# pylint: disable=line-too-longr"""Maps a vector of bits to a PAM constellation points with Gray labeling    This recursive function maps a binary vector to Gray-labelled PAM    constellation points. It can be used to generated QAM constellations.    The constellation is not normalized.    Input    -----    b : [n], `np.array`        Tensor with with binary entries    Output    ------    : `signed int`        PAM constellation point taking values in        :math:`\{\pm 1,\pm 3,\dots,\pm (2^n-1)\}`.    Note    ----    This algorithm is a recursive implementation of the expressions found in    Section 5.1 of [3GPPTS38211]_. It is used in the 5G standard.    """# pylint: disable=C0301iflen(b)>1:return(1-2*b[0])*(2**len(b[1:])-pam_gray(b[1:]))return1-2*b[0]defqam(num_bits_per_symbol,normalize=True,precision=None):r"""Generates a QAM constellation    This function generates a complex-valued vector, where each element is    a constellation point of an M-ary QAM constellation. The bit    label of the ``n`` th point is given by the length-``num_bits_per_symbol``    binary represenation of ``n``.    Input    -----    num_bits_per_symbol : `int`        Number of bits per constellation point.        Must be a multiple of two, e.g., 2, 4, 6, 8, etc.    normalize: `bool`, (default `True`)        If `True`, the constellation is normalized to have unit power.    precision : `None` (default) | "single" | "double"        Precision used for internal calculations and outputs.        If set to `None`,        :attr:`~sionna.phy.config.Config.precision` is used.    Output    ------    : [2**num_bits_per_symbol], np.complex64        QAM constellation points    Note    ----    The bit label of the nth constellation point is given by the binary    representation of its position within the array and can be obtained    through ``np.binary_repr(n, num_bits_per_symbol)``.    The normalization factor of a QAM constellation is given in    closed-form as:    .. math::        \sqrt{\frac{1}{2^{n-2}}\sum_{i=1}^{2^{n-1}}(2i-1)^2}    where :math:`n= \text{num_bits_per_symbol}/2` is the number of bits    per dimension.    This algorithm is a recursive implementation of the expressions found in    Section 5.1 of [3GPPTS38211]_. It is used in the 5G standard.    """# pylint: disable=C0301try:assertnum_bits_per_symbol%2==0# is evenassertnum_bits_per_symbol>0# is larger than zeroexceptAssertionErroraserror:raiseValueError("num_bits_per_symbol must be a multiple of 2") \fromerrorassertisinstance(normalize,bool),"normalize must be boolean"ifprecisionisNone:rdtype=config.np_rdtypecdtype=config.np_cdtypeelse:rdtype=dtypes[precision]["np"]["rdtype"]cdtype=dtypes[precision]["np"]["cdtype"]# Build constellation by iterating through all pointsc=np.zeros([2**num_bits_per_symbol],dtype=cdtype)foriinrange(0,2**num_bits_per_symbol):b=np.array(list(np.binary_repr(i,num_bits_per_symbol)),dtype=np.int32)c[i]=pam_gray(b[0::2])+1j*pam_gray(b[1::2])# PAM in each dimensionifnormalize:# Normalize to unit energyn=int(num_bits_per_symbol/2)qam_var=1/(2**(n-2))*np.sum(np.linspace(1,2**n-1,2**(n-1),dtype=rdtype)**2)c/=np.sqrt(qam_var)returncdefpam(num_bits_per_symbol,normalize=True,precision=None):r"""Generates a PAM constellation    This function generates a real-valued vector, where each element is    a constellation point of an M-ary PAM constellation. The bit    label of the ``n`` th point is given by the length-``num_bits_per_symbol``    binary represenation of ``n``.    Input    -----    num_bits_per_symbol : `int`        Number of bits per constellation point.        Must be positive.    normalize: `bool`, (default `True`)        If `True`, the constellation is normalized to have unit power.    precision : `None` (default) | "single" | "double"        Precision used for internal calculations and outputs.        If set to `None`,        :attr:`~sionna.phy.config.Config.precision` is used.    Output    ------    : [2**num_bits_per_symbol], `np.float32`        PAM constellation symbols    Note    ----    The bit label of the nth constellation point is given by the binary    representation of its position within the array and can be obtained    through ``np.binary_repr(n, num_bits_per_symbol)``.    The normalization factor of a PAM constellation is given in    closed-form as:    .. math::        \sqrt{\frac{1}{2^{n-1}}\sum_{i=1}^{2^{n-1}}(2i-1)^2}    where :math:`n= \text{num_bits_per_symbol}` is the number of bits    per symbol.    This algorithm is a recursive implementation of the expressions found in    Section 5.1 of [3GPPTS38211]_. It is used in the 5G standard.    """# pylint: disable=C0301try:assertnum_bits_per_symbol>0# is larger than zeroexceptAssertionErroraserror:raiseValueError("num_bits_per_symbol must be positive") \fromerrorassertisinstance(normalize,bool),"normalize must be boolean"ifprecisionisNone:rdtype=config.np_rdtypecdtype=config.np_cdtypeelse:rdtype=dtypes[precision]["np"]["rdtype"]cdtype=dtypes[precision]["np"]["cdtype"]# Build constellation by iterating through all pointsc=np.zeros([2**num_bits_per_symbol],dtype=cdtype)foriinrange(0,2**num_bits_per_symbol):b=np.array(list(np.binary_repr(i,num_bits_per_symbol)),dtype=np.int32)c[i]=pam_gray(b)ifnormalize:# Normalize to unit energyn=int(num_bits_per_symbol)pam_var=1/(2**(n-1))*np.sum(np.linspace(1,2**n-1,2**(n-1),dtype=rdtype)**2)c/=np.sqrt(pam_var)returnc
[docs]classConstellation(Block):# pylint: disable=line-too-longr""" Constellation that can be used by a (de-)mapper This class defines a constellation, i.e., a complex-valued vector of constellation points. The binary representation of the index of an element of this vector corresponds to the bit label of the constellation point. This implicit bit labeling is used by the :class:`~sionna.phy.mapping.Mapper` and :class:`~sionna.phy.mapping.Demapper`. Parameters ---------- constellation_type : "qam" | "pam" | "custom" For "custom", the constellation ``points`` must be provided. num_bits_per_symbol : int Number of bits per constellation symbol, e.g., 4 for QAM16. points : `None` (default) | [2**num_bits_per_symbol], `array_like` Custom constellation points normalize : `bool`, (default `False`) If `True`, the constellation is normalized to have unit power. Only applies to custom constellations. center : `bool`, (default `False`) If `True`, the constellation is ensured to have zero mean. Only applies to custom constellations. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- : `None` Output ------ : [2**num_bits_per_symbol], `tf.complex` (Possibly) centered and normalized constellation points """# pylint: enable=C0301def__init__(self,constellation_type,num_bits_per_symbol,points=None,normalize=False,center=False,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)ifconstellation_typenotin("qam","pam","custom"):raiseValueError("Wrong `constellation_type`{constellation_type}")self._constellation_type=constellation_typeifnum_bits_per_symbolisNone:raiseValueError("No value for `num_bits_per_symbol`")n=num_bits_per_symbolif(n<=0)or(n%1!=0):raiseValueError("`num_bits_per_symbol` must be a"+" positive integer")ifself.constellation_type=="qam":ifn%2!=0:raiseValueError("`num_bits_per_symbol` must"+"be a positive integer multiple of 2")self._num_bits_per_symbol=int(n)self._num_points=2**self._num_bits_per_symbolself.normalize=normalizeself.center=centerif(pointsisnotNone)and(constellation_type!="custom"):raiseValueError("`points` can only be provided for"+" `constellation_type`='custom'")elif(pointsisNone)and(constellation_type=="custom"):raiseValueError("You must provide a value for `points`")self._points=Noneifself.constellation_type=="qam":points=qam(self.num_bits_per_symbol,normalize=True,precision=precision)elifself.constellation_type=="pam":points=pam(self.num_bits_per_symbol,normalize=True,precision=precision)self.points=points@propertydefconstellation_type(self):""" "qam" | "pam" | "custom" : Constellation type"""returnself._constellation_type@propertydefnum_bits_per_symbol(self):""" `int` : Number of bits per symbol"""returnself._num_bits_per_symbol@propertydefnum_points(self):""" `int` : Number of constellation points"""returnself._num_points@propertydefnormalize(self):""" `bool` : Get/set if the constellation is normalized"""returnself._normalize@normalize.setterdefnormalize(self,value):assertisinstance(value,bool),"`normalize` must be boolean"self._normalize=value@propertydefcenter(self):""" `bool` : Get/set if the constellation is centered"""returnself._center@center.setterdefcenter(self,value):assertisinstance(value,bool),"`center` must be boolean"self._center=value@propertydefpoints(self):# pylint: disable=line-too-long""" [2**num_bits_per_symbol], `tf.complex` : Get/set constellation points"""returnself._points@points.setterdefpoints(self,v):ifnot(self._pointsisNone)and \(self.constellation_type!="custom"):msg="`points` can only be modified for custom constellations"raiseValueError(msg)iftf.executing_eagerly():ifnottf.shape(v)==[2**self.num_bits_per_symbol]:err_msg="`points` must have shape [2**num_bits_per_symbol]"raiseValueError(err_msg)ifisinstance(v,tf.Variable):ifv.dtype!=self.cdtype:msg=f"`points` must have dtype={self.cdtype}"raiseTypeError(msg)else:self._points=velse:self._points=tf.cast(v,self.cdtype)defcall(self):x=self.pointsifself.constellation_type=="custom":ifself._center:x=x-tf.reduce_mean(x)ifself.normalize:energy=tf.reduce_mean(tf.square(tf.abs(x)))energy_sqrt=tf.complex(tf.sqrt(energy),tf.constant(0.,dtype=self.rdtype))x=x/energy_sqrtreturnx
[docs]defshow(self,labels=True,figsize=(7,7)):"""Generate a scatter-plot of the constellation Input ----- labels : `bool`, (default `True`) If `True`, the bit labels will be drawn next to each constellation point. figsize : Two-element Tuple, `float`, (default `(7,7)`) Width and height in inches Output ------ : matplotlib.figure.Figure Handle to matplot figure object """p=self().numpy()maxval=np.max(np.abs(p))*1.05fig=plt.figure(figsize=figsize)ax=fig.add_subplot(111)plt.xlim(-maxval,maxval)plt.ylim(-maxval,maxval)plt.scatter(np.real(p),np.imag(p))ax.set_aspect("equal",adjustable="box")plt.xlabel("Real Part")plt.ylabel("Imaginary Part")plt.grid(True,which="both",axis="both")plt.title("Constellation Plot")iflabelsisTrue:forj,pinenumerate(p):plt.annotate(np.binary_repr(j,self.num_bits_per_symbol),(np.real(p),np.imag(p)))returnfig
@staticmethoddefcheck_or_create(*,constellation_type=None,num_bits_per_symbol=None,constellation=None,precision=None):"""Either creates a new constellation or checks an existing one"""ifisinstance(constellation,Constellation):returnconstellationelifconstellation_typein["qam","pam"]:returnConstellation(constellation_type,num_bits_per_symbol,precision=precision)else:raiseValueError("You must provide a valid `constellation`")
[docs]classMapper(Block):# pylint: disable=line-too-longr""" Maps binary tensors to points of a constellation This class defines a block that maps a tensor of binary values to a tensor of points from a provided constellation. Parameters ---------- constellation_type : "qam" | "pam" | "custom" For "custom", an instance of :class:`~sionna.phy.mapping.Constellation` must be provided. num_bits_per_symbol : `int` The number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. constellation : `None` (default) | :class:`~sionna.phy.mapping.Constellation` If no constellation is provided, ``constellation_type`` and ``num_bits_per_symbol`` must be provided. return_indices : bool, (default `False`) If enabled, symbol indices are additionally returned. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- : [..., n], `tf.float` or `tf.int` Tensor with with binary entries Output ------ : [...,n/Constellation.num_bits_per_symbol], `tf.complex` Mapped constellation symbols : [...,n/Constellation.num_bits_per_symbol], `tf.int32` Symbol indices corresponding to the constellation symbols. Only returned if ``return_indices`` is set to True. Note ---- The last input dimension must be an integer multiple of the number of bits per constellation symbol. """def__init__(self,constellation_type=None,num_bits_per_symbol=None,constellation=None,return_indices=False,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)self._constellation=Constellation.check_or_create(constellation_type=constellation_type,num_bits_per_symbol=num_bits_per_symbol,constellation=constellation,precision=precision)self._return_indices=return_indicesn=self.constellation.num_bits_per_symbolself._bit_positions=tf.cast(tf.range(n-1,-1,-1),dtype=tf.int32)@propertydefconstellation(self):""" :class:`~sionna.phy.mapping.Constellation` : Constellation used by the Mapper """returnself._constellationdefcall(self,bits):# Convert to int32bits=tf.cast(bits,dtype=tf.int32)# Reshape last dimensions to the desired formatn1=int(bits.shape[-1]/self.constellation.num_bits_per_symbol)new_shape=[n1,self.constellation.num_bits_per_symbol]bits=split_dim(bits,new_shape,axis=tf.rank(bits)-1)# Use bitwise left shift to compute powers of twoshifted_bits=tf.bitwise.left_shift(bits,self._bit_positions)# Compute the integer representation using bitwise operationsint_rep=tf.reduce_sum(shifted_bits,axis=-1)# Map integers to constellation symbolsx=tf.gather(self._constellation(),int_rep,axis=0)ifself._return_indices:returnx,int_repelse:returnx
[docs]classDemapper(Block):# pylint: disable=line-too-longr""" Computes log-likelihood ratios (LLRs) or hard-decisions on bits for a tensor of received symbols Prior knowledge on the bits can be optionally provided. This class defines a block implementing different demapping functions. All demapping functions are fully differentiable when soft-decisions are computed. Parameters ---------- demapping_method : "app" | "maxlog" Demapping method constellation_type : "qam" | "pam" | "custom" For "custom", an instance of :class:`~sionna.phy.mapping.Constellation` must be provided. num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. constellation : `None` (default) | :class:`~sionna.phy.mapping.Constellation` If no constellation is provided, ``constellation_type`` and ``num_bits_per_symbol`` must be provided. hard_out : bool, (default `False`) If `True`, the demapper provides hard-decided bits instead of soft-values. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- y : [...,n], `tf.complex` Received symbols no : Scalar or [...,n], `tf.float` The noise variance estimate. It can be provided either as scalar for the entire input batch or as a tensor that is "broadcastable" to ``y``. prior : `None` (default) | [num_bits_per_symbol] or [...,num_bits_per_symbol], `tf.float` Prior for every bit as LLRs. It can be provided either as a tensor of shape `[num_bits_per_symbol]` for the entire input batch, or as a tensor that is "broadcastable" to `[..., n, num_bits_per_symbol]`. Output ------ : [...,n*num_bits_per_symbol], `tf.float` LLRs or hard-decisions for every bit Note ---- With the "app" demapping method, the LLR for the :math:`i\text{th}` bit is computed according to .. math:: LLR(i) = \ln\left(\frac{\Pr\left(b_i=1\lvert y,\mathbf{p}\right)}{\Pr\left(b_i=0\lvert y,\mathbf{p}\right)}\right) =\ln\left(\frac{ \sum_{c\in\mathcal{C}_{i,1}} \Pr\left(c\lvert\mathbf{p}\right) \exp\left(-\frac{1}{N_o}\left|y-c\right|^2\right) }{ \sum_{c\in\mathcal{C}_{i,0}} \Pr\left(c\lvert\mathbf{p}\right) \exp\left(-\frac{1}{N_o}\left|y-c\right|^2\right) }\right) where :math:`\mathcal{C}_{i,1}` and :math:`\mathcal{C}_{i,0}` are the sets of constellation points for which the :math:`i\text{th}` bit is equal to 1 and 0, respectively. :math:`\mathbf{p} = \left[p_0,\dots,p_{K-1}\right]` is the vector of LLRs that serves as prior knowledge on the :math:`K` bits that are mapped to a constellation point and is set to :math:`\mathbf{0}` if no prior knowledge is assumed to be available, and :math:`\Pr(c\lvert\mathbf{p})` is the prior probability on the constellation symbol :math:`c`: .. math:: \Pr\left(c\lvert\mathbf{p}\right) = \prod_{k=0}^{K-1} \text{sigmoid}\left(p_k \ell(c)_k\right) where :math:`\ell(c)_k` is the :math:`k^{th}` bit label of :math:`c`, where 0 is replaced by -1. The definition of the LLR has been chosen such that it is equivalent with that of logits. This is different from many textbooks in communications, where the LLR is defined as :math:`LLR(i) = \ln\left(\frac{\Pr\left(b_i=0\lvert y\right)}{\Pr\left(b_i=1\lvert y\right)}\right)`. With the "maxlog" demapping method, LLRs for the :math:`i\text{th}` bit are approximated like .. math:: \begin{align} LLR(i) &\approx\ln\left(\frac{ \max_{c\in\mathcal{C}_{i,1}} \Pr\left(c\lvert\mathbf{p}\right) \exp\left(-\frac{1}{N_o}\left|y-c\right|^2\right) }{ \max_{c\in\mathcal{C}_{i,0}} \Pr\left(c\lvert\mathbf{p}\right) \exp\left(-\frac{1}{N_o}\left|y-c\right|^2\right) }\right)\\ &= \max_{c\in\mathcal{C}_{i,0}} \left(\ln\left(\Pr\left(c\lvert\mathbf{p}\right)\right)-\frac{|y-c|^2}{N_o}\right) - \max_{c\in\mathcal{C}_{i,1}}\left( \ln\left(\Pr\left(c\lvert\mathbf{p}\right)\right) - \frac{|y-c|^2}{N_o}\right) . \end{align} """def__init__(self,demapping_method,constellation_type=None,num_bits_per_symbol=None,constellation=None,hard_out=False,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)# Create constellation objectself._constellation=Constellation.check_or_create(constellation_type=constellation_type,num_bits_per_symbol=num_bits_per_symbol,constellation=constellation,precision=precision)num_bits_per_symbol=self._constellation.num_bits_per_symbolself._logits2llrs=SymbolLogits2LLRs(demapping_method,num_bits_per_symbol,hard_out=hard_out,precision=precision,**kwargs)self._no_threshold=tf.cast(np.finfo(self.rdtype.as_numpy_dtype).tiny,self.rdtype)@propertydefconstellation(self):""" :class:`~sionna.phy.mapping.Constellation` : Constellation used by the Demapper """returnself._constellationdefcall(self,y,no,prior=None):# Reshape constellation points to [1,...1,num_points]points_shape=[1]*y.shape.rank+self.constellation.points.shapepoints=tf.reshape(self.constellation.points,points_shape)# Compute squared distances from y to all points# shape [...,n,num_points]squared_dist=tf.pow(tf.abs(tf.expand_dims(y,axis=-1)-points),2)# Add a dummy dimension for broadcasting. This is not needed when no# is a scalar, but also does not do any harm.no=tf.expand_dims(no,axis=-1)# Deal with zero or very small values.no=tf.math.maximum(no,self._no_threshold)# Compute exponentsexponents=-squared_dist/nollr=self._logits2llrs(exponents,prior)# Reshape LLRs to [...,n*num_bits_per_symbol]out_shape=tf.concat([tf.shape(y)[:-1],[y.shape[-1]* \self.constellation.num_bits_per_symbol]],0)llr_reshaped=tf.reshape(llr,out_shape)returnllr_reshaped
[docs]classSymbolDemapper(Block):# pylint: disable=line-too-longr""" Computes normalized log-probabilities (logits) or hard-decisions on symbols for a tensor of received symbols Prior knowldge on the transmitted constellation points can be optionnaly provided. The demapping function is fully differentiable when soft-values are computed. Parameters ---------- constellation_type : "qam" | "pam" | "custom" For "custom", an instance of :class:`~sionna.phy.mapping.Constellation` must be provided. num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. constellation : `None` (default) | :class:`~sionna.phy.mapping.Constellation` If no constellation is provided, ``constellation_type`` and ``num_bits_per_symbol`` must be provided. hard_out : bool, (default `False`) If `True`, the demapper provides hard-decided symbols instead of soft-values. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- y : [...,n], `tf.complex` Received symbols no : Scalar or [...,n], `tf.float` Noise variance estimate. It can be provided either as scalar for the entire input batch or as a tensor that is "broadcastable" to ``y``. prior : `None` (default) | [num_points] or [...,num_points], `tf.float` Prior for every symbol as log-probabilities (logits). It can be provided either as a tensor of shape `[num_points]` for the entire input batch, or as a tensor that is "broadcastable" to `[..., n, num_points]`. Output ------ : [...,n, num_points] or [...,n], `tf.float` or `tf.int32` A tensor of shape `[...,n, num_points]` of logits for every constellation point if `hard_out` is set to `False`. Otherwise, a tensor of shape `[...,n]` of hard-decisions on the symbols. Note ---- The normalized log-probability for the constellation point :math:`c` is computed according to .. math:: \ln\left(\Pr\left(c \lvert y,\mathbf{p}\right)\right) = \ln\left( \frac{\exp\left(-\frac{|y-c|^2}{N_0} + p_c \right)}{\sum_{c'\in\mathcal{C}} \exp\left(-\frac{|y-c'|^2}{N_0} + p_{c'} \right)} \right) where :math:`\mathcal{C}` is the set of constellation points used for modulation, and :math:`\mathbf{p} = \left\{p_c \lvert c \in \mathcal{C}\right\}` the prior information on constellation points given as log-probabilities and which is set to :math:`\mathbf{0}` if no prior information on the constellation points is assumed to be available. """def__init__(self,constellation_type=None,num_bits_per_symbol=None,constellation=None,hard_out=False,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)self._hard_out=hard_out# Create constellation objectself._constellation=Constellation.check_or_create(constellation_type=constellation_type,num_bits_per_symbol=num_bits_per_symbol,constellation=constellation,precision=precision)defcall(self,y,no,prior=None):points=expand_to_rank(self._constellation.points,tf.rank(y)+1,axis=0)y=tf.expand_dims(y,axis=-1)d=tf.abs(y-points)no=expand_to_rank(no,tf.rank(d),axis=-1)exp=-d**2/noifpriorisnotNone:prior=expand_to_rank(prior,tf.rank(exp),axis=0)exp=exp+priorifself._hard_out:returntf.argmax(exp,axis=-1,output_type=tf.int32)else:returntf.nn.log_softmax(exp,axis=-1)
[docs]classSymbolLogits2LLRs(Block):# pylint: disable=line-too-longr""" Computes log-likelihood ratios (LLRs) or hard-decisions on bits from a tensor of logits (i.e., unnormalized log-probabilities) on constellation points. Prior knowledge on the bits can be optionally provided Parameters ---------- method : "app" | "maxlog" Method used for computing the LLRs num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. hard_out : `bool`, (default `False`) If `True`, the layer provides hard-decided bits instead of soft-values. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- logits : [...,n, num_points], `tf.float` Logits on constellation points prior : `None` (default) | [num_bits_per_symbol] or [...n, num_bits_per_symbol], `tf.float` Prior for every bit as LLRs. It can be provided either as a tensor of shape `[num_bits_per_symbol]` for the entire input batch, or as a tensor that is "broadcastable" to `[..., n, num_bits_per_symbol]`. Output ------ : [...,n, num_bits_per_symbol], `tf.float` LLRs or hard-decisions for every bit Note ---- With the "app" method, the LLR for the :math:`i\text{th}` bit is computed according to .. math:: LLR(i) = \ln\left(\frac{\Pr\left(b_i=1\lvert \mathbf{z},\mathbf{p}\right)}{\Pr\left(b_i=0\lvert \mathbf{z},\mathbf{p}\right)}\right) =\ln\left(\frac{ \sum_{c\in\mathcal{C}_{i,1}} \Pr\left(c\lvert\mathbf{p}\right) e^{z_c} }{ \sum_{c\in\mathcal{C}_{i,0}} \Pr\left(c\lvert\mathbf{p}\right) e^{z_c} }\right) where :math:`\mathcal{C}_{i,1}` and :math:`\mathcal{C}_{i,0}` are the sets of :math:`2^K` constellation points for which the :math:`i\text{th}` bit is equal to 1 and 0, respectively. :math:`\mathbf{z} = \left[z_{c_0},\dots,z_{c_{2^K-1}}\right]` is the vector of logits on the constellation points, :math:`\mathbf{p} = \left[p_0,\dots,p_{K-1}\right]` is the vector of LLRs that serves as prior knowledge on the :math:`K` bits that are mapped to a constellation point and is set to :math:`\mathbf{0}` if no prior knowledge is assumed to be available, and :math:`\Pr(c\lvert\mathbf{p})` is the prior probability on the constellation symbol :math:`c`: .. math:: \Pr\left(c\lvert\mathbf{p}\right) = \prod_{k=0}^{K-1} \Pr\left(b_k = \ell(c)_k \lvert\mathbf{p} \right) = \prod_{k=0}^{K-1} \text{sigmoid}\left(p_k \ell(c)_k\right) where :math:`\ell(c)_k` is the :math:`k^{th}` bit label of :math:`c`, where 0 is replaced by -1. The definition of the LLR has been chosen such that it is equivalent with that of logits. This is different from many textbooks in communications, where the LLR is defined as :math:`LLR(i) = \ln\left(\frac{\Pr\left(b_i=0\lvert y\right)}{\Pr\left(b_i=1\lvert y\right)}\right)`. With the "maxlog" method, LLRs for the :math:`i\text{th}` bit are approximated like .. math:: \begin{align} LLR(i) &\approx\ln\left(\frac{ \max_{c\in\mathcal{C}_{i,1}} \Pr\left(c\lvert\mathbf{p}\right) e^{z_c} }{ \max_{c\in\mathcal{C}_{i,0}} \Pr\left(c\lvert\mathbf{p}\right) e^{z_c} }\right) . \end{align} """def__init__(self,method,num_bits_per_symbol,*,hard_out=False,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)assertmethodin("app","maxlog"),"Unknown demapping method"self._method=methodself._hard_out=hard_outself._num_bits_per_symbol=num_bits_per_symbolnum_points=int(2**num_bits_per_symbol)# Array composed of binary representations of all symbols indicesa=np.zeros([num_points,num_bits_per_symbol])foriinrange(0,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 1c0=np.zeros([int(num_points/2),num_bits_per_symbol])c1=np.zeros([int(num_points/2),num_bits_per_symbol])foriinrange(num_bits_per_symbol-1,-1,-1):c0[:,i]=np.where(a[:,i]==0)[0]c1[:,i]=np.where(a[:,i]==1)[0]self._c0=tf.constant(c0,dtype=tf.int32)# Symbols with ith bit=0self._c1=tf.constant(c1,dtype=tf.int32)# Symbols with ith bit=1# Array of labels from {-1, 1} of all symbols# [num_points, num_bits_per_symbol]a=2*a-1self._a=tf.constant(a,dtype=self.rdtype)# Determine the reduce function for LLR computationifself._method=="app":self._reduce=tf.reduce_logsumexpelse:self._reduce=tf.reduce_max@propertydefnum_bits_per_symbol(self):""" `int` : Number of bits per symbol """returnself._num_bits_per_symboldefcall(self,logits,prior=None):# Compute exponentsexponents=logits# Gather exponents for all bits# shape [...,n,num_points/2,num_bits_per_symbol]exp0=tf.gather(exponents,self._c0,axis=-1,batch_dims=0)exp1=tf.gather(exponents,self._c1,axis=-1,batch_dims=0)# Process the prior informationifpriorisnotNone:# Expanding `prior` such that it is broadcastable with# shape [..., n or 1, 1, num_bits_per_symbol]prior=expand_to_rank(prior,tf.rank(logits),axis=0)prior=tf.expand_dims(prior,axis=-2)# Expand the symbol labeling to be broadcastable with prior# shape [..., 1, num_points, num_bits_per_symbol]a=expand_to_rank(self._a,tf.rank(prior),axis=0)# Compute the prior probabilities on symbols exponents# shape [..., n or 1, num_points]exp_ps=tf.reduce_sum(tf.math.log_sigmoid(a*prior),axis=-1)# Gather prior probability symbol for all bits# shape [..., n or 1, num_points/2, num_bits_per_symbol]exp_ps0=tf.gather(exp_ps,self._c0,axis=-1)exp_ps1=tf.gather(exp_ps,self._c1,axis=-1)# Compute LLRs using the definition log( Pr(b=1)/Pr(b=0) )# shape [..., n, num_bits_per_symbol]ifpriorisnotNone:llr=self._reduce(exp_ps1+exp1,axis=-2)\-self._reduce(exp_ps0+exp0,axis=-2)else:llr=self._reduce(exp1,axis=-2)-self._reduce(exp0,axis=-2)ifself._hard_out:returnhard_decisions(llr)else:returnllr
[docs]classLLRs2SymbolLogits(Block):# pylint: disable=line-too-longr""" Computes logits (i.e., unnormalized log-probabilities) or hard decisions on constellation points from a tensor of log-likelihood ratios (LLRs) on bits Parameters ---------- num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. hard_out : `bool`, (default `False`) If `True`, the layer provides hard-decided constellation points instead of soft-values. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- llrs : [..., n, num_bits_per_symbol], `tf.float` LLRs for every bit Output ------ : [...,n, num_points], `tf.float` or [..., n], `tf.int32` Logits or hard-decisions on constellation points Note ---- The logit for the constellation :math:`c` point is computed according to .. math:: \begin{align} \log{\left(\Pr\left(c\lvert LLRs \right)\right)} &= \log{\left(\prod_{k=0}^{K-1} \Pr\left(b_k = \ell(c)_k \lvert LLRs \right)\right)}\\ &= \log{\left(\prod_{k=0}^{K-1} \text{sigmoid}\left(LLR(k) \ell(c)_k\right)\right)}\\ &= \sum_{k=0}^{K-1} \log{\left(\text{sigmoid}\left(LLR(k) \ell(c)_k\right)\right)} \end{align} where :math:`\ell(c)_k` is the :math:`k^{th}` bit label of :math:`c`, where 0 is replaced by -1. The definition of the LLR has been chosen such that it is equivalent with that of logits. This is different from many textbooks in communications, where the LLR is defined as :math:`LLR(i) = \ln\left(\frac{\Pr\left(b_i=0\lvert y\right)}{\Pr\left(b_i=1\lvert y\right)}\right)`. """def__init__(self,num_bits_per_symbol,hard_out=False,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)self._hard_out=hard_outself._num_bits_per_symbol=num_bits_per_symbolnum_points=int(2**num_bits_per_symbol)# Array composed of binary representations of all symbols indicesa=np.zeros([num_points,num_bits_per_symbol])foriinrange(0,num_points):a[i,:]=np.array(list(np.binary_repr(i,num_bits_per_symbol)),dtype=np.int32)# Array of labels from {-1, 1} of all symbols# [num_points, num_bits_per_symbol]a=2*a-1self._a=tf.constant(a,dtype=self.rdtype)@propertydefnum_bits_per_symbol(self):returnself._num_bits_per_symboldefcall(self,llrs):# Expand the symbol labeling to be broadcastable with prior# shape [1, ..., 1, num_points, num_bits_per_symbol]a=expand_to_rank(self._a,tf.rank(llrs),axis=0)# Compute the prior probabilities on symbols exponents# shape [..., 1, num_points]llrs=tf.expand_dims(llrs,axis=-2)logits=tf.reduce_sum(tf.math.log_sigmoid(a*llrs),axis=-1)ifself._hard_out:returntf.argmax(logits,axis=-1,output_type=tf.int32)else:returnlogits
[docs]classSymbolLogits2Moments(Block):# pylint: disable=line-too-longr""" Computes the mean and variance of a constellation from logits (unnormalized log-probabilities) on the constellation points More precisely, given a constellation :math:`\mathcal{C} = \left[ c_0,\dots,c_{N-1} \right]` of size :math:`N`, this layer computes the mean and variance according to .. math:: \begin{align} \mu &= \sum_{n = 0}^{N-1} c_n \Pr \left(c_n \lvert \mathbf{\ell} \right)\\ \nu &= \sum_{n = 0}^{N-1} \left( c_n - \mu \right)^2 \Pr \left(c_n \lvert \mathbf{\ell} \right) \end{align} where :math:`\mathbf{\ell} = \left[ \ell_0, \dots, \ell_{N-1} \right]` are the logits, and .. math:: \Pr \left(c_n \lvert \mathbf{\ell} \right) = \frac{\exp \left( \ell_n \right)}{\sum_{i=0}^{N-1} \exp \left( \ell_i \right) }. Parameters ---------- constellation_type : "qam" | "pam" | "custom" For "custom", an instance of :class:`~sionna.phy.mapping.Constellation` must be provided. num_bits_per_symbol : `int` The number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. constellation : `None` (default) | :class:`~sionna.phy.mapping.Constellation` If no constellation is provided, ``constellation_type`` and ``num_bits_per_symbol`` must be provided. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- logits : [...,n, num_points], `tf.float` Logits on constellation points Output ------ mean : [...,n], `tf.float` Mean of the constellation var : [...,n], `tf.float` Variance of the constellation """def__init__(self,constellation_type=None,num_bits_per_symbol=None,constellation=None,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)# Create constellation objectself._constellation=Constellation.check_or_create(constellation_type=constellation_type,num_bits_per_symbol=num_bits_per_symbol,constellation=constellation,precision=precision)defcall(self,logits):p=tf.math.softmax(logits,axis=-1)p_c=tf.complex(p,tf.cast(0.0,self.rdtype))points=self._constellation()points=expand_to_rank(points,tf.rank(p),axis=0)mean=tf.reduce_sum(p_c*points,axis=-1,keepdims=True)var=tf.reduce_sum(p*tf.square(tf.abs(points-mean)),axis=-1)mean=tf.squeeze(mean,axis=-1)returnmean,var
[docs]classSymbolInds2Bits(Block):# pylint: disable=line-too-longr""" Transforms symbol indices to their binary representations Parameters ---------- num_bits_per_symbol : `int` Number of bits per constellation symbol precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- : Tensor, `tf.int` Symbol indices Output ------ : input.shape + [num_bits_per_symbol], `tf.float` Binary representation of symbol indices """def__init__(self,num_bits_per_symbol,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)num_symbols=2**num_bits_per_symbolb=np.zeros([num_symbols,num_bits_per_symbol])foriinrange(0,num_symbols):b[i,:]=np.array(list(np.binary_repr(i,num_bits_per_symbol)),dtype=np.int32)self._bit_labels=tf.constant(b,self.rdtype)defcall(self,symbol_ind):returntf.gather(self._bit_labels,symbol_ind)
[docs]classQAM2PAM(Object):r"""Transforms QAM symbol indices to PAM symbol indices For indices in a QAM constellation, computes the corresponding indices for the two PAM constellations corresponding the real and imaginary components of the QAM constellation. Parameters ---------- num_bits_per_symbol : `int` Number of bits per QAM constellation symbol, e.g., 4 for QAM16. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- ind_qam : Tensor, `tf.int32` Indices in the QAM constellation Output ------- ind_pam1 : Tensor, `tf.int32` Indices for the first component of the corresponding PAM modulation ind_pam2 : Tensor, `tf.int32` Indices for the first component of the corresponding PAM modulation """def__init__(self,num_bits_per_symbol,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)base=[2**iforiinrange(num_bits_per_symbol//2-1,-1,-1)]base=np.array(base)pam1_ind=np.zeros([2**num_bits_per_symbol],dtype=np.int32)pam2_ind=np.zeros([2**num_bits_per_symbol],dtype=np.int32)foriinrange(0,2**num_bits_per_symbol):b=np.array(list(np.binary_repr(i,num_bits_per_symbol)),dtype=np.int32)pam1_ind[i]=np.sum(b[0::2]*base)pam2_ind[i]=np.sum(b[1::2]*base)self._pam1_ind=tf.constant(pam1_ind,dtype=tf.int32)self._pam2_ind=tf.constant(pam2_ind,dtype=tf.int32)def__call__(self,ind_qam):ind_pam1=tf.gather(self._pam1_ind,ind_qam,axis=0)ind_pam2=tf.gather(self._pam2_ind,ind_qam,axis=0)returnind_pam1,ind_pam2
[docs]classPAM2QAM(Object):r"""Transforms PAM symbol indices/logits to QAM symbol indices/logits For two PAM constellation symbol indices or logits, corresponding to the real and imaginary components of a QAM constellation, compute the QAM symbol index or logits. Parameters ---------- num_bits_per_symbol : `int` Number of bits per QAM constellation symbol, e.g., 4 for QAM16 hard_in_out : `bool`, (default `True`) Determines if inputs and outputs are indices or logits over constellation symbols. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- pam1 : Tensor, `tf.int32`, or [...,2**(num_bits_per_symbol/2)], `tf.float` Indices or logits for the first PAM constellation pam2 : Tensor, `tf.int32`, or [...,2**(num_bits_per_symbol/2)], `tf.float` Indices or logits for the second PAM constellation Output ------- qam : Tensor, `tf.int32`, or [...,2**num_bits_per_symbol], `tf.float` Indices or logits for the corresponding QAM constellation """def__init__(self,num_bits_per_symbol,hard_in_out=True,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)num_pam_symbols=2**(num_bits_per_symbol//2)base=np.array([2**iforiinrange(num_bits_per_symbol-1,-1,-1)])# Create an array of QAM symbol indices, index by two PAM indicesind=np.zeros([num_pam_symbols,num_pam_symbols],np.int32)foriinrange(0,num_pam_symbols):forjinrange(0,num_pam_symbols):b1=np.array(list(np.binary_repr(i,num_bits_per_symbol//2)),dtype=np.int32)b2=np.array(list(np.binary_repr(j,num_bits_per_symbol//2)),dtype=np.int32)b=np.zeros([num_bits_per_symbol],np.int32)b[0::2]=b1b[1::2]=b2ind[i,j]=np.sum(b*base)self._qam_ind=tf.constant(ind,dtype=tf.int32)self._hard_in_out=hard_in_outdef__call__(self,pam1,pam2):# PAM indices to QAM indicesifself._hard_in_out:shape=tf.shape(pam1)ind_pam1=tf.reshape(pam1,[-1,1])ind_pam2=tf.reshape(pam2,[-1,1])ind_pam=tf.concat([ind_pam1,ind_pam2],axis=-1)ind_qam=tf.gather_nd(self._qam_ind,ind_pam)ind_qam=tf.reshape(ind_qam,shape)returnind_qam# PAM logits to QAM logitselse:# Compute all combination of sums of logitslogits_mat=tf.expand_dims(pam1,-1)+tf.expand_dims(pam2,-2)# Flatten to a vectorlogits=flatten_last_dims(logits_mat)# Gather symbols in the correct ordergather_ind=tf.reshape(self._qam_ind,[-1])logits=tf.gather(logits,gather_ind,axis=-1)returnlogits
[docs]classBinarySource(Block):""" Generates a random binary tensor Parameters ---------- seed : `None` (default) | `int` Set the seed for the random generator used to generate the bits. If set to `None`, :attr:`~sionna.phy.config.Config.tf_rng` is used. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- shape : 1D tensor/array/list, `int` Desired shape of the output tensor Output ------ : ``shape``, `tf.float` Tensor filled with random binary values """def__init__(self,precision=None,seed=None,**kwargs):super().__init__(precision=precision,**kwargs)self._seed=seedifself._seedisnotNone:self._rng=tf.random.Generator.from_seed(self._seed)else:self._rng=config.tf_rngdefcall(self,inputs):returntf.cast(self._rng.uniform(inputs,0,2,tf.int32),dtype=self.rdtype)
[docs]classSymbolSource(Block):# pylint: disable=line-too-longr""" Generates a tensor of random constellation symbols Optionally, the symbol indices and/or binary representations of the constellation symbols can be returned. Parameters ---------- constellation_type : `str`, "qam" | "pam" | "custom"] For "custom", an instance of :class:`~sionna.phy.mapping.Constellation` must be provided. num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. constellation : `None` (default) | :class:`~sionna.phy.mapping.Constellation` If no constellation is provided, ``constellation_type`` and ``num_bits_per_symbol`` must be provided. return_indices : `bool`, (default `False`) If enabled, the function also returns the symbol indices. return_bits : `bool`, (default `False`) If enabled, the function also returns the binary symbol representations (i.e., bit labels). seed : `None` (default) | `int` Set the seed for the random generator used to generate the bits. If set to `None`, :attr:`~sionna.phy.config.Config.tf_rng` is used. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- shape : 1D tensor/array/list, `int` Desired shape of the output tensor Output ------ symbols : ``shape``, `tf.complex` Tensor filled with random symbols of the chosen ``constellation_type``. symbol_indices : ``shape``, `tf.int32` Tensor filled with the symbol indices. Only returned if ``return_indices`` is `True`. bits : [``shape``, ``num_bits_per_symbol``], `tf.float` Tensor filled with the binary symbol representations (i.e., bit labels). Only returned if ``return_bits`` is `True`. """def__init__(self,constellation_type=None,num_bits_per_symbol=None,constellation=None,return_indices=False,return_bits=False,seed=None,precision=None,**kwargs):super().__init__(precision=precision,**kwargs)constellation=Constellation.check_or_create(constellation_type=constellation_type,num_bits_per_symbol=num_bits_per_symbol,constellation=constellation,precision=precision)self._num_bits_per_symbol=constellation.num_bits_per_symbolself._return_indices=return_indicesself._return_bits=return_bitsself._binary_source=BinarySource(seed=seed,precision=precision)self._mapper=Mapper(constellation=constellation,return_indices=return_indices,precision=precision)defcall(self,inputs):shape=tf.concat([inputs,[self._num_bits_per_symbol]],axis=-1)b=self._binary_source(tf.cast(shape,tf.int32))ifself._return_indices:x,ind=self._mapper(b)else:x=self._mapper(b)result=tf.squeeze(x,-1)ifself._return_indicesorself._return_bits:result=[result]ifself._return_indices:result.append(tf.squeeze(ind,-1))ifself._return_bits:result.append(b)returnresult
[docs]classQAMSource(SymbolSource):# pylint: disable=line-too-longr""" Generates a tensor of random QAM symbols Optionally, the symbol indices and/or binary representations of the constellation symbols can be returned. Parameters ---------- num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. return_indices : `bool`, (default `False`) If enabled, the function also returns the symbol indices. return_bits : `bool`, (default `False`) If enabled, the function also returns the binary symbol representations (i.e., bit labels). seed : `None` (default) | `int` Set the seed for the random generator used to generate the bits. If set to `None`, :attr:`~sionna.phy.config.Config.tf_rng` is used. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- shape : 1D tensor/array/list, `int` Desired shape of the output tensor Output ------ symbols : ``shape``, `tf.complex` Tensor filled with random QAM symbols symbol_indices : ``shape``, `tf.int32` Tensor filled with the symbol indices. Only returned if ``return_indices`` is `True`. bits : [``shape``, ``num_bits_per_symbol``], `tf.float` Tensor filled with the binary symbol representations (i.e., bit labels). Only returned if ``return_bits`` is `True`. """def__init__(self,num_bits_per_symbol=None,return_indices=False,return_bits=False,seed=None,precision=None,**kwargs):super().__init__(constellation_type="qam",num_bits_per_symbol=num_bits_per_symbol,return_indices=return_indices,return_bits=return_bits,seed=seed,precision=precision,**kwargs)
[docs]classPAMSource(SymbolSource):# pylint: disable=line-too-longr""" Generates a tensor of random PAM symbols Optionally, the symbol indices and/or binary representations of the constellation symbols can be returned. Parameters ---------- num_bits_per_symbol : `int` Number of bits per constellation symbol, e.g., 4 for QAM16. Only required for ``constellation_type`` in ["qam", "pam"]. return_indices : `bool`, (default `False`) If enabled, the function also returns the symbol indices. return_bits : `bool`, (default `False`) If enabled, the function also returns the binary symbol representations (i.e., bit labels). Defaults to `False`. seed : `None` (default) | `int` Set the seed for the random generator used to generate the bits. If set to `None`, :attr:`~sionna.phy.config.Config.tf_rng` is used. precision : `None` (default) | "single" | "double" Precision used for internal calculations and outputs. If set to `None`, :attr:`~sionna.phy.config.Config.precision` is used. Input ----- shape : 1D tensor/array/list, `int` Desired shape of the output tensor Output ------ symbols : ``shape``, `tf.complex` Tensor filled with random PAM symbols symbol_indices : ``shape``, `tf.int32` Tensor filled with the symbol indices. Only returned if ``return_indices`` is `True`. bits : [``shape``, ``num_bits_per_symbol``], `tf.float` Tensor filled with the binary symbol representations (i.e., bit labels). Only returned if ``return_bits`` is `True`. """def__init__(self,num_bits_per_symbol=None,return_indices=False,return_bits=False,seed=None,precision=None,**kwargs):super().__init__(constellation_type="pam",num_bits_per_symbol=num_bits_per_symbol,return_indices=return_indices,return_bits=return_bits,seed=seed,precision=precision,**kwargs)