#
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""
Class that stores coverage map
"""
import matplotlib as mpl
from matplotlib.colors import from_levels_and_colors
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from sionna.utils import expand_to_rank, insert_dims, log10
from .utils import rotation_matrix, mitsuba_rectangle_to_world, watt_to_dbm
import warnings
[docs]
class CoverageMap:
# pylint: disable=line-too-long
r"""
CoverageMap()
Stores the simulated coverage maps
A coverage map is generated for the loaded scene for all transmitters using
:meth:`~sionna.rt.Scene.coverage_map`. Please refer to the documentation of this function
for further details.
Example
-------
.. code-block:: Python
import sionna
from sionna.rt import load_scene, PlanarArray, Transmitter, Receiver
scene = load_scene(sionna.rt.scene.munich)
# Configure antenna array for all transmitters
scene.tx_array = PlanarArray(num_rows=8,
num_cols=2,
vertical_spacing=0.7,
horizontal_spacing=0.5,
pattern="tr38901",
polarization="VH")
# Configure antenna array for all receivers
scene.rx_array = PlanarArray(num_rows=1,
num_cols=1,
vertical_spacing=0.5,
horizontal_spacing=0.5,
pattern="dipole",
polarization="cross")
# Add a transmitters
tx = Transmitter(name="tx",
position=[8.5,21,30],
orientation=[0,0,0])
scene.add(tx)
tx.look_at([40,80,1.5])
# Compute coverage map
cm = scene.coverage_map(max_depth=8)
# Show coverage map
cm.show();
.. figure:: ../figures/coverage_map_show.png
:align: center
"""
def __init__(self,
center,
orientation,
size,
cell_size,
path_gain,
scene,
dtype=tf.complex64):
self._rdtype = dtype.real_dtype
if (tf.rank(center) != 1) or (tf.shape(center)[0] != 3):
msg = "`center` must be shaped as [x,y,z] (rank=1 and shape=[3])"
raise ValueError(msg)
if (tf.rank(orientation) != 1) or (tf.shape(orientation)[0] != 3):
msg = "`orientation` must be shaped as [a,b,c]"\
" (rank=1 and shape=[3])"
raise ValueError(msg)
if (tf.rank(size) != 1) or (tf.shape(size)[0] != 2):
msg = "`size` must be shaped as [w,h]"\
" (rank=1 and shape=[2])"
raise ValueError(msg)
if (tf.rank(cell_size) != 1) or (tf.shape(cell_size)[0] != 2):
msg = "`cell_size` must be shaped as [w,h]"\
" (rank=1 and shape=[2])"
raise ValueError(msg)
num_cells_x = tf.cast(tf.math.ceil(size[0]/cell_size[0]), tf.int32)
num_cells_y = tf.cast(tf.math.ceil(size[1]/cell_size[1]), tf.int32)
if (tf.rank(path_gain) != 3)\
or (tf.shape(path_gain)[1] != num_cells_y)\
or (tf.shape(path_gain)[2] != num_cells_x):
msg = "`path_gain` must have shape"\
" [num_tx, num_cells_y, num_cells_x]"
raise ValueError(msg)
self._center = tf.cast(center, self._rdtype)
self._orientation = tf.cast(orientation, self._rdtype)
self._size = tf.cast(size, self._rdtype)
self._cell_size = tf.cast(cell_size, self._rdtype)
self._path_gain = tf.cast(path_gain, self._rdtype)
#self._path_gain = tf.where(tf.math.is_nan(self._path_gain),
# 0, self._path_gain)
self._transmitters = scene.transmitters
self._scene = scene
# Dict mapping names to index for transmitters
self._tx_name_2_ind = {}
for tx_ind, tx_name in enumerate(self._transmitters):
self._tx_name_2_ind[tx_name] = tx_ind
###############################################################
# Position of the center of the cells in the world
# coordinate system
###############################################################
# [num_cells_x]
x_positions = tf.range(num_cells_x, dtype=self._rdtype)
x_positions = (x_positions + 0.5)*self._cell_size[0]
# [num_cells_x, num_cells_y]
x_positions = tf.expand_dims(x_positions, axis=1)
x_positions = tf.tile(x_positions, [1, num_cells_y])
# [num_cells_y]
y_positions = tf.range(num_cells_y, dtype=self._rdtype)
y_positions = (y_positions + 0.5)*self._cell_size[1]
# [num_cells_x, num_cells_y]
y_positions = tf.expand_dims(y_positions, axis=0)
y_positions = tf.tile(y_positions, [num_cells_x, 1])
# [num_cells_x, num_cells_y, 2]
cell_pos = tf.stack([x_positions, y_positions], axis=-1)
# Move to global coordinate system
# [1, 1, 2]
size = expand_to_rank(self._size, tf.rank(cell_pos), 0)
# [num_cells_x, num_cells_y, 2]
cell_pos = cell_pos - size*0.5
# [num_cells_x, num_cells_y, 3]
cell_pos = tf.concat([cell_pos,
tf.zeros([num_cells_x, num_cells_y, 1],
dtype=self._rdtype)],
axis=-1)
# [3, 3]
rot_cm_2_gcs = rotation_matrix(self._orientation)
# [1, 1, 3, 3]
rot_cm_2_gcs_ = expand_to_rank(rot_cm_2_gcs, tf.rank(cell_pos)+1,
axis=0)
# [num_cells_x, num_cells_y, 3]
cell_pos = tf.linalg.matvec(rot_cm_2_gcs_, cell_pos)
# [num_cells_x, num_cells_y, 3]
cell_pos = cell_pos + self._center
# [num_cells_y, num_cells_x, 3]
cell_pos = tf.transpose(cell_pos, [1, 0, 2])
self._cell_pos = cell_pos
######################################################################
# Position of the transmitters, receivers, and RIS in the coverage map
######################################################################
# [num_tx/num_rx/num_ris, 3]
tx_pos = [tx.position for tx in scene.transmitters.values()]
tx_pos = tf.stack(tx_pos, axis=0)
rx_pos = [rx.position for rx in scene.receivers.values()]
rx_pos = tf.stack(rx_pos, axis=0)
if len(rx_pos) == 0:
rx_pos = tf.zeros([0, 3], dtype=self._rdtype)
ris_pos = [ris.position for ris in scene.ris.values()]
ris_pos = tf.stack(ris_pos, axis=0)
if len(ris_pos) == 0:
ris_pos = tf.zeros([0, 3], dtype=self._rdtype)
# [num_tx/num_rx/num_ris, 3]
center_ = tf.expand_dims(self._center, axis=0)
tx_pos = tx_pos - center_
rx_pos = rx_pos - center_
ris_pos = ris_pos - center_
# [3, 3]
rot_gcs_2_cm = tf.transpose(rot_cm_2_gcs)
# [1, 3, 3]
rot_gcs_2_cm_ = tf.expand_dims(rot_gcs_2_cm, axis=0)
# Positions in the coverage map system
# [num_tx/num_rx/num_ris, 3]
tx_pos = tf.linalg.matvec(rot_gcs_2_cm_, tx_pos)
rx_pos = tf.linalg.matvec(rot_gcs_2_cm_, rx_pos)
ris_pos = tf.linalg.matvec(rot_gcs_2_cm_, ris_pos)
# Keep only x and y
# [num_tx/num_rx/num_ris, 2]
tx_pos = tx_pos[:, :2]
rx_pos = rx_pos[:, :2]
ris_pos = ris_pos[:, :2]
# Quantizing, using the bottom left corner as origin
# [num_tx/num_rx/num_ris, 2]
tx_pos = self._pos_to_idx_cell(tx_pos)
rx_pos = self._pos_to_idx_cell(rx_pos)
ris_pos = self._pos_to_idx_cell(ris_pos)
self._tx_pos = tx_pos
self._rx_pos = rx_pos
self._ris_pos = ris_pos
@property
def center(self):
"""
[3], tf.float : Center of the coverage map in the
global coordinate system
"""
return self._center
@property
def orientation(self):
r"""
[3], tf.float : Orientation of the coverage map
:math:`(\alpha, \beta, \gamma)`
specified through three angles corresponding to a 3D rotation
as defined in :eq:`rotation`.
An orientation of :math:`(0,0,0)` corresponds to a
coverage map that is parallel to the XY plane.
"""
return self._orientation
@property
def size(self):
"""
[2], tf.float : Size of the coverage map
"""
return self._size
@property
def cell_size(self):
"""
[2], tf.float : Resolution of the coverage map, i.e., width
(in the local X direction) and height (in the local Y direction) in
of the cells of the coverage map
"""
return self._cell_size
@property
def cell_centers(self):
"""
[num_cells_y, num_cells_x, 3], tf.float : Positions of the
centers of the cells in the global coordinate system
"""
return self._cell_pos
@property
def num_cells_x(self):
"""
int : Number of cells along the local X-axis
"""
return self.path_gain.shape[2]
@property
def num_cells_y(self):
"""
int : Number of cells along the local Y-axis
"""
return self.path_gain.shape[1]
@property
def num_tx(self):
"""
int : Number of transmitters
"""
return self.path_gain.shape[0]
@property
def tx_pos(self):
"""
[num_tx, 2], int : (column, row) cell index position of each transmitter
"""
return self._tx_pos
@property
def rx_pos(self):
"""
[num_rx, 2], int : (column, row) cell index position of each receiver
"""
return self._rx_pos
@property
def ris_pos(self):
"""
[num_ris, 2], int : (column, row) cell index position of each RIS
"""
return self._ris_pos
@property
def path_gain(self):
"""
[num_tx, num_cells_y, num_cells_x], tf.float : Path gains across the
coverage map from all transmitters
"""
return self._path_gain
@property
def rss(self):
"""
[num_tx, num_cells_y, num_cells_x], tf.float : Received signal strength
(RSS) across the coverage map from all transmitters
"""
tx_powers = [tx.power for tx in self._scene.transmitters.values()]
tx_powers = tf.convert_to_tensor(tx_powers)
return tx_powers[:, tf.newaxis, tf.newaxis] * self.path_gain
@property
def sinr(self):
"""
[num_tx, num_cells_y, num_cells_x], tf.float : SINR
across the coverage map from all transmitters
"""
# Total received power from all transmitters
# [num_tx, num_cells_y, num_cells_x]
total_pow = tf.reduce_sum(self.rss, axis=0)
# Interference for each transmitter
interference = total_pow[tf.newaxis] - self.rss
# Thermal noise
noise = self._scene.thermal_noise_power
# SINR
return self.rss / (interference + noise)
def _pos_to_idx_cell(self, pos):
"""
Convert local position [m] in the coverage map to cell index
Input
-----
pos : [num_pos, 2], tf.float
Local positions within the coverage map
Output
------
[num_pos, 2], tf.int32 : Cell index corresponding to each position
"""
idx_cell = pos + self._size * 0.5
idx_cell = tf.cast(tf.math.floor(idx_cell / self._cell_size), tf.int32)
return idx_cell
[docs]
def cell_to_tx(self, metric):
r""" Computes cell-to-transmitter association. Each cell
is associated with the transmitter providing the highest
metric, such as path gain, received signal strength (RSS), or
SINR.
Input
-------
metric : str, one of ["path_gain", "rss", "sinr"]
Metric to be used
Output
-------
cell_to_tx : [num_cells_y, num_cells_x], tf.int64
Cell-to-transmitter association
"""
# Get tensor for desired metric
if metric not in ["path_gain", "rss", "sinr"]:
raise ValueError("Invalid metric")
cm = getattr(self, metric)
# Assign each cell to the transmitter guaranteeing the highest metric
# [num_cells_y, num_cells_x]:
cell_to_tx = tf.math.argmax(cm, axis=0)
# No transmitter assignment for the cells with no coverage
mask = tf.equal(tf.reduce_max(cm, axis=0), 0)
cell_to_tx = tf.where(
mask, tf.constant(-1, dtype=cell_to_tx.dtype), cell_to_tx)
return cell_to_tx
[docs]
def cdf(self, metric="path_gain", tx=None):
r"""Computes and visualizes the CDF of a metric of the coverage map
Input
-----
metric : str, one of ["path_gain", "rss", "sinr"]
Metric to be shown. Defaults to "path_gain".
tx : int | str | None
Index or name of the transmitter for which to show the coverage
map. If `None`, the maximum value over all transmitters for each
cell is shown.
Defaults to `None`.
Output
------
: :class:`~matplotlib.pyplot.Figure`
Figure showing the CDF
x : tf.float, [num_cells_x * num_cells_y]
Data points for the chosen metric
cdf : tf.float, [num_cells_x * num_cells_y]
Cummulative probabilities for the data points
"""
if metric not in ["path_gain", "rss", "sinr"]:
raise ValueError("Invalid metric")
if isinstance(tx, int):
if tx >= self.num_tx:
raise ValueError("Invalid transmitter index")
elif isinstance(tx, str):
if tx in self._tx_name_2_ind:
tx = self._tx_name_2_ind[tx]
else:
raise ValueError(f"Unknown transmitter with name '{tx}'")
elif tx is None:
pass
else:
msg = "Invalid type for `tx`: Must be a string, int, or None"
raise ValueError(msg)
x = getattr(self, metric)
if tx is not None:
x = x[tx]
else:
x = tf.reduce_max(x, axis=0)
x = tf.reshape(x, [-1])
x = 10 * log10(x)
# Add 30dB for RSS to acount for dBm
if metric=="rss":
x += 30
x = tf.sort(x)
cdf = tf.range(1, tf.size(x) + 1, dtype=tf.float32) \
/ tf.cast(tf.size(x), tf.float32)
fig, _ = plt.subplots()
plt.plot(x.numpy(), cdf.numpy())
plt.grid(True, which="both")
plt.ylabel("Cummulative probability")
# Set x-label and title
if metric=="path_gain":
xlabel = "Path gain [dB]"
title = "Path gain"
elif metric=="rss":
xlabel = "Received signal strength (RSS) [dBm]"
title = "RSS"
else:
xlabel = "Signal-to-interference-plus-noise ratio (SINR) [dB]"
title = "SINR"
if (tx is None) & (self.num_tx > 1):
title = 'Highest ' + title + ' across all TXs'
elif tx is not None:
title = title + f' for TX {tx}'
plt.xlabel(xlabel)
plt.title(title)
return fig, x, cdf
[docs]
def show(self,
metric="path_gain",
tx=None,
vmin=None,
vmax=None,
show_tx=True,
show_rx=False,
show_ris=False):
r"""Visualizes a coverage map
The position of the transmitter is indicated by a red "+" marker.
The positions of the receivers are indicated by blue "x" markers.
The positions of the RIS are indicated by black "*" markers.
Input
-----
metric : str, one of ["path_gain", "rss", "sinr"]
Metric to be shown. Defaults to "path_gain".
tx : int | str | None
Index or name of the transmitter for which to show the coverage
map. If `None`, the maximum value over all transmitters for each
cell is shown.
Defaults to `None`.
vmin,vmax : float | `None`
Define the range of values [dB] that the colormap covers.
If set to `None`, the complete range is shown.
Defaults to `None`.
show_tx : bool
If set to `True`, then the position of the transmitters are shown.
Defaults to `True`.
show_rx : bool
If set to `True`, then the position of the receivers are shown.
Defaults to `False`.
show_ris : bool
If set to `True`, then the position of the RIS are shown.
Defaults to `False`.
Output
------
: :class:`~matplotlib.pyplot.Figure`
Figure showing the coverage map
"""
if metric not in ["path_gain", "rss", "sinr"]:
raise ValueError("Invalid metric")
if isinstance(tx, int):
if tx >= self.num_tx:
raise ValueError("Invalid transmitter index")
elif isinstance(tx, str):
if tx in self._tx_name_2_ind:
tx = self._tx_name_2_ind[tx]
else:
raise ValueError(f"Unknown transmitter with name '{tx}'")
elif tx is None:
pass
else:
msg = "Invalid type for `tx`: Must be a string, int, or None"
raise ValueError(msg)
# Select metric for a specific transmitter or compute max
cm = getattr(self, metric)
if tx is not None:
cm = cm[tx]
else:
cm = tf.reduce_max(cm, axis=0)
# Convert to dB-scale
if metric in ["path_gain", "sinr"]:
with warnings.catch_warnings(record=True) as _:
# Convert the path gain to dB
cm = 10.*np.log10(cm.numpy())
else:
with warnings.catch_warnings(record=True) as _:
# Convert the signal strengmth to dBm
cm = watt_to_dbm(cm).numpy()
# Visualization the coverage map
fig_cm = plt.figure()
plt.imshow(cm, origin='lower', vmin=vmin, vmax=vmax)
# Set label
if metric == "path_gain":
label = "Path gain [dB]"
title = "Path gain"
elif metric == "rss":
label = "Received signal strength (RSS) [dBm]"
title = 'RSS'
else:
label = "Signal-to-interference-plus-noise ratio (SINR) [dB]"
title = 'SINR'
if (tx is None) & (self.num_tx > 1):
title = 'Highest ' + title + ' across all TXs'
elif tx is not None:
title = title + f' for TX {tx}'
plt.colorbar(label=label)
plt.xlabel('Cell index (X-axis)')
plt.ylabel('Cell index (Y-axis)')
plt.title(title)
# Show transmitter, receiver, RIS positions
if show_tx:
if tx is not None:
tx_pos = self._tx_pos[tx]
fig_cm.axes[0].scatter(*tx_pos, marker='P', c='r')
else:
for tx_pos in self._tx_pos:
fig_cm.axes[0].scatter(*tx_pos, marker='P', c='r')
if show_rx:
for rx_pos in self._rx_pos:
fig_cm.axes[0].scatter(*rx_pos, marker='x', c='b')
if show_ris:
for ris_pos in self._ris_pos:
fig_cm.axes[0].scatter(*ris_pos, marker='*', c='k')
return fig_cm
[docs]
def show_association(self,
metric="path_gain",
show_tx=True,
show_rx=False,
show_ris=False):
r"""Visualizes cell-to-tx association for a given metric
The position of the transmitter is indicated by a red "+" marker.
The positions of the receivers are indicated by blue "x" markers.
The positions of the RIS are indicated by black "*" markers.
Input
-----
metric : str, one of ["path_gain", "rss", "sinr"]
Metric based on which the cell-to-tx association
is computed.
Defaults to "path_gain".
show_tx : bool
If set to `True`, then the position of the transmitters are shown.
Defaults to `True`.
show_rx : bool
If set to `True`, then the position of the receivers are shown.
Defaults to `False`.
show_ris : bool
If set to `True`, then the position of the RIS are shown.
Defaults to `False`.
Output
------
: :class:`~matplotlib.pyplot.Figure`
Figure showing the cell-to-transmitter association
"""
if metric not in ["path_gain", "rss", "sinr"]:
raise ValueError("Invalid metric")
# Create the colormap and normalization
colors = mpl.colormaps['Dark2'].colors[:self.num_tx]
cmap, norm = from_levels_and_colors(
list(range(self.num_tx+1)), colors)
fig_tx = plt.figure()
plt.imshow(self.cell_to_tx(metric).numpy(),
origin='lower', cmap=cmap, norm=norm)
plt.xlabel('Cell index (X-axis)')
plt.ylabel('Cell index (Y-axis)')
plt.title('Cell-to-TX association')
cbar = plt.colorbar(label="TX")
cbar.ax.get_yaxis().set_ticks([])
for tx_ in range(self.num_tx):
cbar.ax.text(.5, tx_ + .5, str(tx_), ha='center', va='center')
# Visualizing transmitter, receiver, RIS positions
if show_tx:
for tx_pos in self._tx_pos:
fig_tx.axes[0].scatter(*tx_pos, marker='P', c='r')
if show_rx:
for rx_pos in self._rx_pos:
fig_tx.axes[0].scatter(*rx_pos, marker='x', c='b')
if show_ris:
for ris_pos in self._ris_pos:
fig_tx.axes[0].scatter(*ris_pos, marker='*', c='k')
return fig_tx
[docs]
def sample_positions(self,
num_pos,
metric="path_gain",
min_val_db=None,
max_val_db=None,
min_dist=None,
max_dist=None,
tx_association=True,
center_pos=False):
# pylint: disable=line-too-long
r"""Sample random user positions in a scene based on a coverage map
For a given coverage map, ``num_pos`` random positions are sampled
around each transmitter,
such that the selected metric, e.g., SINR, is larger
than ``min_val_db`` and/or smaller than ``max_val_db``.
Similarly, ``min_dist`` and ``max_dist`` define the minimum and maximum
distance of the random positions to the transmitter under consideration.
By activating the flag ``tx_association``, only positions are sampled
for which the selected metric is the highest across all transmitters.
This is useful if one wants to ensure, e.g., that the sampled positions for
each transmitter provide the highest SINR or RSS.
Note that due to the quantization of the coverage map into cells it is
not guaranteed that all above parameters are exactly fulfilled for a
returned position. This stems from the fact that every
individual cell of the coverage map describes the expected *average*
behavior of the surface within this cell. For instance, it may happen
that half of the selected cell is shadowed and, thus, no path to the
transmitter exists but the average path gain is still larger than the
given threshold. Please enable the flag ``center_pos`` to sample only
positions from the cell centers.
.. figure:: ../figures/cm_user_sampling.png
:align: center
The above figure shows an example for random positions between 220m and
250m from the transmitter and a maximum path gain of -100 dB.
Keep in mind that the transmitter can have a different height than the
coverage map which also contributes to this distance.
For example if the transmitter is located 20m above the surface of the
coverage map and a ``min_dist`` of 20m is selected, also positions
directly below the transmitter are sampled.
Input
-----
num_pos: int
Number of returned random positions for ech transmitter
metric : str, one of ["path_gain", "rss", "sinr"]
Metric to be considered for sampling positions. Defaults to
"path_gain".
min_val_db: float | None
Minimum value for the selected metric ([dB] for path gain and SINR;
[dBm] for RSS).
Positions are only sampled from cells where the selected metric is
larger than or equal to this value.
Ignored if `None`.
Defaults to `None`.
max_val_db: float | None
Maximum value for the selected metric ([dB] for path gain and SINR;
[dBm] for RSS).
Positions are only sampled from cells where the selected metric is
smaller than or equal to this value.
Ignored if `None`.
Defaults to `None`.
min_dist: float | None
Minimum distance [m] from transmitter for all random positions.
Ignored if `None`.
Defaults to `None`.
max_dist: float | None
Maximum distance [m] from transmitter for all random positions.
Ignored if `None`.
Defaults to `None`.
tx_association : bool
If `True`, only positions associated with a transmitter are chosen,
i.e., positions where the chosen metric is the highest among all
all transmitters. Else, a user located in a sampled position for a
specific transmitter may perceive a higher metric from another TX.
Defaults to `True`.
center_pos: bool
If `True`, all returned positions are sampled from the cell center
(i.e., the grid of the coverage map). Otherwise, the positions are
randomly drawn from the surface of the cell.
Defaults to `False`.
Output
------
: [num_tx, num_pos, 3], tf.float
Random positions :math:`(x,y,z)` [m] that are in cells fulfilling the
configured constraints
: [num_tx, num_pos, 2], tf.float
Cell indices corresponding to the random positions
"""
if metric not in ["path_gain", "rss", "sinr"]:
raise ValueError("Invalid metric")
# allow float values for batch_size
if not isinstance(num_pos, (int, float)) or not num_pos % 1 == 0:
raise ValueError("num_pos must be int.")
# cast batch_size to int
num_pos = int(num_pos)
if min_val_db is None:
min_val_db = -1. * np.infty
min_val_db = tf.constant(min_val_db, self._rdtype)
if max_val_db is None:
max_val_db = np.infty
max_val_db = tf.constant(max_val_db, self._rdtype)
if min_val_db > max_val_db:
raise ValueError("min_val_d cannot be larger than max_val_db.")
if min_dist is None:
min_dist = 0.
min_dist = tf.constant(min_dist, self._rdtype)
if max_dist is None:
max_dist = np.infty
max_dist = tf.constant(max_dist, self._rdtype)
if min_dist > max_dist:
raise ValueError("min_dist cannot be larger than max_dist.")
# Select metric to be used
cm = getattr(self, metric)
# Convert to dB-scale
if metric in ["path_gain", "sinr"]:
with warnings.catch_warnings(record=True) as _:
# Convert the path gain to dB
cm = 10.*np.log10(cm.numpy())
else:
with warnings.catch_warnings(record=True) as _:
# Convert the signal strengmth to dBm
cm = watt_to_dbm(cm).numpy()
# [num_tx, 3]: tx_pos_xyz[i, :] contains the i-th tx (x,y,z) coordinate
# positions
tx_pos_xyz = tf.stack([tx.position for tx
in self._scene.transmitters.values()])
# Compute distance from each tx to all cells
# [num_tx, num_cells_y. num_cells_x]
cell_distance_from_tx = tf.math.reduce_euclidean_norm(
self.cell_centers[tf.newaxis] -
insert_dims(tx_pos_xyz, 2, axis=1), axis=-1)
# [num_tx, num_cells_y. num_cells_x]
distance_mask = tf.logical_and(cell_distance_from_tx >= min_dist,
cell_distance_from_tx <= max_dist)
# Get cells for which metric criterion is valid
# [num_tx, num_cells_y. num_cells_x]
cm_mask = tf.logical_and(cm >= min_val_db,
cm <= max_val_db)
# Get cells for which the tx association is valid
tx_ids = insert_dims(tf.range(self.num_tx, dtype=tf.int64), 2, 1)
association_mask = tx_ids == self.cell_to_tx(metric)[tf.newaxis]
# Compute combined mask
mask = distance_mask & cm_mask
if tx_association:
mask = mask & association_mask
mask = tf.cast(mask, tf.int64)
sampled_cell_ids = []
sampled_cell_pos = []
for i, m in enumerate(mask):
valid_ids = tf.where(m)
num_valid_ids = len(valid_ids)
if num_valid_ids == 0:
msg = f"No valid cells for transmitter {i} to sample from."
raise RuntimeError(msg)
cell_ids = tf.random.uniform(shape=[num_pos],
minval=0, maxval=num_valid_ids,
dtype=tf.int64)
sampled_ids = tf.gather(valid_ids, cell_ids, axis=0)
sampled_cell_ids.append(sampled_ids)
sampled_pos = tf.gather_nd(self.cell_centers,
sampled_ids)
sampled_cell_pos.append(sampled_pos)
sampled_cell_ids = tf.stack(sampled_cell_ids, axis=0)
# swap cell indexes to produce (column, row) index pairs
sampled_cell_ids = tf.gather(sampled_cell_ids, [1, 0], axis=-1)
sampled_cell_pos = tf.stack(sampled_cell_pos, axis=0)
# Add random offset within cell-size, if positions should not be
# centered
if not center_pos:
# cell can be rotated
dir_x = tf.expand_dims(0.5*(self.cell_centers[0, 0] -
self.cell_centers[1, 0]), axis=0)
dir_y = tf.expand_dims(0.5*(self.cell_centers[0, 0] -
self.cell_centers[0, 1]), axis=0)
rand_x = tf.random.uniform((num_pos, 1),
minval=-1.,
maxval=1.,
dtype=self._rdtype)
rand_y = tf.random.uniform((num_pos, 1),
minval=-1.,
maxval=1.,
dtype=self._rdtype)
sampled_cell_pos += rand_x * dir_x + rand_y * dir_y
return sampled_cell_pos, sampled_cell_ids
def to_world(self):
r"""
Returns the `to_world` transformation that maps a default Mitsuba
rectangle to the rectangle that defines the coverage map surface
Output
-------
to_world : :class:`mitsuba.ScalarTransform4f`
Rectangle to world transformation
"""
return mitsuba_rectangle_to_world(self._center, self._orientation,
self._size)