# -*- coding: utf-8 -*-
"""Module for performing 'analog' related PHY tasks such as the power amplifier"""
import numpy as np
try:
from .structures import MemoryPolynomial
except:
from structures import MemoryPolynomial
[docs]class PowerAmp(MemoryPolynomial):
"""Power amplifier class that implements a baseband, memory-polynomial based PA """
def __init__(self, order: int = 5, memory_depth: int = 4, memory_stride: int = 1,
noise_variance: float = 0.05, add_lo_leakage: bool = True,
add_iq_imbalance: bool = True, seed: int = 1):
"""Creates an instance of a parallel Hammerstein PA model extracted from a WARP PA board"""
super().__init__(order, memory_depth, memory_stride)
# Seed the random number generator for reproducibility
np.random.seed(seed)
if noise_variance < 0:
raise Exception("The noisevarriance must be >=0")
else:
self.noise_variance = noise_variance
if add_lo_leakage:
self.lo_leakage = 0.01*np.random.randn() + 0.01j*np.random.randn()
else:
self.lo_leakage = 0
if add_iq_imbalance:
gm = 1.07
pm = 5*np.pi/180
k1 = 0.5*(1 + gm * np.exp(1j*pm))
k2 = 0.5*(1 - gm * np.exp(1j*pm))
sc_iq = 1/np.sqrt(np.abs(k1)**2 + np.abs(k2)**2)
self.k1 = sc_iq*k1
self.k2 = sc_iq*k2
else:
self.k1 = 1
self.k2 = 0
default_poly_coeffs = np.array([[0.9295 - 0.0001j, 0.2939 + 0.0005j, -0.1270 + 0.0034j, 0.0741 - 0.0018j], # 1st order coeffs
[0.1419 - 0.0008j, -0.0735 + 0.0833j, -0.0535 + 0.0004j, 0.0908 - 0.0473j], # 3rd order
[0.0084 - 0.0569j, -0.4610 + 0.0274j, -0.3011 - 0.1403j, -0.0623 - 0.0269j], # 5th order
[0.1774 + 0.0265j, 0.0848 + 0.0613j, -0.0362 - 0.0307j, 0.0415 + 0.0429j]], # 7th order
np.complex64)
self.coeffs = default_poly_coeffs[:self.n_rows, :self.memory_depth]
self.nmse_of_fit = None # In case we fit the PA to some model
[docs] def transmit(self, x):
x = self.k1*x + self.k2*np.conj(x)
return super().transmit(x) + self.noise_variance*np.random.rand(x.size)
[docs] def make_new_model(self, pa_input, pa_output):
"""Learn new coefficients based on pa_inputs and pa_outputs"""
self.coeffs = self.perform_least_squares(pa_input, pa_output).reshape(self.coeffs.shape)
model_pa_output = self.transmit(pa_input)
self.nmse_of_fit = self.calculate_nmse(pa_output, model_pa_output)
[docs] @staticmethod
def calculate_nmse(desired, actual):
"""Calculate the normalized mean squared error
Todo:
- Check this. I think I need to divide by number of samples
"""
return np.linalg.norm(desired-actual)**2 / np.linalg.norm(desired)**2
if __name__ == "__main__":
import matplotlib.pyplot as plt
pa = PowerAmp(order=7, noise_variance=0, add_iq_imbalance=False, add_lo_leakage=False, memory_stride=2)
pa.coeffs = np.zeros(pa.coeffs.shape)
pa.coeffs[(0, 0)] = 1
x = np.exp(1j * (2 * np.pi * 1e6 * np.arange(100)/10e6))
y = pa.transmit(x)
pa.make_new_model(x, y)
plt.plot(x.real)
plt.plot(y.real)
plt.show()