#!/usr/bin/env python
"""
Module with channel estimation implementations based on the reference signals in
this package.
"""
from typing import Union
import numpy as np
from .dmrs import DmrsUeSequence
from .srs import SrsUeSequence, UeSequence
[docs]class CazacBasedChannelEstimator:
"""
Estimated the (uplink) channel based on CAZAC (Constant Amplitude Zero
AutoCorrelation) reference sequences sent by one user (either SRS or
DMRS).
The estimation is performed according to the paper [Bertrand2011]_,
where the received signal in the FREQUENCY DOMAIN is used by the
estimator.
Note that for SRS sequences usually a comb pattern is employed such
that only half of the subcarriers is used to send pilot
symbols. Therefore, an FFT in the during the estimation will
effectively interpolate for the other subcarriers. This is controlled
by the `size_multiplier` argument (default is 2 to accommodate comb
pattern). If all subcarriers are used to send pilot symbols then set
`size_multiplier` to 1.
Parameters
----------
ue_ref_seq : SrsUeSequence | DmrsUeSequence | np.ndarray
The reference signal sequence.
size_multiplier : int, optional
Multiplication factor for the FFT to get the actual channel size.
When using the comb pattern for SRS this should be 2 (default value),
but for DMRS, which does not employ the comb pattern, this should be
set to 1.
Notes
-----
.. [Bertrand2011] Bertrand, Pierre, "Channel Gain Estimation from
Sounding Reference Signal in LTE," Conference: Proceedings of the
73rd IEEE Vehicular Technology Conference.
"""
def __init__(self,
ue_ref_seq: Union[SrsUeSequence, DmrsUeSequence, np.ndarray],
size_multiplier: int = 2) -> None:
# If ue_ref_seq is not an instance of UeSequence (or a subclass)
# assume it is a numpy array.
if isinstance(ue_ref_seq, UeSequence):
self._normalized_ref_seq = ue_ref_seq.normalized
ue_ref_seq = ue_ref_seq.seq_array()
else:
self._normalized_ref_seq = False
self._ue_ref_sequence = ue_ref_seq
self._size_multiplier = size_multiplier
@property
def ue_ref_seq(self) -> np.ndarray:
"""Get the sequence of the UE."""
return self._ue_ref_sequence
[docs] def estimate_channel_freq_domain(self, received_signal: np.ndarray,
num_taps_to_keep: int) -> np.ndarray:
"""
Estimate the channel based on the received signal.
Parameters
----------
received_signal : np.ndarray
The received reference signal after being transmitted through the
channel (in the frequency domain). If this is a 2D numpy array
the first dimensions is assumed to be "receive antennas" while
the second dimension are the received sequence elements. The
number of elements in the received signal (per antenna) is
equal to the channel size (number of subcarriers) divided by
`size_multiplier`.
num_taps_to_keep : int
Number of taps (in delay domain) to keep. All taps from 0 to
`num_taps_to_keep`-1 will be kept and all other taps will be
zeroed before applying the FFT to get the channel response in
the frequency domain.
Returns
-------
freq_response : np.ndarray
The channel frequency response. Note that for SRS sequences
this will have twice as many elements as the sent SRS signal,
since the SRS signal is sent every other subcarrier.
"""
# Reference signal sequence
r = self.ue_ref_seq
if received_signal.ndim == 1:
# First we multiply (element-wise) the received signal by the
# conjugate of the reference signal sequence
y = np.fft.ifft(np.conj(r) * received_signal, r.size)
# The channel impulse response consists of the first
# `num_taps_to_keep` elements in `y`.
tilde_h = y[0:num_taps_to_keep + 1]
elif received_signal.ndim == 2:
# Case with multiple receive antennas
y = np.fft.ifft(
np.conj(r)[np.newaxis, :] * received_signal, r.size)
# The channel impulse response consists of the first
# `num_taps_to_keep` elements in `y`.
tilde_h = y[:, 0:num_taps_to_keep + 1]
else:
raise ValueError( # pragma: no cover
"received_signal must have either one dimension ("
"one receive antenna) or two dimensions (first "
"dimension being the receive antenna dimension).")
# Now we can apply the FFT to get the frequency response.
# The number of subcarriers is twice the number of elements in the
# SRS sequence due to the comb pattern
Nsc = r.size
tilde_H = np.fft.fft(tilde_h, self._size_multiplier * Nsc)
if self._normalized_ref_seq is True:
tilde_H *= Nsc
return tilde_H
[docs]class CazacBasedWithOCCChannelEstimator(CazacBasedChannelEstimator):
"""
Estimated the (uplink) channel based on CAZAC (Constant Amplitude Zero
AutoCorrelation) reference sequences sent by one user including the
Orthogonal Cover Code (OCC).
With OCC the user will send reference signal in multiple time slots, in
each slot multiplied with the respective OCC sequence element.
Parameters
----------
ue_ref_seq : DmrsUeSequence
The reference signal sequence.
"""
def __init__(self, ue_ref_seq: DmrsUeSequence) -> None:
cover_code = ue_ref_seq.cover_code
ue_ref_seq_array = ue_ref_seq.seq_array()
reference_seq = ue_ref_seq_array[0] * cover_code[0]
super().__init__(reference_seq, size_multiplier=1)
self._cover_code = cover_code
self._normalized_ref_seq = ue_ref_seq.normalized
@property
def cover_code(self) -> np.ndarray:
"""Get the cover code of the UE."""
return self._cover_code
[docs] def estimate_channel_freq_domain(
self,
received_signal: np.ndarray,
num_taps_to_keep: int,
extra_dimension: bool = True) -> np.ndarray:
"""
Estimate the channel based on the received signal with cover codes.
Parameters
----------
received_signal : np.ndarray
The received reference signal after being transmitted through
the channel (in the frequency domain).
Dimension: Depend if there are multiple receive antennas and if
`extra_dimension` is True or False. Let :math:`Nr` be the
number of receive antennas, :math:`Ne` be the number of reference
signal elements (reference signal size without cover code) and
:math:`Nc` be the cover code size. The dimension of
`received_signal` must match the table below.
================= ======================= ======================
/ extra_dimension: True extra_dimension: False
================= ======================= ======================
Single Antenna Nc x Ne (2D) Ne * Nc (1D)
Multiple Antennas Nr x Nc x Ne (3D) Nr x (Ne * Nc) (2D)
================= ======================= ======================
num_taps_to_keep : int
Number of taps (in delay domain) to keep. All taps from 0 to
`num_taps_to_keep`-1 will be kept and all other taps will be
zeroed before applying the FFT to get the channel response in
the frequency domain.
extra_dimension : bool
If True then the should be an extra dimension in
`received_signal` corresponding to the cover code dimension. If
False then the cover code is included in the dimension of the
reference signal elements.
Returns
-------
freq_response : np.ndarray
The channel frequency response.
"""
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# xxxxx Add the extra dimension if it does not exist xxxxxxxxxxxxxx
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Create a view for the received signals. If extra_dimension is
# false we will reshape this view to add a dimension for the cover
# code
r = received_signal.view()
if extra_dimension is False:
# Let's reorganize the received signal so that we have the
# extra dimension
if received_signal.ndim == 1:
# Case with a single antenna. Cover code dimension will be
# the first dimension.
r.shape = (self.cover_code.size, -1)
elif received_signal.ndim == 2:
# Case with multiple antennas. Cover code dimension will be
# the second dimension.
num_antennas = r.shape[0]
r.shape = (num_antennas, self.cover_code.size, -1)
else:
raise RuntimeError(
'Invalid dimension for received_signal: {0}'.format(
r.ndim))
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# xxxxxxxxxxxxxxx Average over the cover code dimension xxxxxxxxxxx
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Now we can consider the case with the extra cover_code dimension
if r.ndim == 2:
# Apply the cover code
r_mean = np.mean(r * self.cover_code[:, np.newaxis], axis=0)
elif r.ndim == 3:
r_mean = np.mean(r * self.cover_code[np.newaxis, :, np.newaxis],
axis=1)
else:
raise RuntimeError(
'Invalid dimension for received_signal: {0}'.format(r.ndim))
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# xxxxxxxxxxxxxxx Perform the estimation xxxxxxxxxxxxxxxxxxxxxxxxxx
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Call the estimate_channel_freq_domain from the base class
return super().estimate_channel_freq_domain(r_mean, num_taps_to_keep)