From 63e18858820985b67a21da8faf63071a46d8643c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=A4ufele?= Date: Tue, 9 Jul 2024 16:49:46 +0000 Subject: [PATCH] Add phase correction for generation of 5G NR time domain signals --- sionna/nr/carrier_config.py | 14 ++++++++++++++ sionna/nr/pusch_config.py | 16 +++++++++++++++- sionna/nr/pusch_receiver.py | 4 ++++ sionna/nr/pusch_transmitter.py | 4 +++- test/unit/nr/test_pusch_config.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/sionna/nr/carrier_config.py b/sionna/nr/carrier_config.py index f008e5a2..71ec311d 100644 --- a/sionna/nr/carrier_config.py +++ b/sionna/nr/carrier_config.py @@ -145,6 +145,20 @@ def frame_number(self, value): assert value in range(0,1024), "frame_number must be in [0, 1023]" self._frame_number = value + #---carrier_frequency---# + @property + def carrier_frequency(self): + r""" + float: Carrier frequency :math:`f_0` in Hz + """ + self._ifndef("carrier_frequency", 0.) + return self._carrier_frequency + + @carrier_frequency.setter + def carrier_frequency(self, value): + assert value > 0, "carrier_frequency must be positive" + self._carrier_frequency = value + #--------------------------# #---Read-only parameters---# #--------------------------# diff --git a/sionna/nr/pusch_config.py b/sionna/nr/pusch_config.py index 4b1766fc..30fdc817 100644 --- a/sionna/nr/pusch_config.py +++ b/sionna/nr/pusch_config.py @@ -967,6 +967,19 @@ def tb_size(self): return tb_size + @property + def phase_correction_sequence(self): + r""" + np.ndarray[float], read-only: Vector of phase rotations (implicitly) + defined in Section 5.4 [3GPP38211]_. + """ + symbol_durations = (self.carrier.cyclic_prefix_length + + self.fft_size / self.sample_rate) + # Reference point is start of first symbol, so ignore first cyclic prefix + symbol_durations[0] = 0. + symbol_start_times = np.cumsum(symbol_durations) + return np.exp(1j*2*np.pi*self.carrier.carrier_frequency*symbol_start_times) + #-------------------# #---Class methods---# #-------------------# @@ -1170,7 +1183,8 @@ def check_pusch_configs(pusch_configs): "tb_size" : pc.tb_size, "dmrs_length" : pc.dmrs.length, "dmrs_additional_position" : pc.dmrs.additional_position, - "num_cdm_groups_without_data" : pc.dmrs.num_cdm_groups_without_data + "num_cdm_groups_without_data" : pc.dmrs.num_cdm_groups_without_data, + "phase_correction_sequence" : pc.phase_correction_sequence, } params["bandwidth"] = params["num_subcarriers"]*params["subcarrier_spacing"] params["cyclic_prefix_length"] = np.ceil(carrier.cyclic_prefix_length * diff --git a/sionna/nr/pusch_receiver.py b/sionna/nr/pusch_receiver.py index 54f9f2a6..680f42cf 100644 --- a/sionna/nr/pusch_receiver.py +++ b/sionna/nr/pusch_receiver.py @@ -162,6 +162,8 @@ def __init__(self, l_min=self._l_min, cyclic_prefix_length=pusch_transmitter._cyclic_prefix_length) + self._phase_correction_sequence = pusch_transmitter._phase_correction_sequence + # Use or create default ChannelEstimator self._perfect_csi = False self._w = None @@ -246,6 +248,8 @@ def call(self, inputs): # (Optional) OFDM Demodulation if self._input_domain=="time": y = self._ofdm_demodulator(y) + y *= tf.expand_dims(tf.cast( + self._phase_correction_sequence, y.dtype), -1) # Channel estimation if self._perfect_csi: diff --git a/sionna/nr/pusch_transmitter.py b/sionna/nr/pusch_transmitter.py index cc064816..01ddf50f 100644 --- a/sionna/nr/pusch_transmitter.py +++ b/sionna/nr/pusch_transmitter.py @@ -257,7 +257,9 @@ def call(self, inputs): # (Optionally) apply OFDM modulation if self._output_domain=="time": - x = self._ofdm_modulator(x_pre) + x = x_pre / tf.expand_dims(tf.cast( + self._phase_correction_sequence, x_pre.dtype), -1) + x = self._ofdm_modulator(x) else: x = x_pre diff --git a/test/unit/nr/test_pusch_config.py b/test/unit/nr/test_pusch_config.py index 469d354e..9593da42 100644 --- a/test/unit/nr/test_pusch_config.py +++ b/test/unit/nr/test_pusch_config.py @@ -301,3 +301,33 @@ def test_cyclic_prefix_length(self): np.testing.assert_array_almost_equal(carrier_config.cyclic_prefix_length * 1e6, [0.81] + [0.29] * 13, decimal=2) carrier_config = CarrierConfig(subcarrier_spacing=240, slot_number=1) np.testing.assert_array_almost_equal(carrier_config.cyclic_prefix_length * 1e6, [0.29] * 14, decimal=2) + + +class TestPUSCHConfig(unittest.TestCase): + """Tests for the PUSCHConfig class""" + + def test_phase_correction_sequence(self): + """Generate carrier signal for upconversion and compare phase shift at + the start of each symbol with the generated phase correction sequence""" + for subcarrier_spacing in [15, 30, 60, 120, 240]: + pusch_config = PUSCHConfig(subcarrier_spacing=subcarrier_spacing, + sample_rate="standard") + for carrier_frequency in np.arange(1e9, 10e9, .5e9): + pusch_config.carrier.carrier_frequency = carrier_frequency + + fft_size = pusch_config.fft_size + cp_lengths = np.round(pusch_config.carrier.cyclic_prefix_length * + pusch_config.sample_rate).astype(int) + + sample_idx = np.arange(-cp_lengths[0], np.sum(cp_lengths[1:]) + + fft_size * len(cp_lengths)) + upconversion_vector = np.exp(2.j*np.pi*carrier_frequency* + sample_idx/pusch_config.sample_rate) + + symbol_end_idx = np.cumsum(cp_lengths + fft_size) + phase_rotations = [] + for idx in symbol_end_idx: + phase_rotations.append(upconversion_vector[idx - fft_size]) + + np.testing.assert_array_almost_equal(phase_rotations, + pusch_config.phase_correction_sequence)