Source code for sionna.phy.fec.crc

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0#
"""Blocks for cyclic redundancy checks (CRC) and utility functions"""

import numpy as np
import tensorflow as tf
from sionna.phy import Block
from sionna.phy.fec.utils import int_mod_2

[docs] class CRCEncoder(Block): """Adds a Cyclic Redundancy Check (CRC) to the input sequence. The CRC polynomials from Sec. 5.1 in [3GPPTS38212_CRC]_ are available: `{CRC24A, CRC24B, CRC24C, CRC16, CRC11, CRC6}`. Parameters ---------- crc_degree: str, 'CRC24A' | 'CRC24B' | 'CRC24C' | 'CRC16' | 'CRC11' | 'CRC6' Defines the CRC polynomial to be used. Can be any value from `{CRC24A, CRC24B, CRC24C, CRC16, CRC11, CRC6}`. precision : `None` (default) | 'single' | 'double' Precision used for internal calculations and outputs. If set to `None`, :py:attr:`~sionna.phy.config.precision` is used. Input ----- bits : [...,k], tf.float Binary tensor of arbitrary shape where the last dimension is `[...,k]`. Output ------ x_crc : [...,k+crc_degree], tf.float Binary tensor containing CRC-encoded bits of the same shape as ``inputs`` except the last dimension changes to `[...,k+crc_degree]`. Note ---- For performance enhancements, a generator-matrix-based implementation is used for fixed `k` instead of the more common shift register-based operations. Thus, the encoder must trigger an (internal) rebuild if `k` changes. """ def __init__(self, crc_degree, *, precision=None, **kwargs): super().__init__(precision=precision, **kwargs) assert isinstance(crc_degree, str), "crc_degree must be a string." self._crc_degree = crc_degree # init 5G CRC polynomial self._crc_pol, self._crc_length = self._select_crc_pol(self._crc_degree) self._k = None self._n = None ######################################### # Public methods and properties ######################################### @property def crc_degree(self): """CRC degree as string""" return self._crc_degree @property def crc_length(self): """Length of CRC. Equals number of CRC parity bits.""" return self._crc_length @property def crc_pol(self): """CRC polynomial in binary representation""" return self._crc_pol @property def k(self): """Number of information bits per codeword""" if self._k is None: print("Warning: CRC encoder is not initialized yet."\ "Input dimensions are unknown.") return self._k @property def n(self): """Number of codeword bits after CRC encoding.""" if self._n is None: print("Warning: CRC encoder is not initialized yet."\ "Output dimensions are unknown.") return self._n ######################### # Utility methods ######################### def _select_crc_pol(self, crc_degree): """Select 5G CRC polynomial according to Sec. 5.1 [3GPPTS38212_CRC]_.""" if crc_degree=="CRC24A": crc_length = 24 crc_coeffs = [24, 23, 18, 17, 14, 11, 10, 7, 6, 5, 4, 3, 1, 0] elif crc_degree=="CRC24B": crc_length = 24 crc_coeffs = [24, 23, 6, 5, 1, 0] elif crc_degree=="CRC24C": crc_length = 24 crc_coeffs = [24, 23, 21, 20, 17, 15, 13, 12, 8, 4, 2, 1, 0] elif crc_degree=="CRC16": crc_length = 16 crc_coeffs = [16, 12, 5, 0] elif crc_degree=="CRC11": crc_length = 11 crc_coeffs = [11, 10, 9, 5, 0] elif crc_degree=="CRC6": crc_length = 6 crc_coeffs = [6, 5, 0] else: raise ValueError("Invalid CRC Polynomial") # invert array (MSB instead of LSB) crc_pol_inv = np.zeros(crc_length + 1) crc_pol_inv[[crc_length - c for c in crc_coeffs]] = 1 return crc_pol_inv.astype(int), crc_length def _gen_crc_mat(self, k, pol_crc): """ Build (dense) generator matrix for CRC parity bits. The principle idea is to treat the CRC as systematic linear code, i.e., the generator matrix can be composed out of ``k`` linear independent (valid) codewords. For this, we CRC encode all ``k`` unit-vectors `[0,...1,...,0]` and build the generator matrix. To avoid `O(k^2)` complexity, we start with the last unit vector given as `[0,...,0,1]` and can generate the result for next vector `[0,...,1,0]` via another polynomial division of the remainder from the previous result. This allows to successively build the generator matrix at linear complexity `O(k)`. """ crc_length = len(pol_crc) - 1 g_mat = np.zeros([k, crc_length]) x_crc = np.zeros(crc_length, dtype=int) x_crc[0] = 1 for i in range(k): # shift by one position x_crc = np.concatenate([x_crc, [0]]) if x_crc[0]==1: x_crc = np.bitwise_xor(x_crc, pol_crc) x_crc = x_crc[1:] g_mat[k-i-1,:] = x_crc return g_mat ######################## # Sionna Block functions ######################## def build(self, input_shape): """Build the generator matrix The CRC is always added to the last dimension of the input. """ k = input_shape[-1] # we perform the CRC check on the last dimension assert k is not None, "Shape of last dimension cannot be None." g_mat_crc = self._gen_crc_mat(k, self.crc_pol) self._g_mat_crc = tf.constant(g_mat_crc, dtype=self.rdtype) self._k = k self._n = k + g_mat_crc.shape[1] def call(self, bits, /): """Cyclic Redundancy Check (CRC) function. This function adds the CRC parity bits to ``inputs`` and returns the result of the CRC validation. Args: bits (tf.float): Tensor of arbitrary shape `[...,k]`. Returns: `tf.float`: CRC encoded bits ``x_crc`` of shape `[...,k+crc_degree]`. """ # re-init if shape has changed, update generator matrix if bits.shape[-1] != self._g_mat_crc.shape[0]: self.build(bits.shape) # note: as the code is systematic, we only encode the crc positions # thus, the generator matrix is non-sparse and a "full" matrix # multiplication is probably the fastest TF implementation. x_exp = tf.expand_dims(bits, axis=-2) # row vector of shape 1xk # tf.matmul only supports floats (and int32 but not uint8 etc.) x_crc = tf.matmul(tf.cast(x_exp, self.rdtype), self._g_mat_crc) # calculate crc bits # take modulo 2 of x_crc (bitwise operations instead of tf.mod) # cast to tf.int64 first as TF 2.15 has an XLA bug with casting directly # to tf.int32 x_crc = tf.cast(x_crc, dtype=tf.int64) x_crc = int_mod_2(x_crc) # cast back to original dtype (to support also int8 inputs etc.) x_crc = tf.cast(x_crc, dtype=x_exp.dtype) x_conc = tf.concat([x_exp, x_crc], -1) x_out = tf.squeeze(x_conc, axis=-2) return x_out
[docs] class CRCDecoder(Block): # pylint: disable=line-too-long """Allows Cyclic Redundancy Check (CRC) verification and removes parity bits. The CRC polynomials from Sec. 5.1 in [3GPPTS38212_CRC]_ are available: `{CRC24A, CRC24B, CRC24C, CRC16, CRC11, CRC6}`. Parameters ---------- crc_encoder: CRCEncoder An instance of :class:`~sionna.phy.fec.crc.CRCEncoder` associated with the CRCDecoder. precision : `None` (default) | 'single' | 'double' Precision used for internal calculations and outputs. If set to `None`, :py:attr:`~sionna.phy.config.precision` is used. Input ----- x_crc: [...,k+crc_degree], tf.float Binary tensor containing the CRC-encoded bits (the last `crc_degree` bits are parity bits). Output ------ bits : [...,k], tf.float Binary tensor containing the information bit sequence without CRC parity bits. crc_valid : [...,1], tf.bool Boolean tensor containing the result of the CRC check per codeword. """ def __init__(self, crc_encoder, *, precision=None, **kwargs): super().__init__(precision=precision, **kwargs) assert isinstance(crc_encoder, CRCEncoder), \ "crc_encoder must be a CRCEncoder instance." self._encoder = crc_encoder # to detect changing input dimensions self._bit_shape = None ######################################### # Public methods and properties ######################################### @property def crc_degree(self): """CRC degree as string.""" return self._encoder.crc_degree @property def encoder(self): """CRC Encoder used for internal validation.""" return self._encoder ######################## # Sionna Block functions ######################## def build(self, input_shape): """Nothing to build but check shapes.""" self._bit_shape = input_shape if input_shape[-1] < self._encoder.crc_length: msg ="Input length must be greater than or equal to the CRC length." raise ValueError(msg) def call(self, x_crc, /): """Cyclic Redundancy Check (CRC) verification function. This function verifies the CRC of ``x_crc``. It returns the result of the CRC validation and removes parity bits from ``x_crc``. Args: x_crc (tf.float32): Tensor of arbitrary shape `[...,k+crc_degree]`. Returns: Tuple [`tf.float32`, `tf.bool`]: ``[x_info, crc_valid]`` list, where ``x_info`` contains the information bits without CRC parity bits, of shape `[...,k]`, and ``crc_valid`` contains the result of the CRC validation for each codeword, of shape `[...,1]`. """ if x_crc.shape[-1] != self._bit_shape: self.build(x_crc.shape) # re-encode information bits of x and verify that CRC bits are correct x_info = x_crc[...,0:-self._encoder.crc_length] x_parity = self._encoder(x_crc)[...,-self._encoder.crc_length:] # cast output to desired precision as encoder can have a different # precision x_parity = tf.cast(x_parity, self.rdtype) # return if x fulfils the CRC crc_check = tf.reduce_sum(x_parity, axis=-1, keepdims=True) crc_check = tf.where(crc_check>0, False, True) return x_info, crc_check