Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OFDM interference #520

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/source/api/channel.wireless.rst
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,21 @@ cir_to_ofdm_channel
Rayleigh block fading
======================

RayleighBlockFading
---------------------

.. autoclass:: sionna.channel.RayleighBlockFading
:members:
:exclude-members: call, build

MultiTapRayleighBlockFading
----------------------------

.. autoclass:: sionna.channel.MultiTapRayleighBlockFading
:members:
:exclude-members: call, build


3GPP 38.901 channel models
===========================

Expand Down
21 changes: 18 additions & 3 deletions doc/source/api/ofdm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ that automatically generates orthogonal pilot transmissions for all transmitters
and streams.

Additionally, the module contains layers for channel estimation, precoding,
equalization, and detection,
equalization, detection, and interference simulation and estimation,
such as the :class:`~sionna.ofdm.LSChannelEstimator`, the
:class:`~sionna.ofdm.ZFPrecoder`, and the :class:`~sionna.ofdm.LMMSEEqualizer` and
:class:`~sionna.ofdm.LinearDetector`.
:class:`~sionna.ofdm.ZFPrecoder`, the :class:`~sionna.ofdm.LMMSEEqualizer` the
:class:`~sionna.ofdm.LinearDetector`, and :class: `sionna.ofdm.OFDMInterferenceSource`.
These are good starting points for the development of more advanced algorithms
and provide robust baselines for benchmarking.

Expand Down Expand Up @@ -310,3 +310,18 @@ MMSEPICDetector
.. autoclass:: sionna.ofdm.MMSEPICDetector
:exclude-members: call, build
:members:


Interference
***************

OFDMInterferenceSource
-----------------------
.. autoclass:: sionna.ofdm.OFDMInterferenceSource
:exclude-members: call, build

CovarianceEstimator
--------------------
.. autoclass:: sionna.ofdm.CovarianceEstimator
:exclude-members: call, build
:members:
4 changes: 4 additions & 0 deletions doc/source/api/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ complex_normal
--------------
.. autofunction:: sionna.utils.complex_normal

complex_uniform_disk
----------------------
.. autofunction:: sionna.utils.complex_uniform_disk

log2
----
.. autofunction:: sionna.utils.log2
Expand Down
2 changes: 1 addition & 1 deletion sionna/channel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .apply_time_channel import ApplyTimeChannel
from .ofdm_channel import OFDMChannel
from .time_channel import TimeChannel
from .rayleigh_block_fading import RayleighBlockFading
from .rayleigh_block_fading import RayleighBlockFading, MultiTapRayleighBlockFading
from .cir_dataset import CIRDataset
from .constants import *
from .utils import deg_2_rad, rad_2_deg, wrap_angle_0_360, drop_uts_in_sector, relocate_uts, set_3gpp_scenario_parameters, gen_single_sector_topology, gen_single_sector_topology_interferers, subcarrier_frequencies, cir_to_ofdm_channel, cir_to_time_channel, time_to_ofdm_channel, time_lag_discrete_time_channel, exp_corr_mat, one_ring_corr_mat, time_frequency_vector
Expand Down
116 changes: 116 additions & 0 deletions sionna/channel/rayleigh_block_fading.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,119 @@ def __call__(self, batch_size, num_time_steps, sampling_frequency=None):
# Tile the response over the block
h = tf.tile(h, [1, 1, 1, 1, 1, 1, num_time_steps])
return h, delays



# TODO should we introduce a parameter for time delay between paths?
class MultiTapRayleighBlockFading(ChannelModel):
# pylint: disable=line-too-long
r"""
Generate channel impulse responses corresponding to a Rayleigh block
fading channel model.

The channel impulse responses generated are formed of M paths with delays of
:math:`\tau_m = \frac{m}{\mathtt{sampling\_frequency}}, 0 \leq m \leq M-1`,
and a normally distributed fading coefficient.
All time steps of a batch example share the same channel coefficient
(block fading).

This class can be used in conjunction with the classes that simulate the
channel response in time or frequency domain, i.e.,
:class:`~sionna.channel.OFDMChannel`,
:class:`~sionna.channel.TimeChannel`,
:class:`~sionna.channel.GenerateOFDMChannel`,
:class:`~sionna.channel.ApplyOFDMChannel`,
:class:`~sionna.channel.GenerateTimeChannel`,
:class:`~sionna.channel.ApplyTimeChannel`.

Parameters
----------

num_rx : int
Number of receivers (:math:`N_R`)

num_rx_ant : int
Number of antennas per receiver (:math:`N_{RA}`)

num_tx : int
Number of transmitters (:math:`N_T`)

num_tx_ant : int
Number of antennas per transmitter (:math:`N_{TA}`)

num_paths: int
Number of paths (:math:`M`)

dtype : tf.DType
Complex datatype to use for internal processing and output.
Defaults to `tf.complex64`.

Input
-----
batch_size : int
Batch size

num_time_steps : int
Number of time steps

sampling_frequency : float
Sampling frequency [Hz]

Output
-------
a : [batch size, num_rx, num_rx_ant, num_tx, num_tx_ant, num_paths], num_time_steps], tf.complex
Path coefficients

tau : [batch size, num_rx, num_tx, num_paths], tf.float
Path delays [s]
"""

def __init__( self,
num_rx,
num_rx_ant,
num_tx,
num_tx_ant,
num_paths,
dtype=tf.complex64):

assert dtype.is_complex, "'dtype' must be complex type"
self._dtype = dtype

# We don't set these attributes as private so that the user can update
# them
self.num_tx = num_tx
self.num_tx_ant = num_tx_ant
self.num_rx = num_rx
self.num_rx_ant = num_rx_ant
self.num_paths = num_paths

def __call__(self, batch_size, num_time_steps, sampling_frequency):

# Delays
delays = tf.range(0, self.num_paths, dtype=self._dtype.real_dtype) / sampling_frequency
delays = tf.tile(delays[tf.newaxis, tf.newaxis, tf.newaxis, :], [batch_size, self.num_rx, self.num_tx, 1])

# Fading coefficients
std = tf.cast(tf.sqrt(0.5), dtype=self._dtype.real_dtype)
h_real = tf.random.normal(shape=[ batch_size,
self.num_rx,
self.num_rx_ant,
self.num_tx,
self.num_tx_ant,
self.num_paths,
1], # Same response over the block
stddev=std,
dtype = self._dtype.real_dtype)
h_img = tf.random.normal(shape=[ batch_size,
self.num_rx,
self.num_rx_ant,
self.num_tx,
self.num_tx_ant,
self.num_paths,
1], # Same response over the block
stddev=std,
dtype = self._dtype.real_dtype)
h = tf.complex(h_real, h_img)
# Tile the response over the block
h = tf.tile(h, [1, 1, 1, 1, 1, 1, num_time_steps])
return h, delays
2 changes: 2 additions & 0 deletions sionna/ofdm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@
from .equalization import OFDMEqualizer, LMMSEEqualizer, ZFEqualizer, MFEqualizer
from .detection import OFDMDetector, OFDMDetectorWithPrior, MaximumLikelihoodDetector, MaximumLikelihoodDetectorWithPrior, LinearDetector, KBestDetector, EPDetector, MMSEPICDetector
from .precoding import ZFPrecoder
from .interference import OFDMInterferenceSource
from .interference_estimation import CovarianceEstimator
151 changes: 151 additions & 0 deletions sionna/ofdm/interference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Class definition for signal interference generation"""

import sionna
import tensorflow as tf

class OFDMInterferenceSource(tf.keras.layers.Layer):
# pylint: disable=line-too-long
r"""OFDMInterferenceSource(density_subcarriers=1.0, sampler="uniform", domain="freq", cyclic_prefix_length=0, dtype=tf.complex64)

Layer to simulate interference transmitter symbols for OFDM systems in frequency or time domain.
These can then be sent through a OFDM channel to simulate interference at the receiver.

The transmit symbols can be sampled form different distributions or constellations, to be configured through the parameter `sampler`.

When `domain` is set to "freq", the interference generated is meant to be used in frequency domain simulations.
The simulation thus implicitly assumes that the interfering devices are synchronized with the receiver, and send their signal with a cyclic prefix of sufficient length.

When `domain` is set to "time", the interference is generated in time domain. A cyclic prefix may be added to the interference symbols, which can be controlled through the parameter `cyclic_prefix_length`.

This class supports simulation of narrowband interference through the parameter `density_subcarriers`, which controls the fraction of subcarriers on which the interference takes place.
The subcarriers on which the interference takes place are randomly selected for each call to the layer.

Parameters
----------
density_subcarriers: float
Fraction of subcarriers which are effected by interference. Must be in between 0 and 1.
The number of subcarriers effected is rounded to the next integer, and the subcarriers are randomly selected for each call to the layer.
sampler: str, instance of :class:`~sionna.mapping.Constellation`, or callable
If str, one of ["uniform", "gaussian"].
If instance of :class:`~sionna.mapping.Constellation`, the constellation is sampled randomly.
If callable, the callable is used as sampling function. It should have signature ``(shape, var, dtype) -> tf.Tensor``.
The sampled symbols will always have an expected mean energy of 1.
domain: str
Domain in which the interference is generated. One of ["time", "freq"].
cyclic_prefix_length: int
Length of the cyclic prefix. Only relevant if `domain` is "time".
fft_size: int, optional (ignored if `domain` is "freq")
FFT size. Ignored for frequency domain, as this is separately provided by shape.
This parameter is relevant in time domain, as a cyclic prefix might be added in, and sparsity over subcarriers mandates the FFT size.
dtype: tf.complex
Data type of the interference symbols. Defaults to tf.complex64.

Input
-----
shape: 1D tensor/array/list, int
List of integers, specifying the shape of the interference symbols to be generated.
Should consist of `(batch_size, num_tx, num_tx_ant, num_ofdm_symbols, fft_size)` in frequency domain,
and `(batch_size, num_tx, num_tx_ant, num_time_samples)` in time domain.


Output
------
x_itf: ``output_shape``, ``dtype``
Interference symbols in time or frequency domain, depending on the parameter `domain`. ```output_shape``` is ```shape``` if in frequency domain, and `(batch_size, num_tx, num_tx_ant, num_ofdm_symbols * (fft_size + cyclic_prefix_length))` if in time domain.

"""
def __init__(self,
density_subcarriers=1.0,
sampler="uniform",
domain="freq",
cyclic_prefix_length=0,
fft_size=None,
dtype=tf.complex64,
**kwargs):

super().__init__(trainable=False, dtype=dtype, **kwargs)
self._density_subcarriers = density_subcarriers
self._domain = domain
self._cyclic_prefix_length = cyclic_prefix_length
self._fft_size = fft_size
self._dtype_as_dtype = tf.as_dtype(self.dtype)
# if sampler is string, we use the corresponding function. Otherwise assign the function directly
self._sample_function = self._sampler_to_callable(sampler, self._dtype_as_dtype)
if self._domain == "time":
self._modulator = sionna.ofdm.OFDMModulator(cyclic_prefix_length)
self._check_settings()

def _check_settings(self):
assert self._density_subcarriers >= 0.0 and self._density_subcarriers <= 1.0, "density_subcarriers must be in [0, 1]"
assert self._domain in ["time", "freq"]
if self._domain == "time":
assert self._cyclic_prefix_length >= 0, "cyclic_prefix_length must be non-negative"
assert self._fft_size is not None, "fft_size must be provided in time domain"
assert self._dtype_as_dtype.is_complex

def call(self, inputs):
if self._domain == "freq":
self._fft_size = inputs[-1]
num_ofdm_symbols = inputs[-2]
else:
num_ofdm_symbols = tf.math.ceil(tf.cast(inputs[-1], tf.float32) / (self._fft_size + self._cyclic_prefix_length))
x_itf = self._sample_function(tf.concat([inputs[:3], [num_ofdm_symbols, self._fft_size]], axis=0))
x_itf = self._make_sparse(x_itf)
if self._domain == "time":
x_itf = self._modulator(x_itf)
x_itf = x_itf[..., :inputs[-1]]
return x_itf

def _make_sparse(self, data):
shape = tf.shape(data)
num_subcarriers = shape[-1]
num_nonzero_subcarriers = tf.cast(tf.round(self._density_subcarriers * tf.cast(num_subcarriers, tf.float32)), tf.int32)

# create sparse masks
subcarrier_mask = tf.concat([tf.ones([num_nonzero_subcarriers], dtype=self._dtype),
tf.zeros([num_subcarriers - num_nonzero_subcarriers], dtype=self._dtype)], axis=0)
subcarrier_mask = tf.random.shuffle(subcarrier_mask)

return data * subcarrier_mask

def _sampler_to_callable(self, sampler, dtype):
# pylint: disable=line-too-long
r"""
Returns callable which samples from a constellation or a distribution.

Input
-----
sampler : str | Constellation | callable
String in `["uniform", "gaussian"]`, an instance of :class:`~sionna.mapping.Constellation`, or function with signature ``(shape, dtype) -> tf.Tensor``,
where elementwise :math:`E[|x|^2] = 1`.
dtype : tf.Dtype
Defines the datatype the returned function should return.

Output
------
callable
Function with signature ``shape -> tf.Tensor`` which returns a tensor of shape ``shape`` with dtype ``dtype``.
"""
if isinstance(sampler, sionna.mapping.Constellation):
assert sampler.dtype == dtype
sampler.normalize = True
ret = sionna.utils.SymbolSource('custom', constellation=sampler, dtype=dtype)
else:
if isinstance(sampler, str):
if sampler == "uniform":
f = sionna.utils.complex_uniform_disk
elif sampler == "gaussian":
f = sionna.utils.complex_normal
else:
raise ValueError(f"Unknown sampler {sampler}")
elif callable(sampler):
f = sampler
else:
raise ValueError(f"Unknown sampler {sampler}")
# pylint: disable=unnecessary-lambda-assignment
ret = lambda s: f(shape=s, var=1.0, dtype=dtype)
return ret
Loading