Source code for hermespy.simulation.rf_chain.rf_chain
# -*- coding: utf-8 -*-
"""
=======================================
Hardware Radio Frequency Chain Modeling
=======================================
Isolation model (to be implemented): :footcite:t:`2018:kiayni`
"""
from __future__ import annotations
import numpy as np
from hermespy.core.signal_model import Signal
from hermespy.core.factory import Serializable
from .analog_digital_converter import AnalogDigitalConverter
from .phase_noise import PhaseNoise, NoPhaseNoise
from .power_amplifier import PowerAmplifier
__author__ = "André Noll Barreto"
__copyright__ = "Copyright 2023, Barkhausen Institut gGmbH"
__credits__ = ["André Barreto", "Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.1.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"
[docs]
class RfChain(Serializable):
"""Implements an RF chain model.
Only PA is modelled.
"""
yaml_tag = "RfChain"
__phase_offset: float
__amplitude_imbalance: float
__power_amplifier: PowerAmplifier | None
__phase_noise: PhaseNoise
__adc: AnalogDigitalConverter
def __init__(
self,
phase_offset: float | None = None,
amplitude_imbalance: float | None = None,
adc: AnalogDigitalConverter | None = None,
) -> None:
# Initialize class attributes
self.__phase_offset = 0.0
self.__amplitude_imbalance = 0.0
self.__power_amplifier = None
self.__phase_noise = NoPhaseNoise()
self.adc = AnalogDigitalConverter() if adc is None else adc
if phase_offset is not None:
self.__phase_offset = phase_offset
if amplitude_imbalance is not None:
self.amplitude_imbalance = amplitude_imbalance
@property
def amplitude_imbalance(self) -> float:
"""I/Q amplitude imbalance.
Raises:
ValueError: If the imbalance is less than -1 or more than one.
"""
return self.__amplitude_imbalance
@amplitude_imbalance.setter
def amplitude_imbalance(self, val) -> None:
if abs(val) >= 1:
raise ValueError("Amplitude imbalance must be within interval (-1, 1).")
self.__amplitude_imbalance = val
@property
def phase_offset(self) -> float:
"""I/Q phase offset.
Returns: Phase offset in radians.
"""
return self.__phase_offset
@phase_offset.setter
def phase_offset(self, value: float) -> None:
self.__phase_offset = value
@property
def adc(self) -> AnalogDigitalConverter:
"""The analog to digital converter at the end of the RF receive chain."""
return self.__adc
@adc.setter
def adc(self, value: AnalogDigitalConverter) -> None:
self.__adc = value
[docs]
def transmit(self, input_signal: Signal) -> Signal:
"""Returns the distorted version of signal in "input_signal".
According to transmission impairments.
"""
transmitted_signal = input_signal.copy()
# Simulate IQ imbalance
transmitted_signal.samples = self.add_iq_imbalance(transmitted_signal.samples)
# Simulate phase noise
transmitted_signal = self.phase_noise.add_noise(transmitted_signal)
# Simulate power amplifier
if self.power_amplifier is not None:
transmitted_signal.samples = self.power_amplifier.send(transmitted_signal.samples)
return transmitted_signal
[docs]
def add_iq_imbalance(self, input_signal: np.ndarray) -> np.ndarray:
"""Adds Phase offset and amplitude error to input signal.
Notation taken from https://en.wikipedia.org/wiki/IQ_imbalance.
Args:
input_signal (np.ndarray):
Signal to be deteriorated as a matrix in shape `#no_antennas x #no_samples`.
`#no_antennas` depends if on receiver or transmitter side.
Returns:
np.ndarray:
Deteriorated signal with the same shape as `input_signal`.
"""
x = input_signal
eps_delta = self.__phase_offset
eps_a = self.__amplitude_imbalance
eta_alpha = np.cos(eps_delta / 2) + 1j * eps_a * np.sin(eps_delta / 2)
eta_beta = eps_a * np.cos(eps_delta / 2) - 1j * np.sin(eps_delta / 2)
return eta_alpha * x + eta_beta * np.conj(x)
[docs]
def receive(self, input_signal: Signal) -> Signal:
"""Returns the distorted version of signal in "input_signal".
According to reception impairments.
"""
input_signal = input_signal.copy()
# Simulate IQ imbalance
input_signal.samples = self.add_iq_imbalance(input_signal.samples)
# Simulate phase noise
input_signal = self.phase_noise.add_noise(input_signal)
return input_signal
@property
def power_amplifier(self) -> PowerAmplifier:
"""Access the `PowerAmplifier` of the rf chain.
Returns:
A handle to the `PowerAmplifier`.
"""
return self.__power_amplifier
@power_amplifier.setter
def power_amplifier(self, value: PowerAmplifier) -> None:
self.__power_amplifier = value
@property
def phase_noise(self) -> PhaseNoise:
"""Phase Noise model configuration.
Returns: Handle to the pase noise model.
"""
return self.__phase_noise
@phase_noise.setter
def phase_noise(self, value: PhaseNoise) -> None:
self.__phase_noise = value