# # Copyright 2005,2006,2011,2013 Free Software Foundation, Inc. # # This file is part of GNU Radio # # SPDX-License-Identifier: GPL-3.0-or-later # # """ QAM modulation and demodulation. """ from math import pi, sqrt, log from gnuradio import gr from .generic_mod_demod import generic_mod, generic_demod from .generic_mod_demod import shared_mod_args, shared_demod_args from .utils.gray_code import gray_code from .utils import mod_codes from . import modulation_utils from . import digital_python as digital # Default number of points in constellation. _def_constellation_points = 16 # Whether the quadrant bits are coded differentially. _def_differential = True # Whether gray coding is used. If differential is True then gray # coding is used within but not between each quadrant. _def_mod_code = mod_codes.NO_CODE def is_power_of_four(x): v = log(x) / log(4) return int(v) == v def get_bit(x, n): """ Get the n'th bit of integer x (from little end).""" return (x & (0x01 << n)) >> n def get_bits(x, n, k): """ Get the k bits of integer x starting at bit n(from little end).""" # Remove the n smallest bits v = x >> n # Remove all bits bigger than n+k-1 return v % pow(2, k) def make_differential_constellation(m, gray_coded): """ Create a constellation with m possible symbols where m must be a power of 4. Points are laid out in a square grid. Bits referring to the quadrant are differentilly encoded, remaining bits are gray coded. """ sqrtm = pow(m, 0.5) if (not isinstance(m, int) or m < 4 or not is_power_of_four(m)): raise ValueError("m must be a power of 4 integer.") # Each symbol holds k bits. k = int(log(m) / log(2.0)) # First create a constellation for one quadrant containing m/4 points. # The quadrant has 'side' points along each side of a quadrant. side = int(sqrtm / 2) if gray_coded: # Number rows and columns using gray codes. gcs = gray_code(side) # Get inverse gray codes. i_gcs = dict([(v, key) for key, v in enumerate(gcs)]) else: i_gcs = dict([(i, i) for i in range(0, side)]) # The distance between points is found. step = 1 / (side - 0.5) gc_to_x = [(i_gcs[gc] + 0.5) * step for gc in range(0, side)] # Takes the (x, y) location of the point with the quadrant along # with the quadrant number. (x, y) are integers referring to which # point within the quadrant it is. # A complex number representing this location of this point is returned. def get_c(gc_x, gc_y, quad): if quad == 0: return complex(gc_to_x[gc_x], gc_to_x[gc_y]) if quad == 1: return complex(-gc_to_x[gc_y], gc_to_x[gc_x]) if quad == 2: return complex(-gc_to_x[gc_x], -gc_to_x[gc_y]) if quad == 3: return complex(gc_to_x[gc_y], -gc_to_x[gc_x]) raise Exception("Impossible!") # First two bits determine quadrant. # Next (k-2)/2 bits determine x position. # Following (k-2)/2 bits determine y position. # How x and y relate to real and imag depends on quadrant (see get_c # function). const_map = [] for i in range(m): y = get_bits(i, 0, (k - 2) // 2) x = get_bits(i, (k - 2) // 2, (k - 2) // 2) quad = get_bits(i, k - 2, 2) const_map.append(get_c(x, y, quad)) return const_map def make_non_differential_constellation(m, gray_coded): side = int(pow(m, 0.5)) if (not isinstance(m, int) or m < 4 or not is_power_of_four(m)): raise ValueError("m must be a power of 4 integer.") # Each symbol holds k bits. k = int(log(m) / log(2.0)) if gray_coded: # Number rows and columns using gray codes. gcs = gray_code(side) # Get inverse gray codes. i_gcs = mod_codes.invert_code(gcs) else: i_gcs = list(range(0, side)) # The distance between points is found. step = 2.0 / (side - 1) gc_to_x = [-1 + i_gcs[gc] * step for gc in range(0, side)] # First k/2 bits determine x position. # Following k/2 bits determine y position. const_map = [] for i in range(m): y = gc_to_x[get_bits(i, 0, k // 2)] x = gc_to_x[get_bits(i, k // 2, k // 2)] const_map.append(complex(x, y)) return const_map # ///////////////////////////////////////////////////////////////////////////// # QAM constellation # ///////////////////////////////////////////////////////////////////////////// def qam_constellation(constellation_points=_def_constellation_points, differential=_def_differential, mod_code=_def_mod_code, large_ampls_to_corners=False): """ Creates a QAM constellation object. If large_ampls_to_corners=True then sectors that are probably occupied due to a phase offset, are not mapped to the closest constellation point. Rather we take into account the fact that a phase offset is probably the problem and map them to the closest corner point. It's a bit hackish but it seems to improve frequency locking. """ if mod_code == mod_codes.GRAY_CODE: gray_coded = True elif mod_code == mod_codes.NO_CODE: gray_coded = False else: raise ValueError("Mod code is not implemented for QAM") if differential: points = make_differential_constellation( constellation_points, gray_coded=False) else: points = make_non_differential_constellation( constellation_points, gray_coded) side = int(sqrt(constellation_points)) width = 2.0 / (side - 1) # No pre-diff code # Should add one so that we can gray-code the quadrant bits too. pre_diff_code = [] if not large_ampls_to_corners: constellation = digital.constellation_rect(points, pre_diff_code, 4, side, side, width, width) else: sector_values = large_ampls_to_corners_mapping(side, points, width) constellation = digital.constellation_expl_rect( points, pre_diff_code, 4, side, side, width, width, sector_values) return constellation def find_closest_point(p, qs): """ Return in index of the closest point in 'qs' to 'p'. """ min_dist = None min_i = None for i, q in enumerate(qs): dist = abs(q - p) if min_dist is None or dist < min_dist: min_dist = dist min_i = i return min_i def large_ampls_to_corners_mapping(side, points, width): """ We have a grid that we use for decision making. One additional row/column is placed on each side of the grid. Points in these additional rows/columns are mapped to the corners rather than the closest constellation points. Args: side: The number of rows/columns in the grid that we use to do decision making. points: The list of constellation points. width: The width of the rows/columns. Returns: sector_values maps the sector index to the constellation point index. """ # First find the indices of the corner points. # Assume the corner points are the 4 points with the largest magnitudes. corner_indices = [] corner_points = [] max_mag = 0 for i, p in enumerate(points): if abs(p) > max_mag: corner_indices = [i] corner_points = [p] max_mag = abs(p) elif abs(p) == max_mag: corner_indices.append(i) corner_points.append(p) if len(corner_indices) != 4: raise ValueError("Found {0} corner indices. Expected 4." .format(len(corner_indices))) # We want an additional layer around the constellation # Value in this extra layer will be mapped to the closest corner rather # than the closest constellation point. extra_layers = 1 side = side + extra_layers * 2 # Calculate sector values sector_values = [] for real_x in range(side): for imag_x in range(side): sector = real_x * side + imag_x # If this sector is a normal constellation sector then # use the center point. c = ((real_x - side / 2.0 + 0.5) * width + (imag_x - side / 2.0 + 0.5) * width * 1j) if (real_x >= extra_layers and real_x < side - extra_layers and imag_x >= extra_layers and imag_x < side - extra_layers): # This is not an edge row/column. Find closest point. index = find_closest_point(c, points) else: # This is an edge. Find closest corner point. index = corner_indices[find_closest_point(c, corner_points)] sector_values.append(index) return sector_values modulation_utils.add_type_1_constellation('qam', qam_constellation)